Home > Latest topics

Latest topics 近況報告

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

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

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

Page 21/241: « 17 18 19 20 21 22 23 24 25 »

Excelの「AA」とかのカラム名とカラム番号の数値を相互変換する - Feb 15, 2011

96桁目のセルに所定の値が入ったCSVを作る、みたいな事をやらなきゃいけなくなったんだけど、GnumericにしろExcelにしろカラム名が数字じゃなくてA, B, C, ... Z, AA, AB, ... ZZ, AAA, ...というヘンテコ表記になってるからどこのセルに値を入れればいいかわかんないよウワァァァァン!!!! となったのでJavaScriptで解決した。


var input = prompt('input number or column name');
if (!input) return;

var symbols = 'abcdefghijklmnopqrstuvwxyz';
var result;
if (/^[0-9]+$/.test(input)) {
   input = parseInt(input);
   result = [];
   while (input > 0) {
     result.unshift(symbols.charAt((input - 1) % symbols.length));
     input = Math.floor((input - 1) / symbols.length);
   }
   result = result.join('').toUpperCase();
}
else {
  result = 0;
  input = input.toLowerCase().split('').reverse();
  for (var i = 0, maxi = input.length; i < maxi; i++) {
    result += (symbols.indexOf(input[i]) + 1) * Math.pow(symbols.length, i);
  }
}
alert(result);

カラム名→カラム番号の計算方法は分かったけど、頭が悪い僕にはカラム番号→カラム名の計算式が分からなかったので、検索して出てきたエクセルの1000列目は ALL - つまみ食うのアルゴリズムを丸パクリした。阿呆ですんません……

なんでそこで詰まったかっていうと、単純な26進数の変換ではうまくいかなかったからなんですよね。A==0、B==1という感じで26進数として扱うと、26==Zで27==BAになるんだけど、実際は繰り上がった後はBAじゃなくてAAにならなきゃいけない。そこの所を考慮した計算を多分簡単なループで実現できるとは思ったんだけど、頭が働かなくて無理だった。

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.

Firefoxでtarget="_blank"なリンクから新しいタブやウィンドウが開かれないようにしたい人向けの話 - Feb 04, 2011

Back to Owner Tab公開したよという話を書いた時にそもそも勝手に新しいタブを開かないようにしたいという意見のトラックバックがあった。そこのコメント欄で「なんでブラウザがそういう機能を提供してくれてないのさ」って議論になってた。

それでふと思い出したんだけど、そういえば昔のMozillaにはtarget属性の指定を無視して現在のタブでリンクを開く機能がなかったっけ? 削除されたのかな? と思って調べてみたら、自分でもすっかり存在を忘れてたけどこの機能はFirefox 4でも健在で、隠し設定としてちゃんと生き残っていた。about:configを開いて「browser.link.open_newwindow」を「1」に変更すれば、target属性によって新しいウィンドウやタブを開くように指定されているリンクでも、普通のリンクと同じように元のページを置き換える形で遷移するようになる。(この機能が入る前に「新しいウィンドウを開くリンク」の挙動を置き換えようとして、旧タブブラウザ拡張では相当な無茶をやっておりました。懐かしい話です。)

実は、Firefoxの設定ダイアログで「新しいウィンドウではなく新しいタブで開く」っていうチェックボックスを切り替えると、この設定の値が「2(新規ウィンドウで開く)」と「3(新規タブで開く)」でトグルするようになってる。「1(現在のタブで開く)」は普通に設定ダイアログを使ってたら選択できないので、about:configか何か別の手段を使わないといけないというわけ。

変更履歴をどんどん遡っていってみたところ、ごく初期の設定ダイアログでは確かに3つの選択肢から1つを選ぶようなUIになってたんだけど、これがFirefox 2でprefwindowベースの設定ダイアログ置き換えられた時に、しれっと「新しいウィンドウで開く」と「新しいタブで開く」の2者択一になってた(現在のタブで開くという選択肢が消えていた)。その時の議論には特に情報はなかったんだけど、「新しいウィンドウで開く」と「新しいタブで開く」の2者択一のUIから今のチェックボックス型UIに変わった時のバグの方を見てみると、まさに前述のエントリで述べられているような議論が繰り広げられていた。

で、ざっと見た感じでは、 「リンクのtarget属性とそれ以外の場合のために2つも設定項目作る意味なくね? 1個でいいんじゃね?」→ 「そもそもなんでUIから1を選択できるようになってないんだ?」→ 「破壊的な挙動になるオプションだから敢えて選択できないようにしたんだよ。」 「そのオプションを選択してたらクラッシュしたこともあったしね。」 「実際、open_newwindow=1に設定するとかなりのWebサイトがぶっ壊れるよ。そりゃウィンドウなんて開くべきじゃないのはみんな分かってるけど、ユーザを混乱させないためにはIEと互換性のある挙動にしとかないと。」→ 「JavaScriptで開かれるウィンドウについてはopen_newwindow=1にしたら色々危ないのは分かるけど、リンクのtarget属性だったら無視しても問題ないでしょ?」→ 「どうしてもアドオン無しでやりたいならこういう方法もあるよ。

という感じで、

  • リンクの場合とJavaScriptの場合で新規ウィンドウ要求をどう扱うかの2つの設定項目は、初心者ユーザの事を優先して1つにまとめることにする。これは譲らない。
  • そうすると、open_newwindow=1にするとWebサイトの動作を壊すなどのトラブルが起こりうる。なので、この値は設定UIには表示しない非推奨の隠し設定のままとする。
  • リンクの場合とJavaScriptの場合で新規ウィンドウ要求をどう扱うかというのを細かく制御したい上級者ユーザは、アドオンを使うなり何なりして対処するように。

という結論に至った……みたいだ。

ということで、すっかり忘れてた&調べ直してて改めて思い出したわけだけれども、browser.link.open_newwindowを1にすると、target属性があるリンクだけでなく、window.open()の読み込み先も現在のタブになってしまうために、親ウィンドウを参照するようなスクリプトが含まれてるページはまともに動かなくなる恐れがあるので、これはお薦めできません。トラブル覚悟でこの設定を使ってもいいけど、target属性があるリンクだけ同じタブで開くようにしたいという場合には、そういう機能を提供するアドオンを使うなり何なりしないといけないよーです。

まあ、いろんな場合に対して細かく挙動を振り分けたいという人は既に(当時の)Mozillaが考える所のメインターゲットではなかったのでした、そういう人は自分がマイノリティであることを自覚してDIYの精神で暮らすしかないですね、という話なのでした。

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

いつの間にかインラインフレームを透過できるようになっていた - Jan 25, 2011

Bug 591652 – Make the tab view (Panorama) background transparent to reveal glass (if enabled) on Windowsというバグがfixされて、Panoramaの背景が透過されるようになった。Windows Vista以降でAero Glassを有効にしてれば、見事なスケスケになる。

で、ほうほうと思ってチェックインされたパッチを見てみたんだけど、「Aero Glassじゃない時だけ背景を指定する」というコードしかない。ひょっとしてフレームを透過するバックエンドってもうずっと前から入ってたの? そういえばPanorama用のiframeには transparent="true" という指定がずいぶん前からあったような気がするけど、特に透過されてる様子もなかったから、まだ実装されてないのかと思ってた。

それでMinefield 4.0b10preを起動してDOM InspectorでFirefoxのDOMツリーをいじって試してみたら、browser要素とかiframe要素とかに transparent="true" という属性の指定を加えて、そのフレーム要素・祖先要素すべて・フレームの中に含まれるドキュメントのbodyを background: transparent !important; にするだけで、普通のWebページでもデスクトップの壁紙が透けて見えるようになった。これは面白い。

最小構成だとこう。

parent.xul:

<?xml version="1.0"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
        style="-moz-appearance: -moz-win-glass; background: transparent;"
        width="200" height="200">
  <iframe transparent="true"
          flex="1"
          style="background: transparent;"
          src="child.xul"/>
</window>

child.xul:

<?xml version="1.0"?>
<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
      style="-moz-appearance: none; background: transparent;">
  <label value="Hellom, transparent world!"/>
</page>

この2つのファイルをテキスト形式でデスクトップに保存して、エラーコンソールから window.openDialog('file:///c:/Users/username/Desktop/parent.xul'); とやれば試せる。

Aero Glassじゃない時は普通の背景を表示させたいなら、CSSの方にこう書いておけばいい。

window:-moz-system-metric(windows-compositor) {
  background: -moz-field;
}

夢が広がりまくりですね!

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

Nautilusで「ここでターミナルを開く」 - Jan 17, 2011

Windowsには昔からよく知られた便利機能でCommand Prompt Hereという物がありまして、要するに「フォルダの右クリックメニューから、そのフォルダをカレントディレクトリにした状態でコマンドプロンプトを開く」というやつなんですが、Windows 95の頃にはPower Toysというユーティリティ集でこの機能を簡単に追加できたりもしました。

これと同じ事をGNOMEのNautilusでやるパッケージがあったんですね。その名もnautilus-open-terminal。Debianにも同名のパッケージがあるようです。

CUIメインで使ってる人には用がなさそうですが、僕みたいにヌルい人にはあると便利です。

ツリー型タブとマルチプルタブハンドラのイベント周りの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にするようにしたからこれは結果的には余計だったかもしれない……

transitionendイベントが発火されないことがあるというバグが修正されそう - Jan 10, 2011

CSS Transitionsではアニメーションの終了時にtransitionendというイベント(webkitではwebkitTransitionEnd)が発行される事になってて、Minefieldに既に入ってる「タブを開く時にアニメーションする」「タブを閉じる時にアニメーションする」という機能でもこれが使われてる。具体的には、max-widthをアニメーションさせることでタブがぬるっと生えてきたりぬるっと縮んでいったりという効果を与えて、アニメーションが終わった後で実際にタブの要素を削除するという風な事が行われている。

このtransitionendイベントがたまに発行されない事があって、そのせいで「閉じたはずなのにタブがそのまま残ってしまう」「タブが開かれた後に行われるはずの処理が行われない」といった問題が発生していた。特に、ツリー型タブではタブバーの向きを横長ではなく縦長に変更していて、その絡みでmax-widthのアニメーションの代わりにmax-heightとかmargin-topとかopacityとかをアニメーションさせるようにしてるんだけど、そのように複数のプロパティについて異なるdurationでアニメーションを行うようにしてる時にこの問題が顕著に発生するようで、しかし確実に問題を再現させる条件がはっきりと分かっておらず、Bugzillaに報告しようにも報告できない状態が長く続いてた。

それが今回、ひょんな事から確実に問題が再現する条件を特定できたので、Bugzillaに既に存在していたBug 613888 – Sometimes transitionend doesn't fireに再現性100%のテストケースを投稿してみた。

そしたら、そこからの動きが早かった。タイトルが示すように「時々しか起こらない」ということで割と優先度低めの扱いだったっぽいのが、hardblockerになってblocking2.0になって(つまりこのバグが直らなきゃFirefox 4はリリースされないという致命的なバグと認識された)、David Baronさんがパッチを書いてくれて、ついでに僕の投稿したテストケースを元にした自動テストもパッチに含めてくれて、あとはレビューとチェックイン待ちという状態になっている。

べつに僕が最初に問題を見つけた訳ではないし、僕が実際にパッチを書いたわけでもないけれども、自分のやったことがきっかけになって厄介なバグが修正に至ったと思うと、率直に言って「やったぜ!!!」と自分の事のように嬉しく感じてる。

このエントリで何を言いたかったのかというと、要するにその「やったぜ」の自慢話ということなんだけど、それと同時に、再現性100%のテストケースや再現手順を特定することは大事なんだよ、それを自動化することも大事なんだよという事も伝えたかったのです。

再現性100%の手順が判明していればすぐに開発者の環境でデバッグできるし、自動テストがあればそれをそのままリポジトリに取り込むこともできる(=今後のregressionの発生を予防できる)。パッチを書けない人でも書けないなりの協力はできるのです。単に「動かないよ」と言うだけの場合よりも、原因特定のための検証を開発者がやる手間が省けるから、効率がいいしバグも早く直る訳です。

  • Mozillaの自動テストには、Mozillaのリポジトリに含まれているテストで実際に使われてるフレームワークが列挙されている。
  • 今回のテストケースではその中の1つのMochitestを使った。
    • Mochitestは基本的にはMozillaのビルド環境を整えた上で使う物だけど、MochiTest makerと最新のナイトリービルドを使えば、Mozillaのビルド無しにテストケースを書いて試すことができる。
    • 実際にどんなテストがあるかは、MXRで「bug_」を名前に含むファイルを検索してみると色々な例を見れる。
  • FirefoxのUIの上で操作した時に起こるバグの再現だったら、MozmillUxUが使えるはず。

というわけで、バグ報告には確実に問題を再現できる手順(できれば自動テスト)を添えて下さいね、という話なのでした。

16日追記。パッチがチェックインされた後のナイトリービルドで、自分の環境では上記の問題が解消されていることを確認できた。よかった……これで安心してFirefox 4を迎えられるよ。

Page 21/241: « 17 18 19 20 21 22 23 24 25 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき