Now browsing the archives for 9月, 2007.

Firefox 3 でのポップアップ仕様変更

Firefox 3 (Gecko 1.9) では XUL ポップアップの仕様が大幅に変更される。
Neil’s Place » Blog Archive » XUL Popup Improvements

特に注意すべき点

  1. popup 要素は非推奨となり、その代わりにまったく等価な menupopup 要素を使用することが推奨されている。
  2. panel という汎用のポップアップ型要素が新たに追加される。子に menuitem を配置するなら menupopup 、それ以外の色々なUI部品を配置したければ panel 、というふうに使い分ける。
  3. showPopup メソッドが非推奨となり、代わりに openPopup と openPopupAtScreen メソッドが追加される。
  4. state プロパティが追加され、ポップアップが現在開いているか、閉じているかなどが判別可能になる。
  5. ポップアップを開く動作が非同期的なイベントになる。したがってポップアップが開いた直後に何らかの処理をしたければ、 popupshown イベントハンドラを使用する必要がある。
  6. popup / menupopup 要素の見た目がOSネイティブな見た目になる。

openPopup / openPopupAtScreen メソッド

showPopup メソッドは引数の指定方法がわかりにくく混乱を招いていたが、 Firefox 3 で新たに追加される2つのメソッドは、スクリーンに対する絶対位置へポップアップを開くための openPopupAtScreen と、ある要素に対する相対位置へポップアップを開くための openPopup というように明瞭化されている。
例1) スクリーン上の位置 (100, 200) にポップアップ popupElt を開く

Firefox 2
document.popupNode = null;  // 位置ズレ防止
popupElt.showPopup(document.documentElement, 100, 200, "popup", null, null);
Firefox 3
popupElt.openPopupAtScreen(100, 200, false);

例2) 要素 aAnchorElt の左下にポップアップ popupElt を開く

Firefox 2
popupElt.showPopup(aAnchorElt, -1, -1, "popup", "bottomleft", "topleft");
Firefox 3
popupElt.openPopup(aAnchorElt, "after_start", 0, 0, false, true);

openPopup メソッドの第5引数 isContextMenu はコンテキストメニューかどうかを示す。しかし、コンテキストメニューにした場合、実際にどのような変化が現れるのかは不明。 openPopup メソッドの第6引数 attributesOverride は position 属性をメソッドの第2引数で上書きするかどうかを示す。

state プロパティ

nsIPopupBoxObject#popupState プロパティへのエイリアス。
Firefox 2 では特定のポップアップが開いているか否かという状態を調べるためには popupshowing や popuphidden などのイベントを監視して自前のフラグを管理したりする必要があったが、そういった苦労は state プロパティで一気に解消される。

state プロパティの値 意味
showing ポップアップは開く途中である。 popupshowing イベント発生時はこの状態。
open ポップアップは開いている。 popupshown イベント発生後はこの状態。
hiding ポップアップは閉じる途中である。 popuphiding イベント発生時はこの状態。
closed ポップアップは閉じている。 popuphidden イベント発生後はこの状態。

ポップアップの非同期的な動作

例えば以下のようなコードを実行すると Firefox 2 では「123」の順番で出力されるが、 Firefox 3 では「132」の順番になる。

XUL
<menupopup id="testPopup" onpopupshowing="dump('1');" onpopupshown="dump('2');"> ...
JavaScript
document.getElementById("testPopup").showPopup( ... ); dump('3');

他には?

nsIPopupBoxObject#enableKeyboardNavigator について、現段階 (Minefield 3.0a8pre) では実行すべきタイミングや得られる効果が Firefox 2 と異なる。効果が逆なのは明らかにバグなのでBug 279703のコメントに書いたところ、Bug 396517としてパッチも提供されてチェックイン間近となっている。なお、代替として ignorekeys 属性を使用することが推奨されており、こちらの動作は問題ない。

nsIPopupBoxObject#enableRollup について、現段階 (Minefield 3.0a8pre) では何もしない
代わりに、新しく追加された setConsumeRollupEvent を使うことで Windows + Minefield 3.0a8pre では期待通りの動作結果が得られた。しかし Linux + Minefield 3.0a8pre では効果がない模様?これも詳細を調べ中。

また、C++レベルでの内部的な実装が一新されたことに伴い、ポップアップの位置ズレやサイズ不正、クラッシュバグ、複数のポップアップを開いたときに閉じられなくなることがあるバグなど、多くのバグが解消されているようだ。

関連リンク

mozilla mozilla/toolkit/content/widgets/popup.xml
mozilla mozilla/layout/xul/base/public/nsIPopupBoxObject.idl
XUL:PopupGuide – MDC

TOP

カスタムツリービューの基本的な使い方(その2~追加・削除)

その1ではデータをツリー表示するだけであったが、その2ではデータを追加・削除してツリーの表示へ反映させる。

fruits.xul

まず、追加・削除するためのボタンを xul:tree 要素の上側へ配置する。

    <hbox>
        <button label="Add" oncommand="addFruit();" />
        <button label="Delete" oncommand="deleteFruits();" />
    </hbox>

fruits.js

fruits.xul へ追加した各ボタンを押下したときに呼び出される2つの関数を定義する。
addFruit 関数は新たに追加する果物の名前を入力するためのプロンプトを表示し、入力が確定すると後述の FruitsTreeView#appendItem メソッドで実際のデータ追加処理を行う。

function addFruit() {
    var name = window.prompt("Enter fruit name.", "", "Add Fruit");
    if (!name)
        return;
    gFruitsTreeView.appendItem(name);
}

deleteFruits 関数は現在選択しているツリーの行番号を後述の FruitsTreeView#selectedIndexes プロパティから取得し、各行のデータを FruitsTreeView#removeItemAt メソッドで実際に削除する。なお、行番号のズレを防ぐため、削除は下側の行から順に行う。

function deleteFruits() {
    var indexes = gFruitsTreeView.selectedIndexes;
    indexes.reverse();
    indexes.forEach(function(index) {
        gFruitsTreeView.removeItemAt(index);
    });
}

FruitsTreeView.prototype へ appendItem メソッドを追加する。
このメソッドは、引数 aName の名前を持つアイテムをデータ _data へ追加し、 nsITreeBoxObject.rowCountChanged を呼び出すことでツリー表示を更新する。
ついでに新しく追加された列を自動的に選択してフォーカスする。

    /**
     * @param String aName The name of the new item.
     */
    appendItem: function(aName) {
        this._data.push(aName);
        var newIndex = this.rowCount - 1;
        this._treeBoxObject.rowCountChanged(newIndex, 1);
        // select the new item now
        this.selection.select(newIndex);
        this._treeBoxObject.ensureRowIsVisible(newIndex);
        this._treeBoxObject.treeBody.focus();
    },

FruitsTreeView クラスへ、現在選択している行番号を配列として取得するための読み込み専用プロパティ selectedIndexes を追加する。
ツリー上の選択は nsITreeView#selection プロパティから nsITreeSelection 型オブジェクトとして取得できる。
今回ツリーは複数選択を許可しているため、選択範囲を意識する必要がある。ちなみに <tree seltype=”single”> とすれば複数選択を不許可にできる。

    /**
     * readonly property to get selected row indexes as array.
     */
    get selectedIndexes() {
        var ret = [];
        var sel = this.selection;    // nsITreeSelection
        for (var rc = 0; rc < sel.getRangeCount(); rc++) {
            var start = {}, end = {};
            sel.getRangeAt(rc, start, end);
            for (var idx = start.value; idx <= end.value; idx++) {
                ret.push(idx);
            }
        }
        return ret;
    },

FruitsTreeView.prototype へ removeItemAt メソッドを追加する。
このメソッドは、引数 aRow の行番号に対応するアイテムをデータ _data から削除し、 nsITreeBoxObject.rowCountChanged を呼び出すことでツリー表示を更新する。
ついでに現在の選択状態を解除する。

    /**
     * @param Number aIndex The row index where we want to remove.
     */
    removeItemAt: function(aIndex) {
        this._data.splice(aIndex, 1);
        this._treeBoxObject.rowCountChanged(aIndex, -1);
        this.selection.clearSelection();
    },

関連記事

TOP

カスタムツリービューの基本的な使い方(その1~表示)

何らかのデータをツリー (xul:tree 要素) で表示するためにはいくつかの方法がありますが、最も一般的なのはカスタムツリービュー方式であるかと思います。ここでは、「何らかのデータ」として最も単純な一次元の配列を想定しますが、二次元配列や何らかのオブジェクトの配列など、他のデータ構造にも応用可能です。

fruits.xul

はじめに以下のように tree 要素を配した fruits.xul を作成します。

<?xml version="1.0"?>

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
      title="Fruits"
      onload="init();"
      onunload="uninit();">

    <script type="application/x-javascript" src="fruits.js" />

    <tree id="fruitsTree" flex="1">
        <treecols>
            <treecol label="Name" flex="1" primary="true" />
        </treecols>
        <treechildren flex="1" />
    </tree>

</page>

fruits.js

次に、 fruits.js を作成します。はじめに、ツリーに表示したいデータとして、果物の名前の配列 gFruitsData を定義します。配列内の null はツリー上で区切りとして表示する要素です。

var gFruitsData = [
    "Grape",
    "Apple",
    "Orange",
    "Banana",
    null,    // separator
    "Pear",
    "Peach",
    "Strawberry",
    "Cherry",
    "Melon",
    null,    // separator
    "Watermelon",
    "Plum",
    "Papaya",
    "Lemon",
];

次に、後述する FruitsTreeView クラスのインスタンスである gFruitsTreeView と、ウィンドウを開いた直後(onload イベント発生時)に呼び出される init 関数、ウィンドウを閉じる直前(onunload イベント発生時)に呼び出される uninit 関数を定義します。 init 関数では gFruitsData を引数として FruitsTreeView クラスのインスタンスを生成し、グローバル変数 gFruitsTreeView として保持します。これを xul:tree 要素の view プロパティとしてセットすることで、実際にツリーへの表示が行われます。 uninit 関数では null をセットすることで、ツリーへの表示を終了します。

var gFruitsTreeView = null;

function init() {
    gFruitsTreeView = new FruitsTreeView(gFruitsData);
    document.getElementById("fruitsTree").view = gFruitsTreeView;
}

function uninit() {
    document.getElementById("fruitsTree").view = null;
}

nsITreeView インタフェース

ここからは FruitsTreeView クラスを定義します。 FruitsTreeView クラスはコンストラクタの引数として配列形式のデータを受け取り、 _data プロパティとして内部的に参照を保持します。また、 nsITreeView.idl で定義された各プロパティ・メソッドを実装します。ただしすべてのメンバをきちんと実装する必要は無く、ツリーで実現したい機能に応じて適宜実装を加えていくことになります。 nsITreeView メンバの概要は以下の通りです。

メンバ名 概要
rowCount ツリーの行数を表す読み込み専用プロパティ。今回の場合はデータの配列の長さを返せば良い。
selection ツリー上の現在の選択を nsITreeSelection 型オブジェクトとして返す。このプロパティは自前で実装する必要は無い。
getRowProperties
getCellProperties
getColumnProperties
ツリーのセル・列・行に対してクラスを設定し、 CSS を使って見た目の詳細なカスタマイズを行う場合に使用する。今回は使用しない。
isContainer
isContainerOpen
isContainerEmpty
階層構造を有するツリーの場合に実装が必要となるが、今回は階層構造がない単純なツリーであるため使用しない。すべて false を返す。
isSeparator 引数 index の行がセパレータかどうかを表す。今回は前述したとおり、データが null の場合にセパレータとして表示したいので、その行に対応するデータの値が null なら true を返せばよい。
isSorted カラムをクリックして並び替え可能なツリーの場合に使用する。今回は常に未ソート状態なので false を返す。
canDrop ドラッグ&ドロップで移動可能なツリーの場合に使用する。現時点では必要ないので false を返す。
drop ドラッグ&ドロップで移動可能なツリーの場合に使用し、ドロップされたときに呼び出される。
getParentIndex
hasNextSibling
getLevel
これらも階層構造を有するツリーを正しく描画するために必要となる。 getParentIndex は階層構造を持たない場合は常に -1 を返さなければならない。
getImageSrc ブックーマークツリーのようにアイコンなどの画像を表示する場合に使用する。
getProgressMode セルにプログレスバーを表示するツリーを実装する場合に使用する。
getCellValue プログレスバー型のセルおよびチェックボックス型のセルで、その値を返す。
getCellText 引数で指定されたセルの表示文字列の値を返す。引数 row は行番号、 col は nsITreeColumn 型オブジェクトで、 index プロパティから列番号を取得できる。複数の列を有するツリーの場合は列番号に応じた表示文字列から取り出すが、今回は列番号は常に 0 となり、データ中の行番号に応じた要素そのものを返すだけでよい。
setTree ツリービューを xul:tree 要素の view プロパティへセットした際に呼び出される。引数 tree は nsITreeBoxObject 型オブジェクトであるが、この nsITreeBoxObject は後々利用することが多いため、 FruitsTreeView クラスの _treeBoxObject プロパティとして内部的に参照を保持しておく。
引数が null の場合、終了処理として内部的に保持していたいくつかの参照を破棄する。
toggleOpenState 階層構造を有するツリーのフォルダを開閉した時に呼び出される。
cycleHeader カラムをクリックして並び替えが可能なツリーの場合、カラムクリック時に呼び出される。
selectionChanged ツリーの選択が変更された時に何らかの処理を実行したい場合、 xul:tree 要素の onselect=”this.view.selectionChainged();” イベントハンドラをセットして呼び出すようにする?
cycleCell カラムをクリックして並び替えが可能なツリーの場合に使用する?
isEditable 引数で指定されたセルがチェックボックス型で変更可能であれば true を返す。
引数で指定されたセルをダブルクリックしてインライン編集可能であれば true を返す。
isSelectable 詳細不明。特定の行またはセルを選択不可にするためのもの?
setCellValue プログレスバー型のセルおよびチェックボックス型のセルで、その値を変更するためのメソッド。
setCellText インライン編集可能なツリー用。引数で指定されたセルの表示文字列を指定した値に変更するためのメソッド。
performAction
performActionOnRow
performActionOnCell
IDL によればツリー上で Del キーを押下すると引数 action に「delete」が渡されて呼び出されるとあるが、実際はどうやら xul:key 要素を追加するなどして自前で performActionOnRow などを呼び出す必要があるようだ。使い道がよくわからん。

FruitsTreeView クラス

今回はツリーへビューをセットしてデータ(果物の名前と区切り)を表示させるだけなので、きちんと実装する必要があるのは rowCount, isSeparator, getCellText, setTree の4つだけです。その他のメソッドは適宜 return false などにしておきます。

fruits.js
////////////////////////////////////////////////////////////////
// Custom Tree View

function FruitsTreeView(aData) {
    this._data = aData;
}

FruitsTreeView.prototype = {

    /**
     * nsITreeBoxObject
     */
    _treeBoxObject: null,

    ////////////////////////////////////////////////////////////////
    // implements nsITreeView

    get rowCount() {
        return this._data.length;
    },
    selection: null,
    getRowProperties: function(index, properties) {},
    getCellProperties: function(row, col, properties) {},
    getColumnProperties: function(col, properties) {},
    isContainer: function(index) { return false; },
    isContainerOpen: function(index) { return false; },
    isContainerEmpty: function(index) { return false; },
    isSeparator: function(index) {
        return this._data[index] == null;
    },
    isSorted: function() { return false; },
    canDrop: function(targetIndex, orientation, dataTransfer) { return false; },
    drop: function(targetIndex, orientation, dataTransfer) {},
    getParentIndex: function(rowIndex) { return -1; },
    hasNextSibling: function(rowIndex, afterIndex) { return false; },
    getLevel: function(index) { return 0; },
    getImageSrc: function(row, col) {},
    getProgressMode: function(row, col) {},
    getCellValue: function(row, col) {},
    getCellText: function(row, col) {
        switch (col.index) {
            case 0: return this._data[row];
        }
    },
    setTree: function(tree) {
        if (tree) {
            // initialize view
            this._treeBoxObject = tree;
        }
        else {
            // finalize view
            this._treeBoxObject = null;
            this._data = null;
        }
    },
    toggleOpenState: function(index) {},
    cycleHeader: function(col) {},
    selectionChanged: function() {},
    cycleCell: function(row, col) {},
    isEditable: function(row, col) { return false; },
    isSelectable: function(row, col) {},
    setCellValue: function(row, col, value) {},
    setCellText: function(row, col, value) {},
    performAction: function(action) {},
    performActionOnRow: function(action, row) {},
    performActionOnCell: function(action, row, col) {},

};

関連記事

TOP

待望の document.elementFromPoint が実装

待望の document.elementFromPoint が Firefox 3.0a8pre にて実装された。仕様は nsIDOMNSDocument.idl に詳しく書いてあるが、おおよそ以下の通りである。

  • HTML, XUL どちらの document に対しても使用可能
  • document の左上を (0, 0) とし、位置 (x, y) にある実際に見えている要素を取得する
  • 同一の document 内に存在する要素のみ取得可能。例えばインナーフレーム内の document 内に存在する要素は取得できず、代わりに iframe 要素を返す。
  • 位置 (x, y) が document の可視領域の外側にある場合、null を返す。
  • XUL document で使用する場合、例えば textbox 要素のスクロールバーのように XBL で生成された無名要素は取得できない。この場合、 textbox 要素を返す。
  • XUL document で使用する場合、 onload イベント発生以降でなければならない。

さっそく使い心地を試すべく、 HTML の document についてのサンプルをブックマークレットとして作った。使い方は、 Firefox 3 で適当なページを開き、下記のコードを丸ごとコピーしてロケーションバーへ貼り付けて移動するだけ。マウスポインタ上に位置する要素を elementFromPoint で取得してツールチップ風に表示する。

サンプル1

javascript:(function(){
    var tooltip = document.createElement("DIV");
    tooltip.style.cssText = "position: absolute; z-index: 1000; background-color: lightgreen;";
    document.body.appendChild(tooltip);
    var scanElement = function(event) {
        var elt = document.elementFromPoint(event.clientX, event.clientY);
        tooltip.innerHTML = "(" + event.clientX + ", " + event.clientY + ") " + elt.tagName;
        tooltip.style.left = (event.clientX + window.scrollX) + "px";
        tooltip.style.top  = (event.clientY + window.scrollY + 21) + "px";
    };
    document.addEventListener("mousemove", scanElement, false);
})();

しかし、上記の例はわざわざ elementFromPoint を使わなくても event.target で代用可能なので面白みがない。そこで、より機械的に指定した座標の要素を取得する例を作った。ページ左上から斜めに移動しながら要素名を取得して表示する。

サンプル2

javascript:(function(){
    var tooltip = document.createElement("DIV");
    tooltip.style.cssText = "position: absolute; z-index: 1000; background-color: lightgreen;";
    document.body.appendChild(tooltip);
    var scanElement = function(posX, posY) {
        var elt = document.elementFromPoint(posX, posY);
        tooltip.innerHTML = "(" + posX + ", " + posY + ") " + elt.tagName;
        tooltip.style.left = (posX + 2) + "px";
        tooltip.style.top  = (posY + 2) + "px";
        setTimeout(function(){ scanElement(++posX, ++posY); }, 20);
    };
    window.scrollTo(0, 0);
    scanElement(0, 0);
})();

TOP