Now browsing the archives for 9月, 2006.

「nsIRDFObserver を使ってブックマークのデータソースの動きを調べる」の使用例

nsIRDFObserver を使ってブックマークのデータソースの動きを調べる」の使用例。

(1) ブックマークのプロパティを開いて新しくキーワードを設定してやると、以下のようなメッセージがエラーコンソールへ出力される。

onAssert
rdf:bookmarks
rdf:#$NQocU2
http://home.netscape.com/NC-rdf#ShortcutURL
test
---
onChange
rdf:bookmarks
rdf:#$NQocU2
http://home.netscape.com/WEB-rdf#LastModifiedDate
1159195753515707
1159195758372691

これはすなわち、リソース「rdf:#$NQocU2」へ新たに値が「test」であるノードを指し示すアーク「http://home.netscape.com/NC-rdf#ShortcutURL」が生成されたことを意味する。さらに、リソース「rdf:#$NQocU2」から発するアーク「http://home.netscape.com/WEB-rdf#LastModifiedDate」が指し示す先のノードの値が「1159195753515707」から「1159195758372691」に変わったことを意味する。
以上のことから、ブックマークの名前やキーワードを変更すると、それに伴って最終更新日時も変更されることがわかる。
ちなみになぜ「キーワード」プロパティに相当するアークのURIが「http:// … #ShortcutURL」なんていうわかりにくいものであるのかは不明。おそらく昔の名残?

(2) ブックマークをブラウザで開くと、ページのロードが完了したタイミングで、以下のようなメッセージがエラーコンソールへ出力される。

onChange
rdf:bookmarks
rdf:#$JzBPv1
http://home.netscape.com/WEB-rdf#LastVisitDate
1159194536946366
1159196268746572
---
onChange
rdf:bookmarks
rdf:#$JzBPv1
http://home.netscape.com/WEB-rdf#LastCharset
UTF-8
UTF-8

これはすなわち、リソース「rdf:#$JzBPv1」から発するアーク「http://home.netscape.com/WEB-rdf#LastVisitDate」が指し示す先のノードの値「1159194536946366」が「1159196268746572」に変更され、同様にアーク「http://home.netscape.com/WEB-rdf#LastCharset」が指し示す先のノードの値「UTF-8」が「UTF-8」に変更された(実質変化なし)ことを意味する。
以上のことから、ブックマークをブラウザで開くと、最終訪問日時とページの文字コードの2つのプロパティが更新されることがわかる。これについてもう少し詳しく調べると、以下のような処理の流れであることがわかる。

gBrowser上で何らかのページがロードされる

pageShowEventHandlers が呼ばれる

nsIBookmarksService の updateLastVisitedDate が呼ばれ、もしロードされたページの URL がブックマークされたものであれば、最終訪問日時とページの文字コードの2つのプロパティを更新する

TOP

Inspecting Bookmarks data with nsIRDFObserver

Japanese version of this post is also available.

nsIRDFObserver enables us to observe various events occurred at nsIRDFDataSource. The Firefox’s Bookmarks is managed internally as nsIRDFDataSource, so we can inspect it with nsIRDFObserver described below and Error Console (formally known as JavaScript Console).

First, you should define nsIRDFObserver object which observes the datasource. The two methods, “_targetToString” and “_log” are defined as original private methods.

var rdfObserver = 
{
    onAssert : function(aData, aRes, aProp, aTarget)
    {
        this._log(["onAssert", aData.URI, aRes.Value, aProp.Value, this._targetToString(aTarget)].join("
"));
    },
    onBeginUpdateBatch : function(aData)
    {
        this._log(["onBeginUpdateBatch", aData.URI].join("
"));
    },
    onChange : function(aData, aRes, aProp, aOldTarget, aNewTarget)
    {
        this._log(["onChange", aData.URI, aRes.Value, aProp.Value, this._targetToString(aOldTarget), this._targetToString(aNewTarget)].join("
"));
    },
    onEndUpdateBatch : function(aData)
    {
        this._log(["onEndUpdateBatch", aData.URI].join("
"));
    },
    onMove : function(aData, aOldRes, aNewRes, aProp, aTarget)
    {
        this._log(["onMove", aData.URI, aOldRes.Value, aNewRes.Value, aProp.Value, this._targetToString(aTarget)].join("
"));
    },
    onUnassert : function(aData, aRes, aProp, aTarget)
    {
        this._log(["onUnassert", aData.URI, aRes.Value, aProp.Value, this._targetToString(aTarget)].join("
"));
    },
    /**
     * refer to an appropriate interface for nsIRDFNode and get the string-type value
     */
    _targetToString : function(aTarget)
    {
        const Ci = Components.interfaces;
        if ( aTarget instanceof Ci.nsIRDFLiteral )
            // String type
            return aTarget.QueryInterface(Ci.nsIRDFLiteral).Value;
        else if ( aTarget instanceof Ci.nsIRDFInt )
            // Number type
            return aTarget.QueryInterface(Ci.nsIRDFInt).Value;
        else if ( aTarget instanceof Ci.nsIRDFDate )
            // Date type
            return aTarget.QueryInterface(Ci.nsIRDFDate).Value;
        else
            // And more...?
            return "";
    },
    /**
     * output string to Error Console
     */
    _log : function(aMsg)
    {
        const CONSOLE_SERVICE = Components.classes['@mozilla.org/consoleservice;1'].getService(Components.interfaces.nsIConsoleService);
        CONSOLE_SERVICE.logStringMessage(aMsg);
    },
};

Then, the only thing you have to do is adding the observer to Bookmarks datasource.

// get the datasource of Bookmarks
const RDF_SERVICE = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService(Components.interfaces.nsIRDFService);
var BMDS = RDF_SERVICE.GetDataSource("rdf:bookmarks");
// add the observer to datasource
BMDS.RemoveObserver(rdfObserver);
BMDS.AddObserver(rdfObserver);

References:
Interface Reference – nsIRDFObserver
Interface Reference – nsIRDFDataSource

TOP

nsIRDFObserver を使ってブックマークのデータソースの動きを調べる

English version of this post is also available.

nsIRDFObserver によってRDFデータソースに起こった様々な変化を監視することができる。ブックマークのデータも Firefox の内部ではRDFデータソースとして管理されているので、後述するような nsIRDFObserver によってブックマークのデータがどのように管理されているかを、エラーコンソール(JavaScript コンソール)を使って調べることができる。

まずは、データソースを監視するための nsIRDFObserver オブジェクトを定義する。なお、 _targetToString と _log は、独自に定義したプライベートなメソッドである。

var rdfObserver = 
{
    onAssert : function(aData, aRes, aProp, aTarget)
    {
        this._log(["onAssert", aData.URI, aRes.Value, aProp.Value, this._targetToString(aTarget)].join("
"));
    },
    onBeginUpdateBatch : function(aData)
    {
        this._log(["onBeginUpdateBatch", aData.URI].join("
"));
    },
    onChange : function(aData, aRes, aProp, aOldTarget, aNewTarget)
    {
        this._log(["onChange", aData.URI, aRes.Value, aProp.Value, this._targetToString(aOldTarget), this._targetToString(aNewTarget)].join("
"));
    },
    onEndUpdateBatch : function(aData)
    {
        this._log(["onEndUpdateBatch", aData.URI].join("
"));
    },
    onMove : function(aData, aOldRes, aNewRes, aProp, aTarget)
    {
        this._log(["onMove", aData.URI, aOldRes.Value, aNewRes.Value, aProp.Value, this._targetToString(aTarget)].join("
"));
    },
    onUnassert : function(aData, aRes, aProp, aTarget)
    {
        this._log(["onUnassert", aData.URI, aRes.Value, aProp.Value, this._targetToString(aTarget)].join("
"));
    },
    /**
     * nsIRDFNode から適切なインタフェースを参照し、String型の値を取得する
     */
    _targetToString : function(aTarget)
    {
        const Ci = Components.interfaces;
        if ( aTarget instanceof Ci.nsIRDFLiteral )
            // 文字列
            return aTarget.QueryInterface(Ci.nsIRDFLiteral).Value;
        else if ( aTarget instanceof Ci.nsIRDFInt )
            // 数値
            return aTarget.QueryInterface(Ci.nsIRDFInt).Value;
        else if ( aTarget instanceof Ci.nsIRDFDate )
            // 日時
            return aTarget.QueryInterface(Ci.nsIRDFDate).Value;
        else
            // 他にもあったっけ?
            return "";
    },
    /**
     * 文字列をエラーコンソールへ出力
     */
    _log : function(aMsg)
    {
        var consoleSvc = Components.classes['@mozilla.org/consoleservice;1'].getService(Components.interfaces.nsIConsoleService);
        consoleSvc.logStringMessage(aMsg);
    },
};

あとは、ブックマークのデータソースに対して先ほどのオブザーバを追加してやるだけで良い。

// ブックマークのデータソースを取得
var rdfSvc = Components.classes['@mozilla.org/rdf/rdf-service;1'].getService(Components.interfaces.nsIRDFService);
var bmds = rdfSvc.GetDataSource("rdf:bookmarks");
// データソースへオブザーバを追加
bmds.RemoveObserver(rdfObserver);
bmds.AddObserver(rdfObserver);

リファレンス:
Interface Reference – nsIRDFObserver
Interface Reference – nsIRDFDataSource

TOP

about:foxkeh

9月21日追記
陽極日記 – 電気分解部 – About ウィンドウで遊ぶ
↑こちらの方がインパクトがあって色使いも綺麗でいいですね。

ちょうどいいサイズのフォクすけ画像があったので、Firefox のバージョン情報ダイアログの背景にしてみる。PNGアイコンが邪魔ですが。

フォクすけ

“フォクすけ” (C) 2006 Mozilla Japan

userChrome.css

dialog#aboutDialog vbox#clientBox {
    background-image: url(http://spreadfirefox.jp/foxkeh/downloads/materials/front-thumb.gif) !important;
    background-position: top center;
}

TOP

ライブブックマークとRSSフィードのプレビュー表示機能に関する考察

9月19日一部追記

Firefox にはライブブックマークという機能があって、それが人気の機能No.3であることは知っているが、実は自分はこの機能をまったくといっていいほど使ったことが無い。そこで、ライブブックマークについて少し調べてわかったことをまとめてみる。ただし多少推測も含みます。

  1. ライブブックマークを登録するにはロケーションバーのRSSアイコンをクリックしてブックマークへ追加する
  2. ライブブックマークはそのRSSを配信しているサイトをブックマークするのではなく、配信しているRSS自体をブックマークする
  3. ライブブックマークは、ブックマークメニューやブックマークツールバー上ではRSSアイコンとして表示されるが、サイドバーのブックマークツリーではなぜかフォルダやFaviconで表示される。
  4. ライブブックマークの更新はデフォルトでは30分毎に行われる
    (登録された全ライブブックマークの更新チェックがバックグラウンドで30分毎に行われるということ?)
  5. ライブブックマークが更新されているかどうかは、ブックマークメニュー、ブックマークツールバー、サイドバーのブックマークツリーのいずれかで、そのライブブックマークを選択して展開させてみないとわからない。
  6. そもそもライブブックマークの更新チェックは「前回更新チェックとの比較」ではなく、「常に最新の状態へ更新」するためのものである。したがってライブブックマークが更新されたかどうかは、ユーザが記憶に頼って判断しなければならない。

また、ライブブックマークに関連して、Firefox 2.0ではRSSフィードのプレビュー(整形表示)が可能となる。この機能についても以下のような疑問を抱いている。

  1. RSSフィードのプレビュー表示はあくまでもユーザに購読するための手段(ライブブックマークか、はてなRSSやLivedoorReaderといったWebサービスへ登録するかなど)を選択させることに主眼を置いたものなのか?「フィードの購読は常に○○○を利用する」を選択してしまえばそれ以降はプレビュー表示はされず、お役御免となるのか?
    だとすると、RSSフィードの内容を整形表示させる意味はあるのか?単純に購読手段を選ぶためのUIがダイアログで表示されるだけで良いのではないだろうか?
  2. あるいは、RSSフィードが Firefox で読めるようになるということに主眼を置いたものであるのか?例えば Sage のような拡張機能では今までは独自でRSSフィードを整形表示させていたが、 Firefox 本体に整形表示機能が搭載されたことにより、その必要がなくなるのだろうか?
    だとすると、購読手段を選ぶためのUIはかなり邪魔である。しかもユーザが購読手段を選択してしまうと、当然ながらRSSフィードのプレビュー表示は行なわれなくなってしまう。

TOP

[userChrome.js] 検索バーのポップアップから検索エンジンを中クリックしてクリップボードの文字列を検索する

日々のブラウジング中での Firefox の検索バーの使い方というのは人それぞれ異なるものだと思うが、自分の場合は以下の2通りに大別される。

  • たいていの検索は Google を使用する。
    その方法は、検索バーに直接入力してEnterキー、またはWebページ中のキーワードを選択して右クリックメニューから検索のどちらかである。
  • ごくたまに Amazon などの異なるエンジンを使用して検索する。
    その手順は、Webページ中のキーワードを選択してコピー→検索バーへ貼り付け→検索バーのポップアップから検索エンジンを切り替え→虫眼鏡アイコンをクリック、という煩雑なものである。しかも、たいていの場合切り替えたエンジン(ここでは Amazon)では一度しか検索を行わず、次からはまた Google を使用するため、検索エンジンが切り替わってしまうのが嫌。

そこで思いついた便利な機能が、検索バーのポップアップから目的の検索エンジンを中クリックすることによって、クリップボードの文字列をそのエンジンで検索するという機能である。これなら検索エンジンを切り替える必要なく検索が行える。
前の記事同様、 userChrome.js によって使用可能なスクリプトとして実装した。ただし、Firefox 2.0 限定です。
スクリプトの内容としては、検索バーのポップアップの中クリック時に以下のような流れで処理が行われる。

  1. クリップボード内の文字列を取得
  2. クリックされた検索エンジンの menuitem 要素の engine プロパティから、 nsISearchEngine を取得
  3. nsISearchEngine から、送信先URLやPOSTするデータを取得
  4. 検索結果を新しいタブで開くかどうかの設定値を取得
  5. ブラウザのタブでロードする
  6. ポップアップを閉じる


10月26日追記
少し改良しました。詳しくはこちら:
[userChrome.js] Search Clipboard

TOP

canvas要素によるWebページのスクリーンショット保存機能

今年4月に行なわれたMozillaParty7.0において、いくつか有用な情報を得ることができたが、中でもcanvas 要素の toDataURL メソッドで取得した data:URL をファイルへ保存するという Taken さんの情報は、 ScrapBook で保存したWebページのコレクションをサムネイル画像によって一望するというプランを一気に実現へと近づけることができるありがたいものであった。その具体的な方法はTaken SPC : Mozilla Party JP 7.0 に行ってきましたのポストでも説明されているが、これを利用して現在ブラウザに表示されているWebページのスクリーンショット(今回はサムネイルではなく、原寸大のスクリーンショット)をPNG画像として保存する機能を実装してみる。

(1) XUL

html:canvas要素を chrome://browser/content/browser.xul へオーバーレイする。オーバーレイする位置はどこでも構わないが、下記の例ではステータスバー内にオーバーレイしている。また、サイズの大きな canvas 要素を擬似的に隠すために、scrollbox 要素内に押し込んで表示させている。これは PearlCrescent PageSaver で用いられているテクニックである。

<overlay id="myOverlay"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">

    <statusbar id="status-bar">
        <scrollbox width="1" height="1">
            <html:canvas id="myCanvas" display="none" />
        </scrollbox>
    </statusbar>

</overlay>

(2) JavaScript

まずは対象となるWebページのWindowオブジェクトと、Webページのサイズを取得する。ただし、documentElement.clientHeightは、Quirks(後方互換)モードではWebページ全体の高さとなるが、 Standards Compliant(標準準拠)モードでは実際に見えている部分のみの高さとなる。PearlCrescent PageSaver では、どちらのモードにも対応した GetWindowWidth, GetWindowHeight 関数というのを自前で実装している。

var win = window._content;
var w = win.document.documentElement.clientWidth;
var h = win.document.documentElement.clientHeight;

var w = win.document.width;
var h = win.document.height;

「display: none;」となっていたcanvas要素を一時的に表示させ、そのサイズを先ほど取得したWebページのサイズと一致させる。canvas要素は scrollbox 内に押し込まれているので、見かけ上は1×1ピクセルとして表示される。

var canvas = document.getElementById("myCanvas");
canvas.style.display = "inline";
canvas.width = w;
canvas.height = h;

いよいよ、canvas要素へWebページを描画する。

var ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(1.0, 1.0);    // 1.0なら原寸大
ctx.drawWindow(win, 0, 0, w, h, "rgb(255,255,255)");
ctx.restore();

続いて、PNG画像データをBASE64エンコードしたdata:URLを取得する。これは Firefox 2.0 以外では例外となるので、 try~catch する。さらに、取得した string型の data:URL から nsIURI オブジェクトを生成する。

try {
    var url = canvas.toDataURL("image/png");
} catch(ex) {
    return alert("This feature requires Firefox 2.0.
" + ex);
}
const IO_SERVICE = Components.classes['@mozilla.org/network/io-service;1']
                   .getService(Components.interfaces.nsIIOService);
url = IO_SERVICE.newURI(url, null, null);

ファイルピッカーを使って保存先ファイルを決定する。

var fp = Components.classes['@mozilla.org/filepicker;1']
          .createInstance(Components.interfaces.nsIFilePicker);
fp.init(window, "Save Screenshot As", fp.modeSave);
fp.appendFilters(fp.filterImages);
fp.defaultExtension = "png";
fp.defaultString = "screenshot.png";
if ( fp.show() == fp.returnCancel || !fp.file ) return;

nsIWebBrowserPersist を使って data:URL をファイルへ保存する。
nsIWebBrowserPersist はURIがhttp:プロトコルであるインターネット上のデータをダウンロードするためによく使われるが、data:プロトコルに対しても有効であるというのがミソ。

var wbp = Components.classes['@mozilla.org/embedding/browser/nsWebBrowserPersist;1']
          .createInstance(Components.interfaces.nsIWebBrowserPersist);
wbp.saveURI(url, null, null, null, null, fp.file);

最後に、一時的に表示されたキャンバスを非表示にして後始末する。

canvas.style.display = "none";
canvas.width = 1;
canvas.height = 1;

以上のような処理でおおよそスクリーンショットを保存する機能を実装することができた。あとは、適当なUIを加えれば、 PearlCrescent PageSaver Basic 相当の拡張機能がいとも簡単に実装できてしまう。

しかし、ひとつ厄介な問題がある。スクリーンショットを撮る対象のWebページのサイズが非常に大きい場合、一時的に膨大なメモリを食ってしまうということである。例えば ScrapBook の「リクエスト+バグ」のページのスクリーンショットを保存すると、一時的にメモリ使用量が50MBほど上昇する。PCに搭載されたメモリサイズに依存するが、最悪PCの動作が不安定になり、Firefox を強制終了するしかなくなることもある。原因はもちろん data:URL を取得して string型の変数へ格納した際に発生するのだが、対策は難しいだろう。

TOP

XULのシングルクォーテーション内にDTDのエンティティを使用するときの注意

ScrapBookをいくつかの特定のロケールで使用している場合に「設定」→「データ保存先」の「参照」ボタンが反応しないという重大なバグが発覚した。原因は、XULのシングルクォーテーション内に埋め込まれたDTDのエンティティにシングルクォーテーションが含まれているため、文法エラーが発生することによる。下記の例をご覧ください。

sample.xul

<button label="Click me!" oncommand="alert('&sample.hello;');" />

sample.dtd

<!ENTITY sample.hello "やあ、'みなさん'こんにちは。">

このとき、シングルコーテーション内で展開されたエンティティ内にシングルコーテーションが含まれるので、以下のような文法エラーとなる。

エラー: missing ) after argument list
ソースファイル: sample.xul
行: 1, 列: 11
ソースコード:
alert('やあ、'みなさん'こんにちは。');

DTDのエンティティ内にどのような文字列が展開されるかは、そのDTDの作成者すなわち翻訳者しだいで予想が付かない。下記のように独自の属性の中にエンティティを埋め込むような回りくどいやり方にしたほうが安全である。

sample.xul

<button label="Click me!"
        oncommand="alert(this.getAttribute('alerttext'));"
        alerttext="&sample.hello;" />

ローカライズ可能な文字列を properties ファイルでなく DTD ファイルで定義するテクニックも今回の件と少し関連しているため、十分に注意が必要である。

TOP

Rethinking ScrapBook’s Browser Context Menu :: Part 2

Japanese version of this post is also available.

The 2nd topic in the series, “Rethinking ScrapBook’s Browser Context Menu”.
This time, I kick around the [Capture Frame] menu which apprears with [Capture Page] when we click in a frame / inner-frame. In the Firefox’s standart context menu, all menus related to frame are submenus inside the [This Frame] menu. Hence [Capture Frame] should be inside the [This Frame] menu in a similar way.

Contorary to that, the standpatters might have such viewpoints:
Additional menus of extensions should not scatter and should be placed in one place.
It is easier to choice between [Capture Page] and [Capture Frame].

As a developer, I have a following viewpoint. [Capture Frame] inside [This Frame] is surely sensible, however, I’m afraid some people mistakenly thought that [Capture Frame] was gone!

Please let me hear your thoghts or comments.

current

Current: [Capture Frame] and [Capture Page] are in parallel position.

alternative

Alternative: [Capture Frame] should be the submenus inside [This Frame].

TOP

Rethinking ScrapBook’s Browser Context Menu :: Part 1

Japanese version of this post is also available.

The 1st topic in the series, “Rethinking ScrapBook’s Browser Context Menu”.
First time, I kick around the most popular [Capture Page] and [Capture Page As…] menus.
Today we have these menus at the bottom of the browser context menu. However, the feature of [Capture Page (As…)] is similar to Firefox’s [Save Page As…] one, so they should be ‘neighborhood’ each other. At least, both should be placed in a same category between two separators.

Contorary to that, the standpatters might have such a viewpoint: additional menus of extensions are below Firefox’s standard menus in rank.

Please let me hear your thoghts or comments.

current

Current
We have [Capture Page (As…)] at the bottom.

alternative

Alternative
[Capture Page (As…)] and [Save Page As…] should be placed in close position.

TOP