Movatterモバイル変換


[0]ホーム

URL:


ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

この広告は、90日以上更新していないブログに表示しています。

Firefox のツールバーの中身の位置決定方法などについて調べた (Firefox 拡張機能開発にまつわる内容)

Firefox拡張機能を開発する時に、Firefox のツールバー周りのことがよくわからなかったので色々調べてみた。 ドキュメントを調べただけじゃなくて、実際の動作を見たりFirefoxソースコード (Firefox 20 beta 5 のソースコード) を見たりして調べたこともあるので、全部が全部ここに書かれていることが正しいとは限らない。 個人用メモ程度。 指摘等歓迎。

toolbar 周りの基本

ユーザーによるカスタマイズが可能なツールバー

  • ツールバー項目はユーザーが自由に移動することができるようになっている
  • Firefox アプリケーションは、ツールバー項目がどこに配置されているのかを管理するためにツールバー項目の要素の id を使用する
    • あるツールバーの中身の id 一覧はtoolbar 要素の currentset 属性 に保持される
      • カンマ区切りの文字列
      • スペーサーなどは id ではなくてスペーサーを表す文字列が代わりに使われる (“separator” など)
    • この属性は永続化されるようになっているのでユーザーによる変更がFirefox 再起動後も保持されるようになっている (?)
  • Firefox 拡張でツールバー項目を追加する場合は、toolbar 要素にオーバーレイするのではなく の toolbarpalette 要素にオーバーレイするのが良いらしい
  • 新たに追加したツールバー項目を、初期状態で既存のツールバー上に表示させるようにするには、拡張機能がインストールされた後の最初の実行時に JS 側でtoolbar 要素の currentSet プロパティ を変更する必要がある。 さらに、Firefox 再起動後にも有効になるように永続化させる必要もある (詳細は後述)

Firefox のデフォルトのツールバーたち

  • Firefox のアプリケーションウィンドウに元から存在するツールバー (toolbar 要素) は のtoolbox 要素 に結びつけられている
    • ここでいう 「結び付けられている」 というのは、toolbar の toolbox プロパティの値が の toolbox 要素である、ということ
  • id="navigator-toolbox" の toolbox 要素は、子要素に のtoolbarpalette 要素 を持っている

toolbar の currentSet プロパティでツールバーの中身を指定する

  • toolbar の currentSet プロパティを変更 (そのツールバーが子として持つツールバー項目の id 一覧を設定) した時、toolbar 要素の中の子要素も変更される
  • そのとき、指定した currentSet プロパティに渡した id の要素は、その toolbar に結びつけられている toolbox 要素の中から探される
    • すなわち、その指定の id の要素の親要素 (toolbar 要素) の toolbox プロパティが、その toolbox 要素と同一でなければならない
    • あるいは、その toolbox の palette プロパティの値 (toolbarpalette 要素) の子要素でなければならない
  • ユーザー操作による変更ではなく JS 側からの変更を永続化 (Firefox 再起動後にも有効に) するには、currentset 属性を変更して、この属性に対して document.persist メソッドを使う
    • currentSet プロパティと currentset 属性は別物なので、片方を変更したからといってもう一方にも変更が及ぶわけではない (ウィンドウのロード完了時に currentset 属性をもとにして currentSet プロパティが初期化される、という関係)
  • id 指定でツールバーにツールバー項目を追加する方法としては、currentSet プロパティに代入する他にもtoolbar の insertItem メソッド を使う方法もある

toolbar の中身がいつ初期化されるのか

  • toolbar の中身が初期化されるのは、
    • Chrome ウィンドウの document の readyState が "complete" になったとき*2
    • toolbar が作られたときに既にChrome ウィンドウの readyState が "complete" だったならば、toolbar が作られたときだと思われる (DOM ツリーに挿入されたタイミングかもしれない。ソースコード読んだけどちゃんと理解してない)

toolbar の currentSet プロパティをいじるなら、document の readyState が "complete" になったあとにしないとおかしくなるので注意。

ブートストラップ型の拡張機能でツールバー項目を追加する場合

そういうわけなので、ブートストラップ型の拡張機能を開発している場合で、ツールバー項目を追加する場合には、Chrome ウィンドウの readyState が "complete" になるまでに toolbarpalette にツールバー項目を追加しておくとよい。 そうするとFirefox 側の機能で自動的にツールバー項目が適切な位置に配置される。 (それより遅いタイミングでツールバー項目を追加する場合は、拡張機能側でどこに追加するかを指定しなければならない。)

ちなみにブートストラップ型の拡張機能のインストール直後には既にはChrome ウィンドウは存在しているはずなので、そのタイミングでは適切な位置に拡張機能が挿入する必要がある。 アップデート時にはアップデート前と同じ位置に挿入する必要がある。 上記の 「readyState が "complete" になるまでに...」 というのはあくまでFirefox 再起動後の拡張機能の setup 時の話である。

toolbarpalette 要素にツールバー項目を追加する方法は次の節を参照のこと。

toolbarpalette 要素の扱い

  • toolbarpalette 要素は、toolbar の初期化に合わせて (つまり、基本的にはChrome ウィンドウの readyState が "complete" になったとき) DOM ツリーから取り除かれる
    • 要素自体は toolbox の palette プロパティで参照できるように残される

なので、id="BrowserToolbarPalette" の toolbarpalette 要素に子要素を追加するには、条件分岐しなければならない。

var browserToolbarPaletteElem =document.getElementById("BrowserToolbarPalette") ||document.getElementById("navigator-toolbox").palette;browserToolbarPaletteElem.appendChild(newToolbarItem);

基本的にはXUL オーバーレイが使える場合は特に上記のようなことをする必要は無いと思うが、動的にツールバー項目を追加する場合や、XUL オーバーレイが使えないブートストラップ型の拡張機能を開発する場合等は上のようにすれば良いと思われる。

ツールバーの表示、非表示

toolbar の collapsed プロパティをいじることで、ツールバーを非表示にしたり表示したりできる。 アドオンバーは初期状態で非表示なので、拡張機能の初回実行時にアドオンバーにツールバー項目を追加する場合は、ついでにアドオンバーを表示するようにするのが良さそう。

toolbar の collapsed プロパティを false にするだけだと永続化されないので collapased 属性も書き換えて、document.persist メソッドを使って永続化するのが良い。

// 拡張機能の初回起動時の処理; これらは document.readyState が "complete" になったあとに実行すべき (?)toolbar.collapsed =false;toolbar.setAttribute("collapsed","false");// プロパティを変更したら属性も変更されるのかもしれないが未確認document.persist(toolbar.id,"collapsed");

ブートストラップ型の拡張機能でのツールバー項目

piroor さんにより公開されているブートストラップ型のXUL ベースの拡張機能のテンプレート (restartless) の中に、ToolbarItem というモジュールがあるので、ブートストラップ型の拡張機能でツールバー項目を使いたい場合はこれを使うとか参考にすると良さそう。

最初にこのモジュールを読んだときは何をしているのか全然分からなかったけど、前提知識としてこの記事に書いたような内容を知っていれば大体読めそうな気がする。

サンプルコード

XUL オーバーレイを用いるFirefox 拡張で、アドオンバーにツールバー項目を表示するようにする

まず、以下のようにオーバーレイを指定するXML ファイルで、id="BrowserToolbarPalette" の toolbarpalette 要素に toolbarbutton 要素を追加するように書いておく。

<?xml version="1.0"?><overlayid="Screenshot-xulOverlay"xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"><toolbarpaletteid="BrowserToolbarPalette"><toolbarbuttonid="sample-toolbar-button"label="サンプルツールバーボタン"image="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="tooltiptext="サンプルです"></toolbarbutton></toolbarpalette><scripttype="text/javascript; version=1.8"src="overlay.js"charset="utf-8" /></overlay>

上のXML ファイルで読み込まれる overlay.js として以下のようなファイルを作っておく。

(function (){"use strict";// https://developer.mozilla.org/en-US/docs/Code_snippets/Toolbar のサンプルコードをベースに少し書き換えた関数function installButton(toolbarId, id, afterId){var doc =window.document;// 既にどこかに配置されている場合は位置変更しないif (!doc.getElementById(id)){var toolbar = doc.getElementById(toolbarId);// If no afterId is given, then append the item to the toolbarvar before =null;if (afterId){var elem = doc.getElementById(afterId);if (elem && elem.parentNode == toolbar)            before = elem.nextElementSibling;}// 1 回実行すれば, 次に別のウィンドウを開くときにはこの設定が使われる        toolbar.insertItem(id, before);        toolbar.setAttribute("currentset", toolbar.currentSet);        doc.persist(toolbar.id,"currentset");// 属性値の永続化// 追加先のツールバーが非表示なっている可能性があるので, 表示する        toolbar.setAttribute("collapsed","false");        doc.persist(toolbar.id,"collapsed");// 属性値の永続化}}window.addEventListener("load",function el(evt){if (evt.target !==window.document)return;window.removeEventListener("load", el,false);// オーバーレイされたときに毎回この関数を呼び出すと、ユーザーがボタンを// 表示したくない場合にもアドオンバーに追加されてしまう。 実際の拡張機能では// Preferences などで管理して、最初に 1 回だけ実行するようにすること。    installButton("addon-bar","sample-toolbar-button");},false);}).call(this);

そうすると、XUL オーバーレイされるたびに (つまり、新しいFirefox アプリケーションウィンドウが開かれるたびに) JS ファイルが実行され、この拡張機能のツールバーボタンがまだツールバーに表示されていない場合は、アドオンバーに追加されるようになる。 コメント中にも書いているように、実際のFirefox 拡張では、初回起動時のみに実行する様にするなどの配慮が必要である。

ブートストラップ型拡張機能でツールバー項目を追加する

ブートストラップ型のXUL ベースの拡張機能では、XUL オーバーレイが使えない。 なので、ツールバー項目を追加するには JS 側から追加してやる必要がある。XUL オーバーレイで の toolbarpalette 要素にツールバー項目を追加するのと大体同じことをしようと思うと、以下のような感じにすれば良さそう。

下記は bootstrap.js のサンプルコードである。 インストール時や無効になっていた拡張機能が有効にされたときなど、Firefox のアプリケーションウィンドウが既に存在する状態で startup 関数が実行されたときには、既に表示されているウィンドウの toolbarpalette 要素にツールバー項目を追加する。 また、ウィンドウの新規立ち上げを監視し、必要に応じて新しいウィンドウの toolbarpalette 要素にツールバー項目を追加する。 初期状態でどこかのツールバー上に項目を表示したりはしない。 ユーザーによる項目移動がなされた場合はFirefox アプリケーション側で位置が保存されるので、拡張機能側ではどこに表示するかを覚えておくことはしていない。

ブートストラップ型のXUL ベースの拡張機能でツールバーボタンを追加する方法を調べてると 「Firefox 拡張側でツールバーボタンの挿入位置を覚えておくようにして、ツールバーボタン挿入時に適切な位置に挿入するようにすればよい」 と書かれているものがほとんどだったが、Firefox 拡張側で位置を保存しておく必要は必ずしもないんじゃないかなーと思う。

"use strict";var Cc = Components.classes;var Ci = Components.interfaces;var PromptService  = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService);var windowWatcher  = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);// window ごとのツールバー項目管理var ToolbarItemManager =function (win){this._win = win;};ToolbarItemManager.prototype.setupToolbarItems =function (){var doc =this._win.document;if (doc.readyState ==="complete"){this._addToolbarItemsToPalette();this._reinitializeToolbars();}else{this._addToolbarItemsToPalette();// 配置は Firefox がやってくれる}};ToolbarItemManager.prototype._addToolbarItemsToPalette =function (){var doc =this._win.document;var toolbarItemElems =this._createToolbarItemElems();this._toolbarItemElems = toolbarItemElems;var paletteElem = doc.getElementById("BrowserToolbarPalette") ||                      doc.getElementById("navigator-toolbox").palette;if (!paletteElem){throw"デフォルトのツールバーボタンパレットが見つかりません";}    toolbarItemElems.forEach(function (toolbarItemElem){        paletteElem.appendChild(toolbarItemElem);});};ToolbarItemManager.prototype._reinitializeToolbars =function (){var win =this._win;var doc =this._win.document;var navToolboxElem = doc.getElementById("navigator-toolbox");var NS ="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";var toolbarElems =Array.prototype.slice.call(doc.getElementsByTagNameNS(NS,"toolbar"));    toolbarElems.forEach(function (toolbarElem){if (toolbarElem.toolbox !== navToolboxElem)return;var curset = toolbarElem.getAttribute("currentset");        toolbarElem.currentSet = curset;});};// パレットに追加するツールバー項目の配列を返す// ブートストラップ型でない拡張機能における id="BrowserToolbarPalette" の// toolbarpalette 要素へのオーバーレイに相当ToolbarItemManager.prototype._createToolbarItemElems =function (){var doc =this._win.document;var NS ="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";var e = doc.createElementNS(NS,"toolbarbutton");var id ="toolbar";    e.setAttribute("label","Test Button! " +Date.now());    e.setAttribute("id", id);// 小さな赤丸    e.setAttribute("image","data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAU" +"AAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO" +"9TXL0Y4OHwAAAABJRU5ErkJggg==");return[e];};// 終了処理ToolbarItemManager.prototype.releaseToolbarItems =function (){var toolbarItemElems =this._toolbarItemElems;if (!toolbarItemElems)return;    toolbarItemElems.forEach(function (e){var pe = e.parentNode;if (pe){            pe.removeChild(e);}});};// ToolbarItemManager を使って全ウィンドウのツールバー項目を管理var ToolbarItemManagerForAllWindows =function (){this._finalizerSet =new Set();};var finalizerSet =new Set();ToolbarItemManagerForAllWindows.prototype.initForWindow =function (win){var finalizerSet =this._finalizerSet;var toolbarButtonManager =new ToolbarItemManager(win);var finalizer =function (){        win.removeEventListener("unload", unloadEventListener,false);        toolbarButtonManager.releaseToolbarItems();        finalizerSet.delete(finalizer);        finalizer = void 0;        unloadEventListener = void 0;};var unloadEventListener =function (evt){if (evt.target !== win.document)return;        finalizer.call(null);};    win.addEventListener("unload", unloadEventListener,false);    finalizerSet.add(finalizer);    toolbarButtonManager.setupToolbarItems();};ToolbarItemManagerForAllWindows.prototype.finalizeForAllWindows =function (){var finalizerSet =this._finalizerSet;for (var finalizer of finalizerSet){        finalizer.call(null);}};var toolbarItemManagerForAllWindows =new ToolbarItemManagerForAllWindows();// 既に表示されているウィンドウに対する初期化処理function _initForWindows(){var type ="navigator:browser";var enumerator = windowMediator.getEnumerator(type);while(enumerator.hasMoreElements()){// |win| は [Object ChromeWindow] である (|window| と同等)。これに何かをするvar win = enumerator.getNext();        toolbarItemManagerForAllWindows.initForWindow(win);}}// 新たに開かれたウィンドウに対する初期化処理を行うためのオブザーバvar windowObserver ={    observe:function (aSubject, aTopic, aData){var win = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow);var targetLoc ="chrome://browser/content/browser.xul";if (aTopic ==="domwindowopened"){            win.addEventListener("DOMContentLoaded",function el(evt){// DOMContentLoaded 前に確認すると win.location.href が "about:blank" だったりしたif (win.location.href !== targetLoc){// 対象とする window でない場合はイベントリスナを解除して終了                    win.removeEventListener("DOMContentLoaded", el,false);return;}// DOMContentLoaded は何回か発生するので, 対象のものでない場合は何もしないif (evt.target !== win.document)return;                win.removeEventListener("DOMContentLoaded", el,false);                toolbarItemManagerForAllWindows.initForWindow(win);},false);}}};function install(aData, aReason){    PromptService.alert(null,"Bootstrapped Extension Sample","Installed");}function startup(aData, aReason){    PromptService.alert(null,"Bootstrapped Extension Sample","Startup!");    _initForWindows();    windowWatcher.registerNotification(windowObserver);}function shutdown(aData, aReason){    PromptService.alert(null,"Bootstrapped Extension Sample","Shutdown!");    windowWatcher.unregisterNotification(windowObserver);    toolbarItemManagerForAllWindows.finalizeForAllWindows();}function uninstall(aData, aReason){    PromptService.alert(null,"Bootstrapped Extension Sample","Uninstall!");}

*1:というのをどこかで見かけたがどこで見たのかわからない

*2:ちなみに readyState が "complete" になるタイミングは、load イベントの発生するタイミングである

注目記事
検索
最近のコメント
    カテゴリー

    引用をストックしました

    引用するにはまずログインしてください

    引用をストックできませんでした。再度お試しください

    限定公開記事のため引用できません。

    読者です読者をやめる読者になる読者になる

    [8]ページ先頭

    ©2009-2025 Movatter.jp