Home > Latest topics

Latest topics 近況報告

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

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

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

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

XBLの使用を避けたがるのは何故か - Oct 22, 2010

僕がアドオン開発でXBLの利用を避けることが多いのは、XBLを使ってると、他のアドオンやサードパーティ製のテーマと衝突した時ににっちもさっちもいかなくなってしまうことが多かった(という印象が強い)り、複数のバージョンのFirefoxに対応しようと思うとドツボにハマったりしたからだ。

例えばツリー型タブのようなアドオンを作る時に、「タブのDOMノードに、子タブの一覧を取得するためのchildTabsというプロパティを追加したいな」と思ったとする。思ったっていうか、タブブラウザ拡張でかつてツリー表示機能を実装した時には実際そうしてたんだけど。それをXBLでやるとこんな風になるだろう。

<binding id="tabbrowser-tab"
         extends="chrome://browser/content/tabbrowser.xml#tabbrowser-tab">
  <implementation>
    <property name="childTabs" readonly="true">
      <getter><![CDATA[
        ...
      ]]></getter>
    </property>
  </implementation>
</binding>

同時に、こういうCSSも書くことになる。

.tabbrowser-tab {
  -moz-binding: url("mybinding.xml#tabbrowser-tab");
}

XBLではextendsで他のバインディング定義を継承することができる。chrome://browser/content/tabbrowser.xml というのは、Firefox 3.6でタブブラウズ関係の機能を定義してるバインディングなので、これで「Firefox本来のタブの機能に加えて新しい機能を定義する」という事が簡単にできる。

が、これは同時に欠点でもある。XULの機能のかなりの部分はXBLで定義されているので、extendsを書き忘れるとマトモに動かなくなってしまうことが結構ある。だから、独自のバインディングを適用する時は、適用先の要素に既にバインディングが適用されているかどうかを調べて、現在適用されているバインディングのURIを独自のバインディングのextendsに書いておかないといけない。

そして、「元々適用されているバインディングのURI」は簡単には同定できない。Firefoxのバージョンによっても変わるし、WindowsかMac OS Xかによっても変わるし、サードパーティ製のテーマでバインディングが上書きされているかもしれないし、アドオンがバインディングを適用しているかもしれない。ひょっとしたらユーザがuserChrome.cssでバインディングの指定を変えているかもしれない。継承の順番は、自分のXBL→tabbrowser.xml→tabbox.xml→general.xml かもしれないし、自分のXBL→テーマのXBL→tabbrowser.xml→tabbox.xml→general.xml かもしれないし、自分のXBL→他のアドオンのXBL→テーマのXBL→tabbrowser.xml→tabbox.xml→general.xml かもしれない。「こう書いておけば大丈夫」という単一の継承元のURIは存在しないのだ。

さらにタチが悪いことに、XBLの定義ファイルの中に書かれたextendsは動的には書き換えられない。現在適用されているバインディングのURIをgetComputedStyle()で取得しても、それを後からXBLのextendsに指定するという事はできない。(ひょっとしたらdata: URLを使えばできるかもしれないけど、僕はそんなのやりたくないし見たくもない……)

だから、XBLを使うアドオン同士ではバインディングの優先権の取り合いになる。CSSは後から読み込まれたもの・セレクタの指定の詳細度が高いものほど優先的に適用されるから、後からインストールしたアドオンのせいでそれまで使っていたアドオンのバインディングが適用されなくなる、なんてこともしょっちゅうあった。タブブラウザのタブ要素なんて、激戦区中の激戦区と言っていいだろう。

そう考えると、ほんの2~3個のプロパティだとかメソッドだとかを追加するためだけにこれだけのリスクを負うのは到底割に合わない。それだったら、要素のDOMノードに直接プロパティを加えるのを諦めて、コントローラやサービスになるようなクラスを作ってそっちに必要な処理を集約させる方がずっと楽だ。(だからツリー型タブでは、前述の例のようなバインディングを使う代わりに、gBrowser.treeStyleTab.getChildTabs(aTab)で子タブの配列を得るように設計した。)

というのが、僕の出した結論だった。

そういう話なので、僕は、「何が何でもXBLを使うな」とまで言うつもりはない。前述のようなバインディングの適用の優先権争いが起こらない場所、例えば独自のXULRunnerアプリケーションだったり、独自のサイドバーパネルだったり……という部分でなら、XBLはいくらでも使っていいと思ってる。

例えば、派生の要素型がたくさんあるようなケースではXBLの継承が威力を発揮する。会社で一時期やってたXULRunnerアプリのプロジェクトでは、継承を多用することで結構工数を削減できた(と思う)。また、anonymous contentsの追加は(特に、既に存在しているDOMツリーのノードの親子関係の間に安全に割り込むような物は)、XBLでなければできないことだ。

ただ、XBLは、1つのウィンドウの中にいろんな人が好き勝手に書いたコードが同居する「Firefoxのアドオン」という分野とは、すこぶる相性が悪い。JavaScriptで書く物でもCSSで書く物でも「最初にやった者勝ち」な所はどこかしらあるけれども、XBLの場合はそれが顕著だし、「後から来た者が上手く隙間に入り込む」という風な余地が全然無い。だから僕は、XBLをなるべく避ける形でコードを書くように努めてるんだな。

未だにXBLを使うことを避けられない場面 - Oct 22, 2010

XBLはアドオン同士の衝突の原因になりやすい。だからXBLはあまり使わないように僕はしてる。

XBLを使うと、DOMノードにgetterやsetterになってるプロパティを定義したり、独自のメソッドを追加したりできる。でも、それらはJavaScriptのテクニックで代用できないこともない。JavaScriptのレベルで目的を達成するやり方として、僕は最近よく、こんな設計をしてる。

function MyController(aNode) {
  this._node = aNode;
  this.init();
}
MyController.prototype = {
  get property() {
    ...
  },
  set property(aVaule) {
    ...
  },
  method : function() {
    ...
  },
  init : function() {
    this._node.addEventListener('...', this, false);
    ...
  },
  destroy : function() {
    this._node.removeEventListener('...', this, false);
    ...
  },
  handleEvent : function(aEvent) {
    ...
  }
};

var node = document.createElement('box');
node.controller = new MyController(node);

かなりの部分はこういったやり方で目的を達成できると思う。ツリー型タブなんかもこれに近い実装になってる。

ただ、オートコンプリートのテキストボックスの挙動を変えるだとかの、本体で定義されている物を置き換える場面では、たまにこの方法だけでは不十分なことがある。例えば<textbox type="autocomplete" />な要素のmaxDropMarkerRows<panel type="autocomplete" />な要素のoverrideValueやなんかはreadonlyなプロパティとしてXBLで定義されてしまっているので、これらが返す値を変えたいと思うと結構厄介な事になる。

XBLを使わないでサクッと済ませようとすると、__defineGetter__()を使う方法がまずは思い浮かぶ。Firefox 3.0以降ではDOMノードに対して__defineGetter__()を使えるので、上記の例のコードのinit()あたりでそれを使ってやるという感じだ。実際、XUL/MigemoではoverrideValueで任意の値を返すためにそうしてる。

でも、この方法はできれば使わない方がいいのかもなと思ってる。そう思ったきっかけは、同じようなことをやるコードの自動テストを書いていた時。setUpとtearDownで毎回ウィンドウを開いたり閉じたりとやってると時間がかかってしょうがないからUxU組み込みのフレームにページを読み込ませて……という風にしてみたら、セキュリティの制限に引っかかってしまった。object.__defineGetter__(name, getter)objectgetterの属してる名前空間が違うと、Illegal valueとか言われてエラーになってしまった。それでTrunkでの__defineGetter__()の実装を見てみたら、この両者のコンパートメントが違う場合はゲッタの登録を拒否するような設計になってた。こういうセキュリティの制限を回避してreadonlyなプロパティの働きを置き換えようと思ったら、どうもやはり、XBLを使うしかないようだ。

そもそもなんでoverrideValueがreadonlyなんだよ、なんで書き換え可能なただのフィールドになってないんだよ、って思って来歴を調べてみたら、nsIAutoCompletePopupインターフェースのoverrideValue昔のオートコンプリートの実装におけるgetOverrideValue()メソッドがその祖先で、当時のコードには「こいつの働きを変えたかったらXBLでオーバーライドしろ」ってコメントが書いてあった。今のFirefoxのオートコンプリートの実装にはこのコメントがなかったので、なんでreadonlyになってるんだよという不満しか抱きようがなかった。getXXXとなってたメソッドをプロパティの形に置き換えるなら、確かにそれはreadonlyになるだろう。

でもどうせプロパティに変えたんだったらwritableにしたってよかったはず。ほんとに、何でこんな設計にするんだろう……

omni.jarを展開してデバッグした話と、Minefield(Firefox 4)で起動時のセッション復元が働かなくなる問題について - Sep 27, 2010

最近のMinefieldのナイトリービルドで、ツリー型タブがあると前回終了時のセッションが復元されない(タブのタイトルだけ復元されて実際には空のページになる)という現象が起こっている。この問題の原因の究明のためにずいぶん遠回りをした。

  • 最初は、SSTabRestoringやSSTabRestoredイベントを監視してる所が悪いのかと思った。なのでツリー型タブのイベントリスナの所をコメントアウトしてみた。しかしそれでもまだ問題が起こる。
  • 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の編集方法は、こう。

  1. omni.jarを作業フォルダにExplzhで展開する。
  2. 必要な変更を行う(デバッグ用のコードを埋め込むなど)。
  3. 展開されたファイル群を7-Zip等でZIP形式で圧縮する。
  4. ファイル名をomni.jarに変更する。
  5. 4で作ったomni.jarで、Minefieldのomni.jarを置き換える。
  6. %localappdata%\Mozilla\Firefox\Profiles\プロファイル名\ の中にあるキャッシュファイルを消す。

この方法で、Minefieldが正常に起動する状態を保ちながらomni.jarの中にあるファイルに変更を加える事ができた。

nsSessionRestore.jsに大量のdump()を入れて調べてみた所、最初のタブ(現在のタブ)の復元が終わった後で次のタブを復元する時に、すぐに処理が終わってしまっていた。さらによく調べると、復元対象のタブを決定する時にもしタブのセッション情報の_tabStillLoadingというフラグがfalseになっていたら(=タブのセッションが復元完了していたら)、そのタブをセッションの復元対象から除外するという設計になっていた。

_tabStillLoadingというフラグには見覚えがあったのでツリー型タブのソースを検索し直してみたら、別のバグの対策でこのフラグを敢えてfalseにするような処理を入れていて、これが誤爆しているせいで上記の問題が起こっているようだった(本当はそのタブはまだセッションが復元されていない・読み込み中の状態なのに、_tabStillLoadingfalseにセットされてしまうため、タブがセッション復元の対象から除外されるようになってしまっていた)。

まとめると、こういう事だった。

  • Firefox 4 の最適セッションリストア原文)で行われた変更で、ブラウザ起動時のセッション復元が、すべてのタブを一度に復元するのではなく、3個ずつ復元するように変更された。
  • その時、まだ復元が完了していないタブがいつまでも「読み込み中」の状態に見えてしまうと格好が悪いので、タブの要素のbusy属性を取り除いて、一見すると既にタブの復元が完了しているかのように見せるようになった。その代わり、まだタブの復元が必要であることを示す_restoringというフラグをbrowser要素の方に立てるようになった。→Bug 602555でリファクタリングされて、また仕様が変わった。前述の「_restoringのフラグが立っている状態」に相当するのは「browser要素の__SS_restoreState1の時」になった。
  • ツリー型タブは、タブのbusy属性だけを見て、そのタブが読み込み完了しているかどうか・セッション復元中かどうかを判断して、読み込みが完了していると判断したタブのセッション情報の_tabStillLoadingを強制的にfalseにして、タブの読み込みが完了しているとnsSessionStoreに認識させるようにしていた。
  • そのため、Minefieldでは「一見すると読み込みが完了しているように見えるが、内部的にはまだセッション復元が完了しておらず読み込み中の状態である」タブまでもがツリー型タブによって「内部的にも読み込みが完了した状態である」という扱いになるようになってしまっていた。よって、タブのセッションが復元されないという現象が発生してしまっていた。

別のバグの対策用のコードの条件分岐にもうひとつ条件を足して、「一見すると読み込みが完了しているように見えるが、内部的にはまだセッション復元が完了しておらず読み込み中の状態である」というケースを判別するようにしたら、この問題は起こらなくなった。同じコードを他にもいくつかのアドオンで使っていたので、それらも合わせて修正しておくことにした。もし同じ事をやっている人が他にもいたら、十分気をつけて欲しい。

DOM要素に対して起こったイベントのロギング - Sep 17, 2010

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にそういうユーティリティを追加してみようかな。

タブバーをドラッグしたらウィンドウが動くとかその辺のタイトルバーっぽい挙動を無効化する - Sep 15, 2010

ツリー型タブではタブバーをウィンドウの横に置く使い方を基本的には想定してるワケだけれども、普通にタブバーを縦型にするだけだと、Minefieldではタブバーをダブルクリックしたらウィンドウが最大化されるという謎の結果になってしまうことがある。

MinefieldではBug 555081で行われた変更により、ツールバーの空白の部分がウィンドウのタイトルバーと同じ扱いになるようになっている。ツールバーの空白部分をドラッグすればウィンドウが動くし、ダブルクリックすればウィンドウの最大化になる(Windowsの場合)し、ぴたすちおのようにタイトルバーを右クリックしたらウィンドウシェードするようなユーティリティを使っていれば、それも動く。

ただ、ウィンドウの横に移動されたタブバーのようにぱっと見ツールバーに見えない部分までもがこのような挙動になってしまうと、違和感があるし混乱の元ではある。また、ツリー型タブの場合はタブバーの空白の領域をドラッグするとタブバーの位置を変えられるという機能があるけれども、上記の通りの仕様なのでこのままではタブバーの位置を変えられないということにもなってしまう。こういう時、どうすればいいのか。

タイトルバーのように振る舞うツールバーの挙動は、chrome://global/content/bindings/toolbar.xml#toolbar-drag で定義されている。コードを見てみると、WindowDraggingUtils.jsmというモジュールを読み込んで自分自身をWindowDraggingElementという物に登録しており、こうして登録された要素がタイトルバーのように振る舞うようだ。じゃあこの初期化処理を無効化するか初期化処理で行われた結果を取り消してやればいいんじゃないか、と思ったけど、設計的にそれは無理だった。

仕方がないのでWindowDraggingUtils.jsmの方を見てみると、「タイトルバー上にあるボタン等をドラッグした時だけは上記のような動作にしないようにする」というのをどうやって実現しているのかが分かった。

  • Windowsでは、マウスのボタンが押下された時にMozMouseHittestというイベントが発行されている。
  • 任意で登録された関数(mouseDownCheck)がfalseを返した時か、クリックされた要素からその祖先までの間にドラッグ操作を受け付けそうな要素がある時は、preventDefault()している。
  • preventDefault()された場合、マウスのボタンがそもそも押下されなかったという扱いになるみたい。

端的に言うと、MozMouseHittestイベントをpreventDefault()すれば、ツールバーのタイトルバー的な振る舞いを無効化することができるということのようだ。

gBrowser.tabContainer.addEventListener(
  'MozMouseHittest',
  function(aEvent) {
    aEvent.preventDefault();
  },
  true
);

ただ、タブバー内で発生するMozMouseHittestイベントを常にpreventDefault()してしまうと、タブをクリックしてもタブが切り替わらないということになってしまう。preventDefault()するのは祖先要素にタブまたはクリック可能な要素が無い場合だけに制限しないといけない。

Minefield 4.0b6preでXUL要素とiframeの重ね合わせが鼻血出るくらい簡単になってた - Sep 11, 2010

ツリー型タブには「ポインタがタブバーを離れている時はタブバーを隠して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は?

panelやmenupopupを使うと、OSのレベルで別のウィンドウを開いて任意の位置に重ねることができる。影や枠を表示しないようにすることもできるから、うまくやればこれが一番確実かもしれない。

ただ、自分で0から作る場合ならともかく、Firefoxのタブを後から改変するという場合には、これは激しくお勧めできない。Firefoxのタブ周りのDOMツリーを不用意にいじると、Firefoxの本来の構造を期待して開発されたアドオンが動かなくなってしまう可能性が高いからだ。

結局どうやったのか

アレも駄目これも駄目という感じだったので、Firefox 3.6以前では結局、他のアドオンに対して与える影響が一番少なそうなやり方として、いくつかのテクニックの組み合わせでそれっぽく見せるようにしてみた。

まずタブバーの幅を普通に増やす。 (タブバーの幅が増えた状態のスクリーンショット)

それだけだとコンテンツ領域が押し潰されてしまうので、そうならないようにネガティブマージンでappcontentあたりの幅を広げてレイアウトを維持する。 (コンテンツ領域の幅を広げた状態のスクリーンショット)

次にnsIContentViewerのmoveメソッドで描画内容をずらす。このメソッドは、フレームの内容の描画位置を任意の座標分平行移動する物だ。 (描画内容をずらした状態のスクリーンショット)

仕上げとして、nsIContentViewerのmoveメソッドで描画位置を変えたせいでレンダリングされていない「タブバーと重なり合う部分」を、canvasのdrawWindowで描画する。 (canvasで半透明の効果を再現した状態のスクリーンショット) スクロールしてる場合やフルズームが使われている場合に位置をうまく合わせるのが難しいけど、なんとか頑張った。

Minefield 4.0の古いベータへの対応

ちょっと前までの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)にすればもうそれだけで、「半透明のタブバーの下にコンテンツ領域が透けて見える」状態になるということだ。 (canvasによる再現ではない本当の半透明が実現された様子のスクリーンショット) こうしてひとまず現状のMinefieldには対応することができた。

まとめ

  • Minefieldでは、position:fixed等で任意のXUL要素をコンテンツ領域の上に重ねて配置できるようになった。ポジショニングを気軽に使えるようになった。
  • 今まで必要だった、制限の回避のための工夫は、もういらない(多分)。

いい時代になったものですね。

可変フレームなアニメーションを管理するためのライブラリをmozRequestAnimationFrame/MozBeforePaintに対応させた - Sep 01, 2010

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のイベントをトリガーにしないといけなかったり次のフレームを描画するためにいちいちメソッドを呼ばないといけなかったりというのは煩わしいと思ったので、せっかくだからライブラリ側で面倒を見るようにしてみた。

nsIWindowWatcher::openWindow()で複数の引数をウィンドウに渡すには? - Sep 01, 2010

nsIWindowWatcherのopenWindow()って何ですか

アドオンで新しいChromeウィンドウ(特権付きのウィンドウ、XPCOMとか自由に使えるやつ。ブラウザウィンドウ等、Firefoxのアプリケーションとしてのウィンドウはだいたいそう。)を開く方法はいくつかある。

  1. window.openDialog()を使う
  2. nsIWindowWatcherのopenWindow()を使う

普通にXULドキュメントの中で読み込まれてるスクリプトからやるのなら、1の方法でいい。

でも、JavaScriptコードモジュールやXPCOMコンポーネントのスクリプトのように、グローバルオブジェクトがDOMWindowじゃない場面ではこの方法が使えない。特にFirefoxが起動した直後でまだブラウザウィンドウすら開かれていないという場面では、nsIWindowMediatorのgetMostRecentWindow()でDOMWindowを取得してそのメソッドを呼ぶ、というようなこともできない。なのでこういう時は2の方法を使わないといけない。

やりたかったこと

window.openDialog()の引数は window.open()と同じだ。

  1. 開くウィンドウのChrome URL(文字列)
  2. ウィンドウ名(大抵は'_blank'決めうち)
  3. ウィンドウの挙動の指定('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()の引数は

  1. 親ウィンドウ(DOMWindow)
  2. 開くウィンドウのChrome URL(文字列)
  3. ウィンドウ名(大抵は'_blank'決めうち)
  4. ウィンドウの挙動の指定
  5. ウィンドウに渡す引数

となっていて、最初の引数が加わったことを除けば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で配列の要素としてそれぞれを受け取ることなのですよ。というか、開かれる側のウィンドウがそういう実装になっているため、どうしてもその様式で引数を渡さないといかんのです。

nsISupportsArrayとnsISupportsなんちゃらで……

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のオブジェクトになってしまって、元のオブジェクトが持ってた情報を取り出せなかった。

回避方法として思いつくのは

  1. JSON.stringify()して渡して、受け取り側でJSON.parse()する
  2. nsIPropertyBagにする

くらい。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;
}

という風にしてハッシュに戻す。とか。

まとめ

ウィンドウ間の情報の引き渡しはマジ鬼門。

あと、上に書いた諸々の処理をライブラリとしてまとめておいたので、同じような事をしようとしてる人は使うといいよ。

モックが必要な場面、モックが有効な場面 - Aug 11, 2010

モック(Mock)とスタブ(Stub)の違いがよく分かってなかったんだけど、何が違うのか、そしてモックはどう使う物なのかということを、すとうさんに教えてもらって今更理解した。あとで会社のブログに書くつもりだけど、メモとして要点だけまとめておく。

  • テストは基本的に、粒度の細かいブラックボックステストにした方がいい。
    • 内部に持ってる隠しプロパティの値が正しいかどうか?という風な実装べったりのテストは、実装の変更に非常に弱い。
    • なので、関数の返り値だけ見て検証できるような設計が、自動テストしやすい設計という意味で「良い設計」と言える。
  • しかしブラックボックステストには限界がある。
    • 色々な副作用を伴う機能だったり、非同期で処理するような機能だったり、1回の実行で複数の状態を遷移する機能だったり、という風に、単純に関数の実行→返り値を検証 とするだけではテストできない機能もある。
      • ユニットテストのレベルでそういう機能があるのは設計が良くない証拠なので、こういう物は関数を細かい単位に解体して、単純に関数の実行→返り値を検証 というブラックボックステストを行えるような設計に直すべき。
      • 処理待ちしてやりさえすればいいような場合、処理待ちのための機能を持ったテスティングフレームワークを使うと、単純な非同期処理なら簡単にブラックボックステスト化できる。
    • 色々な副作用を伴う機能や、状態遷移があるような機能をブラックボックステストできるようにしようと思うと、「内部で状態の遷移のログを取っておいて、最後にそのログの内容が期待通りになっているかどうかを検証する」という風な形にならざるを得ない。
      • ということをやろうと思うと、本番用のコードの中に「ログ取り用の処理」のような「実際に使う場面では無駄」なコードが増えていってしまう。
      • テストしやすくするためにテスト対象の実装に手を入れるのはよくあることだし、そうやって手を入れた結果として関数が小さな単位に分割されていったり関数名と入出力の対応が分かりやすくなっていったりするのなら、それによって動作が安定するようになったりコードのメンテナンス性が高くなったりするから、いいことだ。でも「テストのためだけの実装」が増えていって、動作が不安定になったりコードのメンテナンス性が落ちたりするのでは本末転倒だ。
  • ブラックボックステストにし続けるためのコストが、本来の実装に悪影響を及ぼすようなレベルになってしまったら、そろそろホワイトボックステストに移行していい頃合いだ。
    • 検証対象の機能の粒度が大きくなってくると、これはもう避けられない事と考えた方がいい。

自分は今まで、とりあえずユニットテストに注力していて、ある意味脅迫観念的な勢いで、ブラックボックス度合いを高くする事を心がけてた。今まではそれでだいたい問題なかった。でも最近になって、ブラックボックステストにしようとすると無理があるというケースにぶち当たるようになった。1つの機能の中でコロコロと遷移する内部状態を、どうにかして検証したいというようなケースが出てきた。

それですとうさんに相談したら、そういう時はモックを使えばいいと言われた。でも、話を聞く限りだとモックというのはテスト対象の実装の中の処理の流れを追う物のようなので、それじゃブラックボックステストにならないじゃないかと思った。それをそのまま言ったら、確かにテストはできるだけブラックボックステストになってた方がいいけど、機能テストやインテグレーションテストのような粒度の大きな単位のテストでは、処理の中で起こる様々な出来事や副作用を色々モニタリングして、すべての処理が期待通りに動いているかどうかを検証しないといけないから、必然的にホワイトボックステストにならざるを得ないと言われた。

それを聞いて、目の覚めるような思いをした。そうか、ブラックボックステストとホワイトボックステストの使い分けはそこが基準になるのか、と。今まで自分がホワイトボックステストを書かずに済んでいたのは、状態の遷移を伴うような機能を作る必要がなかったからだったんだ、テストをどうも書きにくいなあと思っていた機能は本当はホワイトボックステストにしたほうがいい物だったんだ、と。

そんなわけでUxUにモックの機能を実装した

「JavaScript Mock」で検索するとJSMockjqmockが上位に出てきたので、最初はそれらを参考にするように(メジャーな実装があるんだったらそれをそのまま取り入れるなりAPIを合わせるようにするのが望ましい)と言われたんだけど、ドットで繋げるメソッドチェインの記法がガンガン出てきて頭パンクした。

もう少し下の方までスクロールするとMockObject.jsというのが出てきて、こっちはファイル全体で3KBに満たない小さなライブラリなので、まずはここから始めることにした。何せコードが短いから、読むのもそんなに苦にはならない。モックの概念を言葉で説明されてもさっぱりだったけど、一通りの処理の流れを見たら、「モックというのは一体何をやらなきゃいけないのか」「どういう振る舞いが期待されているのか」ということがよく分かった。

MockObject.jsと同等の機能を一通り実装した後でもう一度JSMockの方を見たら、なるほどこれはこういう意味だったのかというのがやっと分かった。サンプルコードを見ても、モックという物の意味をそもそもよく知らない時点では、どこからどこまでがJSMockの部分なのかさっぱり分からなかったんだよね。ということで、MockObject.jsに加えてJSMock互換のAPIも付け加えてみた。jqunitの方は……もう別言語だからシラネ。

あと有名なのはJsMockito? これも頑張ったらできるかなあ、というかMITライセンスだしそのままぶち込んだ方が早いか……

タブの追加・削除のアニメーション - Aug 08, 2010

Firefoxで、タブを閉じる時にいきなりタブが消えるんじゃなくちょっとずつ消えるようなアニメーションが実装されたようだ。ツリー型タブにアニメーション効果を加えた時にも書いたけど、状態が不連続に変化するよりも連続的に変化した方が分かりやすいことは間違いないので、この改良は歓迎すべき事だと思う。

使い勝手的な面とか見た目的な面とかでは多分他の人が言及していると思うので、僕は実装とかAPIとかの面で感心した点に言及してみる。

このパッチが入るちょっと前に「新しいタブを開く時にアニメーション効果を適用する」という処理も入ったんだけど、この2つには違う所が1点ある。それは、既定の状態がアニメーション有りか無しかという点だ。

  • gBrowser.addTab(uri)で、アニメーション有りでタブが開かれる。
  • gBrowser.addTab(uri, { skipAnimation : true })で、アニメーション無しでタブが開かれる。
  • gBrowser.removeTab(tab)で、アニメーション無しでタブが閉じられる。
  • gBrowser.removeTab(tab, { animate : true })で、アニメーション有りでタブが閉じられる。

今回のremoveTab()の既定の挙動とフラグ指定の仕様は大いに評価したい。何故かというと、これは互換性を最大限保つために必要なことだからだ。

タブを閉じる時にアニメーション効果を適用させるということは、タブに「閉じるという操作は行われたが、まだ実際にはDOMツリー状に存在している」という状態が加わったということだ。そのため、gBrowser.mTabContainer.childNodesで取得したタブの一覧の中に、本来であれば処理対象にしては行けないタブが含まれてしまうようになる。

これは大いに混乱を招く。アドオンの多くはタブを閉じる際のアニメーション効果のことなど知らないから、タブが閉じられたらすぐにDOMツリーからも消えることを期待している。そういう前提で書かれたコードが、片っ端から動かなくなってしまう恐れがある。

今回のパッチでは、この混乱を最小限にするための配慮が行われている。このような状態が発生する場面を「タブのクローズボックスがクリックされた時」と「Ctrl-Wなどのキーボードショートカットで現在のタブが閉じられようとした時」だけに限定することで、それ以外の場合は明示的に指示されない限りはアニメーションしないようにしてある。アドオンが要であるFirefoxとしては、APIの変更でアドオンが作りにくくなる・人気のアドオンが動作しなくなることのデメリットの方が、美しい設計のAPIにすることのメリットを上回ると考えられるから、この判断は合理的と言っていい。

今後の課題は、互換性を重視したことでAPIが分かりにくくなってしまった点をどうするか、ということではないかと個人的には思っている。特に、addTab()removeTab()という対になったAPIにおいて、似たような事をやるのにそれぞれやり方が違うというのは、あまり誉められる事ではないから。

ただ、今はとにかく、Mozillaが互換性を重視する決定をしてくれたことをありがたく思っている。

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

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき