たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。
の動向はもえじら組ブログで。
宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能!
最近のMinefieldのナイトリービルドで、ツリー型タブがあると前回終了時のセッションが復元されない(タブのタイトルだけ復元されて実際には空のページになる)という現象が起こっている。この問題の原因の究明のためにずいぶん遠回りをした。
こういう時は、処理がどこで止まってしまっているのかを特定するのが僕が知ってる唯一の(そして多分一番ストレートな)原因調査の仕方だ。今回だったらセッション復元が止まってしまってるので、nsSessionStore.jsの中に沢山デバッグ用のdump()
を埋め込んで、どこの時点でおかしくなっているのかを調べるという事になる。
が、Minefieldではそれが一筋縄ではいかなくなってた。
構成ファイルのほとんどを1つのアーカイブの中に入れてしまうというomni.jarという変更が反映されて、今までだったらcomponentsフォルダの中にそのまま置かれていたnsSessionStore.jsも、omni.jarの中に格納されている。なので上記のような調査法を取ろうとすると、omni.jarの中にあるファイルを書き換えないといけない。
ところが7-Zipではこのomni.jarを開くことができない。多くの場合、jarファイルはただのZIPアーカイブなので7-Zipのファイルマネージャで開いて中身を編集→再圧縮ということが簡単にできるんだけど、omni.jarの場合はアーカイブ自体が壊れていると判断されてしまって中身を見ることすら適わない。
検索してみたら同じようなことで悩んでる人が他にもいたみたいで、リンク先に書かれてる情報によるとExplzhを使えばomni.jarを開けるらしい。
ということで早速Explzhを入れてみた。Explzhでomni.jarを開いてみると、確かに中身を見ることができる。しかし、中にあるファイルを編集してエディタを終了すると、omni.jarの再圧縮に失敗してしまう。ファイル名を変えるとかファイルを1個だけ削除するとかするとなんとか編集後のファイルをアーカイブに含められたんだけど、その状態でMinefieldを起動すると、ファイルが部分的に読み込めなくなってウィンドウの背景が透明になってしまうとかの謎なトラブルが発生してしまった。Explzhでもomni.jarを部分的に変更しようとすると駄目みたいだ。
追記。Windows XP以降の「圧縮フォルダ」機能でもomni.jarを展開できるらしい。圧縮フォルダなんて微妙に不便だから使ってなかったのに、こんな所で役に立つとは……
まとめると、トラブルが起こらないようなomni.jarの編集方法は、こう。
この方法で、Minefieldが正常に起動する状態を保ちながらomni.jarの中にあるファイルに変更を加える事ができた。
nsSessionRestore.jsに大量のdump()
を入れて調べてみた所、最初のタブ(現在のタブ)の復元が終わった後で次のタブを復元する時に、すぐに処理が終わってしまっていた。さらによく調べると、復元対象のタブを決定する時にもしタブのセッション情報の_tabStillLoading
というフラグがfalse
になっていたら(=タブのセッションが復元完了していたら)、そのタブをセッションの復元対象から除外するという設計になっていた。
_tabStillLoading
というフラグには見覚えがあったのでツリー型タブのソースを検索し直してみたら、別のバグの対策でこのフラグを敢えてfalse
にするような処理を入れていて、これが誤爆しているせいで上記の問題が起こっているようだった(本当はそのタブはまだセッションが復元されていない・読み込み中の状態なのに、_tabStillLoading
がfalse
にセットされてしまうため、タブがセッション復元の対象から除外されるようになってしまっていた)。
まとめると、こういう事だった。
busy
属性を取り除いて、一見すると既にタブの復元が完了しているかのように見せるようになった。その代わり、まだタブの復元が必要であることを示す_restoring
というフラグをbrowser要素の方に立てるようになった。→Bug 602555でリファクタリングされて、また仕様が変わった。前述の「_restoring
のフラグが立っている状態」に相当するのは「browser要素の__SS_restoreState
が1
の時」になった。busy
属性だけを見て、そのタブが読み込み完了しているかどうか・セッション復元中かどうかを判断して、読み込みが完了していると判断したタブのセッション情報の_tabStillLoading
を強制的にfalse
にして、タブの読み込みが完了しているとnsSessionStoreに認識させるようにしていた。別のバグの対策用のコードの条件分岐にもうひとつ条件を足して、「一見すると読み込みが完了しているように見えるが、内部的にはまだセッション復元が完了しておらず読み込み中の状態である」というケースを判別するようにしたら、この問題は起こらなくなった。同じコードを他にもいくつかのアドオンで使っていたので、それらも合わせて修正しておくことにした。もし同じ事をやっている人が他にもいたら、十分気をつけて欲しい。
History of the Add-ons Managerのオレオレ翻訳です。誤訳がありそうですが気にしません。
Firefox 4用の新しいアドオンマネージャのために行ってきた作業を通じて、これは興味深い話題だろうと思ったので、Firefoxのアドオンマネージャについてのこれまでの歴史と今後についてざっとまとめてみました。
Firefoxの最初期のバージョンにおいても、拡張機能は古いXPInstall形式のパッケージの仕組みを用いてサポートされていました。拡張機能の削除や無効化のための仕組みがFirefox自体に含まれていなかったため、これらの初期のアドオンマネージャは根本的な部分でいくつか問題がありました。また、あなたが初めてアドオンをインストールした後に目にするような拡張機能マネージャのウィンドウもありませんでした。拡張機能とテーマの一覧がFirefoxに初めて追加されたのはバージョン0.2の時で、その頃はこの製品はPhoenixと呼ばれていました。それは非常に基本的なUIで、設定ウィンドウの中に表示されていました。
製品がFirebirdという新しい名前を得た後、拡張機能マネージャは設定ウィンドウ内の「テーマ」と「拡張機能」という個別のペインとして生まれ変わりました。
Firefox 0.9ですべてが変わりました。拡張機能マネージャは個別のウィンドウとなり、
テーマと拡張機能はそれぞれ別々のウィンドウで表示され、基本的な自動更新のための仕組みが搭載されました。これらはFirefox 1.0に向けたバックエンドのコードにおいて改良されることになっていました。この最初のバージョンは主にBen Goodgerによって書かれました。
Firefox 1.5では拡張機能マネージャの表面的な部分について、ほんの少しだけ違いが見られます。このバージョンではFirefoxの他の部分の外観のスタイル付けに合わせるためにごくわずかな変更が行われました。
この舞台裏で、Darin Fisherは大きな変更を行いました。彼は拡張機能マネージャに対して、Windowsのレジストリを含む、システム上の異なる場所から拡張機能を読み込むことを可能にしたのです。彼はまた、拡張機能をプロファイルフォルダ内に抽出することを可能にし、次にFirefoxが起動された時に自動的にそれらを認識できるようにもしました。この時、リリースに向けて拡張機能マネージャを安定させるために、Rob Strongがモジュールオーナーの権限を引き継ぎました。また、このバージョンには拡張機能マネージャに対する私(※訳註:原文の著者のDave Townsend氏)の最初のパッチも含まれています――bug 307925においての物で、ごく小さな物ですが。
Firefox 2.0では最終的に、分割されていた拡張機能とテーマのウィンドウが一つのアドオンマネージャに統合されました。
水面下では、ユーザにとって危険と判断された拡張機能をMozillaの手で外部から無効化する事を可能にするためのブロックリスト機能の最初のバージョンを含む、さらに多くの変更が行われていました。この機能が作られて以来、私たちはこの機能を本当にごく希にしか使っていませんが、セキュリティ上の弱点やクラッシュを防ぐ際の助けとなってきました。
Firefox 3において、私たちはプラグインをアドオンマネージャの中に含めるようにし、また初めての利用において皆さんがアドオンマネージャを使ってaddons.mozilla.orgからアドオンを直接ダウンロードしてインストールできるようにしました。これは私(※訳註:原文の著者のDave Townsend氏)がFirefoxにおいて作業した最初の大きな機能で、この機能がどれだけ使われているかをアドオンチームから聞く度に、私はいつも非常に嬉しい気持ちになります。
内部的には、ブロックリスト機能はプラグインをブロックできるように拡張され、Windows以外のプラットフォームにおいてもシステム上の他のアプリケーションとの連携に対応するためにインストール対象となるファイルの置き場所が新しく追加されました(※訳註:bug 311008)。
Firefox 3.5と3.6においては視覚的な変更点は全くありませんでしたが、Firefox 3.5ではブロックリスト機能がいくつかの異なる重要度のレベルに対応できるように再度改良され、Firefox 3.6ではシステム上にあるアップデートが必要なプラグインについてユーザに警告を行う機能への対応に加えて、Firefoxのメインウィンドウの単調な背景を簡単に切り替えられるようにする、産まれたばかりのペルソナがアドオンマネージャに搭載されました。
Firefox 4では、再起動が不要な拡張機能への対応の追加や、ユーザの操作をより妨げない自動的な更新のための仕組み、そして新しいアドオンに出会うためのより便利な方法などを含む、アドオンマネージャの全面的な再設計が見られます。
Firefox 4 betaのアドオンマネージャ(2010年初頭)
裏側においては、新しいアドオンマネージャは新しい種類のアドオンを、これまでよりもさらに簡単に管理できるようになっています。
ユーザ・エクスペリエンスチームは、Firefox 4のリリース版でアドオンマネージャがどのように見えるようになるかの最終的なデザインを作成するために、一生懸命働いています。あくまで仮の物ですが、これは彼らが行おうとしていることのスケッチです:
Firefox 4最終版用に検討中のデザイン(2010年末頃?)
将来的に、私たちはさらに多くの種類のアドオンをこのマネージャの中に持ち込むことを計画しています。現在はそれ用の独自のウィンドウで管理されている検索プラグインなどのような物は、ここにうまく収まるでしょう。また、私たちは皆さんが拡張機能をより単純な形でカスタマイズするようにしたいとも考えています。可能性は低いですが、私たちは拡張機能の開発者達に対して、独自の設定ウィンドウを作る事を許容するのをやめて、新しいウィンドウを開くことを必要とせずに、単純な設定をアドオンマネージャの中で直接変更できるようにする機能を加える事を検討しています。私たちは、皆さんがインストール済みのプラグイン――それは時にセキュリティ上の問題や、安定性を損なう問題の元になります――を自動的に更新する方法を実現できたらと考えていますし、テーマの選択について、どんなテーマが利用可能かを見たり、それらを切り替えたりするのを、より簡単に行えるよう大幅な改良を行いたいとも考えています。
これらのことのうちのいくつかはFirefox 4.0までの時間で実際に行われるかもしれませんが、最終的なリリースの前に新しい物を取り入れるには、時間は残り少ないです。
transitionendをトリガーとしてタブを閉じる処理が完了されるはずなのに、それが実行されなくて閉じられないタブが残ってしまう問題について、実際にどのアニメーションが成功したのか・失敗したのか、何がトリガーになっているのか、というのをデバッグしたかったんだけど、「閉じられないタブ」が発生した後からそれを調べる方法がなかったので、じゃあ先にログを収集しておこうという事でこういうコードを書いてみた。
var startDOMLogging = function(aElement) {
var log = aElement.DOMLog = Array.slice(aElement.DOMLog || []);
aElement.addEventListener('DOMAttrModified', function(aEvent) {
if (aEvent.originalTarget != aEvent.currentTarget) return;
log.push({
type : aEvent.type,
attrName : aEvent.attrName,
prevValue : aEvent.prevValue,
newValue : aEvent.newValue,
timeStamp : Date.now()
});
}, false);
aElement.addEventListener('transitionend', function(aEvent) {
if (aEvent.originalTarget != aEvent.currentTarget) return;
log.push({
type : aEvent.type,
propertyName : aEvent.propertyName,
elapsedTime : aEvent.elapsedTime,
timeStamp : Date.now()
});
}, false);
};
gBrowser.tabContainer.addEventListener('TabOpen', function(aEvent) {
startDOMLogging(aEvent.originalTarget);
}, false);
DOM InspectorでJavaScript Objectを表示してSubjectのEvaluate JavaScriptで alert(JSON.stringify(target.DOMLog))
とすれば、そのDOM要素で何が起こっていたのかが分かる。
エラーコンソールとかデバッグ系のツールで最初からこういう事ができるといいのにね。UxUにそういうユーティリティを追加してみようかな。
ツリー型タブではタブバーをウィンドウの横に置く使い方を基本的には想定してるワケだけれども、普通にタブバーを縦型にするだけだと、Minefieldではタブバーをダブルクリックしたらウィンドウが最大化されるという謎の結果になってしまうことがある。
MinefieldではBug 555081で行われた変更により、ツールバーの空白の部分がウィンドウのタイトルバーと同じ扱いになるようになっている。ツールバーの空白部分をドラッグすればウィンドウが動くし、ダブルクリックすればウィンドウの最大化になる(Windowsの場合)し、ぴたすちおのようにタイトルバーを右クリックしたらウィンドウシェードするようなユーティリティを使っていれば、それも動く。
ただ、ウィンドウの横に移動されたタブバーのようにぱっと見ツールバーに見えない部分までもがこのような挙動になってしまうと、違和感があるし混乱の元ではある。また、ツリー型タブの場合はタブバーの空白の領域をドラッグするとタブバーの位置を変えられるという機能があるけれども、上記の通りの仕様なのでこのままではタブバーの位置を変えられないということにもなってしまう。こういう時、どうすればいいのか。
タイトルバーのように振る舞うツールバーの挙動は、chrome://global/content/bindings/toolbar.xml#toolbar-drag で定義されている。コードを見てみると、WindowDraggingUtils.jsmというモジュールを読み込んで自分自身をWindowDraggingElementという物に登録しており、こうして登録された要素がタイトルバーのように振る舞うようだ。じゃあこの初期化処理を無効化するか初期化処理で行われた結果を取り消してやればいいんじゃないか、と思ったけど、設計的にそれは無理だった。
仕方がないのでWindowDraggingUtils.jsmの方を見てみると、「タイトルバー上にあるボタン等をドラッグした時だけは上記のような動作にしないようにする」というのをどうやって実現しているのかが分かった。
mouseDownCheck
)がfalseを返した時か、クリックされた要素からその祖先までの間にドラッグ操作を受け付けそうな要素がある時は、preventDefault()
している。preventDefault()
された場合、マウスのボタンがそもそも押下されなかったという扱いになるみたい。端的に言うと、MozMouseHittestイベントをpreventDefault()
すれば、ツールバーのタイトルバー的な振る舞いを無効化することができるということのようだ。
gBrowser.tabContainer.addEventListener(
'MozMouseHittest',
function(aEvent) {
aEvent.preventDefault();
},
true
);
ただ、タブバー内で発生するMozMouseHittestイベントを常にpreventDefault()
してしまうと、タブをクリックしてもタブが切り替わらないということになってしまう。preventDefault()
するのは祖先要素にタブまたはクリック可能な要素が無い場合だけに制限しないといけない。
ツリー型タブには「ポインタがタブバーを離れている時はタブバーを隠してor縮めて、ポインタがタブバーに近づいたら(フルサイズで)表示する」という機能がある。
Firefox 4に向けて開発が進んでいるMinefield 4.0b6preでこの機能が動かなくなっていたので修正をしよう……と思ったら根本的に作りなおさなきゃいけなかった。でも以前のやり方に比べたらずっとスッキリと実装できるようになっていた。
これはそんなお話。
Firefox 3.6までは、上記の機能を実現するのにえらく苦労していた。
tabbrowser要素の仕様変更のまとめで過去に図解したけど、今までのFirefoxではタブバーとコンテンツ表示領域が1つのボックスの中に収まっていたので、ボックスの縦横の配置を変えるだけでタブバーを「縦置き」できていた。そうして「縦置き」したタブバーの幅を自動的に増やしたり減らしたりしてやりさえすれば、最も単純な「タブバーを自動で隠す」機能は実現できる。
ただ、実際にはそれだと実際使っててストレスを感じる。タブバーの幅が変化する度にタブバーによって押し潰される形でコンテンツ領域の幅が縮んでしまい見た目に激しくガタつくのと、それに加えて再描画のためにFirefoxが一瞬でも無反応になってしまう事がある、というのが多分その理由だろう。
じゃあタブバーの幅が増えた分をコンテンツ領域の上にかぶせてやればガタつきがなくなってイイよね、ということで上のスクリーンショットのような事をやろうとして、当時の僕はFirefoxの仕様に頭を抱える羽目になってしまった。
要素同士を重ねる、というと一番手っ取り早いのはネガティブマージンを使う方法だろう。タブバーにネガティブマージンを指定してやれば万事解決する……そう考えていた時期が僕にもありました。
見るも無残。
どうも現行バージョンのGeckoはインラインフレームだけ特別な描画の仕方をしているらしく、iframeやbrowserの内容はオーバーレイのウィンドウのように他の内容より前に表示されるみたいだ。browser要素がDOMツリーに尽かされた後でDOMツリーに追加されたtab要素なら、browser要素よりも手前に描画されることもあるようだけど、基本的にはこうなると思っておいた方がいい。
ネガティブマージンで駄目ならposition:absoluteやらposition:fixedやらはどうなんだ、という代替案は当然出てくるだろうけど、これもやっぱり駄目だった。browserをz-index:1に、タブの方をz-index:2にという風にやっても、どう頑張ってもタブがbrowserの下に隠れてしまった。
panelやmenupopupを使うと、OSのレベルで別のウィンドウを開いて任意の位置に重ねることができる。影や枠を表示しないようにすることもできるから、うまくやればこれが一番確実かもしれない。
ただ、自分で0から作る場合ならともかく、Firefoxのタブを後から改変するという場合には、これは激しくお勧めできない。Firefoxのタブ周りのDOMツリーを不用意にいじると、Firefoxの本来の構造を期待して開発されたアドオンが動かなくなってしまう可能性が高いからだ。
アレも駄目これも駄目という感じだったので、Firefox 3.6以前では結局、他のアドオンに対して与える影響が一番少なそうなやり方として、いくつかのテクニックの組み合わせでそれっぽく見せるようにしてみた。
まずタブバーの幅を普通に増やす。
それだけだとコンテンツ領域が押し潰されてしまうので、そうならないようにネガティブマージンでappcontentあたりの幅を広げてレイアウトを維持する。
次にnsIContentViewerのmoveメソッドで描画内容をずらす。このメソッドは、フレームの内容の描画位置を任意の座標分平行移動する物だ。
仕上げとして、nsIContentViewerのmoveメソッドで描画位置を変えたせいでレンダリングされていない「タブバーと重なり合う部分」を、canvasのdrawWindowで描画する。 スクロールしてる場合やフルズームが使われている場合に位置をうまく合わせるのが難しいけど、なんとか頑張った。
ちょっと前までのMinefieldでも、基本的にはこれと同じ事をやってた。 tabbrowser要素の仕様変更のまとめで説明している通り、タブバーそのものはposition:fixedで固定して位置を合わせていて、かつてタブバーがあった所に挿入したダミーの要素と実際のタブバーとの表示サイズを同期させているという点が異なるけど、browser(iframe)とタブが重ならないように工夫を凝らしているという点では変わりはなかった。
そんな感じで今までやってたんだけど、Minefield 4.0b6preをアップデートして試しに機能をONにしてみたら、上記の解決策の重要な要素の1つである「nsIContentViewerのmoveメソッドで描画内容をずらす」が効かなくなってた。
これではタブバーの幅が変わる度にコンテンツ領域がガタついてしまうという問題を回避できない。
もはやこれまでか。そう思った僕を救ったのは、別の(ツリー型タブ)の不具合によって起こっていた現象でした。
先述の通り、Minefield用には既にタブバーをposition:fixedでレイアウトするという変更を反映してた。それが原因で、TabsToolbarの中に配置されたツールバーボタンが、タブバーが自動的に隠された状態であっても表示されたままになってしまうという現象が同時に発生していた。
……ん? ちょっと待てよ! なんでその位置にそのボタンが見えてるわけ?! browserの下に隠れてない!?
検証してみたところ、いつ行われた変更によるものなのかは分からないけど、Minefieldではいつの間にか、前述した「browserやiframeの内容は常に最前面に描画されてしまう。それらと重なる位置に置かれたXUL要素はbrowserやiframeの下に隠れてしまう。」という問題が起こらなくなっているようだ。
そこまで分かれば話は早い。もう前述のような凝ったことをしなくても、普通にposition:fixedで配置してbackground:rgba(0, 0, 0, 0.25)にすればもうそれだけで、「半透明のタブバーの下にコンテンツ領域が透けて見える」状態になるということだ。 こうしてひとまず現状のMinefieldには対応することができた。
いい時代になったものですね。
JavaScriptでアニメーション(モーショントゥイーンなど)をやろうと思うと、setTimeout()
とかsetInterval()
とかのタイマーを使うことになる。でもタイマーを複数個同時に走らせるのはリソース的に無駄だしオーバーヘッドが大きいせいでアニメーション効果ががたついてしまうようになったりするので、複数のアニメーションを同時に進行させるなら、1つのタイマーで複数のアニメーションを同時に処理した方がいい。
というわけで、そのためのライブラリをだいぶ前に作った。これは実際にツリー型タブ等で使ってる。以下のようにすると、タイマーの開始とか停止とかをヨロシクやってくれる。
var animationManager = window['piro.sakura.ne.jp'].animationManager;
// JavaScriptコードモジュールとして読み込むなら
// Components.utils.import('resource://myaddon-module/animationManager.js');
// で animationManager がエクスポートされる
var tab = gBrowser.selectedTab;
var task = function(aTime, aBeginningValue, aTotalChange, aDuration) {
// aTime:アニメーション開始時点からの経過時間(ミリ秒)
tab.style.marginLeft = (aBeginningValue + (aTime / aDuration * aTotalChange))+'px';
return aTime > aDuration; // trueを返すとその時点でアニメーション終了
};
animationManager.addTask(
task, // アニメーションの処理そのものとなる関数
parseInt(getComputedStyle(tab, null).marginLeft), // aBeginningValue:初期値
50, // aTotalChange:変化量
250 // aDuration:アニメーションにかける時間(ミリ秒)
);
この例だとmargin-leftが今の値から50増加するアニメーションだけど、変化量の指定をマイナスにすればそれだけで、今の値からmargin-leftが50減るアニメーションになる。(ちなみに、アニメーションの処理となる関数が受け取る引数の形式がなんでこうなってるのかについては、「高速な環境ではたくさん描画していいけど、低速な環境だと再描画を減らしてほしい。とにかく、1回のアニメーションは決まった時間の中できちんと終わらせたい。」という可変フレームなアニメーションをやりやすくするためです。)
既に動いてるアニメーションを中止する時は、登録した関数をremoveTask()
に渡す。
animationManager.removeTask(task);
// すべてのアニメーションを中止するなら
// animationManager.removeAllTasks();
中止ではなくてアニメーションを一時停止→再開させたい場合は、こう。
animationManager.stop(); //停止
// ...何かの処理...
animationManager.start(); // 再開
という感じのライブラリなんだけど、これを最近mozilla-centralに入った新しいアニメーションのためのAPIに対応させてみた。APIとしての変更は、addTask()
の最後の引数にDOMWindowを受け取るようになったという点のみ。
animationManager.addTask(
task,
parseInt(getComputedStyle(tab, null).marginLeft),
50,
250,
window // アニメーションを行うウィンドウ
);
リンク先で紹介されているmozRequestAnimationFrame/MozBeforePaintが利用できる環境ではそれを使用して、そうじゃない時は今まで通りsetInterval()
で処理する。ライブラリを使う側のコードは、Firefoxのバージョンの違いを意識する必要は全く無い。ただ、APIを生で使う時に比べるとオーバーヘッドがあるから、新APIのメリットぶちこわしかもしんない。
リンク先のエントリを見た感じでは、理屈としては一般的な可変フレームレートのアニメーションの考え方に基づいているみたいなんだけど、DOMのイベントをトリガーにしないといけなかったり次のフレームを描画するためにいちいちメソッドを呼ばないといけなかったりというのは煩わしいと思ったので、せっかくだからライブラリ側で面倒を見るようにしてみた。
openWindow()
って何ですかアドオンで新しいChromeウィンドウ(特権付きのウィンドウ、XPCOMとか自由に使えるやつ。ブラウザウィンドウ等、Firefoxのアプリケーションとしてのウィンドウはだいたいそう。)を開く方法はいくつかある。
window.openDialog()
を使うopenWindow()
を使う普通にXULドキュメントの中で読み込まれてるスクリプトからやるのなら、1の方法でいい。
でも、JavaScriptコードモジュールやXPCOMコンポーネントのスクリプトのように、グローバルオブジェクトがDOMWindowじゃない場面ではこの方法が使えない。特にFirefoxが起動した直後でまだブラウザウィンドウすら開かれていないという場面では、nsIWindowMediatorのgetMostRecentWindow()
でDOMWindowを取得してそのメソッドを呼ぶ、というようなこともできない。なのでこういう時は2の方法を使わないといけない。
window.openDialog()
の引数は window.open()
と同じだ。
'_blank'
決めうち)'chrome,all,dialog=no'
とか'chrome,all,modal'
とかそういうの)window.openDialog()
の場合はこの後に続いてさらに引数を指定することができて、第4引数以降に渡した物がそのまま、開かれたウィンドウの中でwindow.arguments
として参照できるようになっている。例えば
var w = window.openDialog(url, '_blank', 'chrome,all',
arg0, arg1, arg2);
こう指定して開かれたウィンドウでは
window.arguments[0] // => arg0の値
window.arguments[1] // => arg1の値
window.arguments[2] // => arg2の値
となる。Firefoxのブラウザウィンドウは、コマンドライン引数で渡されたURI等をこうやって受け取っている。
これと同じことをnsIWindowWatcherのopenWindow()
でやろうとして、うまくいかなかった。
openWindow()
の引数は
'_blank'
決めうち)となっていて、最初の引数が加わったことを除けば2~4はwindow.openDialog()
の第1~第3引数と同じ。問題は、開かれるウィンドウに引数を渡す方法が違うということ。window.openDialog()
と同じ感覚で引数を渡しても、期待通りに受け渡されない。
var WW = Cc['@mozilla.org/embedcomp/window-watcher;1']
.getService(Ci.nsIWindowWatcher);
// これはダメ
WW.openWindow(null, url, '_blank', 'chrome,all',
arg1, arg2, arg3);
// これもダメ
WW.openWindow(null, url, '_blank', 'chrome,all',
[arg1, arg2, arg3]);
// これは場合によってはOK
WW.openWindow(null, url, '_blank', 'chrome,all',
arg1);
Function.prototype.call()
では引数を普通に並べてFunction.prototype.apply()
では配列で渡す、という様式を真似てJavaScriptの配列を渡してみても、WindowWatcherは「なんですかコレ」って感じでこっちの意図を読み取ってはくれない。開かれたウィンドウの方でwindow.arguments
を見てみても、長さ1の配列になってて、その要素はなんだかよく分からないnsISupportsのオブジェクトになってる。
引数を1つだけ渡す場合だとうまくいくことがあるというのは、このメソッドの第5引数の型がnsISupportsだからだ。nsISupportsをを実装してるオブジェクトならWindowWatcherはちゃんと受け取ってくれて、開かれた側のウィンドウでもwindow.arguments[0].QueryInterface()
して使うことができる。
でもやりたいことはあくまで、複数の引数をウィンドウに渡して、開かれたウィンドウの側でwindow.arguments
で配列の要素としてそれぞれを受け取ることなのですよ。というか、開かれる側のウィンドウがそういう実装になっているため、どうしてもその様式で引数を渡さないといかんのです。
XPCOMの世界で配列を扱わないといけない場面でたまーにnsISupportsArrayというのが出てくる。これは名前の通り配列のような性質を持つインターフェースで、openWindow()
の第5引数はこのインターフェースを実装してるオブジェクトも受け付けるという風にIDL定義には書いてある。
似たような感じでプリミティブ値に対応するようなXPCOMのインターフェースがいくつかあって、nsISupportsStringとかnsISupportsPRBoolとかnsISupportsPRUInt64とかそういうのがいっぱいある。これらのインスタンスをnsISupportsArrayに格納してopenWindow()
に渡してやれば、どうやら、開かれた方のウィンドウでは対応するJavaScriptのプリミティブ値としてそれらを受け取れるらしい。
が、こんな事真面目にやってらんないですよね。値の型に合わせてインターフェースを使い分けてインスタンスを作って……とか、めんどくさすぎる。
幸い、XPCOMにはnsIVariantという便利な物があって、これのsetFromVariant()
にJavaScriptの値を適当に渡してやると、あとはnsIVariant君がよろしくやってくれるのです。これを使わない手はない。
var JSArray = ['string', true, 29];
var array = Cc['@mozilla.org/supports-array;1']
.createInstance(Ci.nsISupportsArray);
JSArray.forEach(function(aItem) {
var variant = Cc['@mozilla.org/variant;1']
.createInstance(Ci.nsIVariant)
.QueryInterface(Ci.nsIWritableVariant);
variant.setFromVariant(aItem);
array.AppendElement(variant);
});
WW.openWindow(null, url, '_blank', 'chrome,all', array);
これでめでたく、複数の引数をWindowWatcherからも渡せるようになりました、と。
これで一件落着と思ってたんだけど、この方法が使えない場合があることが分かった。nsIVariantはプリミティブ値でもXPCOMのオブジェクトでも何でも受け渡せる万能なヤツなんだけど、nsISupportsArrayと組み合わせてWindowWatcherに渡す時は、XPCOMのインターフェースを持ってないJavaScriptのネイティブのオブジェクトはこの方法では渡せなかった。
var variant = Cc['@mozilla.org/variant;1']
.createInstance(Ci.nsIVariant)
.QueryInterface(Ci.nsIWritableVariant);
variant.setFromVariant({ prop : value });
こうやって値を設定した物をnsISupportsArrayに渡しても、開かれたウィンドウの方ではよく分からんnsISupportsのオブジェクトになってしまって、元のオブジェクトが持ってた情報を取り出せなかった。
回避方法として思いつくのは
JSON.stringify()
して渡して、受け取り側でJSON.parse()
するくらい。JSONにできない物は2の方法でやるしかないと思う。
var bag = Cc['@mozilla.org/hash-property-bag;1']
.createInstance(Ci.nsIWritablePropertyBag);
for (var i in hash)
{
if (hash.hasOwnProperty(i))
bag.setProperty(i, hash[i]);
}
で渡して、受け取った側で
var hash = {};
var enum = window.arguments[0]
.QueryInterface(Ci.nsIPropertyBag)
.enumerator;
while (enum.hasMoreElements())
{
let item = enum.getNext()
.QueryInterface(Ci.nsIProperty);
hash[item.name] = item.value;
}
という風にしてハッシュに戻す。とか。
ウィンドウ間の情報の引き渡しはマジ鬼門。
あと、上に書いた諸々の処理をライブラリとしてまとめておいたので、同じような事をしようとしてる人は使うといいよ。
半分眠ったまんまで作業してたのでしょーもないregressionを仕込む→修正して公開→またregressionということを繰り返した結果、既に0.13.3になってしまったわけですが、バージョン0.13.0で結構大きくいじりました。機能的には全然変わってないですが。
nodesFromRect()
を使うようにしたので、見えているスクロール位置から検索を始める処理が相当速くなりました。nodesFromRect()
の効果と合わせて、Minefield 4.0b2preではページ内検索のストレスがほとんど無くなったと思います。Minefieldで検索が速くなったのは、Bug 39098 – Elements with visibility:hidden, visibility:collapse, or display:none get copied to the clipboardがfixされたおかげです。
toString()
で文字列にすると、CSSで非表示になっている要素のテキストまで一緒に取得されてしまうのですが、その後rangefindで検索する時は非表示のテキストは検索対象にならないため、場合によってはヒット箇所が飛ばされてしまったり検索が止まってしまったりという問題が起こります。DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsISelectionDisplay)
.QueryInterface(Ci.nsISelectionController)
.checkVisibility(textNode, 0, textNode.nodeValue.length)
というやり方で個々のテキストノードの可視・不可視の状態を判別して不可視のテキストを除外した結果を取得して、正規表現のマッチングに使うようにしていました。Minefield 4.0b2preにXUL/Migemo 0.13.xを入れてMXRのページでページ内検索してみると、効果の程がよく分かると思います。僕は、今すぐにでもFirefox 3.6を窓から投げ捨ててしまいたくなりました。
あとはJägerMonkeyが入ってくれれば……
ところで、他のアドオンからXUL/MigemoのAPIを呼び出す時はちょっと注意が必要になってます。
migemo
オブジェクトから機能を呼び出す時には常にシステム辞書の内容だけを返すように仕様を変えました。Components.utils.import('resource://xulmigemo-modules/service.jsm')
でユーティリティを読み込んでXMigemoCore
の各メソッドを呼ぶか、Cc['@piro.sakura.ne.jp/xmigemo/factory;1'].getService(Ci.xmIXMigemoFactory).getService('ja')
てな感じでXPCOMコンポーネントを直接呼び出すかする必要があります。XMigemoCore
でもxmIXMigemoCoreでもどっちにしても、これらが持つ機能で正規表現を生成する時は戻り値は常に文字列(正規表現のソース文字列)になるので、その都度new RegExp()
してやる必要があります。migemo
のメソッドを使って問題ありません。XUL/MigemoのUI周りでもそうしてます。nsIDOMWindowUtilsのnodesFromRect、解説がなくてさっぱり使い方が分からなかったので使ってみて調べた結果を記しておく。
インターフェースの定義はこんなん。
nsIDOMNodeList nodesFromRect(in float aX,
in float aY,
in float aTopSize,
in float aRightSize,
in float aBottomSize,
in float aLeftSize,
in boolean aIgnoreRootScrollFrame,
in boolean aFlushLayout);
8個もの引数を取るんですよ……
そういう経緯で実装されたので、第1から第6までの引数は経緯から考えたら納得のいく指定の仕方と言えよう。
0, 0
になる(scrollX, scrollY
を指定すると「ビューポートの左上からscrollX, scrollY」という意味になってしまうので注意!)。戻り値のノードリストは、どういう訳か文書中の登場順の逆順で返ってくるようだ。
作業メモ。
window.watch('gFindBarInitialized', function() { ... })
で、遅延初期化のタイミングで初期化を行える。classDescription
にホワイトスペース文字が入ってると、カテゴリへの登録がうまくいかないかも。nsUpdateTimerManagerに対応するマニフェストファイルなんかを見てみた感じでは、実装のクラス名をそのままclassDescription
にするといいのだろうか?しないといけないなーと思ってる課題。
スマートロケーションバー関係を調べて分かった事。
moz_openpages_temp
という名前のテーブルに保存している。
place_id
とopen_count
という2つのカラムを持ち、ページの遷移に応じて内容が随時更新される。moz_openpages_temp
のopen_count
が0より大きい物は、オートコンプリートの候補として返される時にURIの前にmoz-action:switchtab,
という文字列が付与され、lichlistitemのtype
属性用の値にはaction
という値が設定される。autocompletesearchparam
属性の値にenable-actions
という文字列が含まれている時だけのようだ。
moz-action:switchtab,
という文字列が付与され~」という処理がスキップされる。moz_openpages_temp
のopen_count
はオートコンプリートの候補の並び順には影響しない。あくまでfrequencyベースで検索していて、ヒットした候補の中にたまたま現在タブで開かれているplaceが含まれていた場合にだけ、この機能が発動するという仕様のようだ。