Now browsing the SCRAPBLOG weblog archives.
【つづき】 JavaScript 製 XPCOM で配列構造・列挙構造のデータをメソッドの戻り値にする
前回のエントリでコメントを頂いていたのに気付くのが遅く、だいぶ日があいてしまったが、配列構造のデータをメソッドの戻り値にするためには nsIArray よりも nsIVariant 型を使うのが手っ取り早そうである。
IDL
nsIVariant getFruitsArray();
XPCOM 実装
getFruitsArray: function() { var createStringObject = function(aStr) { var obj = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); obj.data = aStr; return obj; }; var items = [ createStringObject("apple"), createStringObject("orange"), createStringObject("banana"), ]; return items; },
利用する側の JavaScript
var svc = Cc["********"].getService(Ci.********); var arr = svc.getFruitsArray(); arr.forEach(function(elt) { alert(elt.data); });
JavaScript 製 XPCOM で配列構造・列挙構造のデータをメソッドの戻り値にする
JavaScript 製 XPCOM コンポーネントではメソッドの戻り値に JavaScript の配列 (Array オブジェクト) をそのまま使用することができない *1 。その理由は、 XPCOM のインタフェースは各言語固有の仕様に依存しない仕組みであるからだろう。 なんとか配列構造(もしくは列挙構造)のデータを戻り値にする方法はないものかと Firefox のソースなどを探ったところ、以下のような方法があることがわかった。
nsIArray / nsIMutableArray インタフェースで配列構造のデータを受け渡す
nsIArray は、配列構造のデータから個々の要素を取り出すためのインタフェースである。また、 nsIMutableArray は、個々の要素から配列構造のデータを組み立てるためのインタフェースであり、 nsIArray を継承する。
これら2つのインタフェースを利用すると、 JS 製 XPCOM 側のメソッド内にて戻り値を nsIMutableArray 型オブジェクトにしてやり、 XPCOM を利用して戻り値を受け取る側の JS から nsIArray インタフェースを介して配列データおよび個々の要素を取り出すことが可能となる。
まず、IDL には以下のような感じでメソッドを定義する。
nsIArray getFruitsArray();
次に、JS 製 XPCOM で以下のような感じでメソッドを実装する。 nsIMutableArray#appendElement メソッドによって3つの nsISupportsString 型オブジェクトを nsIMutableArray オブジェクトへ追加している。
getFruitsArray: function() { var createStringObject = function(aStr) { var obj = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); obj.data = aStr; return obj; }; var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); items.appendElement(createStringObject("apple"), false); items.appendElement(createStringObject("orange"), false); items.appendElement(createStringObject("banana"), false); return items; },
すると、以下のような感じで XPCOM を利用する側の JS から配列を受け取って個々の要素を取り出すことができる。 nsIArray#queryElementAt メソッドは JavaScript の Array オブジェクトで言うところの [i] のように、指定したインデックスの要素を取り出すことができる。ただし、 nsIArray の IDL は未凍結であるためか IDL に記載された仕様と実際の仕様が異なることに注意。
var svc = Cc["********"].getService(Ci.********); var arr = svc.getFruitsArray(); for (var i = 0; i < arr.length; i++) { var elt = arr.queryElementAt(i, Ci.nsISupportsString); alert(elt.data); }
別の取り出し方として、 nsIArray#enumerate メソッドを使い、 nsISimpleEnumerator インタフェースを介して列挙構造として個々の要素を取得することも可能。
var enum = arr.enumerate(); while (enum.hasMoreElements()) { var elt = enum.getNext().QueryInterface(Ci.nsISupportsString); alert(elt.data); }
nsISimpleEnumerator インタフェースで列挙構造のデータを受け渡す
インデックス付きの配列構造ではなく、単純な列挙構造でよければ、 nsISimpleEnumerator インタフェースを介してデータを受け渡しする手もある。ただ、 nsIMutableArray とは違って列挙構造を組み立てるためのインタフェースというのが存在しないらしく、必要となるメソッドを自前で実装しなければならない。
まず、IDL には以下のような感じでメソッドを定義する。
nsISimpleEnumerator getFruitsEnumerator();
次に、JS 製 XPCOM で以下のような感じでメソッドを実装する。
getFruitsEnumerator: function() { var createStringObject = function(aStr) { var obj = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); obj.data = aStr; return obj; }; var items = []; items.push(createStringObject("apple")); items.push(createStringObject("orange")); items.push(createStringObject("banana")); return new ArrayEnumerator(items); },
最後の return 時に使用する ArrayEnumerator クラスは、内部的なデータとして JavaScript の配列オブジェクトを保持し、 nsISimpleEnumerator インタフェースが持つふたつのメソッド (hasMoreElements と getNext) を実装することで列挙も可能なクラスであり、 nsMicrosummaryService.js を参考に別途実装する。
XPCOM を利用する側の JS からの取り出し方は前述の nsIArray#enumerate メソッドを使用した手順とほぼ同じ。
var svc = Cc["********"].getService(Ci.********); var enum = svc.getFruitsEnumerator(); while (enum.hasMoreElements()) { var elt = enum.getNext().QueryInterface(Ci.nsISupportsString); alert(elt.data); }
[userChrome.js] Vertical Toolbar
ウィンドウ左側に縦置きツールバーを配置するための userChrome.js 用スクリプト。
個人的には縦置きツールバー必須なのだが、それを実現するための拡張機能である Toolbar Enhancements が Firefox 1.5 時代から更新停止状態だし、かといって All-in-One Sidebar は余計な機能が多すぎるので、 userChrome.js で必要最低限な機能のみを実装した。
xuldev.org :: userChrome.js scripts » Vertical Toolbar
カスタマイズ方法
カスタマイズは全てスクリプト直接編集で行うという玄人志向である。残念ながら通常のツールバーのように「ツールバーのカスタマイズ」ウィンドウからボタンをドラッグ&ドロップして配置することはできない。現状のツールバーのカスタマイズ機能は複数の toolbox 要素に対するカスタマイズを考慮した作りになっていないからである。これを複数のtoolbox 要素に対して有効にさせるには、かなりの無茶をしないといけないので諦めた。
ツールバーに配置するアイテム
スクリプト内の currentSet という変数がツールバーに配置するボタンの要素IDの配列となっている。ツールバーボタン以外にも “separator” (区切り)、 “spring” (伸縮自在のスペース)、 “spacer” (スペース)も配置可能。
ツールバーの表示
変数 mode の値を “icons” とするとアイコンのみ表示、 “text” とするとテキストのみ表示、 “full” とするとアイコンとテキスト両方表示になる。
ツールバーアイコンのサイズ
変数 size の値を “large” とすると大きいサイズのアイコン、 “small” とすると小さいサイズのアイコンとなる。
All-in-One Gestures のマウストレイルを改造
All-in-One Gestures のマウスジェスチャ実装を改造し、マウスポインタを水平または垂直に移動させた場合、すぐ隣り合う座標へ新しく aioTrailDot 要素を配置するのではなく、現在の aioTrailDot 要素の大きさを縦や横に広げていくことで直線を描画するようにしてみた。下図の右が通常の実装方式、左が上記の修正を施したものである。わかりやすくするために各 aioTrailDot 要素へ outline: 1px solid blue で枠を付けている。
描く曲線にもよるが、これでジェスチャ中のメモリ消費量が削減できると思う。
All-in-One Gestures のマウストレイルで DOM Inspector が固まる問題
マウストレイルの実装 の中で、 All-in-One Gestures のマウストレイル実装方式の場合に DOM Inspector で Webページの DOM を調べながらマウスジェスチャをすると、Firefox がハングアップすると記したが、その原因はマウストレイル消去時に aioTrailContainer 要素へ appendChild された aioTrailDot 要素(マウストレイルの線を描画するための点)を removeChild せず、 aioTrailContainer 要素自体をいきなり removeChild しているためだとわかった。そこで、
while (elt.lastChild) elt.removeChild(elt.lastChild); elt.parentNode.removeChild(elt);
みたいにして、まず aioTrailDot 要素を削除し、その後に aioTrailContainer 要素を削除するように修正したら DOM Inspector が固まる問題が発生しなくなった。
Download Statusbar 0.9.5
愛用している Download Statusbar に最近新しいバージョンがリリースされた。バージョン番号の変更としては 0.9.4.7 から 0.9.5 と控えめながら、UI・実装ともに大幅にグレードアップしている。ただ、ツールバーやツールチップのフォントサイズが若干小さめで、 MS UI Gothic だと綺麗に表示されないので、 userChrome.css で微調整。
.db_tootipText, .db_tootipTitle, .db_tooltipTextBox, .db_buttonText { font-size: 1em !important; }
xul:popup#showPopup の位置ズレを直す
popup 要素に対して
showPopup(document.documentElement, event.screenX, event.screenY, "popup", null, null);
とかすると、マウスポインタを基点とした位置にポップアップが表示されるが、その後に右クリックのコンテキストメニューを表示させてからもう一度上記の showPopup を実行すると、ポップアップが表示される位置が大きくズレるという問題に悩まされた。
しかし、showPopup する前に、以下のようにしてやることで直った。
document.popupNode = null;
マウストレイルの実装
マウストレイルとは
マウスを右クリックして動かすと、その軌跡を表示する機能。右クリックを放すと軌跡は消滅する。
All-in-One Gestures の場合
DOM のレベルで実現している。右クリックの mousedown でジェスチャを開始すると、HTML の body 要素直下に aioTrailContainer 要素が生成される。その後、マウスを動かすにしたがってサイズが1×1の aioTrailDot 要素を複製して aioTrailContainer 下に appendChild する。この aioTrailDot 要素はサイズが1×1で、マウスポインタと同じ座標へ絶対配置されており、背景色がある。この aioTrailDot 要素を並べることで、あたかも一本のつながった線に見えるようになる。
この方式には以下のような問題点がある。
・縦長のページでマウストレイルを行うと重たい
・曲線が滑らかに描画されない
・DOM Inspector で Webページの DOM を調べながらマウスジェスチャをすると、Firefox がハングアップする。
Optimoz Mouse Gestures の場合
Windows では、C++製?の独自 XPCOM コンポーネント (mgMouseService.dll) を用い、 Windows ネイティブな?実装によってマウストレイルの描画を実現している。
この XPCOM は mgIMouseService というインタフェースを有し、 initTrails と stopTrails の2つのメソッドによって XUL からマウストレイルの描画を制御することができる。
Windows 以外のプラットフォームでは All-in-One Gestures と同じ実装方式である。
他にうまい方法はあるか?
マウストレイル開始時に ブラウザ上に canvas 要素をかぶせ、 dot の stroke を連続で行うことで実現可能。この方法では、 All-in-One Gestures の実装方式の問題点が解決され、曲線が滑らかに描画でき、縦長のページで動作が重たくなることも無い。しかし、以下のような問題点がある。
・canvas 要素が邪魔をしてマウスが通過したリンクを調べることができない。
・一時的にメモリ使用量が激増する。
ロッカージェスチャの実装 Part 1
[userChrome.js] 軽量マウスジェスチャーでの問題点
ロッカージェスチャを使って「戻る」をして、ページロード中に右クリックを放すとそれが認識されない(mouseup イベントが発生しない)。その後通常の左クリックをしただけでロッカージェスチャが実行されてしまう。
All-in-One Gestures の場合
上記とまったく同様のバグが発生することがわかった。ただ、AiOG ではロッカージェスチャ実行後にタイマーが仕掛けられ、3秒間何もしないとロッカージェスチャの待ち状態が解除される(つまり右クリックを放したと見なされる)という仕組みがある。もしかするとこのタイマーはバグが発生したときの被害を最小限に食い止めるための意味があるのかも?つまり、右クリック放しが認識されずにバグ状態に陥っても、3秒間我慢すれば勝手に正常な状態へと戻る。
Optimoz Mouse Gestures の場合
Windows の場合はバグ発生しない。 Linux の場合はバグ発生する。
なぜ Windows の場合にバグが発生しない(mouseup イベントが必ず発生する)かを調べたところ、 OMG では独自に実装したC++製?のXPCOMコンポーネントによって、 DOM のレイヤーで発生する mouseup イベントとは別にもっと上のレイヤー?でマウスの動きを検知しているためである。この XPCOM コンポーネントは mgIMouseService というインタフェースを有し、マウストレイル(マウスジェスチャ中の軌跡描画)が主な仕事であるが、それ以外にもマウスの動きやクリックに応じて nsIObserverService による通知を行う仕組みをもっている。 XUL 側ではこの通知を監視し、クリックの放し (mozgestButtonUp) が発生すると initMouseEvent によって DOM のレイヤーでの mouseup イベントを生成する処理になっている。
結論
マウスジェスチャとホイールジェスチャは mousedown や mousemove といった DOM イベントを捕捉することで実装できた。しかし、まともなロッカージェスチャを DOM のレイヤーのみで実装するのは無理っぽい。
[userChrome.js] 軽量マウスジェスチャー(ホイールジェスチャ・ロッカージェスチャ対応版) ~途中経過~
6/30 追記
ロッカージェスチャ有効時、選択範囲をテキストボックスへドラッグ&ドロップすると右クリックが効かなくなるバグを修正。
「Operaユーザは「右押しながら左クリック」をよく使う」というアンケート結果に衝撃を受けたわけではないですが、軽量マウスジェスチャへ、以下の2機能を追加したバージョンを作成しました。
・ホイールジェスチャ (右クリックしながらホイール回転でタブ切り替え)
・ロッカージェスチャ (右クリックしながら左クリックで戻る、左クリックしながら右クリックで進む)
ホイールジェスチャおよびロッカージェスチャ実行時には、 _performAction メソッドへ以下のような文字列が渡りますので、実行するアクションをカスタマイズ可能です。
・ホイールジェスチャ(下に回転): W+
・ホイールジェスチャ(上に回転): W-
・ロッカージェスチャ(右クリックしながら左クリック): L<R
・ロッカージェスチャ(左クリックしながら右クリック): L>R
ただし、ロッカージェスチャに未解決の問題点があって、例えば右クリックしながら左クリックを連続で何回か押して連続で戻る操作をしているとき、ページのローディングが行われている瞬間(くるくるアイコンが回転している瞬間)を狙って右クリックを放すと、放したことが認識されずに右クリックが続いていると誤認識され、その後の動作がおかしくなる。bfcache が効いていると再現しにくいので、いったんキャッシュをクリアするとページ遷移のたびにローディングされ、上記問題が再現しやすくなる。
とはいえ、そもそも All-in-One Gestures ではロッカージェスチャは連続でできない(右クリック押しながら一回左クリックをするとその時点で終了する)ため、 userChrome.js 版マウスジェスチャでもその動きにすれば上記問題点は解決すると思われる。反面、 Optimoz Mouse Gestures や本家 Opera では連続クリックが可能である。
ロッカージェスチャを望んでいる方は、この連続クリック機能が必須であるか、無くてもいい程度のものか、あるいはまったくもって不要かなどについて教えてくださるとありがたいです。ただ、連続クリックが必須という意見が大半であったとしても、今後がんばっても上記問題を解決できないままである可能性が高いです。