Firefox への Feedly Cloud 用フィードリーダー追加

Google Reader から Feedly Cloud へ乗り換えしたので、 Firefox のブックマークメニューの「このページを購読…」からフィードを追加できるようにした。

手順

(1) Firefox で Feedly Could を開く。

(2) Feedly Could を開いたタブで、スクラッチパッド(「ツール」→「Web 開発」→「スクラッチパッド」)を開く。

(3) 以下のコードを貼り付けて、「実行」する。

navigator.registerContentHandler(
    "application/vnd.mozilla.maybe.feed", 
    "http://cloud.feedly.com/#subscription/feed/%s", 
    "feedly"
);

(4) タブ内の上部に「”feedly” (cloud.feedly.com) をフィードリーダーとして追加しますか?」と通知バーが表示されるので、「フィードリーダーを追加」ボタンをクリックする。

(5) 以降、何らかのフィードを開いてブックマークメニューから「このページを購読…」を選択すると、フィードリーダーの一覧に「feedly」が表示されるようになる。

navigator.registerContentHandler によるフィードリーダーの追加は、現在開いているページと同一ドメインでないと許可されないので、必ず上記(1)の手順で Feedly Could を開いておくことが重要。

別解

エラーコンソールを開いて下記のコードを実行する。この場合、前述のような同一ドメインの制約はない。

Components.classes["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Components.interfaces.nsIWebContentHandlerRegistrar).
registerContentHandler(
    "application/vnd.mozilla.maybe.feed",
    "http://cloud.feedly.com/#subscription/feed/%s",
    "feedly",
    null
);

参考

Firefox へのフィードリーダーの追加 | MDN

TOP

nsIZipWriter を使ってフォルダ丸ごと圧縮

前提

・Firefox 21~24.0a1
・変数 srcDir は圧縮元フォルダの nsILocalFile オブジェクト
・変数 zipFile は圧縮先ファイルの nsILocalFile オブジェクト
srcDir の中身のファイルをすべて圧縮して新規のアーカイブ zipFile を生成する

nsIZipWriter インスタンス生成

はじめに nsIZipWriter インスタンスを生成し、 open メソッドで圧縮先ファイルを開く。
圧縮率はデフォルト(レベル6)とする。

// |zipFile| is a nsILocalFile object corresponding to the zip file
var zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
const PR_WRONLY = 0x02;
const PR_CREATE_FILE = 0x08;
zipWriter.open(zipFile, PR_WRONLY | PR_CREATE_FILE);
var zipLevel = Ci.nsIZipWriter.COMPRESSION_DEFAULT;

フォルダ/ファイルのエントリ追加

srcDir を起点に、フォルダ内のファイルへ再帰的にアクセスする。

(function(dir) {
    var fileEnum = dir.directoryEntries;
    while (fileEnum.hasMoreElements()) {
        var file = fileEnum.getNext().QueryInterface(Ci.nsILocalFile);
        if (file.isDirectory()) {
            // [ToDo]            
            // go to sub folder recursively
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            // [ToDo]            
        }
    }
}).call(this, srcDir);
// |srcDir| is a nsILocalFile object corresponding to the folder where will be archived

変数 file がフォルダなら、 addEntryDirectory でエントリを追加する。第3引数 false だと即座に圧縮される。
変数 file がファイルなら、 addEntryFile でエントリを追加する。第4引数 false だと即座に圧縮される。
変数 file が通常のフォルダでもファイルでもなく、ショートカット(シンボリックリンク)などの場合は何もしない。

エントリ名は、圧縮元フォルダを起点にした圧縮元ファイルのパス。例えば、圧縮元ファイルのパスが「C:UsersHogeSourceDirectoryAAABBBccc.txt」なら、エントリ名は「AAA/BBB/ccc.txt」となる。フォルダの場合はエントリ名の末尾に「/」を付ける。

        var entry = file.path.substr(srcDir.path.length + 1).replace("", "/", "g");
        if (file.isDirectory()) {
            entry += "/";
            zipWriter.addEntryDirectory(entry, file.lastModifiedTime * 1000, false);
            // go to sub folder recursively
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            zipWriter.addEntryFile(entry, zipLevel, file, false);
        }

最後に圧縮先ファイルを閉じる。

zipWriter.close();

processQueue を使って後からまとめて圧縮

addEntryDirectory, addEntryFile の最後の引数を true にすると、その時点では圧縮されず、後で processQueue メソッド呼び出し時にまとめて圧縮される。

        if (file.isDirectory()) {
            entry += "/";
            zipWriter.addEntryDirectory(entry, file.lastModifiedTime * 1000, true);
            arguments.callee.call(this, file);
        }
        else if (file.isFile()) {
            zipWriter.addEntryFile(entry, zipLevel, file, true);
        }

processQueue の第1引数には nsIRequestObserver オブジェクトを渡す。
nsIRequestObserver は最初のファイル圧縮前に呼び出される onStartRequest と、最後のファイル圧縮後に呼び出される onStopRequest メソッドを持つ。

    zipWriter.processQueue({
        onStartRequest: function(aReuqest, aContext) {
        },
        onStopRequest: function(aRequest, aContext) {
            zipWriter.close();
        },
    }, null);

TOP

xul:textbox を最終行までスクロールする

multiline=”true” な複数行の xul:textbox 要素を最終行までスクロールする。

// assuming that elt is a xul:textbox element
elt.inputField.scrollTop = elt.inputField.scrollHeight;

TOP

XUL Document をタブで開いたときにタブにアイコンを表示する

アドオンマネージャ(about:addons)をタブで開いたとき、タブにアドオンを示すパズルピース型のアイコンが表示される。これを自分の拡張機能でも実現したい。

まず、XUL Document 内にXHTML名前空間でlinkタグを埋め込む。

<window title="My Add-on"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">

    <xhtml:link rel="shortcut icon" href="chrome://myaddon/skin/icon.png" />

    ...

</window>

これだけだとなぜかXULのレイアウトがバグるので、CSSでlinkタグを非表示にする。

    <xhtml:link rel="shortcut icon" href="chrome://myaddon/skin/icon.png" style="display: none;" />

TOP

xul:prefwindow の設定ダイアログにヘルプボタンを表示する

Firefox 本体の「オプション」ダイアログのように、 xul:prefwindow 要素で作った設定ダイアログに、「ヘルプ」ボタンを表示したい。

xul:prefwindow の buttons 属性?

xul:prefwindow 要素の buttons 属性の説明を読むと、表示したいボタンをカンマ区切りで指定する、と書いてあるので以下のようにしてみた。しかし、OKボタン・キャンセルボタンは表示されるものの、ヘルプボタンは表示されなかった。

<prefwindow buttons="accpet,cancel,help">

ちなみに、 xul:dialog 要素のダイアログであれば、上記方法でもヘルプボタンが表示される。

xul:prefpane の helpURI 属性?

xul:prefpane 要素の helpURI 属性の説明を読むと、設定用パネルに関連付けられた URI を指定する、と書いてあるので以下のようにしてみた。しかし、依然としてヘルプボタンは表示されなった。

<prefpane helpURI="http://www.example.com/">

xul:prefpane の helpTopic 属性!

MDC のドキュメントには説明が無いが、以下のように各設定パネル(xul:prefpane 要素)に helpTopic 属性を指定することで、ようやくヘルプボタンの表示が可能となった。なお、 helpTopic の値は各設定パネルを識別可能な文字列を適当に入れておけばよい。

<prefpane helpTopic="general">

別解

別解として、スクリプトを使って動的にヘルプボタンを見えるようにする方式もある。

<prefwindow onload="document.documentElement.getButton('help').hidden = false;">

余談

設定ダイアログに一切のボタンを表示させたくない場合、 xul:prefwindow 要素の buttons 属性にカンマ一文字を指定する。

<prefwindow buttons=",">

ヘルプボタンクリック時の動作

次に、ヘルプボタンをクリックして、特定のURLをブラウザで開くようにする。
ヘルプボタンクリック時の動作は xul:prefwindow 要素の ondialoghelp 属性に指定する。

<prefwindow ondialoghelp="openHelpURI();">

URLをブラウザで開く際、設定ダイアログがモーダル(instantApply が false)の場合は新しいウィンドウで、モードレス(instantApply が true)の場合は新しいタブで開きたい。そこで、 chrome://browser/content/utilityOverlay.js を読み込んでおくと色々面倒なことを解決してくれる openUILinkIn というユーティリティ関数が使えるようになる。ただし Firefox のアドオン限定。

<script type="application/x-javascript" src="chrome://browser/content/utilityOverlay.js" />
<script type="application/x-javascript"><![CDATA[
    function openHelpURI() {
        var where = document.documentElement.instantApply ? "tab" : "window";
        openUILinkIn("http://www.example.com/", where);
    }
]]></script>

TOP

ウィンドウがアクティブ(最前面に表示されている)かどうかの判定方法

あるウィンドウが Firefox 以外のアプリケーションのウィンドウも含めた中で最前面に表示されているかどうかを判定する方法。

Firefox 3.6 の場合

最前面に表示されたウィンドウにはXULドキュメントのルート要素に active 属性(値は “true”)が自動的に付加される (参考:nsGlobalWindow.cpp)。

var isActive = document.documentElement.hasAttribute("active");

ちなみに、最初これを教わったときに最前面にあるブラウザウィンドウを DOM Inspector で一生懸命調べたものの active 属性なんてものは無かったが、その場合最前面のウィンドウは DOM Inspector になるので、無くて当然である。

Firefox 4.0 の場合

なぜか active 属性を付加する仕組みが削除されたようなので、代替としてCSSの -moz-window-inactive 擬似クラスを利用する。
例えばブラウザウィンドウ (browser.xul) であれば、以下のようにして調べることが可能。

var isActive = !document.querySelector("#main-window:-moz-window-inactive");

他の方法として、window に対する focus, blur イベントを監視して自分でフラグを上げ下げする方法もある。

TOP

Firefox 4 対応アドオンのプレビュー用スクリーンショット作成手順

Firefox 4 ベータ版の完成度もだいぶ高くなってきて見た目も最終仕様に近づいてきたと思われるので、addons.mozilla.org で公開している Firefox 4 対応済み自作アドオンのプレビュー用スクリーンショットを順次 Firefox 4 ベースに差し替えていこうと思う。以下はプレビュー用スクリーンショットの作成手順メモ。

OS

以前は Windows XP (Royale Theme) + Firefox 3.6 でプレビュー用スクリーンショットを作成していたが、 Firefox 4 になってデザインが Windows 7 と相性が良くなったのと、自分の開発環境が Windows 7 ということもあり、今後は Windows 7 + Firefox 4 とする。

OSのテーマ

Windows 7 デフォルトの Aero テーマで、透明感を有効(Glass効果あり)にし、壁紙を白一色にする。透けたタイトルバーにデスクトップのアイコンが写り込むのを防ぐため、デスクトップ上で右クリック→「表示」→「デスクトップアイコンを表示」をオフにする。
次に、「デザインの詳細設定」でUIに使用するすべてのフォントを、メイリオから Segoe UI に変更する。メイリオも綺麗だが、 Segoe UI は大変美しく、小さいサイズでも可視性に優れている。さらに、アクティブタイトルバーのサイズを18から20に変更する。
次回以降の作業時のために、変更した状態となっている「未保存のテーマ」を右クリックして名前を付けて保存しておく。

Firefox のバージョン

Firefox 4.0b8pre(トランクビルド)だと、タイトルバーのアプリケーションボタンなどが「Firefox」ではなく「Minefield」になってしまうため、現時点で最新のベータ版である、 Firefox 4.0b7 をインストールする。自分はインストーラ版の Firefox 3.6 をインストールしており、追加でインストーラから Firefox 4.0b7 をインストールすると関連付けが変わったり若干面倒なことになりそうな予感がしたので、ZIPパッケージの Firefox 4.0b7 を探したが、見つからなかった。そこで、イレギュラーな方法であるが、Firefox ベータ版 – 各国語版のダウンロードから英語版の Firefox 4.0b7 インストーラをダウンロードし、インストーラの exe ファイルを 7-Zip で展開し、中にある「core」フォルダを取り出して「Mozilla Firefox 4.0b7」にリネームし、「C:Program Files」内に移動することでインストールした。なお、プレビュー画像の国際化対応のためUIを英語にしたいので、英語版の Firefox 4.0b7 をダウンロードする。

Firefox の設定

「”C:Program FilesMozilla Firefox 4.0b7firefox.exe” -p -no-remote」で Firefox を起動し、プロファイルマネージャにて常用プロファイルとは別のプレビュー画像作成用のクリーンなプロファイルを作って起動する。
各種ツールバーの表示は基本的にはデフォルト状態のまま変更しないようにする。つまり、メニューバーは非表示(代わりにアプリケーションボタン表示)、ナビゲーションツールバー表示、タブバーは上部に表示となる。ただし、ナビゲーションツールバー右端の「Feedback」ボタンはベータ版特有のものなので、ツールバーのカスタマイズで取り除いておく。アドオンバーは必要に応じて表示する。
また、GPU固有の問題かもしれないが、自分の環境ではハードウェアアクセラレーションを有効にするとなぜかテキストの描画が劣化するので、オプションの「Use hardware acceleration when available」をオフにする。

Firefox のウィンドウ

Firefox のウィンドウのサイズを addons.mozilla.org で許可されている最大のサイズである 700 x 525 にする。
やり方は、以下のコードをローケーションバーに貼り付けて移動するだけで良い。ただし、 JavaScript の詳細設定で「Move or resize existing windows」がオフになっていると効かないので注意。

javascript:window.resizeTo(700,525);

サイドバー系の拡張機能では以下のコードをエラーコンソールで実行し、サイドバーの幅を一意にする。

var win = Components.classes["@mozilla.org/appshell/window-mediator;1"].
          getService(Components.interfaces.nsIWindowMediator).
          getMostRecentWindow("navigator:browser");
win.document.getElementById("sidebar-box").width = 200;

スクリーンショットのキャプチャ

ここまでの手順で準備は完了で、あとはひたすらアドオンの色々な場面のスクリーンショットをキャプチャしていく。
スクリーンショットのキャプチャには WinShot を使用し、基本的にはアプリケーションのウィンドウ全体をキャプチャする。 Windows Aero 有効時はウィンドウの外側に影が付くが、画像サイズを700 x 525で統一するため、この影は省いてキャプチャすることとなる。ただ、ウィンドウ枠の四隅の角丸の外側が汚れたような見た目になってしまう問題点がある。

実際の例

以上の手順に従って作成した Firefox のスクリーンショットはこちらです。

TOP

persist 属性による属性値の永続化

属性の永続化とは?

XUL要素に persist 属性によって値を保存したい属性の名前を指定しておくと、その属性の値が次回XULドキュメントをロードした時に復元される。保存された属性の値は Firefox 終了時にローカルファイル(プロファイルフォルダ下の localstore.rdf)へ書き出されるので、 Firefox を終了しても保持される。

XULアプリで何らかの設定値を保存するには、 Preferences の仕組みを使って保存する方法が有名だが、UIの見た目に関する設定値(ウィンドウの位置や大きさ、ツールバーのボタンの並び順など)は属性の永続化によって保存される場合が多い。

具体的な例

Firefoxのウィンドウの位置や大きさは、前回開いたときの状態が復元される。これは属性の永続化によって実現されている。 browser.xul のソースを見ると、以下のように persist 属性によって保存したい属性のリストが指定されている。

browser.xul
<window id="main-window"
        ...
        persist="screenX screenY width height sizemode">

localstore.rdf には以下のように {XULドキュメントのURL}#{要素のid} をキーとしてして保存対象の各属性の値が保存される。

localstore.rdf
  <RDF:Description RDF:about="chrome://browser/content/browser.xul#main-window"
                   screenX="115"
                   screenY="87"
                   width="823"
                   height="645"
                   sizemode="normal" />

基本的な使い方

例として、以下のように persist 属性にて value 属性を永続化するようにした xul:label 要素を作成する。idの無い要素は属性の永続化ができないことに注意。

test.xul
<label id="testLabel" value="Test" persist="value" />

次に、以下のようなボタンを作って、 value 属性の値を動的に変更してみる。

test.xul
<button label="Write value"
        oncommand="document.getElementById('testLabel').setAttribute('value', 'xxx');" />

いったん Firefox を終了し、 localstore.rdf を開いてみると、以下のように変更後の foo 属性の値が保存されていることがわかる。

localstore.rdf
  <RDF:Description RDF:about="chrome://.../content/test.xul#testLabel"
                   value="xxx" />

引き続き Firefox を起動して test.xul を開いてみると、ラベルの文言が変更後の xxx になっていることがわかる。

次に、以下のようなボタンを追加し、ボタンをクリックして value 属性の値を削除してみる。

test.xul
<button label="Delete value"
        oncommand="document.getElementById('testLabel').removeAttribute('value');" />

Firefox を終了して localstore.rdf を見ると、 value 属性の値を保存していた部分が削除されていることがわかる。また、 Firefox を起動して test.xul を開いてみると、ラベルの文言が初期値の Test になっていることがわかる。

persist 属性の落とし穴

persist 属性には永続化したい属性の名前(複数ある場合はスペース区切りで連結)を指定するが、注意すべき点として、 persist 属性に指定した属性の名前に部分一致する属性までもが永続化の対象となることが挙げられる。

先ほどの persist="value" とした xul:label 要素の例で、以下のようなボタンを追加する。

test.xul
<button label="Write val"
        oncommand="document.getElementById('testLabel').setAttribute('val', 'xxx');" />

ボタンをクリックして val 属性の値に xxx をセットし、 Firefox を終了して localstore.rdf を見ると、永続化の対象外と思われた val 属性の値までもがちゃっかり保存さていることがわかる。

localstore.rdf
  <RDF:Description RDF:about="chrome://.../content/test.xul#testLabel"
                   val="xxx" />

これはFirefox 本体側のバグと思われる。
Bug 231333 – Unexpected persist when the persisted attribute name contains another

document.persist

document.persist メソッドによって、 persist 属性を使わずに属性の永続化をすることも可能である。

test.xul
<label id="testLabel" value="Test" />
<button label="Write value"
        oncommand="document.getElementById('testLabel').setAttribute('value', 'xxx');
                   document.persist('testLabel', 'value');" />

persist 属性には前述のように部分一致した属性も永続化される問題があるため、こちらの方法を使ったほうが無難かもしれない。

TOP

Jetpack SDK 0.7 の Panel API

Jetpack SDK 0.7 では新たに Panel API が追加され、HTMLで記述されたGUIを表示可能なパネル型UIを追加するこが可能になりました。 Widget API で追加したボタン型UIと連携して、ボタンをクリックするとパネルを開くことも可能です。

基本的な使い方

Panel API を使うためには、まず panel モジュールをインポートします。なお、本記事のサンプルスクリプトでは exports.main の記載などを省略しています。

var panels = require("panel");

次に、 Panel コンストラクタを使ってパネル型UIを作ります。引数には、色々なプロパティを有するオブジェクトを渡します。下記の例では 幅200、高さ150ピクセルのパネル上に指定したURLをロードします。

var myPanel = panels.Panel({
    width : 200,
    height: 150,
    contentURL: "http://www.example.com/",
});

Panel コンストラクタで生成した panel オブジェクトの show メソッドを呼び出すと、パネルを開くことができます。メソッドの引数に何も指定しない場合は親ウィンドウの中央にパネルが開かれます。

myPanel.show();

widget API の Widget コンストラクタの引数オブジェクトの panel プロパティに panel オブジェクトをセットすることで、ボタン型UIをクリックしてパネルを開くこともできます。

const widgets = require("widget");
var button = widgets.Widget({
    label: "Test",
    image: "chrome://browser/skin/Secure24.png",
    panel: myPanel
});
widgets.add(button);

パネル型UIへ自前のHTMLをロードする

self API を使うと、パッケージの data フォルダ内に格納した自前のHTMLをパネル型UIへロードすることができます。

data/panel.html
<html>
<body>
    <p>Jetpack</p>
    <button onclick="say('Hello');">Say Hello</button>
    <button onclick="say('Bye');">Say Bye</button>
</body>
</html>
lib/main.js
const self = require("self");
var myPanel = panels.Panel({
    width : 200,
    height: 150,
    contentURL: self.data.url("panel.html"),
});

パネル型UIへコンテントスクリプトをロードする

引き続き、上記のパネルに配置されたボタンをクリックした際に、 Jetpack SDK の notifications API を使って通知を表示させるようにします。パネル型UIにロードされたHTMLからは直接 notifications API を呼び出すことはできず、2つの JavaScript コンテキスト間でメッセージを受け渡す、少し回りくどいやり方となります。

まず、 Panel コンストラクタの引数オブジェクトへ contentScriptURL プロパティを追加し、 self API を使って data フォルダ内に格納した panel.js のURLを配列で指定します。また、 contentScriptWhen プロパティに “ready” という値を指定することで、パネル内のHTMLロード完了時にスクリプトが実行されるようになります。

lib/main.js
const self = require("self");
var myPanel = panels.Panel({
    width : 200,
    height: 150,
    contentURL: self.data.url("panel.html"),
    contentScriptURL: [self.data.url("panel.js")],
    contentScriptWhen: "ready",
});

contentScriptURL プロパティによって読み込まれるスクリプト(コンテントスクリプト)は、HTMLに<script>タグで記述したスクリプトと異なる特殊な JavaScript コンテキストで実行されます。そのため、HTMLの window オブジェクトにアクセスするには明示的に window. とする必要があります。以下の例では window オブジェクト直下に say という関数を追加し、 button 要素の onclick 属性から呼び出し可能にしています。また、コンテントスクリプトでは特殊な変数 panel の sendMessage メソッドによって main.js 側の JavaScript コンテキストへ文字列(あるいはJSON文字列化可能なオブジェクトなど)を送ることができます。

data/panel.js
window.say = function(text) {
    panel.sendMessage(text);
};

panel オブジェクトの sendMessage で送られた文字列(あるいはJSON文字列化可能なオブジェクトなど)は、 main.js 側の JavaScript コンテキスト内の panel オブジェクトの onMessage コールバック関数によって受け取ることができます。以下の例では受け取った文字列を notifications API を使って通知として表示します。

lib/main.js
const self = require("self");
var myPanel = panels.Panel({
    width : 200,
    height: 150,
    contentURL: self.data.url("panel.html"),
    contentScriptURL: [self.data.url("panel.js")],
    contentScriptWhen: "ready",
    onMessage: function(message, callback) {
        require("notifications").notify({
            title: "Message from Panel",
            text: message
        });
    }
});

Panel

TOP

Jetpack SDK 0.7 の Notifications API

Jetpack SDK 0.7 では新たに Notifications API が追加され、Firefox のダウンロード完了通知などでお馴染みのスライド式の通知UIを表示することが可能になりました。 Notifications API を使うためには、まず notifcations モジュールをインポートします。

const notifications = require("notifications");

通知を表示するためには notify メソッドを呼び出します。引数には、以下のプロパティを有するオブジェクトを渡します。

プロパティ 概要
title
text
通知に表示する文字列。
iconURL 通知に表示する画像のURL。 self APIを使って自パッケージ内の data フォルダに格納した画像を指定することも可能。
data onClick の引数として渡される文字列。
onClick 通知をクリックした際の処理。引数に data プロパティの値が渡される。
notifications.notify({
    title: "Jetpack",
    text: "This is a notification.",
    iconURL: "chrome://browser/skin/Geolocation-64.png",
    data: "test",
    onClick: function(data) {
        console.log(data);
    },
});

notification

TOP