Now browsing the archives for the 'XUL Widgets' category.

prefpane には必ず id を付与する

設定値 browser.preferences.animateFadeIn が true の場合に、 id を持たない prefpane が丸ごと表示されない。

<prefpane id="paneMain">

というように必ず id を付与しなければならない。
確かに MDC の prefpane の解説 にも id は必ず付与すべしと書いてある。

prefwindow を使用している場合はパネルの中身が見切れるなどのトラブルがつき物なので、可能な限り以下のような観点での動作確認をした方がよさそうだ。

  • 各パネルの中身が正しく表示されるか?
  • 最後に選択したパネルが次回正しく初期選択されるか?
  • browser.preferences.animateFadeIn が true の場合の動作確認
  • browser.preferences.instantApply が true の場合の動作確認
  • 上記を {Firefox 2 | Minefield} × {Win | Mac | Linux} の6パターンで動作確認

TOP

カスタムツリービューの基本的な使い方(その3~インライン編集)

Firefox 3 以降限定だが、ツリーのインライン編集が可能となる。この機能を使い、ツリーのアイテムをダブルクリックして果物の名前を変更する機能を追加する。ベースとなるソースコードはその1~表示を参照。

fruits.xul

tree 要素へ editable=”true” 属性を追加する。

    <tree id="fruitsTree" editable="true" flex="1">

fruits.js

nsITreeView#isEditable メソッドを実装する。引数 row で指定された行がセパレータでなければ編集を可能にする。

    isEditable: function(row, col) {
        return !this.isSeparator(row);
    },

ダブルクリックでセルを編集して Enter キー押下で確定させると、 nsITreeView#setCellText メソッドが呼び出される。
setCellText メソッドでは、引数 row で指定されたアイテムのデータそのものを変更してから nsITreeBoxObject#invalidate でツリーを再描画する。

    setCellText: function(row, col, value) {
        this._data[row] = value;
        this._treeBoxObject.invalidate();
    },

関連記事

TOP

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

Prefwindow のパネル切り替えの挙動がおかしい

prefwindow 要素で作成した設定ウィンドウで、ウィンドウ上部のパネル切り替えボタンと実際に選択されるパネルに食い違いが発生するおかしな挙動に悩まされた。

原因は prefwindow 要素直下に script 要素を配置していたことだった。
Preferences System:prefwindow – MDC にも書いてある通り、 prefpane 以外の要素を配置する場合は prefpane 要素よりも後に記述しなければならない。

<prefwindow ... >
    <prefpane label="General" ... >
        ...
    </prefpane>
    <prefpane label="Advanced" ... >
        ...
    </prefpane>
    <script type="application/x-javascript" src="prefs.js" />
</prefwindow>

TOP

横幅に収まらずに crop されたテキストの末尾の「…」

2007/11/?? 追記
この問題は中野雅之氏・dynamis氏らのご尽力により解決しました。

2007/8/19 追記
Bugzilla-jp にバグが登録されていました。
Bug 5843 – UI に (U+2026 ‘HORIZONTAL ELLIPSIS’) が使われると見た目が不自然

タブやツリーセルのラベルが横幅に入りきらなかった場合、今までは「Mozilla Firef…」のように末尾に「…」という文字(ピリオド3つ)が表示されていたが、 Firefox 3 alpha ではいつの間にか「・・・」という表示(三点リーダ)に変わっていた。
少なくとも Windows では「…」が標準であるはずなので非常に違和感を感じる。

と思ったら日本語フォント特有の問題っぽい。

左: Firefox 2.0.0.5
右: Firefox 3.0a8pre
Cropped Text

TOP

ダイアログから開いたモーダルダイアログが Linux ではモーダルでない

Linux で、ダイアログ(dialog 要素)から window.openDialog メソッドを使ってモーダルダイアログを開いた状態で、親のウィンドウへフォーカスして Esc キーを押下すると、親のウィンドウを閉じることができてしまう。
Prefwindow 要素の openSubDialog で開いたモーダルダイアログについても同様。
前提が崩れてやっかいなことになった。

テストケース:

<?xml version="1.0"?>

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

<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
        title="Linux Modal Dialog Test">

    <script type="application/x-javascript"><![CDATA[
    function showModalDialog() {
        window.openDialog(
            'chrome://browser/content/aboutDialog.xul',
            'About',
            'modal,centerscreen,chrome,resizable=no'
        );
        if (!document) {
            dump("*** Linux modal dialog problem");
            return;
        }
    }
    ]]></script>

    <button label="Show Modal Dialog" oncommand="showModalDialog();" />

</dialog>

環境: Ubuntu Linux 6.10 + Firefox 2.0.0.4

TOP

Custom Tree View でドラッグ&ドロップ時に固まる

Custom Tree Views を使って階層構造がない単純なデータを表示するツリーを作成し、ツリーアイテムのドラッグ&ドロップによる並び替えを実装しようとしたところ、ドロップ時に Firefox が固まる問題が発生。

原因は nsITreeView#getParentIndex で「return -1;」していなかったことであることに気付くまで、3時間くらい無駄にした。

CustomTreeView.prototype = {
    ...
    getParentIndex: function(rowIndex) { return -1; },
    ...
};

また、 nsDragAndDrop.js を使ってドラッグしたときの転送データを生成する際、データフレーバに「text/plain」とすると、なぜか実際に転送データの中身が文字化けしたようなデータになってしまうという問題にも悩まされた。これは、データフレーバを「text/unicode」にすることで解決した。

gDragAndDropObserver = {
    ...
    onDragStart: function(aEvent, aXferData, aDragAction) {
        aXferData.data = new TransferData();
        aXferData.data.addDataForFlavour("text/unicode", "Hello!");
        aDragAction.action = Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
    },
    ...
};

TOP

xul:popup#showPopup の位置ズレを直す

popup 要素に対して

showPopup(document.documentElement, event.screenX, event.screenY, "popup", null, null);

とかすると、マウスポインタを基点とした位置にポップアップが表示されるが、その後に右クリックのコンテキストメニューを表示させてからもう一度上記の showPopup を実行すると、ポップアップが表示される位置が大きくズレるという問題に悩まされた。

しかし、showPopup する前に、以下のようにしてやることで直った。

document.popupNode = null;

TOP