Home > Latest topics

Latest topics 近況報告

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

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

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

Page 8/246: « 4 5 6 7 8 9 10 11 12 »

タブバーをドラッグしたらウィンドウが動くとかその辺のタイトルバーっぽい挙動を無効化する - 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;
}

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

まとめ

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

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

XUL/Migemo 0.13.xで中身の方をだいぶ変えて(環境によっては)高速化しました - Jul 09, 2010

半分眠ったまんまで作業してたのでしょーもないregressionを仕込む→修正して公開→またregressionということを繰り返した結果、既に0.13.3になってしまったわけですが、バージョン0.13.0で結構大きくいじりました。機能的には全然変わってないですが。

  • 開発メモにちょっと書いてるけど、このバージョンからFirefox 2とThunderbird 2をサポートしなくなりました。
  • Gecko 1.8どころかGecko 1.9.0も切り捨てたという事で、Firefox 3.5より後で入った便利機能を心置きなく使えるようになったり、Firefox 2→Firefox 3の間で大きく変わったAPIの両方に対応するためのめんどくさい場合分けをゴソッと削除できたりしたので、だいぶスッキリした気がします。
    • ウィンドウごとに分ける必要がないコードでXPCOMコンポーネント化してなかった物は積極的にJavaScriptコードモジュール化しました。
    • Thunderbird専用のコードは、XPCOMコンポーネントにしてはあったものの、そんなに汎用性が無かったので、これも全部JavaScriptコードモジュール化しました。
  • 検索でヒットした箇所が多い時に、Safari風の強調表示等の機能を有効にしているとフリーズしてしまう問題について、ヒット箇所の強調表示を段階的に非同期で行うようにしてみました(0.13.4)。
  • Gecko 1.9.2以降ではnsIDOMWindowUtilsのnodesFromRect()を使うようにしたので、見えているスクロール位置から検索を始める処理が相当速くなりました。
  • Minefieldに最近入った機能のおかげで、DOM Rangeから検索用のテキストを取得する部分が爆速になりました。nodesFromRect()の効果と合わせて、Minefield 4.0b2preではページ内検索のストレスがほとんど無くなったと思います。

Minefieldで検索が速くなったのは、Bug 39098 – Elements with visibility:hidden, visibility:collapse, or display:none get copied to the clipboardがfixされたおかげです。

  • これまでXUL/Migemoのページ内検索が非常に重かったのは、Rangeを一旦文字列にして正規表現でマッチングして、その結果を使ってもう一度ページ内検索するという仕組みに理由があります。
  • この時、RangeをそのままtoString()で文字列にすると、CSSで非表示になっている要素のテキストまで一緒に取得されてしまうのですが、その後rangefindで検索する時は非表示のテキストは検索対象にならないため、場合によってはヒット箇所が飛ばされてしまったり検索が止まってしまったりという問題が起こります
  • そこで現在のXUL/Migemoでは、
    DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIWebNavigation)
       .QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsISelectionDisplay)
       .QueryInterface(Ci.nsISelectionController)
       .checkVisibility(textNode, 0, textNode.nodeValue.length)
    というやり方で個々のテキストノードの可視・不可視の状態を判別して不可視のテキストを除外した結果を取得して、正規表現のマッチングに使うようにしていました。
  • しかしこの方法は、テキストノードの数が増えると判断の回数も同じだけ増えてしまうため、MXRのページのように内容が小さなテキストノードに分断されているページだととんでもなく処理に時間がかかってしまいます。
  • 5月末にBug 39098で入った変更によって、テキストリンクの高速化の時にも使ったnsIDocumentEncoderに、「非表示状態のテキストを結果の文字列に出力しない」というオプションが加わりました。なのでMinefield上では、この機能を使ってRangeの中の可視状態のテキストだけを一気に取得するようにしました。

Minefield 4.0b2preにXUL/Migemo 0.13.xを入れてMXRのページでページ内検索してみると、効果の程がよく分かると思います。僕は、今すぐにでもFirefox 3.6を窓から投げ捨ててしまいたくなりました。

あとはJägerMonkeyが入ってくれれば……

ところで、他のアドオンからXUL/MigemoのAPIを呼び出す時はちょっと注意が必要になってます。

  • ずっと前に「ユーザ辞書の内容がWebページのスクリプトから取れてしまうのはセキュリティ的に宜しくない」という指摘を受けたんですが、どうやってこの問題を回避すればいいものか悩んで、結局、グローバルなJavaScriptのプロパティの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 - Jul 07, 2010

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個もの引数を取るんですよ……

  • Gecko 1.9.2(Firefox 3.6)から使える。
  • 矩形で示された範囲から、その範囲に含まれるDOMノードを収集して返す。
  • Fennec(Firefox Mobile)用に実装された(Bug 489127 – nodesFromRect required for better usability on mobile devices)。
    • Fennecはタッチスクリーン型のスマートフォンで使われる前提のプロダクト。
    • 指で触った所にあるリンクを拾いたい。
    • でも、小さい画面だと正確にリンクをタッチするのはどう考えてもムリ。
    • そういう時は、タッチスクリーンで検出した座標の周囲のある程度の範囲も含めて取得して、リンクが2個以上あったらそれぞれを選択肢として表示するなどの対策を取れたらいいよね。

そういう経緯で実装されたので、第1から第6までの引数は経緯から考えたら納得のいく指定の仕方と言えよう。

  • 第1引数・第2引数は検索の中心座標を示す。「現在のスクロール位置」は0, 0になる(scrollX, scrollYを指定すると「ビューポートの左上からscrollX, scrollY」という意味になってしまうので注意!)。
  • 第3、第4、第5、第6引数は、検索する範囲を中心座標から上・右・下・左のそれぞれの方向に何ピクセル広げるかを示す。この並び順はCSSでお馴染みの物ですね。
  • 第7引数は、親フレームのスクロール状態を考慮するかどうかを示す真偽値。らしい。falseを渡した場合、ビューポートの外の矩形を範囲として指定すると空のノードリストが返るみたい。
  • 第8引数は、未確定状態のレイアウト情報を確定するかどうかを示す真偽値。らしい。

戻り値のノードリストは、どういう訳か文書中の登場順の逆順で返ってくるようだ。

XUL/MigemoのMinefield対応に向けてのメモ - Jul 07, 2010

作業メモ。

  • gFindBarの遅延初期化には対応した。window.watch('gFindBarInitialized', function() { ... })で、遅延初期化のタイミングで初期化を行える。
  • XPCOMコンポーネントの登録方法の仕様変更にも対応した。これから先、できれば仕様は変わらないでいて欲しいけど……
    • classDescriptionにホワイトスペース文字が入ってると、カテゴリへの登録がうまくいかないかも。nsUpdateTimerManagerに対応するマニフェストファイルなんかを見てみた感じでは、実装のクラス名をそのままclassDescriptionにするといいのだろうか?
    • nsSidebar.jsに対応するマニフェストファイルを見て分かったけど、今までカテゴリマネージャに登録する必要があったケースは全部マニフェストファイルでやるという事のようだ。マニフェストファイルの方に書いておかないと、ちゃんと認識されなかった。
  • これまで、Firefox 2からFirefox 3.6の間でメソッド名やプロパティ名が変わった物はFirefox 2に合わせるようにしてたけど、今後はMinefield 4.0b2pre基準にする事にする。今の所はほとんどFirefox 3.6と同じだけど。
  • Firefox 2からFirefox 3.6までの間それぞれのためのコード、特にFirefox 2用とそれ以降用とでコードを書き分けてた部分が多かったので、思い切ってFirefox 2用のコードは全廃する事にした。
  • rangefindを使う時に注意がいる
  • 他アドオンとの連携にも注意がいる

しないといけないなーと思ってる課題。

  • Minefieldに既に入ってる、スマートロケーションバーの候補として出てきたplaceが既にタブで開かれている場合にそのタブに切り替える機能への対応。
  • Thunderbird 3。とりあえずフォルダペイン内での絞り込みはできるようにしとかないと……とは思ってる。

スマートロケーションバー関係を調べて分かった事。

  • Minefieldは、タブで閲覧中のページをtabbrowserが持ってるプログレスリスナで常時監視してて、タブで閲覧中のplaceの一覧をmoz_openpages_tempという名前のテーブルに保存している。
    • このテーブルはplace_idopen_countという2つのカラムを持ち、ページの遷移に応じて内容が随時更新される。
  • 検索でヒットしたplaceについて、このmoz_openpages_tempopen_countが0より大きい物は、オートコンプリートの候補として返される時にURIの前にmoz-action:switchtab,という文字列が付与され、lichlistitemのtype属性用の値にはactionという値が設定される。
  • スマートロケーションバーのオートコンプリートの実装はいつの間にかJavaScriptになっていた。Cより読み慣れてるから助かる。
  • この機能が有効になるのは、オートコンプリート用のtextboxのautocompletesearchparam属性の値にenable-actionsという文字列が含まれている時だけのようだ。
    • 機能が無効になっている時は、単に上記の「URIの前にmoz-action:switchtab,という文字列が付与され~」という処理がスキップされる。
    • この事から分かる通り、moz_openpages_tempopen_countはオートコンプリートの候補の並び順には影響しない。あくまでfrequencyベースで検索していて、ヒットした候補の中にたまたま現在タブで開かれているplaceが含まれていた場合にだけ、この機能が発動するという仕様のようだ。
  • ロケーションバーへの入力時に「タブで開いてる奴だけ表示」という風に制限する時の既定のキーワードは「%」。browser.urlbar.default.behaviorで指定する時のフラグは128(1 << 7)。

Minefield 4.0b2preではどうも同期的な処理がことごとく失敗するようになっている気がする - Jul 06, 2010

XUL/MigemoのMinefield 4.0b2pre対応のために色々検証していて、1つとても困った問題にぶち当たった。rangefindを使ってWebページ中の要素を装飾する時に、前から後ろに向かって処理を行うと検索が止まってしまう。

Components.utils.import('resource://gre/modules/debug.js'); 
const Cc = Components.classes;
const Ci = Components.interfaces;

function decorate() {
  var span = d.createElement('span');
  span.setAttribute('style','font-size:150%;');
  foundRange.surroundContents(span);
}

/* コンテンツ領域にテスト用の内容をロードする */
var d = Cc['@mozilla.org/appshell/window-mediator;1']
          .getService(Ci.nsIWindowMediator)
          .getMostRecentWindow('navigator:browser')
          .content.document;
d.documentElement.innerHTML = 'Firefox, Firefox, Firefox.';
d.documentElement.clientTop; /* ←伏線 */

/* rangefindを初期化する */
var find = Cc['@mozilla.org/embedcomp/rangefind;1'].createInstance(Ci.nsIFind);
find.findBackwards = false; /* 前から後ろに向かって検索 */
find.caseSensitive = false;

var findRange= d.createRange(); /* 検索する範囲 */
findRange.selectNodeContents(d.documentElement);
var startPoint = findRange.cloneRange(); /* 検索の始点 */
startPoint.collapse(true);
var endPoint = findRange.cloneRange(); /* 検索の終点 */
endPoint.collapse(false);

/* 検索を実行 */
var term = 'Firefox';
var foundRange = find.Find(term, findRange, startPoint, endPoint);
NS_ASSERT(foundRange !== null, '1回目で失敗');

decorate(); /* DOMツリーを編集して装飾する */

/* 検索の範囲を変える(編集した箇所より後を検索の範囲にする)*/
findRange.setStart(foundRange.endContainer, foundRange.endOffset);
startPoint.setEnd(foundRange.endContainer, foundRange.endOffset);
startPoint.collapse(false);

/* もう一度検索を実行 */
foundRange = find.Find(term, findRange, startPoint, endPoint);
NS_ASSERT(foundRange !== null, '2回目で失敗');

decorate(); /* DOMツリーを編集して装飾する */

エラーコンソールにこれをコピペして実行してみると、2回目の方で必ず失敗してしまう事が分かる。本当は「Firefox」という文字列が2箇所装飾されて欲しいのに、最初の1箇所だけで処理が止まってしまう。

これ、上のサンプルの中で伏線と書いている部分が鍵なんだけど、どうもこういうことらしい。

  • DOMツリーを編集すると、編集した箇所から先の範囲が「不確定」な状態になる。
  • 「不確定」な範囲に対しては、rangefindは一切の検索を行えない。
  • DOM要素のプロパティにアクセスするなどしてレイアウト情報を参照すると、状態が強制的に「確定」される。
  • または、setTimeout()等で少し遅らせて処理を行えば、その時には状態が「確定」されている。
  • 状態が「確定」されると、その範囲をまた検索できるようになる(最初の方にある d.documentElement.clientTop は、実はそのための物)。

1回目の検索結果のRangeの箇所でDOMツリーを切った貼ったしているので、その箇所より後の部分はどう頑張ってもそのままの流れでは検索できないようになってしまっている、ということのようだ。

なので、

  • 2回目以降の検索を実行する前に span.clientTop あたりにアクセスしてやれば(これ以外にもclientLeftでもoffsetWidthでもレイアウト系のプロパティなら何でもいいっぽい)、whileforのループを回し続ける事ができる。
  • 毎回setTimeout()で状態の「確定」を待ってやるというやり方でもよい。

という風な回避策があると言える。でも、後者は言わずもがな、前者も毎回レイアウト情報を参照するからクソ重くなりそうで、できればどっちの方法もとりたくない所だ。

将来的にどうなるのかは知らんけど、とりあえず今のところは、後方検索(Rangeの後ろの方から前の方に向かって検索する)ならこの問題に引っかからずに済むみたい。編集した箇所から先の部分が「不確定」になっても、後方検索だと「編集した箇所から先=もう検索が終わった範囲」なので。(→と思ってたけどやっぱり動作が怪しいので安全めな方に倒すということで毎回clientTopにアクセスする方法を使う事にした。なんか負けた気分。)

この「rangefindでループを回してDOMツリーを切った貼ったして装飾する」というやり方はFirefox 2以前のページ内検索における「すべて強調表示」の実装方法だったんだけど、今のFirefoxではDOMツリーはいじらずに強調箇所を選択範囲として処理するようになってて、この問題は問題にならないようだ。今でもFirefox本体でこういうことをやってるところがあれば「これってregressionなんじゃないの」とbugzillaに報告できるところだと思うんだけど……

Firefoxのバージョン間の差異を吸収するライブラリをこそ、僕は欲しているというのに。 - Jul 03, 2010

Firefox 3.6以前とMinefieldとでは、アドオンマネージャのAPIがまるっきり変わってしまった。

Firefox 3.6以前のアドオンマネージャ(Cc['@mozilla.org/extensions/manager;1'].getService(Ci.nsIExtensionManager))は同期的なAPIで、頑張ってラップすればvar enabled = isEnabled('treestyletab@piro.sakura.ne.jp');みたいな感じで「その場で結果を取得する」ことができた。でも今のMinefield(アドオンマネージャがタブで開かれるようになった奴)では、インストール済みのアドオンの情報を取得しようと思ったら必ずコールバック関数を使った非同期なAPIでやらなきゃいけないようになってしまった。

// このコードはMinefield(Firefox 4)以降でないと動かない
Components.utils.import('resource://gre/modules/AddonManager.jsm');
AddonManager.getAddonByID(
  'treestyletab@piro.sakura.ne.jp',
  function(aAddon) {
    if (aAddon && aAddon.isActive) {
      // ツリー型タブがインストール済みで、
      // 且つ有効化されている時の処理
    }
    else {
      // ツリー型タブが利用できない時の処理
    }
})

Firefox 3.6以前と今のMinefield(つまり将来のFirefox 4)の両方に対応しようと思うと、この差異をどうやって吸収するかがネックになる。そこで、メインスレッドの処理を一時停止して処理の完了を待つ裏技を使って、同期的なやり方で他のアドオンの有効・無効の状態を調べたり設定ダイアログを開いたりするためのライブラリをMinefieldでもそのまま使えるようにしてみた。

// このライブラリを使うと、Firefox 3.6でもMinefieldでも
// 違いを意識しないでコードを書けるようになる。
var extensions = window['piro.sakura.ne.jp'].extensions;
if (extensions.isAvailable('treestyletab@piro.sakura.ne.jp')) {
  // ツリー型タブが利用できる時の処理
}
else {
  // ツリー型タブが利用できない時の処理
}

そしたら、Minefieldで起動時にセッションが復元されないという現象に遭遇してしまった。条件を絞り込んでいくと、どうもアドオンマネージャのタブが開かれた状態のセッションが復元される時に問題が起こっていて、さらに辿っていくと、上記の裏技で新アドオンマネージャの処理を止めていると、アドオンマネージャのタブの読み込みが阻害されてしまってセッション復元が半端な所で止まってしまうという事のようだった。

仕方がないので、Firefox 3.6以前のやり方に合わせるのではなく今のMinefieldのやり方に合わせる方向でestensions.jsのAPIを拡張して、今後はそっちの使い方を推奨する事にした。

// 新しい書き方。これも、Firefox 3.6でもMinefieldでも
// 違いを意識しないでコードを書いて大丈夫。
var extensions = window['piro.sakura.ne.jp'].extensions;
extensions.isAvailable('treestyletab@piro.sakura.ne.jp', {
  ok : function() { /* ツリー型タブが利用できる時の処理 */ },
  ng : function() { /* ツリー型タブが利用できない時の処理 */ }
});

Firefox 3.6以前でこのAPIを呼んだ場合は、非同期にならずにその場でコールバック関数が呼ばれるという実装上の違いがあるけれども、基本的に非同期で実行される前提でコードを書いておきさえすれば、Firefox 3.6以前でもMinefieldでもそのまま動くようになってる。コールバック関数を渡さなければ今まで通りの同期的なAPIとして動作するので、コールバック関数はどーしても使いたくない!という場合は、非推奨ではあるけど今まで通りの使い方もできる。

FUELとかJavaScriptコードモジュールとか、Firefox本体の方で色々ユーティリティっぽい物が用意されつつあるけど、僕の立場(複数のバージョンのFirefoxをサポートしたいという前提がある)では、それらはまるっきり役に立たない。新しいバージョンのFirefoxで標準のコードモジュールが増えた所で、現行のリリース版のFirefoxにも対応させるなら、結局それは使えないのだから。しかも、Firefoxのバージョンが上がったらAPIが使えなくなっちゃいましたなんて事もザラにある(今回の話も、nsIExtensionManagerがゴッソリなくなってしまったせいで起こった問題だ)。

そういう「簡単に書けますよ」っていうだけのAPIは、もう、ぶっちゃけどうでもいい。そんな物より、Firefoxの複数のバージョン間での差異を吸収するライブラリこそが僕には必要なんだ。新しいやり方に合わせて書いておけば古いバージョンのFirefoxでもそのまま使える、というのでも、古いやり方のままで新しいバージョンのFirefoxでも動く、というのでも、どっちでもいいんだけど、とにかく1つの記述でどっちのバージョンでも動くようにしておきたい。そうじゃないと、新しいFirefox用と古いFirefox用とで目的が重複するコードがどんどん増えていって、片方は直したけどもう片方は直し忘れてたみたいな穴がどんどん増えていって、すぐ破綻してしまう。

FUELはFirefoxのバージョン間の違いを意識しないで使えるようなAPIの提供を目指してたはずだと思ってたけど、今となってはその計画も頓挫してすっかりうち捨てられてしまったような印象がある。実際、今回の件についてもFUELのAPIは互換性を失う形であっさり変更されてしまってて、もうFirefox 3.6の物と同じ使い方はできないし、Firefox 3.6の物の使い方もMinefieldではできない。JetpackはRebootで明後日の方向に飛んで行ってしまって、少なくとも「Firefox 4から先」の事しか眼中になくてFirefox 3.x系はガン無視っぽい。結局、Mozilla本家はアテにならない。アドオン作者が自分達でやる以外にない。

そういう理由で作ったライブラリ類をリポジトリの中にまとめて置いてあるので、似たような事を考えてる人は覗いてみるといいかもしれない。JavaScriptコードモジュールとして使える物はmodestのJavaScriptコードモジュールの紹介ページに簡単な紹介を書いておいたけど、どれもソースの頭の方に用例を付けてあるので、まあ見てもらえばだいたい分かるんじゃないかな。

一応、簡単な説明。

  • jstimer.jsm:JavaScriptコードモジュール等のDOMWindowを参照しづらい場面でsetTimeout()とかsetInterval()とか書けるようにするライブラリ。
  • namespace.jsm:JavaScriptコードモジュール同士で名前空間を共有できるようにするライブラリ。同じJavaScriptコードモジュールの同じ内容のファイルを複数のアドオンでそれぞれ別々に持たせて、それぞれのメモリ空間も別々に確保される、というのがものすごくあほらしく思えたので作ってみた。
  • animationManager.js:JavaScriptでアニメーションさせる時に、複数アドオンで1つのタイマーを使い回して効率よくアニメーションさせるためのライブラリ。jstimer.jsmと併用すればJavaScriptコードモジュールとしても使える。
  • arrowScrollBoxScrollHelper.js:arrowscrollboxの中に1つ大きなボックスが入っていてその中に小さなボックスがたくさんある、という場面でarrowscrollboxのスクロール処理がぶっ壊れる問題を回避するライブラリ。
  • autoScroll.js:mousemoveまたはdragoverイベントに基づいてタブバーの自動スクロールを行うライブラリ。
  • bookmarkMultipleTabs.xul / bookmarkMultipleTabs_bookmarkPropertiesOverlay.xul:複数のタブをまとめて1つのブックマークフォルダにブックマークするためのライブラリ。Firefox本体の機能だと「全部のタブを保存」しかできないので。
  • boxObject.js:HTMLDocumentのgetBoxObjectFor()を使ってたコードを、手直しせずにそのままFirefox 3.5とかで動くようにするためのライブラリ。
  • extensibleToolbarButton.css / extensibleToolbarButton.xml / extensibleToolbarButton.xul:Firefox本体のツールバーボタンに後からappendChild()とかで内容を追加できるようにするためのライブラリ。
  • extensions.js:このエントリでメインの話題にしてる、「他のアドオンがインストールされているかどうかを調べる」「他のアドオンの設定ダイアログを開く」といった事を簡単に行えるようにするライブラリ。
  • operationHistory.js:何かの操作をアンドゥ・リドゥできるようにしたいときのための汎用的な履歴管理ライブラリ。詳しくは解説のエントリを参照してください。
  • prefs.js:FUEL/STEELが無いような古いバージョンも対象にする場合向けの、簡単に設定を読み書きできるようにするライブラリ。
  • stopRendering.js:ウィンドウ内の再描画を一旦止めて、その間に色々GUIをいじくる処理をして、最後にまとめて表示に反映させる、という事をやるためのライブラリ。画面がチラついてなんか気持ち悪い、という不快感を和らげるのが目的。Firefox 3.6以前とMinefieldとでは実装が中身の自動的に切り替わるけど、APIとしてはFirefoxのバージョンの違いを意識せずに使えるようになってる。
  • stringBundle.js:stringbundle要素をXULの方に埋め込まなくても、同じAPIでpropertiesファイルの中の文字列を読めるようにするAPI。
  • tabFx2Compatible.css / tabFx2Compatible.xml / tabFx2Compatible.xul:Firefox 3以降でtabbrowserのtabの中にappendChild()とかで要素を追加できるようにするためのライブラリ。Firefox 2以前のDOMツリー構造を再現して、中にいくつかのボックスを増やす。
  • UninstallationListener.js:アドオンのアンインストールが行われたタイミングで、変更した設定を自動的に元に戻すとかの後片付け的な処理をやるためのライブラリ。Minefieldでは多分動かなくなってる気がするので、後で直しときます。

初期化処理を書く時に注意がいるようになったみたい - Jul 03, 2010

ツリー型タブが入ってるとMinefield 4.0b2preがぶっ壊れる現象に遭遇した。

何が原因なのかちょっとずつ絞り込んでいったところ、DOMContentLoadedイベントが発火するよりも前の時点で<tabbrowser id="content"/>に触っていたのが原因ぽいという事が分かった。ツリー型タブの初期化処理の中でtabbrowser要素の属性値を取ろうとしてdocument.getElementById('content').getAttribute('...')としただけで、XBLのconstructorとかそのへんがぶっ壊れて、tabbrowser要素の初期化処理が全く行われない状態になってしまってたようだ。

ツリー型タブのインストール後にツールバーのカスタマイズ内容が失われるという話も、これが原因だったんだろうか? 状況としてはよく似てるんだけど。

この問題の発生を避けるには、初期化処理を必ずDOMContentLoadedイベントかloadイベントのタイミング以降で行うようにするのが多分一番簡単だと思う。

window.addEventListener('load', function() {
  window.removeEventListener('load', arguments.callee, false);
  // 何か初期化処理
}, false);

他にも、複数バージョンに対応したアドオンでバージョン判別のために'MozBorderImage' in element.styleとかやってる場合も危険かもしれない。とにかく、DOM要素(の属性値であるとかDOMオブジェクトのプロパティであるとか)を参照せずにメタ的な情報(XPCOMで取得できるGeckoのバージョン情報とかnsIPrefBranchで取得できる設定値とか)で判断できる場合はなるべくそっちを使った方が安全っぽい。例えばFirefox 3.5以降かどうかを調べるならこんな感じ。

var comperator = Cc['@mozilla.org/xpcom/version-comparator;1']
                   .getService(Ci.nsIVersionComparator);
var XULAppInfo = Cc['@mozilla.org/xre/app-info;1']
                   .getService(Ci.nsIXULAppInfo);
if (comparator.compare(XULAppInfo.version, '3.5') >= 0) {
  // Firefox 3.5.0およびそれ以降
}
else {
  // Firefox 3.0およびそれ以前
}

Page 8/246: « 4 5 6 7 8 9 10 11 12 »

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき