Now browsing the SCRAPBLOG weblog archives.

nsITreeBoxObject::clearStyleAndImageCaches

ツリーのスタイル付け – MDC の方法によってツリーアイテムのアイコン画像を一時的にアニメーションPNGやアニメーションGIFにすると、アイコンを元に戻した後もCPU使用率が上昇したままとなる問題があった。

アイコンを元に戻した後に nsITreeBoxObject::clearStyleAndImageCaches を呼び出すことで、ツリーのアイコン画像のキャッシュが消去され、この問題が解消された。

nsITreeBoxObject.idl

TOP

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

その2~ドラッグ処理から引き続き、ドラッグ元をドロップ先へドロップ可能にするための処理と、実際にドロップしたときの処理を追加する。

ドロップ先へのドロップを可能にする

ドロップ先の要素では、テキストボックス (html:input 要素や xul:textbox 要素) などを除き、通常はいかなる形式の転送データでもドロップ不可となっている。ドロップ可能にするには、 dragenter と dragover の2つのイベントハンドラで event.preventDefault() を呼び出す。

はじめに、転送データの形式によらず常にドロップ可能にするには、 handleDropEvents 関数にて以下のような処理を追加する。

        case "dragenter": 
        case "dragover": 
            event.preventDefault();
            break;

別の書き方として、 XUL / HTML のイベントハンドラ側で ondragenter=”return false;” のようにする方法もある。

実際は特定の転送データ形式、例えばリンクのドロップのみを可能にする場合が多いので、 dragenter と dragover の2つのイベントハンドラで転送データの形式を調べ、条件付きで event.preventDefault() を呼び出すようにする。

転送データの形式を調べるには、 DataTransfer オブジェクトの types プロパティを使用する。
ひとまずは、 dragenter と dragover イベント発生時に転送データに含まれるすべての形式を列挙してみる。

        case "dragenter": 
        case "dragover": 
            for (var i = 0; i < event.dataTransfer.types.length; i++) {
                dump("    " + event.dataTransfer.types.item(i) + "
");
            }
            break;

DataTransfer の types プロパティに特定のデータ形式が含まれるかどうかは、以下のように contains メソッドを使用すると良い。

        case "dragenter": 
        case "dragover": 
            if (event.dataTransfer.types.contains("text/url-list") || 
                event.dataTransfer.types.contains("text/plain"))
                event.preventDefault();
            break;

動作確認 (1)

ドラッグ元をドラッグし、マウスの右クリックを放さずにドロップ先の枠内へ入ったり出たりすると dragenter, dragover, dragleave の3つのイベントが発生することを確認してください。
なお、 XUL / HTML をブラウザタブで開いている場合、マウスの右クリックを放してドロップすると、ブラウザタブ側でドロップイベントが発生して URL を開く動作となります。

ドロップされたデータを取得する

次に、 drop イベント発生時にドロップされた転送データを取得する処理を追加する。
ドロップされた転送データを取得するには、まず dragenter, dragover イベントハンドラでやったように DataTransfer オブジェクトの types プロパティから転送データに目的の形式が含まれることをチェックした上で、DataTransfer オブジェクトの getData メソッドで指定した形式の転送データを取得する。
なお、下記のサンプルコードでは、 XUL / HTML をブラウザタブで開いている場合を考慮し、 event.preventDefault() を呼び出して、ブラウザタブ側でドロップイベントが発生するのを阻止している。

        case "drop": 
            event.preventDefault();
            var data = null;
            if (event.dataTransfer.types.contains("text/url-list"))
                data = event.dataTransfer.getData("text/url-list");
            else if (event.dataTransfer.types.contains("text/plain"))
                data = event.dataTransfer.getData("text/plain");
            alert("Dropped URL: " + data);
            break;

以下のように、あらかじめドロップ可能なデータ形式を配列で定義しておいたほうが見通しの良いソースコードとなるかもしれない。

        case "drop": 
            event.preventDefault();
            var data = null;
            var supportedTypes = ["text/url-list", "text/plain"];
            for each (type in supportedTypes) {
                if (event.dataTransfer.types.contains(type)) {
                    data = event.dataTransfer.getData(type);
                    break;
                }
            }
            alert("Dropped URL: " + data);
            break;

動作確認 (2)

ドラッグ元をドラッグしてドロップ先へドロップすると drop イベントが発生することを確認してください。
また、ドロップされたURLがメッセージボックスで表示されることを確認してください。

参考

Drag Operations – MDC

関連記事

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

TOP

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)

その1~イベントハンドラの追加から引き続き、ドラッグ元の要素をドラッグ開始した際に、転送データをセットする処理を追加する。

DataTransfer オブジェクト

ドラッグした際に転送データをセットする処理や、ドロップした際に転送データを取得する処理は、 DataTransfer オブジェクト (event.dataTransfer) によって行う。

ドラッグ元をドラッグ開始時、つまり dragstart イベント発生時、転送データをセットするには、 DataTransfer オブジェクトの setData メソッドを使用する。 setData メソッドの第1引数は転送データの形式、第2引数は転送データの値(文字列に限る)である。転送データの形式は、単純な文字列であれば「text/plain」、URL(複数も可)であれば「text/url-list」といった値を用いる。もちろん、一度のドラッグで複数の形式の転送データをセットすることも可能である。

サンプルコード (JavaScript)

handleDragEvents 関数へ以下のような処理を追加する。

        case "dragstart": 
            // 転送データをセットする
            event.dataTransfer.setData("text/url-list", "http://www.mozilla.org/");
            event.dataTransfer.setData("text/plain", "http://www.mozilla.org/");
            break;

動作確認

ドラッグ元をドラッグして dragstart, drag, dragend の3つのイベントが発生することを確認してください。
現段階ではドロップ先の処理が未完ですので、ドラッグ時の転送データが正しくセットされていることを確認するために、 Firefox のロケーションバーなどにドロップするか、メモ帳などの別アプリへドロップして、「http://www.mozilla.org/」という文字列が貼り付けされることを確認してください。

参考

DataTransfer オブジェクトの詳細:
DataTransfer – MDC
nsIDOMDataTransfer.idl

転送データの形式の詳細:
Recommended Drag Types – MDC

関連記事

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

TOP

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)

Firefox 3.1 (b1pre) にて HTML 5 のドラッグ&ドロップ API が実装され、 HTML の Web アプリ、 XUL の拡張機能のいずれからも同じように利用可能となった。

拡張機能開発者としては、ドラッグ&ドロップに関する各種処理を、 XPCOM サービスの nsIDragService や面倒くさい nsDragAndDrop.js を使わずに、単純な DOM の API のみで記述できるようになったので、非常にありがたい。

ドラッグ&ドロップのイベント

ドラッグ&ドロップ操作時、ドラッグ元の要素とドロップ先の要素において、以下のようなイベントが発生する。

イベント名 (event.type) イベント発生対象 (event.target) イベント発生のタイミング
dragstart ドラッグ元 ドラッグ開始時
drag ドラッグ元 ドラッグ中
dragend ドラッグ元 ドラッグ終了時
dragenter ドロップ先 ドラッグオーバー開始時
dragover ドロップ先 ドラッグオーバー中
dragleave ドロップ先 ドラッグオーバー終了時
drop ドロップ先 ドロップ時

サンプルコード (HTML)

2つの div 要素を配置し、一方をドラッグ元、もう一方をドロップ先とする。
ドラッグ元となる div 要素には、「draggable=”true”」属性をセットしなければならない。ただし、リンク(a 要素)や画像 (img 要素)などは、「draggable=”true”」を指定しなくても自動的にドラッグ可能となる。

ドラッグ元の要素には ondragstart, ondrag, ondragend の3つのイベントハンドラを追加し、ドロップ先の要素には ondragenter, ondragover, ondragleave, ondrop の4つのイベントハンドラを追加する。ただし、必要最低限のドラッグ&ドロップを実装するのであれば、 ondrag, ondragend, ondragleave は省略しても問題ない。

<html>
<head>
    <title>Drag and Drop Test</title>
    <script type="text/javascript" src="dragdrop.js"></script>
</head>
<body>
    <div id="DragSource"
         draggable="true"
         ondragstart="handleDragEvents(event);"
         ondrag="handleDragEvents(event);"
         ondragend="handleDragEvents(event);"
         style="border: 1px solid black; padding: 50px; margin: 10px;">Drag Source</div>
    <div id="DropTarget"
         ondragenter="handleDropEvents(event);"
         ondragover="handleDropEvents(event);"
         ondragleave="handleDropEvents(event);"
         ondrop="handleDropEvents(event);"
         style="border: 1px solid black; padding: 50px; margin: 10px;">Drop Target</div>
    <a href="http://www.mozilla.org/">http://www.mozilla.org/</a>
    <img src="http://www.mozilla.org/images/poweredby_200.gif">
</body>
</html>

サンプルコード (XUL)

2つの xul:label 要素を配置し、一方をドラッグ元、もう一方をドロップ先とする。 HTML の場合とほぼ同じであるが、すべての XUL 要素はドラッグ可能となりうるため、「draggable=”true”」属性を指定する必要は無い。

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window title="Drag and Drop Test"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    <script type="application/x-javascript" src="dragdrop.js" />
    <label id="DragSource"
           value="Drag Source"
           ondragstart="handleDragEvents(event);"
           ondrag="handleDragEvents(event);"
           ondragend="handleDragEvents(event);"
           style="border: 1px solid black; padding: 50px; margin: 10px;" />
    <label id="DropTarget"
           value="Drop Target"
           ondragenter="handleDropEvents(event);"
           ondragover="handleDropEvents(event);"
           ondragleave="handleDropEvents(event);"
           ondrop="handleDropEvents(event);"
           style="border: 1px solid black; padding: 50px; margin: 10px;" />
</window>

サンプルコード (JavaScript)

JavaScript のソースコードは、 HTML と XUL の両者に共通となる。また、動作させるために chrome 権限は必要ない。
今回はとりあえず以下のような雛形的なイベントハンドラの処理を作っておき、あとで処理を追加する。
なお、サンプルコードでは window.dump メソッドを使用する。

function handleDragEvents(event) {
    dump("[" + event.target.id + "] " + event.type + "
");
    switch (event.type) {
        case "dragstart": 
            break;
        case "drag": 
            break;
        case "dragend": 
            break;
    }
}

function handleDropEvents(event) {
    dump("[" + event.target.id + "] " + event.type + "
");
    switch (event.type) {
        case "dragenter": 
        case "dragover": 
            break;
        case "dragleave": 
            break;
        case "drop": 
            break;
    }
}

動作確認

現段階ではドラッグ元をドラッグ開始した際の dragstart イベントしか発生しませんが、実際にドラッグしてイベントが発生することを確認してください。

参考

Drag and Drop – MDC

関連記事

Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その1~イベントハンドラの追加)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その2~ドラッグ処理)
Firefox 3.1 の新しいドラッグ&ドロップ API の基本的な使い方 (その3~ドロップ処理)

TOP

nsIConsoleService

// サービス取得
var consoleSvc = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);

// エラーコンソールへの出力内容を消去
consoleSvc.reset();

// #1 Components.utils.reportErrorによるのエラーメッセージ出力
Components.utils.reportError("error #1");

// #2 nsIConsoleService.logStringMessageによるエラーメッセージ出力
consoleSvc.logStringMessage("error #2");

// #3 nsIConsoleService.logMessageによるエラーメッセージ出力
var err = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
err.init("error #3", null, null, null, null, err.errorFlag, null);
consoleSvc.logMessage(err);

// エラーコンソールへの出力内容を列挙
var msgs = {};
consoleSvc.getMessageArray(msgs, {});
msgs.value.forEach(function(msg) {
    dump("---
");
    if (msg instanceof Ci.nsIScriptError) {
        // [JavaScript Error]
        msg.QueryInterface(Ci.nsIScriptError);
        dump(
            msg + "
" + 
            "errorMessage => " + msg.errorMessage + "
" + 
            "sourceName   => " + msg.sourceName   + "
" + 
            "sourceLine   => " + msg.sourceLine   + "
" + 
            "lineNumber   => " + msg.lineNumber   + "
" + 
            "columnNumber => " + msg.columnNumber + "
" + 
            "flags        => " + msg.flags        + "
" + 
            "category     => " + msg.category     + "
"
        );
    }
    else {
        // [xpconnect wrapped nsIConsoleMessage]
        dump(
            msg + "
" + 
            "message     => " + msg.message + "
"
        );
    }
});

リファレンス

nsIConsoleService – MDC
nsIConsoleService.idl
nsIConsoleMessage.idl
nsIScriptError.idl

TOP

Gmail の設定で Always use https を有効にすると Gmail Notifier で新着チェック不可

Gmail で最近追加されたオプション「Always use https」を有効にすると、 Gmail Notifier v1.0.25.0 にてメールの新着チェックができなくなった。 Gmail Notifier といっても、 Firefox 用のアドオンの方ではなく、 Google が公式に配布している方の話。

「gnotify.exe」をバイナリエディタで開き、「http://」の箇所を無理やり「https://」に変え、x00を一個削って全体のバイト長をそろえたら、無事に新着チェックできるようになった。

と思ったら、すでに Google から対策用パッチが配布されていた。

TOP

[userChrome.css] Places Style Throbber

Firefox 3 のスマートロケーションバー入力中にまれに一瞬だけ表示される回転するアイコンがかっこいいので、読み込み中のタブやアドオンマネージャの更新チェック中などの Throbber アイコンに転用するための userChrome.css を作った。ただし、回転が停止しているバージョンのアイコンは今までどおりです。

Places Style Throbber Places Style Throbber

userChrome.css

/* ::::: Places Style Throbber ::::: */

#navigator-throbber[busy="true"],
toolbar[iconsize="small"] #navigator-throbber[busy="true"],
toolbar[mode="text"] #navigator-throbber[busy="true"],
.tabbrowser-tab[busy] > .tab-icon-image,
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon,
#sidebar-throbber[loading="true"],
#checkForUpdates[loading="true"],
#extensionsManager richlistitem[loading="true"] .updateBadge,
#extensionsManager .addonThrobber,
#extensionsManager .throbber {
    list-style-image: url("chrome://browser/skin/places/searching_16.png") !important;
}

TOP

nsISafeOutputStream で安全なファイルの書き込み

(1) 通常のファイルの書き込み

function writeFile(aFile, aText) {
    var stream = Cc["@mozilla.org/network/file-output-stream;1"].
                 createInstance(Ci.nsIFileOutputStream);
    stream.init(aFile, 0x02 | 0x08 | 0x20, 0644, 0);
    stream.write(aText, aText.length);
    stream.close();
}

(2) nsISafeOutputStream を使った安全なファイルの書き込み

function writeFileSafely(aFile, aText) {
    var stream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
                 createInstance(Ci.nsIFileOutputStream);
    stream.init(aFile, 0x02 | 0x08 | 0x20, 0644, 0);
    stream.write(aText, aText.length);
    stream.QueryInterface(Ci.nsISafeOutputStream);
    stream.finish();
}

(1) の方式は多分ファイル書き込み時にいったん0バイトにしてから書き込みが行われるため、ファイルの書き込み中の強制終了などにより破損する可能性が高い。
一方、(2) の方式は “test-1.txt” のような別名でいったんファイルの書き出しが行われた後、その内容を本来のファイル “test.txt” へ上書きコピーするため、破損の可能性が低いと思われる。
パフォーマンスの面では、簡単なベンチマークを試したところ (2) が劣るようであったが、書き込むデータサイズが大きい(数MB以上)場合はほとんど差異がなくなるようだった。

TOP

JavaScript 関数と XPCOM メソッドの例外ハンドリング

JavaScript の関数がスローする例外の内容を知るには、例外オブジェクトの値そのものを調べる。
XPCOM のメソッドがスローする例外の内容を知るには、例外オブジェクト (nsIXPCException オブジェクト) の result プロパティなどを調べる。

例えば以下のような純粋な JavaScript の関数があるとすると、

const Cr = Components.results;
function test() {
    throw Cr.NS_ERROR_FAILURE;
}

関数実行時に catch したオブジェクトの値そのものを調べることで例外の内容を知ることができる。

try {
    test();
}
catch (ex if ex == Cr.NS_ERROR_FAILURE) {
    alert("Failed!");
}

一方、上記の関数 test と同じ内容のメソッドを JavaScript 製 XPCOM のメソッドとして実装した場合、メソッド実行時に catch した例外は nsIXPCException オブジェクトとなり、 result プロパティなどから例外の内容を知ることができる。

try {
    Cc["@XXX"].getService(Ci.XXX).test();
}
catch (ex if ex.result == Cr.NS_ERROR_FAILURE) {
    alert("Failed!");
}

Components.Exception コンストラクタを使えば、純粋な JavaScript の関数で nsIXPCException オブジェクトの例外をスローすることも可能。エラーコンソールに例外発生のソースファイルなどの詳細表示ができるといった利点が挙げられる。

function test() {
    throw new Components.Exception("Failed!", Cr.NS_ERROR_FAILURE);
}

TOP

[Places] フォルダ内のブックマークを列挙する

Places データベースへの問い合わせ結果から得たブックマークのノードは nsINavHistoryResultNode インタフェースを実装するオブジェクトであるが、ブックマークフォルダは nsINavHistoryContainerResultNode インタフェースも合わせて実装しており、 childCount プロパティで子ノードの数を調べたり、 getChild メソッドで指定したインデックスの子ノードを取得することができる。
例えば、[Places] ビューと nsIPlacesView インタフェース の続きとして、右クリックしたブックマークフォルダ内の子ノードにアクセスするには、以下のようにすればよい。ただし、ビュー上でフォルダが開いていることを前提とする。

// assume that node is instanceof Ci.nsINavHistoryContainerResultNode and is open.
for (var i = 0; i < node.childCount; i++) {
    var childNode = node.getChild(i);
}

一方、ビュー上でフォルダが閉じた状態になっている場合、 childCount プロパティなどを使って子ノードへアクセスしようとすると NS_ERROR_NOT_AVAILABLE 例外がスローされる。そこで、ブックマークフォルダのノードの containerOpen プロパティへ true をセットすることで一時的にフォルダを開いてから子ノードへアクセスし、処理が終わった後に false をセットしてフォルダを閉じるようにする。以下は、引数で指定したブックマークフォルダ内のすべてのブックマークを再帰的に取得し、配列として返す関数である。

function flatChildNodes(aNode) {
    var ret = [];
    var closeOriginally = !aNode.containerOpen;
    if (closeOriginally)
        // if the folder is closed, open it.
        aNode.containerOpen = true;
    for (var i = 0; i < aNode.childCount; i++) {
        var childNode = aNode.getChild(i);
        if (PlacesUtils.nodeIsBookmark(childNode))
            ret.push(childNode);
        else if (PlacesUtils.nodeIsFolder(childNode) && 
                 !PlacesUtils.nodeIsLivemarkContainer(childNode))
        // call this function recursive
        ret = ret.concat(arguments.callee(childNode));
    }
    if (closeOriginally)
        // don't forget to restore the folder's original closed state
        aNode.containerOpen = false;
    return ret;
}

上記の例では、ビュー上に表示されているブックマークフォルダ、つまりデータベースへの問い合わせ結果が取得済みのブックマークフォルダを対象としていた。ビューへの表示が無い場面でフォルダ内のノードへアクセスするには、データベースへ問い合わせを行う必要がある。通常 Places データベースへの問い合わせを行うには、 nsINavHistoryService#getNewQuery でクエリオブジェクトを生成し executeQuery で実行するという手続きが必要であるが、 PlacesUtils.getFolderContents を使えばより単純なコードでフォルダ内のノードへアクセス可能である。

var result = PlacesUtils.getFolderContents(node.itemId);    // nsINavHistoryResult
var parentNode = result.root;
for (var i = 0; i < parentNode.childCount; i++) {
    var childNode = parentNode.getChild(i);
}

以下は、ブックマークフォルダの itemId プロパティを引数に、そのフォルダ内の全ブックマークを再帰的に取得して配列として返す関数である。

function flatChildNodes(aItemId) {
    var ret = [];
    var parentNode = PlacesUtils.getFolderContents(aItemId).root;
    for (var i = 0; i < parentNode.childCount; i++) {
        var childNode = parentNode.getChild(i);
        if (PlacesUtils.nodeIsBookmark(childNode))
            ret.push(childNode);
        else if (PlacesUtils.nodeIsFolder(childNode) && 
                 !PlacesUtils.nodeIsLivemarkContainer(childNode))
            // call this function recursive
            ret = ret.concat(arguments.callee(childNode.itemId));
    }
    return ret;
}

TOP