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

xul:textbox のプレースホルダー文字列

modest に投稿した記事と同内容です。

注意: この記事は Firefox 3.7 での新機能について触れています。

HTML のテキストボックス

Firefox 3.7a5pre では HTML 5 の仕様である placeholder 属性 が実装されており、以下のようにしてテキストボックスにヒント用文字列を表示させることができます。

<input type="text" placeholder="Your Name">

XUL のテキストボックス

一方、 XUL の textbox 要素では Firefox 3.0 にてすでに同様の機能が emptytext 属性として実装済みですが、 Firefox 3.7a5pre では placeholder 属性でもヒント用文字列を表示することができます。互換性維持のため emptytext 属性も引き続き利用可能ですので、 Firefox 3.6 と 3.7 両対応の拡張機能などでは emptytext 属性を使用したほうが良いでしょう。

<textbox emptytext="Your Name" />
<textbox placeholder="Your Name" />

また、以下のように xul:textbox 要素の emptyText (大文字小文字に注意)および placeholder プロパティを使って JavaScript で動的にヒント用文字列をセットすることも可能です。

document.getElementById(...).emptyText = "Your Name";
document.getElementById(...).placeholder = "Your Name";

TOP

一定時間ドラッグオーバーし続けたら処理を実行する

modest に投稿した記事と同内容です。

拡張機能(XULアプリ)にて、一定時間ドラッグオーバーし続けたときに何らかの処理を実行したい、例えばツールバーに配置したボタン上にブラウザタブを3秒間ドラッグオーバーし続けたら、そのボタンをクリックしたものとみなして処理を実行したいとします。

これは、HTML5のドラッグ&ドロップAPIを使い、ドラッグオーバーし続けた際に dragover イベントが繰り返し発生する特性を利用すると、以下のように実装可能です。

以下は、ボタン上に何かを3秒間ドラッグオーバーし続けると、テキストボックスに現在時刻を表示するサンプルです。なお、サンプルコード全量はこちらに置いてあります。 chrome 権限は不要ですので、ダウンロードして拡張子を.xulにしてFirefoxで開けば、動作確認可能です。

XUL:

<button label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragover="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '\n';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {

    _dragStartTime: null,

    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter":
                // ドラッグオーバー開始時、ドラッグオーバー開始時刻をセット
                this._dragStartTime = Date.now();
                break;
            case "dragover":
                // ドラッグオーバー中、ドラッグオーバー開始時刻からの経過時間を調べる
                if (this._dragStartTime && Date.now() - this._dragStartTime > 3000) {
                    // 3秒以上経過したら、ドラッグ開始時刻をリセットし、処理を実行する
                    this._dragStartTime = null;
                    event.target.doCommand();
                }
                break;
        }
    }

};

タイマーを用いた実装方式

ドラッグオーバー開始時(dragenter イベント発生時)に setTimeout で一定時間後に処理を実行するためのタイマーを設定し、ドラッグオーバー終了時(dragleave イベント発生時)に clearTimeout でタイマーを解除する、という実装方式ももちろん可能です。

XUL:

<button id="myButton"
        label="Drag something over here for 3 seconds."
        ondragenter="MyExtension.handleDragEvent(event);"
        ondragleave="MyExtension.handleDragEvent(event);"
        oncommand="this.nextSibling.value += new Date() + '\n';" />
<textbox multiline="true" flex="1" />

JavaScript:

var MyExtension = {
    _dragOverTimer: null,
    handleDragEvent: function(event) {
        event.preventDefault();
        switch (event.type) {
            case "dragenter":
                // dragenterイベントが二回連続で発生した場合への対策
                if (this._dragOverTimer)
                    return;
                // ドラッグオーバー開始時にタイマーを設定
                this._dragOverTimer = setTimeout(function() {
                    document.getElementById("myButton").doCommand();
                }, 3000);
                break;
            case "dragleave":
                // ドラッグオーバー終了時にタイマーを解除
                clearTimeout(this._dragOverTimer);
                this._dragOverTimer = null;
                break;
        }
    }
};

TOP

Firefox 3.7 でのナビゲーションツールバーのアイコン画像サイズ

modest に投稿した記事と同内容です。

注意:このトピックは Firefox 3.7 での仕様変更について触れています。また、 Windows 版のデフォルトテーマを前提としており、他のOSについては未確認です。他のOSについての情報求みます。

Firefox 3.7 では、ナビゲーションツールバーに配置するボタン(戻る・進む・更新・ホームなど)のアイコン画像サイズが下表のように変わるようです。なお、下表の「小さいアイコン」とは、「ツールバーのカスタマイズ」で「小さいアイコンを使用」オプションを有効にしている場合、あるいはブックマークツールバー上にボタンを配置した場合のアイコンを意味します。

Firefox 3.6 Firefox 3.7
通常アイコン 24×24ピクセル 18×18ピクセル
小さいアイコン 16×16ピクセル 18×18ピクセル

拡張機能にてナビゲーションツールバーにボタンを追加している場合、この仕様変更の影響を受けるようです。ここでは、例として、拡張機能にて下記のような XUL オーバーレイによってナビゲーションツールバーにボタンを追加するとします。

<toolbarpalette id="BrowserToolbarPalette">
  <toolbarbutton id="myaddon-button"
                   class="toolbarbutton-1 chromeclass-toolbar-additional"
                   label="My Addon" />
</toolbarpalette>

Firefox 3.6 までは、一般的には以下のようなスタイルシートによって通常アイコンと小さいアイコンのスタイルを別々に定義します(参考)。なお、「largeicon.png」は24×24ピクセルの画像、「smallicon.png」は16×16ピクセルの画像とします。

/* 通常アイコン */
#myaddon-button {
    list-style-image: url("chrome://myaddon/skin/largeicon.png");
}

/* 小さいアイコン */
toolbar[iconsize="small"] #myaddon-button {
    list-style-image: url("chrome://myaddon/skin/smallicon.png");
}

このとき、 Firefox 3.6 では通常アイコン・小さいアイコンともに本来の画像サイズできれいに表示されますが、 Firefox 3.7 では通常アイコンは「largeicon.png」を本来のサイズである24×24ピクセルから18×18ピクセルへと縮小され、小さいアイコンは「smallicon.png」を本来のサイズである16×16ピクセルを18×18ピクセルへ拡大されます。下表のように、小さいアイコンの表示がきれいでなくなる傾向があります。

Firefox 3.6 Firefox 3.7
通常アイコン
小さいアイコン

Firefox 3.7 でも本来の画像サイズでアイコンをきれいに表示したい場合、いくつかの方法があるかと思いますが、ここでは Firefox 3.7 以上専用のスタイルシートを別途追加する方式を解説します。

Firefox が特定のバージョンの場合に限り、指定した XUL に対してスタイルシートを適用したい場合、以下のようにクロムマニフェストの「style」命令へ「appversion」フラグをセットします。なお、ツールバーボタン用のスタイルシートは、「browser.xul」(ブラウザウィンドウ)と「customizeToolbar.xul」(ツールバーのカスタマイズウィンドウ)の両方に適用させます。

# apply stylesheet if Firefox 3.7a or later
style  chrome://browser/content/browser.xul  chrome://myaddon/skin/fx37.css  appversion>=3.7a
style  chrome://global/content/customizeToolbar.xul  chrome://myaddon/skin/fx37.css  appversion>=3.7a

拡張機能の skin パッケージに含めるFirefox 3.7 以上専用のスタイルシート「fx37.css」 には以下のような内容を記述します。 xul:toolbarbutton 要素自体に画像を設定するのではなく、内部の匿名 xul:image 要素に対して画像およびサイズを設定します。

/* 通常アイコン・小さいアイコン共通 */
#myaddon-button > .toolbarbutton-icon {
    list-style-image: url("chrome://myaddon/skin/smallicon.png");
    width: 16px;
    height: 16px;
}

これにより、以下のように通常アイコン・小さいアイコンともに「smallicon.png」が本来の16×16ピクセルできれいに表示されます。

Firefox 3.7
通常アイコン
小さいアイコン

Firefox 標準のツールバーボタンと同じ18×18ピクセルの画像を Firefox 3.7 以降用のアイコン画像として別途作成し、上記「fx37.css」にて適用するのもよいかもしれません。

TOP

タブ切り替えパネル風の半透明ポップアップ

modest に投稿した記事と同内容です。

Tab Flick 拡張機能 のポップアップは、 Firefox 3.6 以降の Ctrl+Tab によるタブ切り替えパネルのような半透明の見た目となっています。

ここでは、そこに至るまでの実装の経緯を記しました。なお、簡単のためコードの一部は実際とは異なるものとなっています。

第1段階

まず、 browser.xul にオーバーレイし、 #mainPopupSet をマージポイントとして新しい xul:panel 要素を追加します。

<popupset id="mainPopupSet">
    <panel id="tabFlickPanel" style="width: 200px; height: 200px;" />
</popupset>

openPopup あるいは openPopupAtScreen メソッドでこの xul:panel 要素を開くと、当然見た目はシンプルなポップアップとなります。

第2段階

次に、 xul:panel 要素に対して KUI-panel クラスを指定します。 KUI-panel クラスは browser.xul にて読み込まれているスタイルシート (browser.css) にて定義されています。したがって、 browser.xul へオーバーレイした XUL 内であれば、特にスタイルを定義することなく利用可能となります。

    <panel id="tabFlickPanel" class="KUI-panel" style="width: 200px; height: 200px;" />

これで Windows XP などでの Ctrl+Tab によるタブ切り替えパネルと同じ、黒い半透明の角丸ポップアップとなります。
KUI-panel クラスのスタイルは Firefox 3.5 に同梱されたスタイルシート (browser.css) でも定義済みですので、 Firefox 3.5 でも有効となります。

第3段階

さらに、 xul:panel 要素へ以下のような内容のスタイルシートを適用します。

#tabFlickPanel:-moz-system-metric(windows-compositor) {
    background: transparent;
    -moz-appearance: -moz-win-glass;
    -moz-border-radius: 0;
    border: none;
}

すると、 Windows Vista または Windows 7 で Windows Aero が有効な場合、Aero Glass 効果のある半透明のポップアップとなります。

第4段階

しかし、ここでひとつ問題が生じます。詳しい理由はわかりませんが、ポップアップの右下角に余計な枠線が表示されてしまいます。
これを解決するには、なぜか xul:panel 要素の collapsed を以下のようにして切り替えてあげる必要があります。

    <panel id="tabFlickPanel" class="KUI-panel" style="width: 200px; height: 200px;"
          collapsed="true"
          onpopupshown="this.collapsed = false;"
          onpopuphiding="this.collapsed = true;" />

これでようやく解決しました。

この方法は裏技的なものですので、拡張機能などでご利用の際はご注意ください。

TOP

ツリーのブックマーク風アイコン表示

xul:treecell 要素へ properties="Name" という属性を追加する、あるいはカスタムツリービューの基本的な使い方(その9~階層構造 - 表示)にあるように nsITreeView::getCellProperties でツリーセルのプロパティへ "Name" という nsIAtom を追加することで、そのツリーセルにブックマーク風のアイコン(フォルダやページ)が表示されるようになる。

というのは Firefox 3.0 までの話で、 Firefox 3.1 以上では Bug 464916 – Remove non-global rules from global tree stylesheet の影響で "Name" プロパティによるツリーのアイコン表示は不可となった。 Firefox 3.1 以上では、「chrome://browser/skin/places/places.css」を読み込ませ、 "Name" の代わりに "title" というプロパティを追加することでブックマーク風アイコン表示が可能となる。

TOP

nsITreeBoxObject::clearStyleAndImageCaches

ツリーのスタイル付け - MDC の方法によってツリーアイテムのアイコン画像を一時的にアニメーションPNGやアニメーションGIFにすると、アイコンを元に戻した後もCPU使用率が上昇したままとなる問題があった。

アイコンを元に戻した後に nsITreeBoxObject::clearStyleAndImageCaches を呼び出すことで、ツリーのアイコン画像のキャッシュが消去され、この問題が解消された。

nsITreeBoxObject.idl

TOP

nsIPopupBoxObject::setConsumeRollupEvent (2)

前の記事に書いた時点では、 Firefox 3 で新たに追加された nsIPopupBoxObject::setConsumeRollupEvent メソッドは Windows でのみ動作し、 Linux と Mac OS X では効果が無かった。

Linuxのその後の状況

1月11日にLinuxで動作しない件についてバグを立てた。
Bug 411903 – nsIPopupBoxObject#setConsumeRollupEvent has no effect on Linux
1月20日頃、以下のバグへのパッチ適用に伴い、Linuxでもめでたく正常動作するようになった。
Bug 188126 – gtk2: autocomplete/popup widgets should not block clicks outside of themselves

Mac OS Xのその後の状況

2月3日にMac OS Xで動作しない件についてバグを立てた。有識者による確認待ち。
Bug 415440 – nsIPopupBoxObject#setConsumeRollupEvent has no effect on Mac OS X
Neal Deakin 氏によると、 Mac OS X では単純にまだ実装されていないだけのようだ。

TOP

nsIPopupBoxObject::setConsumeRollupEvent

ポップアップを開いた状態でポップアップの外側のどこかをクリックしたときの挙動は、ウィジェットやプラットフォームに依存する。わかりやすい例として、コンテントエリア内で右クリックメニューを開き、そのままページ内のどこかのハイパーリンクをクリックすると、

プラットフォーム 挙動
Windows ポップアップが閉じ、リンク先へ遷移する
Linux ポップアップが閉じるだけ
Mac ポップアップが閉じるだけ

これに対して、検索バーの検索エンジンのドロップダウンリストを開いた状態で、コンテントエリア内のハイパーリンクをクリックしたときの挙動は、プラットフォームによらずドロップダウンリストが閉じるだけで、リンク先へは遷移しない。

このように、ポップアップの外側をクリックしたときにポップアップを自動で閉じる(ロールアップする)だけで、実際にクリックした何かに対するクリックイベントが発生しないような挙動のことを、「クリックの横取り」と呼ぶ。クリック横取りに関する動作仕様の詳細は、 nsMenuPopupFrame::ConsumeOutsideClicks のソース内に記載されており、通常は以下のルールに従った挙動となる。

ウィジェットの種類 ポップアップの親要素 挙動
メニュー <menu> か <popupset> Windows: クリックの横取りなし
Linux / Mac: クリックの横取りあり
オートコンプリート <textbox type="autocomplete"> プラットフォームによらず、クリックの横取りなし
コンボボックス <menulist> プラットフォームによらず、クリックの横取りあり

Firefox 3 では新たに追加された nsIPopupBoxObject::setConsumeRollupEvent メソッドを使って、クリックの横取りの動作仕様を無理やり変更できるようになった。このメソッドの引数は3種類あり、0(デフォルト、ウィジェット・プラットフォーム依存)、1(クリックの横取りを強制的にありにする)、2(クリックの横取りを強制的になしにする)の3種類が指定可能である。実際に試すために以下の手順を試す。

  1. テストケースを開く
  2. [ROLLUP_DEFAULT] ボタンをクリックしてポップアップを開く
  3. ポップアップを開いたまま [TEST] ボタンをクリックする
  4. [ROLLUP_CONSUME] ボタンについても同様に手順2~3を繰り返す
  5. [ROLLUP_CONSUME] ボタンについても同様に手順2~3を繰り返す

すると、 Windows + Firefox 3.0b3pre では期待通り以下の結果が得られたものの、 Linux / Mac + Firefox 3.0b3pre では期待に反した結果となった。

最初に押下するボタン プラットフォーム [TEST] ボタン押下後の挙動
[ROLLUP_DEFAULT] Windows ポップアップが閉じるだけ
Linux ポップアップが閉じるだけ
Mac ポップアップが閉じるだけ
[ROLLUP_CONSUME] Windows ポップアップが閉じるだけ
Linux ポップアップが閉じるだけ
Mac ポップアップが閉じるだけ
[ROLLUP_NO_CONSUME] Windows ポップアップが閉じてダイアログが表示
Linux ポップアップが閉じるだけ
Mac ポップアップが閉じるだけ

どうやら Linux / Mac では nsIPopupBoxObject#setConsumeRollupEvent の引数に ROLLUP_NO_CONSUME を渡したときの効果が何も無いようなので、 Bugzilla へバグを立ててみた。ここまで自分が述べたことに確信を持っているわけではないので、有識者の判断を待つこととする。

Bug 411903 – nsIPopupBoxObject#setConsumeRollupEvent has no effect on Linux

関連記事

Firefox 3 でのポップアップ仕様変更
nsIPopupBoxObject::enableRollup
nsIPopupBoxObject::enableKeyboardNavigator

TOP

カスタムツリービューの基本的な使い方(その10~階層構造 - フォルダ開閉)

その9~階層構造 - 表示」で作成したツリーは表示のみであったが、今回フォルダの開閉機能を実装する。

フォルダの開閉機能

フォルダの行をダブルクリックしたり、フォルダ上でEnterキーを押下したりすると、 nsITreeView#toggleOpenState メソッドが呼び出される。
toggleOpenState では、 _visibleData の中の引数 index に対応するアイテムの open プロパティを変更し、 _buildVisibleData を使って _visibleData を再構築する。
さらに、フォルダ開閉に伴い行数に変化が生じたため、 nsITreeBoxObject#rowCountChanged を呼び出す必要がある。
rowCountChanged の第1引数は変化が生じた最初の行番号、第2引数は行数の増減値である。
例えば0行目の「Red」フォルダを閉じると、そのフォルダのすぐ下の4行が消滅するため、 rowCountChanged(1, -4) となる。
これでめでたく完了、と思いきやフォルダ上でEnterキーを押した場合にフォルダの開閉状態を示す +/- 記号に変化が無いという問題があった。
そこで、 nsITreeBoxObject#invalidateRow によってその行だけを再描画する必要がある。

    toggleOpenState: function(index) {
        var lastRowCount = this.rowCount;
        // change |open| property
        this._visibleData[index].open = !this._visibleData[index].open;
        this._buildVisibleData();
        this._treeBoxObject.rowCountChanged(index + 1, this.rowCount - lastRowCount);
        // need this to update the -/+ sign when called by pressing enter key
        this._treeBoxObject.invalidateRow(index);
    },

例えば「Yellow」フォルダをダブルクリックして開いたとすると、再構築された _visibleData は下表に示すような配列となる。
item#2 の水色で着色した箇所が、フォルダを開いた際に変更された open プロパティである。
また、緑色で着色した item#9 と item#C が、フォルダを開いたことによって新たに追加されたアイテムである。

id type name parent open empty level hasNext parentIndex
[0] item#1 2 Red root true false 0 true -1
[1] item#5 1 Apple item#1 1 true 0
[2] item#6 1 Cherry item#1 1 true 0
[3] item#7 3 item#1 1 true 0
[4] item#8 1 Peach item#1 1 false 0
[5] item#2 2 Yellow root true false 0 true -1
[6] item#9 2 Citrus item#2 false false 1 true 5
[7] item#C 1 Banana item#2 1 false 5
[8] item#3 3 root 0 true -1
[9] item#4 2 Blue root false true 0 false -1

応用例~シングルクリックでのフォルダの開閉~

上記で実装したように、通常フォルダはダブルクリック時にフォルダの開閉が可能だが、ブックマークツリーのようにシングルクリックでもフォルダも開閉を可能にする。
まず、 fruits.xul の tree または treechildren 要素へ onclick 属性を追加する。

    <tree id="fruitsTree" flex="1" onclick="handleClick(event);">

先ほど onclick 属性で追加したイベントハンドラである handleTreeClick 関数を実装する。
その際、クリックした位置のアイテムを取得するために nsITreeBoxObject#getCellAt を使ってヒットテストを行う。
nsITreeBoxObjcet#getCellAt メソッドは、第1引数、第2引数で指定した座標にセルがあるかを判定し、セルがある場合は第3引数、第4引数、第5引数に引き渡したオブジェクトの value プロパティにそれぞれ行番号、列を表す nsITreeColumn オブジェクト、セル内の部位を表す文字列("", "cell", "text", "image", "twisty" のうちのいずれか)がセットされる。
今回は第1引数、第2引数にはクリックした時のマウスポインタ位置を渡して、返ってきた第3引数、第5引数の value プロパティを調べ、ツリーカラムやツリー内の余白部分などの非セル部分をクリックした場合 (row.value == -1)、フォルダ左端の+/-記号をクリックした場合 (obj.value == twisty") を除外する。クリックした位置がツリーのセルであり、なおかつその行がフォルダである場合のみ、 nsITreeView#toggleOpenState でフォルダの開閉を行う。

////////////////////////////////////////////////////////////////
// Event Handlers

function handleClick(event) {
    if (event.button != 0)
        return;
    // hit test
    var row = {}, obj = {};
    gFruitsTreeView._treeBoxObject.getCellAt(event.clientX, event.clientY, row, {}, obj);
    if (row.value == -1 || obj.value == "twisty")
        return;
    if (gFruitsTreeView.isContainer(row.value))
        gFruitsTreeView.toggleOpenState(row.value);
}

関連記事

TOP

カスタムツリービューの基本的な使い方(その9~階層構造 - 表示)

これまで(その1~その8)は階層構造が無いフラットなツリーを取り扱ってきたが、その9ではいよいよ階層構造を有するツリーの表示を行う。
階層構造を有するツリーは、データの構造次第で実装方式も大きく変わってくるため、データの構造について熟考する必要がある。
今回のサンプルで用いるデータ構造はあくまでも一例に過ぎない。
ベースとなるソースコードはその1~表示を参照。

3種類のアイテム形式

今回ツリー上に表示するすべてのアイテムは、通常のアイテム、フォルダ、セパレータのうちのいずれかの形式となる。
これらを定数として定義しておく。これらの値は、後述の FruitItem オブジェクトの type プロパティとして使用する。

const TYPE_LEAF = 1;
const TYPE_FOLDER = 2;
const TYPE_SEPARATOR = 3;

FruitItem クラス

データに格納する個々のアイテムを表すクラスとして、下表のようなプロパティを有する FruitItem クラスを定義する。
id, type, name, parent, open プロパティはコンストラクタの引数からセットされ、 empty, level, hasNext, parentIndex プロパティは後述する FruitsTreeView#_buildVisibleData の処理内で計算してセットされる。
open, empty の2つのプロパティはフォルダ(つまり type が 2)の場合のみ使用する。

プロパティ 概要
id string 個々の FruitItem オブジェクトを一意に識別するためのID。
type number 前述の3つのタイプのうちのいずれか。
name string ツリー上で表示する文字列。
parent string 親フォルダのID。
open boolean nsITreeView#isContainerOpen 用。フォルダの開閉状態を表す。
empty boolean nsITreeView#isContainerEmpty 用。フォルダが空かどうかを表す。
level number nsITreeView#getLevel 用。ツリー上での深さ(インデントレベル)。
hasNext boolean nsITreeView#hasNextSibling 用。フォルダ内の最下部かどうかを表す。
parentIndex number nsITreeView#getParentIndex 用。親フォルダのツリー上での行番号。
/**
 * FruitItem ctor
 */
function FruitItem(aID, aType, aName, aParent, aOpen) {
    this.id     = aID;
    this.type   = aType;
    this.name   = aName;
    this.parent = aParent;
    this.open   = aOpen;
    // following four properties will be set later 
    // in the process of FruitsTreeView#_buildVisibleData
    this.empty       = null;
    this.level       = null;
    this.hasNext     = null;
    this.parentIndex = null;
}

データ

今回用いる元データは、 FruitItem オブジェクトの配列である。この配列内でのアイテムの順序はツリー上で表示されるべき順序と必ずしも一致する必要は無い。しかし、親フォルダ(parent プロパティ)が同一のアイテムの順序は実際のツリー上での表示に一致している必要がある。
つまり、今回のデータでは "root" フォルダを親とするアイテムが4つ存在するが、これらは配列内の位置が若い順番でツリー表示されることになる。

    // array of FruitItem objects
    var data = [
        new FruitItem("item#A", TYPE_FOLDER   , "Red"       , "root"  , true),
        new FruitItem("item#B", TYPE_LEAF     , "Apple"     , "item#A", null),
        new FruitItem("item#C", TYPE_LEAF     , "Cherry"    , "item#A", null),
        new FruitItem("item#D", TYPE_SEPARATOR, ""          , "item#A", null),
        new FruitItem("item#E", TYPE_LEAF     , "Peach"     , "item#A", null),
        new FruitItem("item#F", TYPE_FOLDER   , "Yellow"    , "root"  , false),
        new FruitItem("item#G", TYPE_FOLDER   , "Citrus"    , "item#F", false),
        new FruitItem("item#H", TYPE_LEAF     , "Lemon"     , "item#G", null),
        new FruitItem("item#I", TYPE_LEAF     , "Grapefruit", "item#G", null),
        new FruitItem("item#J", TYPE_LEAF     , "Banana"    , "item#F", null),
        new FruitItem("item#K", TYPE_SEPARATOR, ""          , "root"  , null),
        new FruitItem("item#L", TYPE_FOLDER   , "Blue"      , "root"  , false),
    ];

上記の元データを引数として new FruitsTreeView(data) した時点で、 gFruitsTreeView._data は下表に示すような FruitItem オブジェクトの配列となる。

id type name parent open empty level hasNext parentIndex
[0] item#A 2 Red root true
[1] item#B 1 Apple item#A
[2] item#C 1 Cherry item#A
[3] item#D 3 item#A
[4] item#E 1 Peach item#A
[5] item#F 2 Yellow root false
[6] item#G 2 Citrus item#F false
[7] item#H 1 Lemon item#G
[8] item#I 1 Grapefruit item#G
[9] item#J 1 Banana item#F
[10] item#K 3 root
[11] item#L 2 Blue root false

ツリー表示用データ _visibleData

階層構造の無いツリーでは、並び替えを行う場合を除けば、 _data として内部的に保持する配列データがすなわちツリーに表示させるデータであった。
今回の階層構造を有するツリーの場合、すべてのフォルダが開いている状態という前提であれば、 _data の配列のインデックス=ツリー上での行番号という等式が成り立つため、 _data をそのままツリー表示用データとして使用することが可能となる。
しかし、階層構造を有するツリーの場合、フォルダの開閉に伴い一部のデータがツリー上で表示されない状態も考慮しなければならない。
そこで、元データ _data とは別に、実際にツリー上で表示するアイテムだけの配列 _visibleData も内部的に保持することにする。
フォルダの開閉状態が変化するたびに _data から _visibleData を構築して、ツリー表示用データとして使用するのである。
このように _visibleData を生成する処理を FruitsTreeView クラスの _buildVisibleData メソッドとして実装する。
また、 _buildVisibleData から呼び出される副次的なメンバとして以下のプロパティやメソッドを実装する。

メンバ名 概要
_getChildItems 引数 aParent で指定したidのフォルダを親とする FruitItem オブジェクトの配列を返す。
_processChildItems FruitItem オブジェクトの配列の個々の要素に対して level, hasNext, parentIndex, open, empty の各プロパティを計算して付与しながら _visibleData を構築する。サブフォルダが存在する場合、サブフォルダ内の孫アイテムに対して再帰的に処理する。
_currentLevel _processChildItems で現在処理中のアイテムのレベルを保持する。
_parentIndex _processChildItems で現在処理中のアイテムの親フォルダの行番号を保持する。

_buildVisibleData メソッドでは、 _data 内の全オブジェクトのうち、 parent が "root" のオブジェクトを _getChildItems を使って取得し、それらについて _processChildItems で処理する。

    ////////////////////////////////////////////////////////////////
    // visible data builder

    _visibleData: [],

    _currentLevel: 0,

    _parentIndex: -1,

    _buildVisibleData: function() {
        this._visibleData = [];
        this._currentLevel = 0;
        this._parentIndex = -1;
        // process for each child of the root folder
        var childItems = this._getChildItems("root");
        this._processChildItems(childItems);
    },

    _getChildItems: function(aParent) {
        return this._data.filter(function(elt) {
            return (elt.parent == aParent);
        });
    },

    _processChildItems: function(aChildItems) {
        // process for each child
        for (var i = 0; i < aChildItems.length; i++) {
            var child = aChildItems[i];
            // compute and set |level|, |hasNext| and |parentIndex| properties
            child.level = this._currentLevel;
            child.hasNext = i < aChildItems.length - 1;
            child.parentIndex = this._parentIndex;
            var grandChildItems = null;
            // if child is a folder, compute and set |empty| properties
            if (child.type == TYPE_FOLDER) {
                grandChildItems = this._getChildItems(child.id);
                child.empty = grandChildItems.length == 0;
            }
            this._visibleData.push(child);
            // if child is an open folder, process grandchildren recursive
            if (child.type == TYPE_FOLDER && child.open) {
                var parentIndexBak = this._parentIndex;
                this._parentIndex = this._visibleData.length - 1;
                this._currentLevel++;
                this._processChildItems(grandChildItems);
                this._currentLevel--;
                this._parentIndex = parentIndexBak;
            }
        }
    },

_buildVisibleData メソッドを使って初回の _visibleData 構築を行うと、下表に示すような FruitItem オブジェクトの配列が生成される。
ピンク色で着色した部分は、 _buildVisibleData メソッドの処理によって計算され、新たに付与されたプロパティである。

今後ツリーの表示に変化が生じる何かが発生したら(例えばフォルダの開閉)、 _buildVisibleData メソッドを使って各プロパティの再計算と _visibleData の再構築を行うことになる。

id type name parent open empty level hasNext parentIndex
[0] item#A 2 Red root true false 0 true -1
[1] item#B 1 Apple item#A 1 true 0
[2] item#C 1 Cherry item#A 1 true 0
[3] item#D 3 item#A 1 true 0
[4] item#E 1 Peach item#A 1 false 0
[5] item#F 2 Yellow root false false 0 true -1
[6] item#K 3 root 0 true -1
[7] item#L 2 Blue root false true 0 false -1

nsITreeView インタフェースの実装

次に、 nsITreeView インタフェース各メンバの実装を行う。
rowCount プロパティは当然 _data ではなく _visibleData 配列の長さを返す。

    get rowCount() {
        return this._visibleData.length;
    },

今回初登場のメソッドの概要を下表に示す。

メソッド名 概要
isContainer 行番号 index の行がフォルダかどうかを返す。
isContainerOpen 行番号 index の行のフォルダの開閉状態を返す。
ある行について isContainer が true かつ isContainerEmpty が false の時、
(1) isContainerOpen が falseを返せばツリー上の左端に 記号が表示される
(2) isContainerOpen が true を返せばツリー上の左端に 記号が表示される
isContainerEmpty 行番号 index の行のフォルダ内に中身があるかどうかを返す。
getParentIndex 行番号 index の行の親となるフォルダの行番号を返す。
親が存在しないレベル0のアイテムについては-1を返すようにする。
このメソッドが正しい値を返さないと、レベル2より深いフォルダの罫線が正しく描画されない。
hasNextSibling 行番号 index の行がフォルダ内の最下部のアイテムかどうかを返す。
(1) true を返せばツリーの罫線が ├ で表示される
(2) falseを返せばツリーの罫線が └ で表示される
getLevel 引数 index の行のインデントレベルを返す。
最上位に位置するアイテムはレベル0、レベル0のフォルダ直下のアイテムはレベル1…となる。
toggleOpenState 行番号 index の行のフォルダの開閉状態を変更しようとしたときに呼び出される。
具体的にはセルをダブルクリックした時、左端の +/- 記号をクリックした時。
あるいはフォルダを選択して Enter キーや ←/→ キーを押下した時。

これら6つのメソッドを含む下記8メソッドは、いずれも _visibleData 内の対応するオブジェクトのプロパティを調べて返すだけで済む。
これらのメソッドはツリー表示時に繰り返し呼び出されるため、このように _buildVisibleData であらかじめ計算しておいてプロパティを返すだけにしておけば、パフォーマンス向上につながる。

    isContainer: function(index) {
        return this._visibleData[index].type == TYPE_FOLDER;
    },
    isContainerOpen: function(index) {
        return this._visibleData[index].open;
    },
    isContainerEmpty: function(index) {
        return this._visibleData[index].empty;
    },
    isSeparator: function(index) {
        return this._visibleData[index].type == TYPE_SEPARATOR;
    },
    getParentIndex: function(rowIndex) {
        return this._visibleData[rowIndex].parentIndex;
    },
    hasNextSibling: function(rowIndex, afterIndex) {
        return this._visibleData[rowIndex].hasNext;
    },
    getLevel: function(index) {
        return this._visibleData[index].level;
    },
    getCellText: function(row, col) {
        switch (col.index) {
            case 0: return this._visibleData[row].name;
        }
    },

setTree はこれまで通りの nsITreeBoxObject を保持する処理に加えて、 _buildVisibleData で初回の _visibleData を生成する。

    setTree: function(tree) {
        this._treeBoxObject = tree;
        this._buildVisibleData();
    },

toggleOpenState の実装は「その10~階層構造 - フォルダ開閉」の記事で別途行う予定である。したがって、現段階ではツリーは表示のみで、フォルダをダブルクリックしても何も起こらない。

    toggleOpenState: function(index) {
        alert("Not implemented yet.");
    },

アイコン表示

以上の階層構造を有するツリー表示では、せいぜいフォルダが+/-記号で表示されるくらいの簡素な見た目だが、実際のブックマークツリーなどではフォルダ型のアイコンなどが表示されている。
このようなアイコン表示を実現するためには、ブックマークツリー用のスタイルシートを読み込ませ、 nsITreeView#getCellProperties メソッドで下記のようにセルのプロパティへ "title" という値を追加する必要がある。これは xul:treecell 要素へ properties="title" 属性を追加することと等価で、 treechildren::-moz-tree-image(title) などの規則で定義されたスタイルがセルに対して適用されるようになる。

TOP