Home > Latest topics

Latest topics 近況報告

たまに18歳未満の人や心臓の弱い人にはお勧めできない情報が含まれることもあるかもしれない、甘くなくて酸っぱくてしょっぱいチラシの裏。RSSによる簡単な更新情報を利用したりすると、ハッピーになるかも知れませんしそうでないかも知れません。

萌えるふぉくす子さんだば子本制作プロジェクトの動向はもえじら組ブログで。

宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

Page 6/248: « 2 3 4 5 6 7 8 9 10 »

「巻き戻し/早送りボタン」でクラッシュする件 - Mar 16, 2011

14日くらいから、Rewind/Fastforward Buttonsを入れてるとFirefoxが頻繁にクラッシュするという現象が発生するようになっているようです。wedataのAutoPagerize用データベースに新しく追加された項目がトリガーになって、長すぎる正規表現か何かの制限に引っかかるようになってクラッシュしているものと考えられます。問題としては認識しているのですが、現在修正のための時間を取れない状態なので、アドオンを無効化するか、about:configで以下の設定を変更して回避して下さい。

  • rewindforward.related.use.siteInfo →falseに変更
  • rewindforward.siteinfo.importFrom → ""(空文字)に変更

Compatibility problem with Tab Mix Plus - Feb 07, 2011

I got a mail from Tab Mix Plus developers team. So I updated compatibility codes in Tree Style Tab for the latest dev-build of TMP. After that I got another mail again, and he said that the latest TST doesn't work with the last public release of TMP. This is the reply:

Hello, onemen.

First, I really think TMP helps very huge people from poor tabbed browsing features of Firefox itself. It is a great thing. So, I hope my addons including TST work with TMP correctly.

However, I'm afraid I can't support both versions of TMP (the latest dev-build and the last public version) anymore, because I believe that they are too different to support simple hacks. I already removed many codes based on the latest dev-build of TMP by this commit, so I can't believe TST works with the last public release only with minor changes only about symbols (function names). And, if I restore codes for old TMP, then both old and latest TMP will override them again and re-introduce many unexpected problems. That is too terrible.

To be honest, it was very painful to read dynamic-eval codes in TMP and TST itself because they are many tree-times eval-ed codes (defined by TMP => overridden by TST => overridden by TMP again). So I don't want to do such a painful work again for the last public release...

Yes, I apologize that I'm also using many eval() to hack TMP. So I believe that both addons TMP and TST should remove all eval-based hacks for each other and make themselves plaggable via their public APIs. TST already defines some public APIs. I agree that they are too less APIs to make compatible TMP with TST now. I'll add new APIs to do it if they are really required for high compatibility. I'll make efforts to keep stable those APIs in future versions. I don't know what APIs are required for TMP, so, I hope to listen your idea.

On the other hand, I hope that TMP provides some public (and stable) APIs for addon developers, like:

  • An API to add extra properties for TabmixSessionData
  • Custom DOM events for TMP specific features

If there is any public document already, could you tell me the URI?

regards,

I can't believe that I keep the current method (eval-based dirty hacks) to make them compatible.

Bootstrapped Extensionsで設定ダイアログを提供するためのライブラリを作ったよ - Jan 28, 2011

Firefox Developers Conference 2010の時に再起動不要なアドオンの作り方を調べた時、調査の成果を元にJetpack SDKもといAdd-on SDKを使わずにFirefox 3.6とMinefieldの両方に対応したBootstrapped Extensionを作るためのテンプレを作った。それ以後、新しく作るアドオンでBootstrapped Extensionにできそうな物はなるべくそのように開発していこうと思ってて、Back to Owner Tabは実際そうして作ったアドオンの第1号ということになる。

SDKの使い方や仕組みを調べた時にもちょっと思ったんだけど、Back to Owner Tabを作って、「設定ダイアログを持てないのがこんなに辛いとは……」ということを改めて感じた。しかしBootstrapped Extensionで設定ダイアログを提供するのは簡単には実現できなさそうだったので、ずっと諦めてた。

今回、うまく解決できそうな方法をふと思いついて、実際試してみたら案外うまくいったので、テンプレの一部としてライブラリ化してみたBack to Owner Tabの設定ダイアログもこれで提供してる

Bootstrapped Extensionで設定ダイアログを提供するのは難しい

Firefox 2以降ではXULにprefwindowというウィジェットが存在していて、これはFirefox自体の設定ダイアログにも使われている物で、非常に出来がよい。今ある拡張機能の多くも、これを使って設定ダイアログを提供している。

prefwindowができるまでは「設定値を保持して、変更を監視して、変更があったらUIに反映して、ダイアログがキャンセルされたら変更を破棄して……」てな事をいちいち考えてゼロから設定UIを設計しなきゃいけなかったから、非常に辛かった。そういう思いがあるので、Bootstrapped Extensionでも是非これを使いたかったんだけど、Bootstrapped Extensionの「Chrome URLを登録できない」という制限のせいで無理だった。

  • prefwindowも含めて、XULを使うにはChrome URLでFirefoxにファイルを読み込ませないといけない。
    • Minefieldではfile:やresource:で始まるURLでXULを読み込ませても表示されなくなった(リモートXULの廃止)。
  • 仮にXULファイルを読み込ませることができても、表示する文字列をローカライズできない。
    • XULでの多言語対応で欠かせないDTDファイルは、セキュリティの制限によりChrome URLに置かれた物(chrome://foobar/locale/foobar.dtdなど)しか読み込めない。
    • propertiesファイルから文字列を読み込んでJavaScriptで動的に埋め込むということは無理ではなさそうだけど、死ぬ程めんどくさい。XULの「タグで書くだけでいい」「静的なファイルに内容をまとめておける」という利点が完全に死ぬ。

どうしてもやりたければ、XUL以外の方法で頑張って設定UIを作るか、XULで物凄く苦労して作るしか無いってことになって、それはどっちも嫌だった。せっかくprefwindowという素晴らしい物が目の前にあるのに、それを使わないでオレオレUIをゼロから作るなんて、馬鹿馬鹿しくてやってられない。

せめて最新のSDKだったら設定ダイアログを作る機能が入ってたりしないだろうか? あるんならそれを丸パクリできないかな? と思ってたんだけど、SDKに設定ダイアログのためのAPIが入るのはまだ先のことらしいと聞いて、それも諦めざるを得なかった。

それで仕方なく「about:configでカスタマイズしてね」ということにしてたんだけど、まあ普通に考えてこれはエンドユーザ向けとしては酷い話なわけですよ。自分で使う時も、マウス操作だけで設定を変えられないのは困る。

解決の糸口

そんな風に悶々としていて、ふと、「そういえばuserChrome.jsとかではdata: URIでXULドキュメントを動的に作ってたよな」ということに思い至った。試しにdata: URIにprefwindowで作った設定ダイアログの内容を突っ込んでnsIWindowWatcherのopenWindow()でウィンドウとして開いてみたら、少なくともMinefield 4.0b11preでは正常に動いてくれた。data: URIでドキュメントを読み込んだりウィンドウを開いたりする時はオープン元のスクリプトの権限が引き継がれるっぽいので、file: URLでXULを読み込ませた時のような問題も起こらないようだ。

また、E4Xを使えばJavaScriptの中に直接XMLを書けて、しかも属性値にJavaScriptの変数を埋め込める。これなら、propertiesファイルベースでのローカライズでもラベル等を埋め込むのが苦にならない。で、そうして作ったE4XのXMLオブジェクトをtoXMLString()すれば、data: URIで読み込ませるダイアログのためのソースコードが得られる。

ということで、「XULのソースコードを文字列として受け取ってdata: URIにしてウィンドウを開く機能」と「アドオンマネージャでアドオンの設定を開くボタンが押下された時に前述の機能を呼び出す機能」を作れば、Bootstrapped Extensionでもprefwindowベースの設定ダイアログを提供できる事が分かった。

propertiesファイルでローカライズする

propertiesファイルの内容はローカライズ済みの文字列をXULのstringbundle要素を介して取得する方法が一般的だと思うけど、より低レベルな実装のnsIStringBundleServiceを直接呼べば、JavaScriptコードモジュールやBootstrapped Extensionの中からでもpropertiesファイルの中身を簡単に取り出せる。(stringbundle要素はXBLでそういう処理を行うようにしてあるに過ぎない。)

上記のテンプレには、nsIStringBundleServiceをラップしてstringbundle要素と同じインターフェースで使えるようにするライブラリを既に入れてある。

var bundle = require('path/to/lib/locale.js')
               .get('file://.../messages.properties');
var title = bundle.getString('config.title');

という風にすると、ブラウザのUIの言語がjaでmessages.properties.jaというファイルがあればそれが使われて、存在しない場合はmessages.properties.en-US→messages.properties(サフィックス無し)とフォールバックしていく。

Back to Owner Tabに入ってるバージョンのライブラリには含まれてないけど、テンプレのHEADでは resolve('path/to/file') とすると現在のファイルを起点にして相対パスを解決できるようにしてあので、こういう風にも書ける。

var bundle = require('path/to/lib/locale.js')
               .get(resolve('locale/messages.properties'));

なお、Bootstrapped Extensionの場合は読み込んだpropertiesファイルの内容がキャッシュされたままになってしまうとまずいので、shutdown()の時にはnsIStringBundleServiceのflushBundles()というメソッドを呼んで、メモリ上のキャッシュを消すようにする必要がある。このライブラリでもそうしてて、自分でnsIStringBundleServiceを使う時はここに気をつけないといけない。

E4Xで作ったXULコード片から動的にChromeウィンドウを開く

Bootstrapped ExtensionやJavaScriptコードモジュールの名前空間からで既存のDOMWindowを取れない状態でウィンドウを開きたい場合には、nsIWindowWatcherのopenWindow()を使う必要がある。このメソッドに渡すURIとしてdata: URIを指定すれば、その内容のウィンドウがChrome権限で開かれる。

var uri = 'data:application/vnd.mozilla.xul+xml,'
         + source;
Cc['@mozilla.org/embedcomp/window-watcher;1']
  .getService(Ci.nsIWindowWatcher)
  .openWindow(null, uri, '_blank',
              'chrome,titlebar,toolbar,centerscreen',
              null);

ソース文字列の生成は、E4Xを使うと楽にできる。

var xml = <>
      <prefwindow id="backtoowner-config"
                  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
                  title={bundle.getString('title')}>
        <prefpane id="prefpane-general"
                  label={bundle.getString('general')}/>
      </prefwindow>
    </>;
var source = '<?xml version="1.0"?>'
             +'<?xml-stylesheet href="chrome://global/skin/"?>'
             +xml.toXMLString();
source = encodeURIComponent(source);

ラベル文字列等に日本語が入る場合を考慮して、ソース文字列はdata: URIに繋げる前にエスケープしておこう(これをしないと文字化けする)。

基本的にはこれでいいんだけど、実際このまま使うと何となく気持ち悪い。というのも、prefwindowは画面上でのウィンドウの位置や大きさ、最後に選択されていたパネルの名前等をプロファイル内のlocalstore.rdfに自動的に保存するんだけど、この時保存されるデータはウィンドウのURIをキーとして紐付けられているから、data: URIのウィンドウだとlocalstore.rdfが肥大化してしまう。しかも、設定項目を追加したりしてウィンドウの中身が変わるとdata: URIも変わるから、前回情報を保存した時のエントリとは別のエントリが作られてしまって、古いエントリは自動的には消えないから、localstore.rdfが際限なく膨らんでいってしまう事になる。長く使うことを考えたら、これはよくない。

で、何かいい方法はないか考えてみた。

  1. data: URIは必要最小限の長さの固定の物にして、document.write()でウィンドウの中身を動的に書き換える。
  2. data: URIは必要最小限の長さの固定の物にして、DOM操作でウィンドウの中身を動的に書き換える。

nsIWindowWatcherのopenWindow()は開かれたウィンドウのDOMWindowオブジェクトを返す。なので、こんな風にできないかと最初は考えた。

var window = ww.openWindow(...);
window.document
  .open('application/vnd.mozilla.xul+xml');
window.document.write(source);
window.document.close();

が、駄目だった。この方法でドキュメントを書き出すと強制的にHTMLのパーサーが使われてしまうらしく、XULのソースを書き出しても全く動作しない状態になってしまった。

次に、DOM操作でやることを考えた。

XUL Documentの中でRangeを作ってcreateContextualFragment()すると、XULのソース文字列からDOMDocumentFragmentを作ることができる。こうしてルート要素も含めたドキュメント全体を作り直してゴソッと入れ替えたらどうなるだろうか。

var window = ww.openWindow(...);
var range = window.document.createRange();
range.selectNode(window.document.documentElement);
var fragment = range.createContextualFragment(xml.toXMLString());
window.document.replaceChild(fragment, window.document.documentElement);

実際やってみたら、これは期待通りには動いてくれなかった。多分、開かれたウィンドウの内容がまだ完全には読み込まれ切っていない状態でDOM操作を外から無理矢理やろうとしたからなんだろうと思う。

開かれたウィンドウのDOMContentLoadedを待ってからDOMを操作すればprefwindowについてはちゃんと動くみたいなんだけど、dialogとかのもっと汎用的な物も受け付ける事や、ダイアログの中にDOMContentLoadedを待って処理を開始するようなコードを含めたい時に、それでは問題が起こる気がする。

そういうわけで今のバージョンでは、ウィンドウを開く時にnsIVariantの形でソース文字列を渡して、開かれた側のウィンドウの中に1つだけ置いたscript要素でarguments[0]を受け取ってすぐにドキュメントを書き換えるようにしてみている。要点だけ抜き出すとこんな感じ。

var variant = Cc['@mozilla.org/variant;1'].createInstance(Ci.nsIWritableVariant);
variant.setFromVariant(source);

ww.openWindow(
  null,
  'data:application/vnd.mozilla.xul+xml,'
  +encodeURIComponent(
   '<?xml version="1.0"?>\n'
   +'<!-- ' + aURI + ' -->\n'
   +'<?xml-stylesheet href="chrome://global/skin/"?>\n'
   +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
      <script type="application/javascript"><![CDATA[
        var d = document;
        var e = d.documentElement;
        var r = d.createRange();
        r.selectNode(e);
        d.replaceChild(r.createContextualFragment(arguments[0]), e);
        r.detach();
      ]]></script>
    </window>.toXMLString()
  ),
  '_blank',
  'chrome,titlebar,toolbar,centerscreen',
  variant
);

URIをコメントで埋め込んでるのは、複数のアドオンでこのライブラリを使ってる時に衝突しないようにしたかったから。

実はここに辿り着くまでに結構紆余曲折があった。最初はルート要素を差し替えられるという事に気づいていなかったので、「ルート要素の属性値の情報」と「子孫要素のソース」をそれぞれ取り出してやってみていた。E4Xは実はよく分かってないからちょっと苦労した。

var content = xml.*.toXMLString(); // 子孫要素だけ抽出
var container = xml.copy(); // ルート要素だけ取り出すためにまず一旦全体を複製
var attributes = container[0].attributes(); // ルート要素の属性のリストを取得
var attributesHash = {};
for each (var attribute in attributes) { // 属性名と値のペアのハッシュにする
  let name = attribute.name();
  attributesHash[name] = attribute.toString(); // 属性値はtoString()で取れる
  delete container[0]['@'+name]; // removeAttribute()に相当する操作はこう書く
}
// 子孫要素を削除して、コンテンツ差し替え用のスクリプトに入れ換える
delete container.*;
container.script = <script type="application/javascript">{this._loader}</script>;
container = container.toXMLString();

最初はこれでいいかと思ってたんだけど、コンテンツ差し替え用のスクリプトが走ってる時点で既にprefwindowのXBLのコンストラクタが実行されてしまっていて、複数のprefpaneがあるときの切り替え機能が働いてなかった。

ルート要素をもう一度作りなおしてドキュメントに挿入すればXBLのコンストラクタが実行されるはず、ということでdocument.removeChild(document.documentElement);の後でdocument.appendChild(newRoot);みたいな風な事も試してみたけど、一瞬documentElementが無くなってしまうのがマズいようで、他のどっかの処理がルート要素を参照するタイミングとぶつかってエラーになってしまった。ここはdocument.replaceChild(newRoot, document.documentElement);で一気に差し替えてしまうのが正解だった。昔のGeckoはreplaceChild()でクラッシュする事があったような気がするので無意識に使用をずっと避けてきてたんだけど、Firefox 3.6以降ともなるとさすがに問題なく動いた。

あと、生成した新しいルート要素を挿入しようとするとエラーになる場合もあって、その時は挿入しようとしているノードをdocument.importNode()に1回通してやれば動いた。まあ最終的にはそれ無しで乗り切れたんだけど。

アドオンマネージャからアドオンの設定ダイアログを開こうとした時に、動的に生成したダイアログを開く処理を呼ぶ

これも難儀した。

Firefoxのアドオンマネージャは、install.rdfのoptionsURLに書かれてるURIをwindow.openDialog()で(Mac OS X以外では)モーダルダイアログとして開くようになっている。しかし前述の通りXULの設定ダイアログを読み込ませたかったらChrome URLにしないといけないので、これはそのままは使えない事になる。

とはいえ、optionsURLにはChrome URLしか使えないという話ではない。

最初は、ここに直接javascript:ダイアログを開くためのコードという感じのスクリプトを書く事を考えてみた。でもメタデータの置き場所であるinstall.rdfにそういう実装を含めるのはどうかと思うし、そもそもここにjavascript:スキームを使って書いたスクリプトは普通にコンテンツ領域の権限でしか実行されないようなので、nsIObserverServiceを使ってイベントを発行するとかそういう高度な事は全然できなかった(実際試して無理だった)。

次に、nsIWindowWatcherでの監視とかnsIObserverServiceを介して送られるchrome-document-global-createdとかcontent-document-global-createdとかの通知でもって「あらかじめ登録されていたURI」が読み込まれた事を検知して、その読み込み(ウィンドウのオープン処理)をキャンセルして別の設定ダイアログを開くという事を考えた。

これは結構イイ線まで行ったんだけど、「Firefoxのアドオンマネージャが開いたウィンドウ」が一瞬だけ見えてしまうという点をどうしても回避できなかった。openDialog()の実装を辿ってnsGlobalWindow::OpenDialog()nsGlobalWindow::OpenInternal()nsWindowWatcher::OpenWindowJS()nsWindowWatcher::OpenWindowJSInternal()と掘り返して、何か抜け道はないか探しまくったんだけど、内部で新しいDOMWindowが生成されてウィンドウが実際に画面に出現する事がほとんど確定した後で初めてnsIDocShellのloadURI()にURIが渡されてる(この間URI文字列はC++の普通の変数で引き回されてるだけでJavaScriptの層からは触りようがない)みたいで、ウィンドウがユーザの目に触れる前にどうこうってのは原理的に無理っぽい。

いや、nsIContentPolicyインターフェースを備えたXPCOMコンポーネントを作ってカテゴリマネージャに登録すれば、もしかしたらそこに割り込む事はなんとかできるかもしれない。けど、設定ダイアログ1つ開くためだけにそこまでする元気はなかったし、求める機能に対してコードが膨大になりすぎるから普通にこれはないわーって感じだしで、そういう方向性は最初から切り捨ててる。

結局、すべてのドキュメントの読み込みをchrome-document-global-createdとcontent-document-global-createdの通知で捕捉して、アドオンマネージャのウィンドウが開かれた時にはボタンのクリック操作を監視するイベントリスナを登録し、「設定」ボタンをユーザがクリックした瞬間を捕捉して、登録済みのURIだったらイベントをキャンセルして別のダイアログを開く、という所に落ち着いた。これだとアドオンマネージャの実装(現在どのアドオンが選択されているのか、そのアドオンの設定ダイアログのURIは何なのか、を取得する方法)にべったりになってしまわざるを得ないんだけど、まあその辺のレイヤだったら変更があっても追随するのはそう大変じゃなかろうと思って、妥協する事にした。

まとめ

冒頭に書いたけど、ここまでのまとめがBootstrapped Extensionsテンプレートライブラリとして入ってる。他のファイルの内容に可能な限り依存しないように作ったつもりなので、改造再利用も不可能ではないと思う。実際にどう使うのかというのはBack to Owner Tabでの利用例が参考になるかもしれない(って単にE4XでダイアログをJavaScriptのコードの中に埋め込んでるだけだけど)。

ほんとはFirefox 4に向けて記事を書いていかなきゃいけないんだけど、こういう「あれも駄目でした、これも駄目でした、結局こういう所に落ち着かざるを得ませんでした」という紆余曲折は本に入れてもウケないんじゃないかなーって思ったので、ここに書く事にした。また次に同じような事をしようと思った時に同じ轍を踏まなくてもいいように。技術情報のドキュメントは、未来の自分のために残しておく物なのです。

XBLとアドオンとDOMツリーの切り貼りとツールバーカスタマイズの危険な関係 - Jan 23, 2011

ツリー型タブPersonal Ttitlebarと併用できるようにするためにずいぶん骨を折った。

Firefox 4のタブバーの仕様

Firefox 4ではタブバーとtabbrowser要素がDOMツリー的に切り離されて、tabs要素がカスタマイズ可能なtoolbarの中に置かれるようになった。この話は以前に勉強会で発表した(この資料の日付を見て「うわーもうあれから1年近く経とうとしてるのか、Firefox 4の開発どんだけ遅れとんねん」とか「そんな昔のことを『ちょっと前に発表しましたが』とか書こうと思ってしまった自分ってどないやねん」とか思ったのですがそれはどうでもいいです)んだけど、その時こんな話をした。

  • 今まではorientとordinalやdirectionで簡単にタブバーの位置を右や左や下には動かせたが、それができなくなった。
  • Tab Mix Plusは、タブバーのtabs要素をDOMツリーから一旦切り離してtabbrowser要素の下にappendChildで再挿入することで、タブバーの位置を変えるようにしたようだ。
  • ツリー型タブでは、DOMツリーは触らずに、toolbarの表示位置をposition:fixedで動的に位置を合わせることで擬似的にタブバーの位置を変えたように見せかける方法を採ることにした。
    • この話をしたら会場から失笑の渦が巻き起こった。「無茶しよるなー」的な。

その時も特には説明しなかったんだけど、何故僕はそうまでしてDOMツリーの変更を避けたがるのか。今回Perosnal Titlebarとの衝突を解決するためにここで散々悩まされたので、改めてここに記しておこうと思う。

Firefoxのツールバーと絡むアドオンを作る時に注意する事

Firefoxのツールバーの各機能は、ユーザが任意にドラッグ&ドロップで並べ替えたりアイテムを追加・削除したりできるようになっている。この時内部的には当然だけどDOMツリーが動的に切り貼りされている。

この時、ツールバーから削除されたボタンに対してDOMのイベントを監視し続けるのは変だし、そのボタンへの参照をアドオンの中の変数でずっと持ったままだと、いわゆるメモリリークが起こることもある。なので、僕はツールバーにボタンを追加するアドオンについては、ツールバーのカスタマイズに入る前に一旦「ボタンの終了処理」を行って、ツールバーのカスタマイズが終わった時点でもう一度「ボタンの初期化処理」を走らせるようにしていることが多い。

ちなみに、こういう事をするためにDOMイベントが使えればいいんだけど、Firefox 3.6までのバージョンではそんな配慮は全然無い不親切な設計なので、カスタマイズを開始する関数の BrowserCustomizeToolbar() とかカスタマイズ終了時に呼ばれる BrowserToolboxCustomizeDone() とかツールボックスの customizeDone() メソッドだとかを上書きして処理を挟み込んでやらないといけなかった。Minefieldではいつの頃からか beforecustomization とか aftercustomization とか customizationchange とかのイベントが発行されるようになったので、こういうダーティなことはやらなくても済むようになった。もっと早くからこうしてくれていればよかったのに……いやそれは今はどうでもいい。

自分で追加したツールバーのボタンについてそういう事が必要なのと同様に、例えば標準のWeb検索バーの挙動を変更するセカンドサーチのような「既存のツールバーボタンの挙動を変える物」も、同じような事をしないといけない。特に、既存のボタンにXBLで追加されたメソッドを後から上書きしているようなケースでは。セカンドサーチの例で言うと、こういう感じだ。

  • セカンドサーチはWeb検索バーにXBLで追加されたメソッド handleSearchCommand() を上書きする。
    • このメソッドが上書きされていることを前提として、他の機能も成り立っている。
  • Web検索バーがDOMツリーから一旦取り除かれ、再度挿入されると、その時点でXBLの定義が再度適用される。そのためセカンドサーチが上書きしたはずの handleSearchCommand() メソッドも元に戻ってしまう
    • Web検索バーがツールバーからパレットに移動されなくてもそうなる。何故なら、ツールバーのカスタマイズ時にはすべてのカスタマイズ可能な要素が一旦DOMツリーから取り除かれた後、toolbarpaletteitem という要素にappendChild()されて、元の要素があった位置に挿入される、という処理が行われるから。
    • ツールバーのカスタマイズを完了する時はこの逆の操作が行われる。つまり、すべての toolbarpaletteitem 要素が削除されて、代わりに、その中にあった要素が toolbarpaletteitem があった位置に再挿入される。

Firefox 4でタブバーがツールバーに移動された時も、これと同じ事が起こるんじゃないかとちょっと思ってたけど、実際はそうならなかった。Firefoxの他の部分が「タブがある事」を前提に設計されているせいで、タブバーをツールバーカスタマイズでツールバー上から完全に取り除いたら、他の機能が色々と破綻してしまうから……という事なんだと思われる。なので今のMinefieldでは、タブバーのtabs要素はメニューバーと同様に「ツールバーの中にあるけど動かせない要素」という扱いになっている。

そういう事情で、Firefoxもタブバーまわりのコードは「DOMツリーが動的に編集されても動作するような堅牢に設計」にするための注意は払われていないし、他のタブ周りのアドオンもそういう設計にはなっていない事が多い。というか、そういうアドオンがあまりに多いからFirefox本体の側でもタブバーをカスタマイズで移動できないようにせざるを得なかったって事なんじゃないかと思うんだけど。

Personal Titlebarを使った時に起こる事

Personal Titlebarというアドオンは、Firefox 4の(Windowsでの)タイトルバーに任意のツールバー用のボタンを配置できるようにするという物だ。それだけでなく、タブバーすらカスタマイズで移動できるようにしてしまう。それが問題だった。

前述した通り、Firefoxのツールバーはカスタマイズのモードに入っただけでもDOMツリーが切り貼りされる。Personal Titlebarがあるとタブバーのtabs要素もそういう処理の対象になる。ツールバーのカスタマイズに入るだけでもタブバーの各プロパティが初期化されてしまい、ツリー型タブが上書きしたメソッドも元に戻ってしまって、表示がグチャグチャにぶっ壊れる。これが衝突の真相だった。

これを回避するためには、単純に考えれば「ツールバーのカスタマイズに入る前にすべてを元に戻し、カスタマイズが終わったら再度初期化する」ということをやればいいということになる。でも話はそう簡単にはいかなかった。

beforecustomization のタイミングでできる事には限りがある

beforecustomizationイベントは、普通の同期型のイベントとして発行されていて、このイベントを捕捉したすべてのイベントリスナの中で処理が終わった後、ツールバーカスタマイズのための本来の処理が始まるようになっている。「このアドオンの終了処理は無事に終わりました。ツールバーのカスタマイズを初めても大丈夫ですよ。」という事を、アドオンの側が明示的に通知する手段はない。単にイベントリスナの処理が終わったらその時点で「終了処理が終わった、もうカスタマイズを初めても大丈夫だ」と判断されるような単純な設計だ。

一方で、ツリー型タブはタブバーの表示を縦にしたり横にしたりとダイナミックな切り替えをする時に、タイマーを多用している。何故かというと、XULの属性値やCSSのプロパティを変更した後、そのイベントループ中では結果が適用されないままなので、現在のイベントループを一旦終了させた上で、setTimeout()で続きの処理を次のイベントループ内で行わないといけない、という場面が多いからだ。

なので、beforecustomization イベントを捕捉した時点でタブバーの「終了処理」を行おうとしても、現在のイベントループの中でできる事までしか終了処理を終わらせられない。中途半端な状態でツールバーのカスタマイズに突入してしまう事になるし、しかも、次のイベントループに回すための続きの処理がツールバーのカスタマイズに突入した後で走ってしまうから、もうあっちもこっちも色々前提が崩れててシッチャカメッチャカな事になる。

必要なのは「今のイベントループの中で終了処理を完結させる」「次のイベントループを待つ必要がある終了処理を行う」この矛盾した2つをどうにかして両立させる事だ。

DOMイベントが発行されるまで今のイベントループを強引に停止させる

結論から言うと、XPCOMのスレッドの機能を使えばできる。

doAndWaitDOMEvent : function TSTUtils_doAndWaitDOMEvent() {
  var type, target, delay, task;
  Array.slice(arguments).forEach(function(aArg) {
    switch(typeof aArg) {
      case 'string': type = aArg; break;
      case 'number': delay = aArg; break;
      case 'function': task = aArg; break;
      default: target = aArg; break;
    }
  });

  if (!target || !type) {
    if (task) task();
    return;
  }

  var done = false;
  var listener = function(aEvent) {
      setTimeout(function() {
        done = true;
      }, delay || 0);
      target.removeEventListener(type, listener, false);
    };

  if (task)
    setTimeout(function() {
      try {
        task();
      }
      catch(e) {
        dump(e+'\n');
        target.removeEventListener(type, listener, false);
        done = true;
      }
    }, 0);

  target.addEventListener(type, listener, false);

  var thread = Components
          .classes['@mozilla.org/thread-manager;1']
          .getService()
          .mainThread;
  while (!done)
  {
    thread.processNextEvent(true);
  }
},

こんなユーティリティメソッドを作った。DOMイベントターゲット、そのターゲットに到達するはずのDOMイベント名、別のイベントループで実行されなくてはならない処理を含む関数、を引数として受け取り、渡された関数を実行して、今のスレッドの処理を止める。指定されたDOMイベントが発火したら、今のスレッドを止めるための無限ループから抜ける。これで、beforecustomization イベントのタイミングで複雑で長い終了処理を実行できるようになった。

まとめ

Personal Titlebarというアドオン1つとの衝突を解消する事だけ考えるなら、タブバーだけは移動できないようにPersonal Titlebarのコードに対してさらに変更を加えてしまうという手もあった。ただ、それでは「既にPersonal Titlebarでタブバーの位置が変更されていた所にツリー型タブがインストールされた」という場合には意味がないし、そもそもFirefox本体の側がタブバーをカスタマイズで位置変更できるようにする可能性もある(Add-on SDKベースで作るアドオンなら互換性が担保されるから、ということでSDKベースでない既存のアドオンをバッサリ切り捨てる事になるような変更もFirefox本体に入れやすくなるから)。なので頑張ってみた。

ただ、こういう事が起こる原因になるから、自分が作るアドオンの中では極力DOMツリーはいじらないようにしたいとは、今も変わらず思ってる。高速化のために最上位のDOMツリーをゴッソリ入れ換えるのもNGだし、ましてや、WebページのbodyのinnnerHTMLを文字列として一括置換するなんてのは論外だ(そういう事をしても既存のイベントリスナに影響を与えないといった風に仕様でちゃんと定められていて実装もそうなっているのなら問題ない。仕様や実装がそう変わったんだったら高速化のためにそういう工夫を取り入れるのはむしろ奨励すべき事だとは思う)。

Firefox本体の側で行われた変更なら他のアドオンも追従するだろうけど、僕個人でこうして細々と開発しているアドオンのためにまで、わざわざ他のアドオン作者の人が頑張って対処してくれるとは思えない。僕だって、他の知らないアドオンのために事前に対処はできない。僕にできる事は、「自分が作る物については、他のアドオンに与える影響を小さくするようなるべく配慮する事」、ただそれしかない。

ツリー型タブとマルチプルタブハンドラのイベント周りのAPIをちょっと変えた - Jan 11, 2011

Tree Style TabMultiple Tab Handlerを更新した。

今回のアップデートでも例によってMinefield対応のための修正をちょっとずつ入れてるんだけど、その中で1つ、なかなか気付いてなくてハマってた所があった。それはカスタムイベントを使ってた部分。

DOM2 Eventsではこんな風にして任意のDOMイベントを発行できる。

var event = document.createEvent('Events');
event.initEvent('MyCustomEvent', true, false);
event.status = 'current status';
event.tab = tab;
gBrowser.dispatchEvent(event);

受け取る側はこれをaddEventListener()で登録したリスナで拾うようにすれば、各々のモジュールの結合度合いを弱められる。なので僕は自分のアドオンでも積極的にこれを使ってる。

が、これがMinefieldでは使えなくなってた。

多分Compartment(JavaScriptのメモリ空間をスクリプトのオリジンだったかウィンドウだったかごとに分ける機能)が入ったからだと思うんだけど、Chrome URLのスクリプトで上記の例のように追加した任意のプロパティを、JavaScriptコードモジュール側のイベントリスナで参照できなくなってた。上記の例だと、捕捉したイベントのevent.tabundefinedになってしまってて、こういうやり方で情報を引き渡してた部分がエラーになってしまってた。wrappedJSObjectundefinedなので、生のオブジェクトを辿る事もできなかった。

MDCにある任意のカスタムイベントを実装する方法の詳しい説明によると、XPIDLでインターフェースを定義してC++で実装を書いてという事をやれば、今までと完全に同じAPIで任意のイベントを発行できるようなんだけど、それはちょっと重たすぎてやる気になれない。

なので次善の策として、汎用のデータを受け渡すためのイベント型があればそれを使おうと思って検索したら、Firefox 3以降ではDataContainerEventとかMessageEventとかの型のイベントが利用可能になってたという事を知った(今更)。

渡すデータがJSON文字列化できる物なら、WebSocketで定義されてるMessageEventがいいっぽい。

var event = document.createEvent('MessageEvent');
event.initMessageEvent('MyCustomEvent', true, false,
  JSON.stringify({ status : 'current status',
                   tab : tab.getAttribute('id') }),
  '', '', null);
gBrowser.dispatchEvent(event);

受け取った側はJSON.parse(event.data)でデータを復元できる。

DOM要素とかも渡したいなら、nsIVariant型でデータを受け渡せるDataContainerEventを使うしか無さげ。

var event = document.createEvent('DataContainerEvent');
event.initEvent('MyCustomEvent', true, false);
event.setData('status', 'current status');
event.setData('tab', tab);
gBrowser.dispatchEvent(event);

受け取った側はevent.getData('tab')のようにしてデータを取得できる。

ということで、プロパティアクセスにしてた所は全部DataContainerEventのやり方を使うように直した。ただ、後方互換性のためにプロパティアクセスでも情報はセットしてあって、同じCompartmentのスクリプトからなら多分今まで通りのやり方でも情報を受け取れると思う。

あと、DataContainerEventの存在を知る前に、MDCのドキュメントに書いてあった「イベント名がnsDOMで始まってない物は任意の情報は受け渡せないよ」という部分を読んでイベント名を「nsDOMTreeStyleTab...」という感じに変えていて、実際これでちゃんと動くようになった部分もあったんだけど、結局DataContainerEventにするようにしたからこれは結果的には余計だったかもしれない……

新しいタブとかウィンドウとかで「戻る」を押しても戻れねえよムキーッとなる問題を解消するアドオン「親のタブに戻る(Back to Owner Tab)」をリリースしたよ - Jan 07, 2011

Back to Owner Tabというアドオンを作ってみた。

例えば初心者ユーザにはこういう事がありがちだろう。

  • ブラウザのウィンドウを常に最大化して使っている。
  • タブとかウィンドウとかよく分かってない。「戻る」ボタンを押せば1つ前のページに戻れる、という事は(教わったので)知っている。
  • リンクをクリックした結果新しいページが表示されたとして、それがただのページ遷移なのか、新しいタブで開かれたのか、新しいウィンドウで開かれたのか、違いがよく分かってない。
  • 新しいウィンドウで開かれたページを見終わって「元のページに戻ろう」と思って「戻る」ボタンをクリックするが、ボタンをクリックしても何も起こらない。

こういう場面で、「戻る」をクリックしたらとにかく元のタブなりウィンドウなりに戻るようにするのが、このアドオンだ。

だいたい、ウィンドウを最大化してたりタブを表示しないようにしてたりすると、「戻る」で戻れるのかそうでないのかすぐには分からないから、初心者でなくてもこういう物には意味があると思う。

Firefox 3.5以降では browser.tabs.selectOwnerOnClose が true で且つそのタブが「新しいフォアグラウンドのタブ」として開かれた時にだけ「このタブの親はこのタブですよ」という情報が保持される。また、JavaScriptではwindow.openerでオープン元のウィンドウを辿ることができる。このアドオンは、これらの情報を手がかりにして「親のタブ」を検索するようになってる(ツリー型タブがあるならツリーの構造から親のタブを検出する)。

確かタブブラウザ拡張にもこういう機能を含めてた気がするんだけど、それらを単機能の拡張機能として再実装した時に取りこぼしてた。その後、ツリー型タブにこの機能を含めてくれという要望が何回か来てたんだけど、今だったら browser.tabs.selectOwnerOnClose があるから別にツリー型タブに依存した機能にしなくてもいいよなーと思ったので、こうして今回新たに1つのアドオンとして作ってみたというわけ。

Add-on SDKは使ってないけどBootstrapped Extensionsの形にしてあるので、Minefieldでは再起動無しでインストールできる。昨年の発表では「Bootstrapped Extensionsでは『戻る』ボタンの挙動をオーバーライドするみたいな事はできない」と言ったけど、やってみたらできてしまった。あと、そのために使った方法がFirefox 3.6では動作しなくてちょっと悩んだんだけど、Firefox 3.6ではそもそも絶対に再起動が必要だから「変化を可逆的にする」事にこだわらなくてもいいので、そこは横着してダーティなやり方を使ってる。

なんとなく既出のような気もしたけど、「back owner」とかその辺の単語で検索してもそういう物を見つけられなかったので、じゃあ作るか……と思って作ってみた。既出だったら恥ずかしい。

ちなみに、似たような目的のアドオンでTab Historyという物もある。こちらは「新しいタブを開く時に、元のタブの『戻る』『進む』の履歴を引き継ぐようにする」というアプローチを取っている。僕もTab Historyを使ってたんだけど、せっかく作ったので、しばらくはBack to Owner Tabを使って様子を見てみようと思ってる。

ツリー型タブがCSSの!importantを使いすぎているせいで他のアドオンと衝突する(Tree Style Tab conflicts Tab Utilities, ColorfulTabs, and other tab-related addons because it uses too many "!important" rule in its CSS.) - Dec 18, 2010

Q

With BOTH Tree Style Tab AND Tab Utilities enabled at the same time, the ColorfulTabs extension makes no difference - no tabs get colored. I have reported this to the author of Tab Utilities since it has worked properly in all previous versions of Tab Utilities. But here's his answer:

TU hacks the ColorfulTabs code to reduce its use of "!important", but Tree Style Tab still uses "!important" for tab background-color. I may revert TU's changes, but IMO it's Tree Style Tab who behaves improperly. It uses too many unnecessary "!important" modifiers in CSS rules, which also leads to the conflict with TU for the pinned tabs' position.

Can you fix this please?

ツリー型タブTab Utilitiesが同時に有効になっている時、ColorfulTabsは何の変化ももたらしません──つまり、タブに色が付きません。Tab Utilitesの以前のバージョンは正しく動いていたため、私はこの件をTab Utilitiesの作者に連絡しました。しかし彼はこう言っています:

Tab Utilitiesは「!important」の使用を減らすようにColorfulTabsのコードに手を加えていますが、ツリー型タブはまだ多くの「!important」指定をタブの背景色に指定しています。私はTab Utilitiesに加えた変更を取り消すかもしれませんが、しかし私の考えでは、ツリー型タブのやり方の方が間違っています。ツリー型タブはあまりに多くの不必要な「!important」指定をCSSの中で使っており、これはTab Utilitiesの「ピン留めされたタブ」の配置を壊す原因にもなっています。

この件について修正してもらえませんか?

A

Those "!important" are surely required, because appearance of tabs can be modified by third-parties' theme. For example, if a theme defines black background with white text, then partially applied styles from TST possibly make lighter background with white text -- yes, it is very hard to be read. To avoid these cases, I decided to use "!important" rules. Actually I got some "bug reports" like this. If I remove those "!important", I'll get similar bug reports again -- I'm very sorry but I don't want that.

Instead, I made an option for "highly compatibile for other addons". See the configuration dialog of TST, "Appearance" panel. There is an option "Default (Specified by the Theme)" in built-in themes. If you choose the option, TST doesn't apply any "!important" rule for tabs. I believe that the option makes TST compatible with Tab Utilities and ColorfulTabs.

それらの「!important」指定は必要な物なのです。タブの外観がサードパーティ製のテーマによって変更される事があるのがその理由です。例えば、あるテーマが背景色を黒に、文字色を白に設定していた時、ツリー型タブのスタイル指定が部分的に適用されてしまうと、明るい背景色と白い文字の組み合わせになってしまうことがあります──これはとても読みにくいですよね。こういったケースの発生を防ぐために、私は「!important」指定を使う事を決めました。実際、私はそういう「バグ報告」を受け取った事があります。もしこれらの「!important」指定を削除したら、私は再びそういった報告を受け取る事になるでしょう──申し訳ありませんが、私はそれを望んでいません。

その代わりに、私は「他のアドオンとの互換性を高く保つ」選択肢を設けました。ツリー型タブの設定ダイアログの「外観」パネルを参照してください。ここでタブバーの表示スタイルとして「なし(テーマ本来の表示スタイル)」を選択すると、ツリー型タブは「!important」指定が付いたスタイル指定を適用しなくなります。これを選択する事で、ツリー型タブとTab UtilitiesやColorfulTabsの衝突は回避できるものと私は考えています。

タブをドメインごとにソートする機能が欲しい / Auto-sorting of tabs by domain (or other conditions). - Dec 08, 2010

Q

Could you add the option to Tree Style Tab or Multiple Tab Handler, to keep tabs with same domain/host in alphabetical order. In other words auto grouping by domain or host.

ツリー型タブまたはマルチプルタブハンドラに対して、同じドメインまたは同じホストのタブをアルファベット順で表示させる、というオプションを付けてもらえませんか? 言い換えるとつまり、ドメイン名やホスト名によるタブのグルーピングということです。

A

Group/Sort Tabs provides the feature, so try it please. However it doesn't work with my Tree Style Tab.

I have no plan to do it on the Tree Style Tab, because I believe that tree of tabs is a "visualized history of web browsing". In the concept, "tree of tabs" and "auto-sorting by domain" mix is like oil and water. (By the way Tab Kit provides both features, so it possibly become your favorite instead of TST.)

Group/Sort Tabsというアドオンがその機能を持っているので、試してみてください。しかし、このアドオンはツリー型タブとは併用できないようです。

私はタブのツリーを「ブラウズ履歴を視覚化した物」と考えていますので、ツリー型タブにこの機能を付ける予定はありません。このコンセプトにおいて「タブのツリー」と「ドメインによる自動的な並べ替え」は相性が悪いです。(ちなみにTab Kitはその両方の機能を提供します。ツリー型タブよりもそちらの方があなたにとって気に入るかもしれません。)

複数のアドオンを一気にインストールする(ような形で配布する)方法 - Dec 06, 2010

多機能オールインワン型のタブ系アドオンをdisったら、こんな反応があった

ただエンドユーザーからみると、タブ機能のために細かい拡張をいくつも入れてられるか、とも思っちゃうのよね。

そんな時のためにFirefoxには複数のアドオンを一発でインストールできる仕組み(ユーザ側で工夫したら複数のアドオンを一括インストールできる、のではなくて、アドオンを公開する側がユーザに「このリンクを辿ればこれらのアドオンを全部インストールできます」的なインターフェースを提供できる)が備わっているのですよ!! 全然活用されてないけど!!!!!

やり方は2つある。

  • マルチプルアイテムパッケージという特殊なXPIを配布する方法
  • InstallTriggerを使う方法

ツリー型タブマルチプルタブハンドラ情報化タブソース表示タブブックマークを新しいタブで開くリンクを新しいタブで開くロケーションバーから新しいタブを開くの7つを一括インストールさせたい場合を例に説明しよう。

マルチプルアイテムパッケージで複数のアドオンをまとめて配布する

これはtabextensions3学生向けアドオンパックが実際に使ってる方法だ。

やり方

まず、こういうinstall.rdfを作る。em:type="32"というのがミソ。

<?xml version="1.0" encoding="UTF-8"?>
<RDF:RDF xmlns:em="http://www.mozilla.org/2004/em-rdf#"
         xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  <RDF:Description RDF:about="urn:mozilla:install-manifest"
                   em:id="tabextensions3@piro.sakura.ne.jp"
                   em:type="32">
    <em:targetApplication>
      <RDF:Description em:id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}"
                       em:minVersion="3.6"
                       em:maxVersion="4.0b2pre" />
    </em:targetApplication>
  </RDF:Description>
</RDF:RDF>

このinstall.rdfと、7つのアドオンのXPIをまとめてZIP圧縮して、ファイル名を「〜.xpi」に変えれば完成。

この時、install.rdfに書く対象アプリケーションのem:minVersionem:maxVersionは、全部のアドオンの最大公約数的な値になるようにしないといけない。 例えばminVersionが3.5の物と3.6の物、maxVersionが4.0b2preと4.0b8preの物があるんだったら、「全部揃った状態で確実に動くのは3.6〜4.0b2preの間」ということでそのように指定する。

この方法の詳しい話はMDCにドキュメントがある。

メリットとデメリット

この方法には、配布するファイルが1個だけでよくなるし、JavaScriptとかを使う必要がないというメリットがある。

その反面、同梱するアドオンを増やしたり減らしたり更新したりしたい時には、パッケージをその都度作り直さないといけないというデメリットがある。

InstallTriggerで複数のアドオンをまとめてインストールする

これは台湾のMozillaコミュニティのプロモーション用サイトで実際に使われてる方法だ。

やり方

こんな感じのスクリプトを実行するだけ。

var targets = {
      treestyletab : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/treestyletab.xpi?update'
      },
      multipletab : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/multipletab.xpi?update'
      },
      informationaltab : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/informationaltab.xpi?update'
      },
      viewsourceintab : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/viewsourceintab.xpi?update'
      },
      openbookmarkintab : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/openbookmarkintab.xpi?update'
      },
      openlinkintab : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/openlinkintab.xpi?update'
      },
      newtabfromlocationbar : {
        URL : 'http://piro.sakura.ne.jp/xul/xpi/openlinkintab.xpi?update'
      }
    };
InstallTrigger.install(targets);

InstallTrigger.install()にはインストールさせたいアドオンのリストをハッシュとして渡す。

ハッシュ値を渡して安全なインストールを可能にするとかの工夫もできる。これも詳しい話はMDCに書いてある。

メリットとデメリット

この方法には、いちいちパッケージを作らなくていいというメリットがある。台湾コミュニティのサイトのように、「ユーザがリストにチェックを入れていって、最後にボタンを押したらチェックが入ってるアドオンだけを一括インストールする」という風な事が簡単にできる。

その代わり、配布するファイルがバラバラになるし、JavaScriptが有効になってないと利用できないというデメリットがある。

なんでAMOでこれを使わないんでしょうね?

AMOにコレクションという機能が加わった時、これを使って「お薦めのアドオンを一括インストールできますよ!」という見せ方ができるようになるものとばかり思ってた。そういうものが絶対に必要だろ、と思ってたから、すごく期待してた。でも実際に出てきた機能はただの「リンク集作成機能」でしかなかったのですごくガッカリした。

せっかくこういう機能があるのに、それを使わないでTab Mix Plusを「お薦めアドオン」として前面に押し出してたりするんですよ。何考えてるんですかねホント。

Page 6/248: « 2 3 4 5 6 7 8 9 10 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき