Now browsing the SCRAPBLOG weblog archives.
FUEL 2.0
FUEL – MDC からリンクされている全オブジェクトのページの和訳が完了した。
以下、 FUEL 2.0 で追加された各オブジェクトについての雑感。
Window
ブラウザウィンドウの新しいタブでURIを開く。
タブを開く・閉じる・移動する・選択するイベントを監視する。
ブラウザウィンドウを開いた直後(つまり browser.xul にて window の load イベント発生時)に、イベントリスナを追加するために、 Application.activeWindow.events.addListener… などとやりたいところだが、アクティブなウィンドウ=今開いたウィンドウとは限らない?つまり、バックグラウンドでウィンドウを開くようなケースもありうるかも?
BrowserTab
タブでURIを読み込む、タブを移動する、タブを選択する、タブの内容ドキュメントを取得する。
タブへの読み込みイベントを監視する。
Window#open と BrowserTab#load は使用頻度高そうだが、引数はどちらも nsIURI オブジェクトってのは痛い。 FUEL の目的は XPCOM を意識しない形へとコードを簡略化することじゃなかったっけ?
BookmarkFolder
Bookmark
ブックマーク・区切り・フォルダの追加、削除、各プロパティの取得と変更。機能的にはそれだけ。
BookmarkFolder#addSeparator の引数にタイトルを指定できないのはなぜ?と思ったら、 Places では区切りにタイトルを付けられなくなっているようだ。
Annotations
Places:Annotation Service 自体は拡張機能開発者的にはかなり期待できそうなシステムだけど、 FUEL の Annotations オブジェクトによってどういうメリットがもたらされるかは現時点では不明。
BabelZilla を使ったローカライズ管理
先のエントリでもちょこっと触れたように、BabelZilla を使うことで拡張機能のローカライズ関連のメンテナンスが楽になる。
BabelZilla へ拡張機能を登録すると、その拡張機能専用のフォーラムが作成されるので、翻訳者とのやりとりを一極化できる。また、 WTS (Web Translation System) と呼ばれるシステムによって、ローカライズファイルのアップロードやダウンロード、さらには編集までも Webベースで行うことが可能になる。
自分も2年前から少しずつ BabelZilla を使い始め、今では完全に BabelZilla へ移行した。たとえ翻訳者がEメールで直接 locale パッケージをよこしても、 BabelZilla 経由しか受け付けておりませんとお断りし、 BabelZilla へのユーザ登録を促すようにしている。
WTS を使う大きなメリットとして、「Download all locale files (missing string: replaced)」というリンクから未翻訳のエンティティ・プロパティをすべて英語に置換した形ですべての locale パッケージを圧縮してダウンロードできることが挙げられる。正式リリース版の XPI ファイルを作成する際に、この置換済み locale パッケージをダウンロードして自分の拡張機能のソースコードに含めてやればよいわけだ。
しかし、後日拡張機能のバージョンアップを実施し、新たに翻訳可能なエンティティ・プロパティを追加し、 XPI を作って BabelZilla へアップロードすると、英語に置換された部分についてはすでに翻訳済みであると認識されてしまう。これは BabelZilla の仕様であり、未翻訳なエンティティ・プロパティについてはその行自体を無い状態にして XPI を作成してアップロードしなければならない。 <!ENTITY hello ""> のように値を空欄にした場合も翻訳済みと認識される。
この問題を解決するために、自分はどうやっているかというと、翻訳が完全に済んだ locale パッケージのみを自分のソースコードへ取り込んでバージョン管理するようにし、正式リリース版 XPI を作成するときだけ一時的に locale フォルダ以下全てを BabelZilla からダウンロードした英語に置換済みのファイルへと置き換えるようにしている。
翻訳者全員の仕事が速くて、正式リリース版 XPI を作る際にすべてのロケールの翻訳が100%完了している、という前提であれば、上記のように開発版の locale フォルダと正式リリース用の locale フォルダを別にする必要は無いが、現実はそうもいかない。仕方ないけど、もう少しうまく管理できる方法はないものか、とも思っている。
ひとつ思いついたのは、 WTS が英語に置換した未翻訳なエンティティの行にコメントで目印をつけることで、次回アップロード時に WTS が未翻訳だと認識してくれるような仕組みである。
<!ENTITY hello "Hello."><!-- untranslated -->
でも、同じように properties ファイルの各プロパティの行へコメント入れるのは無理っぽいな。
HELLO=Hello # untranslated
なんてやってもコメントと認識されるわけないし、せいぜい
HELLO=Hello.
#untranslated HELLO
みたいに別の行コメントで未翻訳キーを示すしかないか。
拡張機能バージョンアップ時の不完全ローカライズの取り扱い
翻訳者ではなく、拡張機能開発者としての疑問。
ある言語の翻訳者がローカライズ (xx-XX の locale パッケージ) を提供してくれたはいいが、その後、拡張機能がバージョンアップして翻訳すべき箇所が追加されても、継続的にローカライズをメンテしてくれないことが多々ある。
そういう場合、どうするか?
案1 不完全なローカライズは含めない
バージョンアップで翻訳可能な箇所を追加するたびに locale パッケージを en-US と ja-JP だけに削減してリリースし、完全なローカライズが提供され次第、それを含めて再リリースする。
ついてこれるやつだけついて来い、という感じの冷たいやり方。
確か Firefox 1.0 の頃はバージョンアップに伴い locale パッケージを減らすと変なエラーが出て、この方法は無理だったような気がする。
案2 未翻訳の部分は英語で置き換えておく
拡張機能のバージョンアップに伴い、DTDファイルに新しいエンティティを追加したとすると、その拡張機能に含まれる日本語以外の全 locale パッケージに対して英語のエンティティを追加する。
おそらくこれが一般的ではないだろうか。 Piro さんもやはりこの方式であるそうだ。
しかし、手作業でやると面倒であり、ミスも発生しやすい。英語のエンティティを追加し忘れると、ブラウザウィンドウ下部に黄色いエラーメッセージが表示されたりして被害が大きい。
BabelZilla を使っていると、この面倒くささを解消できる。ただ…
また、正式リリース前に英語で置き換えられたバージョンを翻訳者に送付し、ローカライズをお願いするというのもいいかもしれない。
案3 案1+案2の折衷案
とりあえず未翻訳の部分は英語で置き換えてリリースし、その後あまりにも未翻訳な部分(英語で置き換えられた部分)が多くなりすぎたら、その locale パッケージを削除する。
ユーザによっては少しでも自分の国の言語に翻訳されている方がありがたいと思うかもしれないので、どの程度未翻訳な部分が増えたら locale パッケージを捨てるか、難しいところ。
WinMerge 7-Zip Plugin (for 7-Zip 4.57)
7-Zip を 4.56 から 4.57 へアップデートしたら、 WinMerge の 7-Zip Plugin が使用できなくなった。
応急処置としてプラグインのDLL 「Merge7z456.dll」「Merge7z456U.dll」を「Merge7z457.dll」「Merge7z457U.dll」という名前に変更することで、今のところ問題なく動いている。
12/24追記
7-Zip 4.57 用の WinMerge 7-Zip Plugin が SourceForge から入手可能になっています。
カスタムツリービューの基本的な使い方(その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);
}
関連記事
- カスタムツリービューの基本的な使い方(その1~表示)
- カスタムツリービューの基本的な使い方(その2~追加・削除)
- カスタムツリービューの基本的な使い方(その3~インライン編集)
- カスタムツリービューの基本的な使い方(その4~並び替え)
- カスタムツリービューの基本的な使い方(その5~ドラッグ&ドロップ)
- カスタムツリービューの基本的な使い方(その6~複数列ツリー)
- カスタムツリービューの基本的な使い方(その7~プログレスバー)
- カスタムツリービューの基本的な使い方(その8~チェックボックス)
- カスタムツリービューの基本的な使い方(その9~階層構造 – 表示)
- カスタムツリービューの基本的な使い方(その10~階層構造 – フォルダ開閉)
カスタムツリービューの基本的な使い方(その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) などの規則で定義されたスタイルがセルに対して適用されるようになる。
const ATOM_SVC = Components.classes["@mozilla.org/atom-service;1"].
getService(Components.interfaces.nsIAtomService);
getCellProperties: function(row, col, properties) {
if (col.index == 0 && this._visibleData[row].type != TYPE_SEPARATOR)
properties.AppendElement(ATOM_SVC.getAtom("title"));
},
関連記事
- カスタムツリービューの基本的な使い方(その1~表示)
- カスタムツリービューの基本的な使い方(その2~追加・削除)
- カスタムツリービューの基本的な使い方(その3~インライン編集)
- カスタムツリービューの基本的な使い方(その4~並び替え)
- カスタムツリービューの基本的な使い方(その5~ドラッグ&ドロップ)
- カスタムツリービューの基本的な使い方(その6~複数列ツリー)
- カスタムツリービューの基本的な使い方(その7~プログレスバー)
- カスタムツリービューの基本的な使い方(その8~チェックボックス)
- カスタムツリービューの基本的な使い方(その9~階層構造 – 表示)
- カスタムツリービューの基本的な使い方(その10~階層構造 – フォルダ開閉)
xul:filefield

ユーザが選択したファイルやフォルダを表示するような設定UIを作る場合には xul:filefield 要素が便利である。例えば Firefox のダウンロード先フォルダの設定UIには xul:filefield 要素が使われている。
XUL
filefield 要素を使うためには、以下の2つのスタイルシート参照する処理命令を追加する必要がある。2つめのスタイルシートは Firefox 専用である。
<?xml-stylesheet href="chrome://mozapps/content/preferences/preferences.css"?> <?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
多くの場合、 filefield 要素はファイルを選択するためのボタンとセットで使用される。
<hbox align="center">
<filefield id="myFileField" flex="1" />
<button label="Browse..." oncommand="selectFile();" />
</hbox>
JavaScript
「Browse…」ボタンを押したときの selectFile 関数を定義する。 nsIFilePicker を使ってファイル選択ダイアログを表示し、ユーザがファイルを選択すると、その選択したファイル (nsILocalFile 型) を filefield 要素の file プロパティへセットする。すると、ファイル名とアイコンが表示される。
Windows の場合、選択したファイルがアプリケーションであれば、ファイルのプロパティで「バージョン情報」の「説明」に設定された文字列が表示される。例えば「iexplore.exe」であれば「Internet Explorer」のようにアプリケーション名となる。
function selectFile() {
var filePicker = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(Components.interfaces.nsIFilePicker);
filePicker.init(window, "Choose a file.", filePicker.modeOpen);
if (filePicker.show() == filePicker.returnOK) {
var fileField = document.getElementById("myFileField");
fileField.file = filePicker.file;
}
}
nsIFilePicker#modeGetFolder を使ってフォルダ選択ダイアログを表示させ、ユーザにフォルダを選択させる場合、 filefield 要素の file プロパティへフォルダを表す nsILocalFile をセットしてもアイコンしか表示されない。この場合、以下のように filefield 要素の label プロパティへフォルダのパスなどを表示させるよう処理を加える。
fileField.label = filePicker.file.path;
応用例
実際のところ filefield 要素は prefwindow 要素で作成した設定画面内に配置することが多い。 preference[type=”file”] の要素と組み合わせて使用する例を下記に示す。
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mozapps/content/preferences/preferences.css"?>
<?xml-stylesheet href="chrome://browser/skin/preferences/preferences.css"?>
<prefwindow id="testPrefWindow"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
style="width: 30em;">
<prefpane id="mainPane" onpaneload="updateUI();">
<preferences>
<preference id="extensions.hoge.testDir" name="extensions.hoge.testDir" type="file" />
</preferences>
<script type="application/x-javascript"><![CDATA[
function updateUI() {
var file = document.getElementById("extensions.hoge.testDir").value;
if (file) {
var fileField = document.getElementById("myFileField");
fileField.file = file;
fileField.label = file.path;
}
}
function selectDir() {
var filePicker = Components.classes["@mozilla.org/filepicker;1"]
.createInstance(Components.interfaces.nsIFilePicker);
filePicker.init(window, "Choose a folder.", filePicker.modeGetFolder);
if (filePicker.show() == filePicker.returnOK) {
document.getElementById("extensions.hoge.testDir").value = filePicker.file;
updateUI();
}
}
]]></script>
<hbox align="center">
<filefield id="myFileField" flex="1" />
<button label="Browse..." oncommand="selectDir();" />
</hbox>
</prefpane>
</prefwindow>
参考
chrome://mozapps/content/preferences/preferences.xml#fileField
カスタムツリービューの基本的な使い方(その8~チェックボックス)

ツリーセルに文字列の代わりにチェックボックス(厳密にはチェックマーク?)を表示させることも可能である。
ベースとなるソースコードはその1~表示を参照。
fruits.xul
まず、チェックボックスの画像を表示させるためのスタイルシート fruits.css を読み込む処理命令を追加する。treecol 要素へ type=”progressmeter” 属性を付加すると、その列はプログレスバーが表示可能になる。
また、チェックボックスを直接クリックして切り替え可能にするため、 tree 要素と treecol 要素の両方へ editable=”true” 属性を追加する。
<?xml-stylesheet href="fruits.css" type="text/css"?>
<tree id="fruitsTree" flex="1" editable="true">
<treecols>
<treecol label="Name" flex="1" primary="true" />
<treecol label="Juicy" type="checkbox" editable="true" />
</treecols>
</tree>
fruits.css
furits.xul から読み込まれるスタイルシート fruits.css を作成する。以下のようにするとチェックマークが表示される。もちろん自前の画像を使用したり、チェックされていない状態でも画像を表示させたりすることも可能である。
treechildren::-moz-tree-checkbox(checked) {
list-style-image: url(chrome://global/skin/checkbox/cbox-check.gif);
}
fruits.js
配列データの内容を二次元配列化する。各アイテムの0番目の要素は「Name」列で表示させる文字列、1番目の要素は「Juicy」列のチェックボックスのオン/オフ状態である。
var gFruitsData = [
["Grape" , true ],
["Apple" , false],
["Orange" , true ],
["Banana" , false],
[null , false], // separator
["Pear" , false],
["Peach" , true ],
["Strawberry", false],
["Cherry" , false],
["Melon" , true ],
[null , false], // separator
["Watermelon", true ],
["Plum" , false],
["Papaya" , false],
["Lemon" , true ],
];
これに伴い、 nsITreeView#isSeparator, getCellText を以下のように修正する。
isSeparator: function(index) {
return this._data[index][0] == null;
},
getCellText: function(row, col) {
switch (col.index) {
case 0: return this._data[row][0];
}
},
各セルのチェックボックスの現在のオン/オフ状態は nsITreeView#getCellValue で決定される。
getCellValue: function(row, col) {
return this._data[row][1];
},
各セルのチェックボックスがクリックでオン/オフ切り替えが可能かどうかは、 nsITreeView#isEditable メソッドで決定される。今回の場合は、列番号が1でなおかつセパレータ行でなければ切り替え可能である。
isEditable: function(row, col) {
return (col.index == 1 && !this.isSeparator(row));
},
チェックボックスをクリックしてオン/オフ切り替えしようとすると、 nsITreeView#setCellValue メソッドが呼び出される。このメソッドではクリックしたセルに対応するアイテムの値を変更し、なおかつ nsITreeBoxObject#invalidateCell でチェック状態が変化したセルだけを再描画する。
setCellValue: function(row, col, value) {
this._data[row][col.index] = value;
this._treeBoxObject.invalidateCell(row, col);
},
関連記事
- カスタムツリービューの基本的な使い方(その1~表示)
- カスタムツリービューの基本的な使い方(その2~追加・削除)
- カスタムツリービューの基本的な使い方(その3~インライン編集)
- カスタムツリービューの基本的な使い方(その4~並び替え)
- カスタムツリービューの基本的な使い方(その5~ドラッグ&ドロップ)
- カスタムツリービューの基本的な使い方(その6~複数列ツリー)
- カスタムツリービューの基本的な使い方(その7~プログレスバー)
- カスタムツリービューの基本的な使い方(その8~チェックボックス)
- カスタムツリービューの基本的な使い方(その9~階層構造 – 表示)
- カスタムツリービューの基本的な使い方(その10~階層構造 – フォルダ開閉)
カスタムツリービューの基本的な使い方(その7~プログレスバー)

ツリーセルに文字列の代わりにプログレスバーを表示させることも可能である。 SeaMonkey のダウンロードマネージャで実際に使われている。
ベースとなるソースコードはその1~表示を参照。
fruits.xul
まず、 treecol 要素をひとつ追加する。 treecol 要素へ type=”progressmeter” 属性を付加すると、その列はプログレスバーが表示可能になる。
<treecol label="Name" flex="1" primary="true" />
<treecol label="Weight" flex="1" type="progressmeter" />
fruits.js
配列データの内容を二次元配列化する。各アイテムの0番目の要素は「Name」列で表示させる文字列、1番目の要素は「Weight」列のプログレスバーで表示させる値である。
var gFruitsData = [
["Grape" , 30 ],
["Apple" , 50 ],
["Orange" , 40 ],
["Banana" , 20 ],
[null , null], // separator
["Pear" , 30 ],
["Peach" , 40 ],
["Strawberry", 5 ],
["Cherry" , 0 ],
["Melon" , 80 ],
[null , null], // separator
["Watermelon", 100 ],
["Plum" , 20 ],
["Papaya" , 70 ],
["Lemon" , 10 ],
];
これに伴い、 nsITreeView#isSeparator, getCellText を以下のように修正する。
isSeparator: function(index) {
return this._data[index][0] == null;
},
getCellText: function(row, col) {
switch (col.index) {
case 0: return this._data[row][0];
}
},
プログレスバーには値をメーターの割合として表示する通常タイプ (nsITreeView#PROGRESS_NORMAL) と、値が定まらずメーターがアニメーションするような不定タイプ (nsITreeView#PROGRESS_UNDETERMINED) の2種類がある。各セルのプログレスバーがどちらのタイプであるかは nsITreeView#getProgressMode メソッドで決定される。今回の場合、列番号が1であればすべて通常のプログレスバー、それ以外は非プログレスバーなので、以下のように実装する。ただし、実際は treecol 要素が type=”progressmeter” 属性となっている列についてのみ getProgressMode が呼び出されるため、今回の場合は col.index の値は常に1である。
getProgressMode: function(row, col) {
switch (col.index) {
case 1 : return Components.interfaces.nsITreeView.PROGRESS_NORMAL;
default: return Components.interfaces.nsITreeView.PROGRESS_NONE;
}
},
通常タイプのプログレスバーの割合は nsITreeView#getCellValue で決定される。
getCellValue: function(row, col) {
return this._data[row][1];
},
関連記事
- カスタムツリービューの基本的な使い方(その1~表示)
- カスタムツリービューの基本的な使い方(その2~追加・削除)
- カスタムツリービューの基本的な使い方(その3~インライン編集)
- カスタムツリービューの基本的な使い方(その4~並び替え)
- カスタムツリービューの基本的な使い方(その5~ドラッグ&ドロップ)
- カスタムツリービューの基本的な使い方(その6~複数列ツリー)
- カスタムツリービューの基本的な使い方(その7~プログレスバー)
- カスタムツリービューの基本的な使い方(その8~チェックボックス)
- カスタムツリービューの基本的な使い方(その9~階層構造 – 表示)
- カスタムツリービューの基本的な使い方(その10~階層構造 – フォルダ開閉)
カスタムツリービューの基本的な使い方(その6~複数列ツリー)

複数列ツリーを作成する。ベースとなるソースコードはその1~表示を参照。
fruits.xul
fruits.xul へ treecol 要素を2つ追加する。
<treecols>
<treecol label="Name" flex="1" primary="true" />
<treecol label="Color" flex="1" />
<treecol label="Season" flex="1" />
</treecols>
fruits.js
データ構造を一次元配列から二次元配列へと拡張する。
var gFruitsData = [
["Grape" , "Purple", "Summer"],
["Apple" , "Red" , "Winter"],
["Orange" , "Yellow", "Always"],
["Banana" , "Yellow", "Always"],
[null , null , null ], // separator
["Pear" , "Green" , "Winter"],
["Peach" , "Red" , "Summer"],
["Strawberry", "Red" , "Winter"],
["Cherry" , "Red" , "Spring"],
["Melon" , "Green" , "Spring"],
[null , null , null ], // separator
["Watermelon", "Green" , "Summer"],
["Plum" , "Purple", "Summer"],
["Papaya" , "Yellow", "Always"],
["Lemon" , "Yellow", "Always"],
];
nsITreeView#isSeparator, getCellText を以下のように修正する。
isSeparator: function(index) {
return this._data[index][0] == null;
},
getCellText: function(row, col) {
switch (col.index) {
case 0: return this._data[row][0];
case 1: return this._data[row][1];
case 2: return this._data[row][2];
}
},
応用例1~ツリーカラムを非表示にする~

複数列ツリーに限った話ではないが、ツリーカラムを非表示にするためには、以下のように tree 要素へ hidecolumnpicker 属性を追加し、各 treecol 要素の label 属性を削除した上で hideheader 属性を追加する。
<tree id="fruitsTree" flex="1" hidecolumnpicker="true">
<treecols>
<treecol flex="1" hideheader="true" primary="true" />
<treecol flex="1" hideheader="true" />
<treecol flex="1" hideheader="true" />
</treecols>
<treechildren flex="1" />
</tree>
応用例2~列の順番を変更できるようにする~

以下のように tree 要素へ enableColumnDrag 属性を追加すると、列をドラッグ&ドロップで順番を変えることが可能になる。
<tree id="fruitsTree" flex="1" enableColumnDrag="true">
応用例3~列の幅を調整できるようにする~

以下のように treecol 要素間に splitter 要素を挿入すると、列の幅をドラッグ&ドロップで調整することが可能になる。
<treecol label="Name" flex="1" primary="true" />
<splitter class="tree-splitter" />
<treecol label="Color" flex="1" />
<splitter class="tree-splitter" />
<treecol label="Season" flex="1" />
応用例4~列の表示状態を保存する~
以下のように各 treecol 要素へ id と persist 属性を追加すると、各列の表示/非表示(カラムヘッダ右端のピッカーから変更可能)、順序(応用例2にて変更可能)、幅(応用例3にて変更可能)といった表示状態が localstore.rdf へ保存され、次回 fruits.xul をロードしたときに自動的に復元されるようになる。
<treecol id="fruitsName" persist="hidden ordinal width" label="Name" flex="1" primary="true" />
<splitter class="tree-splitter" />
<treecol id="fruitsColor" persist="hidden ordinal width" label="Color" flex="1" />
<splitter class="tree-splitter" />
<treecol id="fruitsSeason" persist="hidden ordinal width" label="Season" flex="1" />
関連記事
- カスタムツリービューの基本的な使い方(その1~表示)
- カスタムツリービューの基本的な使い方(その2~追加・削除)
- カスタムツリービューの基本的な使い方(その3~インライン編集)
- カスタムツリービューの基本的な使い方(その4~並び替え)
- カスタムツリービューの基本的な使い方(その5~ドラッグ&ドロップ)
- カスタムツリービューの基本的な使い方(その6~複数列ツリー)
- カスタムツリービューの基本的な使い方(その7~プログレスバー)
- カスタムツリービューの基本的な使い方(その8~チェックボックス)
- カスタムツリービューの基本的な使い方(その9~階層構造 – 表示)
- カスタムツリービューの基本的な使い方(その10~階層構造 – フォルダ開閉)