※上記の広告は60日以上更新のないWIKIに表示されています。更新することで広告が下部へ移動します。

  • ブックマーク編集パネルにあるPlaceフォルダ選択リストメニュー/ツリービューが使いたかったので。
  • editBookmarkOverlay.xul と editBookmarkOverlay.js と editBookmarkOverlay.css のコピーみたいな感じに…。
    • editBookmarkOverlay.dtd はコピーせずそのまま使うことにした。
  • jsコードは editBookmarkOverlay.js を読み込んで gEditItemOverlay のメソッドのいくつかを改造/置き換えた方が早くて楽なんだけど、中の動きを理解するためにあえて必要な部分をまるまるコピーしながら勉強。
    • 2箇所ほどバグを発見した…。


  • 機能としては、メニューからダイアログを開いて、placeフォルダを選択して、okで終わるとprefs.js設定にフォルダidを保存するだけ。

ファイル構成

*.xpi
 ├ install.rdf
 ├ chrome.manifest
 └ content
      ├ browser.xul
      ├ browser.js
      └ PlaceFolderPicker
            ├ dialog.xul
            ├ dialog.js
            ├ PlaceFolderPicker.xul
            ├ PlaceFolderPicker.js
            └ PlaceFolderPicker.css
PlaceFolderPicker.css は本当は skin フォルダに入れるべきなんだけど、面倒なので xul や js と同じフォルダに入れてる。

ブラウザにダイアログを開くメニューを追加

chrome.manifest

パッケージ名はとりあえず安直に test で。
content test content/
overlay chrome://browser/content/browser.xul chrome://test/content/browser.xul

browser.xul

ツールメニュー内にメニュー追加
<script src="browser.js"/>
<menupopup id="menu_ToolsPopup">
    <menuitem id="test_open_dialog" label="PlaceFolderPicker ダイアログを開く"
        oncommand="testBrowser.openDialog();"/>
</menupopup>

browser.js

var testBrowser = {
    openDialog : function() {
        let features = "centerscreen,chrome,modal,resizable=yes";
        window.openDialog('chrome://test/content/PlaceFolderPicker/dialog.xul',
            'testDialog', features);
    },
}; 

ダイアログを作成

dialog.xul

PlaceFolderPicker.xul を overlay で読み込む。
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/"?>
 
<?xul-overlay href="chrome://test/content/PlaceFolderPicker/PlaceFolderPicker.xul"?>
 
<dialog id="testDialog" title="PlaceFolderPicker テスト ダイアログ"
    xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
    onload="testDialog.init();"
    onunload="testDialog.uninit();"
    style="min-width: 30em;"
    buttons="accept,cancel"
    ondialogaccept="return testDialog.ok();"
    ondialogcancel="return testDialog.cancel();">
 
<script src="dialog.js"/>
 
<grid id="PlaceFolderPickerContent"/>
 
</dialog>

dialog.js

var testDialog = {
    init : function() {
        var id;
        try {
            id = Services.prefs.getIntPref("extensions.test.folderid");
        } catch(e) {}
        PlaceFolderPicker.init(id);
    },
    uninit : function() {
        PlaceFolderPicker.uninit();
    },
    ok : function() {
        Services.prefs.setIntPref("extensions.test.folderid", PlaceFolderPicker.id);
        PlaceFolderPicker.save();
        return true;
    },
    cancel : function() {
        return true;
    },
}; 

PlaceFolderPicker.xul

dialog.xulをoverlayする。
editBookmarkOverlay.xul からかなりコピーしてる。
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://browser/skin/places/places.css"?>
<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
<?xul-overlay href="chrome://browser/content/places/placesOverlay.xul"?>
 
<?xml-stylesheet href="PlaceFolderPicker.css"?>
 
<!DOCTYPE overlay [
<!ENTITY % editBookmarkOverlayDTD SYSTEM "chrome://browser/locale/places/editBookmarkOverlay.dtd">
%editBookmarkOverlayDTD;
]>
 
<overlay id="PlaceFolderPicker"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
<script src="PlaceFolderPicker.js"/>
 
<!--
browser/omni.ja/chrome/browser/content/browser/places/editBookmarkOverlay.xul
-->
 
<grid id="PlaceFolderPickerContent" flex="1">
    <columns><column/><column flex="1"/></columns>
    <rows>
        <row id="PFP_folderRow" align="center">
            <label value="&editBookmarkOverlay.folder.label;"
                control="PFP_folderMenuList"/>
            <hbox flex="1">
                <menulist id="PFP_folderMenuList" flex="1"
                    class="folder-icon"
                    oncommand="PlaceFolderPicker.onFolderMenuListCommand(event);">
                    <menupopup>
                        <menuitem id="PFP_toolbarFolderItem"
                            class="menuitem-iconic folder-icon"/>
                        <menuitem id="PFP_bmRootItem"
                            class="menuitem-iconic folder-icon"/>
                        <menuitem id="PFP_unfiledRootItem"
                            class="menuitem-iconic folder-icon"/>
                        <menuseparator id="PFP_chooseFolderSeparator"/>
                        <menuitem id="PFP_chooseFolderMenuItem"
                            label='&editBookmarkOverlay.choose.label;'
                            class="menuitem-iconic folder-icon"/>
                        <menuseparator id="PFP_foldersSeparator"/>
                    </menupopup>
                </menulist>
                <button id="PFP_foldersExpander"
                    class="expander-down"
                    tooltiptext="&editBookmarkOverlay.foldersExpanderDown.tooltip;"
                    tooltiptextdown="&editBookmarkOverlay.foldersExpanderDown.tooltip;"
                    tooltiptextup="&editBookmarkOverlay.expanderUp.tooltip;"
                    oncommand="PlaceFolderPicker.toggleFolderTreeVisibility();"/>
            </hbox>
        </row>
        <row id="PFP_folderTreeRow" collapsed="true" flex="1">
            <spacer/>
            <vbox flex="1">
                <tree id="PFP_folderTree" flex="1"
                    class="placesTree" type="places"
                    height="150" minheight="150"
                    editable="true" hidecolumnpicker="true"
                    onselect="PlaceFolderPicker.onFolderTreeSelect();">
                    <treecols>
                        <treecol anonid="title" flex="1" primary="true" hideheader="true"/>
                    </treecols>
                    <treechildren flex="1"/>
                </tree>
                <hbox>
                    <button id="PFP_newFolderButton"
                        label="&editBookmarkOverlay.newFolderButton.label;"
                        accesskey="&editBookmarkOverlay.newFolderButton.accesskey;"
                        oncommand="PlaceFolderPicker.newFolder();"/>
                </hbox>
            </vbox>
        </row>
    </rows>
</grid>
</overlay>

PlaceFolderPicker.js

editBookmarkOverlay.js からコピーしまくり
// browser/omni.ja/chrome/browser/content/browser/places/editBookmarkOverlay.js
 
// Cu.import("resource://gre/modules/PlacesUtils.jsm");
// Cu.import("resource:///modules/PlacesUIUtils.jsm");
// Cu.import("resource://gre/modules/debug.js");
 
// 最近使用したフォルダのアノテーションマーク。↓はブックマーク編集パネルのと同じ。
// 共有したくなければ独自のを設定すればいい。
const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
// 最近使用したフォルダの表示数
const MAX_FOLDER_ITEM_IN_MENU_LIST = 5;
 
var PlaceFolderPicker = {
    init : function PFP_init(id) {
        this.id = id;
        try {
            if (id === undefined)
                throw new Error('id が指定されていません。');
            if (id == PlacesUtils.placesRootId)
                throw new Error('Placesルートフォルダは指定出来ません。');
            if (id == PlacesUtils.tagsFolderId)
                throw new Error('タグフォルダは指定出来ません。');
            let bms = PlacesUtils.bookmarks;
            let type = bms.getItemType(this.id);
            if (type != bms.TYPE_FOLDER)
                throw new Error('指定の id はフォルダではありません。id:' + this.id + '/type:' + type);
            //bms.getFolderReadonlyはfirefox36で削除された模様
            //if (bms.getFolderReadonly(this.id))
            //    throw new Error('フォルダが読み込み専用です。id:' + this.id);
        } catch(e) {
            console.error(e);
            this.id = PlacesUtils.bookmarksMenuFolderId; // デフォルトのフォルダ
        }
        this._folderMenuList = this._element("folderMenuList");
        this._folderTree = this._element("folderTree");
        this._initFolderMenuList();
        // observe changes
        PlacesUtils.bookmarks.addObserver(this, false);
    },
 
    uninit : function PFP_uninit(save) {
        PlacesUtils.bookmarks.removeObserver(this);
    },
 
    save : function() {
        // 現在のフォルダを最近使用したフォルダとしてマークアップ(特殊フォルダは除く)
        if (this.id != PlacesUtils.unfiledBookmarksFolderId &&
            this.id != PlacesUtils.toolbarFolderId &&
            this.id != PlacesUtils.bookmarksMenuFolderId)
            this._markFolderAsRecentlyUsed(this.id);
    },
 
    _element : function(id) {
        return window.document.getElementById("PFP_" + id);
    },
 
    _initFolderMenuList: function PFP__initFolderMenuList() {
        const bms = PlacesUtils.bookmarks;
        const annos = PlacesUtils.annotations;
 
        // 初期リストの設定
        {
            let unfiledItem = this._element("unfiledRootItem");
            unfiledItem.label = bms.getItemTitle(PlacesUtils.unfiledBookmarksFolderId);
            unfiledItem.folderId = PlacesUtils.unfiledBookmarksFolderId;
            let bmMenuItem = this._element("bmRootItem");
            bmMenuItem.label = bms.getItemTitle(PlacesUtils.bookmarksMenuFolderId);
            bmMenuItem.folderId = PlacesUtils.bookmarksMenuFolderId;
            let toolbarItem = this._element("toolbarFolderItem");
            toolbarItem.label = bms.getItemTitle(PlacesUtils.toolbarFolderId);
            toolbarItem.folderId = PlacesUtils.toolbarFolderId;
        }
 
        // 最近使用したフォルダのリストを取得
        var folderIds = annos.getItemsWithAnnotation(LAST_USED_ANNO);
        // リストをソート
        this._recentFolders = [];
        for (let i = 0; i < folderIds.length; i++) {
            let lastUsed = annos.getItemAnnotation(folderIds[i], LAST_USED_ANNO);
            this._recentFolders.push({ folderId: folderIds[i], lastUsed: lastUsed });
        }
        this._recentFolders.sort(function(a, b) {
            if (b.lastUsed < a.lastUsed)
                return -1;
            if (b.lastUsed > a.lastUsed)
                return 1;
            return 0;
        });
        // 既定の数だけメニューに追加
        var numberOfItems = Math.min(MAX_FOLDER_ITEM_IN_MENU_LIST, this._recentFolders.length);
        for (let i = 0; i < numberOfItems; i++) {
            this._appendFolderItemToMenupopup(this._recentFolders[i].folderId);
        }
 
        // 現在のフォルダをデフォルト選択
        var defaultItem = this._getFolderMenuItem(this.id);
        this._folderMenuList.selectedItem = defaultItem;
 
        // 特殊フォルダアイコン表示のため、menulist要素に独自属性を設定する
        this._folderMenuList.setAttribute("selectedId", defaultItem.id );
    },
 
    _appendFolderItemToMenupopup : function PFP__appendFolderItemToMenuList(aFolderId) {
        var folderMenuItem = window.document.createElement("menuitem");
        var folderTitle = PlacesUtils.bookmarks.getItemTitle(aFolderId)
        folderMenuItem.folderId = aFolderId;
        folderMenuItem.setAttribute("label", folderTitle);
        folderMenuItem.className = "menuitem-iconic folder-icon append";
        this._folderMenuList.menupopup.appendChild(folderMenuItem);
        return folderMenuItem;
    },
 
    _getFolderMenuItem : function PFP__getFolderMenuItem(aFolderId) {
        var menupopup = this._folderMenuList.menupopup;
 
        for (let i = 0; i < menupopup.childNodes.length; i++) {
            if ("folderId" in menupopup.childNodes[i] &&
                menupopup.childNodes[i].folderId == aFolderId)
                return menupopup.childNodes[i];
        }
 
        // 最近使用したフォルダが規定の数かそれ以上の場合、1個削除
        var appendMenu = menupopup.getElementsByClassName("append");
        if (appendMenu.length >= MAX_FOLDER_ITEM_IN_MENU_LIST)
            menupopup.removeChild(menupopup.lastChild);
 
        return this._appendFolderItemToMenupopup(aFolderId);
    },
 
    onFolderMenuListCommand : function PFP_onFolderMenuListCommand(aEvent) {
        if (aEvent.target.id == "PFP_chooseFolderMenuItem") {
            // リストメニューの選択状態を元に戻し、ツリーを表示する
            let item = this._getFolderMenuItem(this.id);
            this._folderMenuList.selectedItem = item;
            setTimeout(function(self) self.toggleFolderTreeVisibility(), 100, this);
            return;
        }
 
        // 現在のフォルダidを更新する
        this.id = this._getFolderIdFromMenuList();
 
        // 特殊フォルダアイコン表示のためのmenulist要素の独自属性を更新する
        this._folderMenuList.setAttribute("selectedId",
            this._folderMenuList.selectedItem.id );
 
        // フォルダツリーを更新する
        var folderTreeRow = this._element("folderTreeRow");
        if (!folderTreeRow.collapsed) {
            var selectedNode = this._folderTree.selectedNode;
            if (!selectedNode || PlacesUtils.getConcreteItemId(selectedNode) != this.id)
                this._folderTree.selectItems([this.id]);
        }
    },
 
    _getFolderIdFromMenuList : function PFP__getFolderIdFromMenuList() {
        var selectedItem = this._folderMenuList.selectedItem;
        NS_ASSERT("folderId" in selectedItem, "Invalid menuitem in the folders-menulist");
        return selectedItem.folderId;
    },
 
    toggleFolderTreeVisibility : function PFP_toggleFolderTreeVisibility() {
        var expander = this._element("foldersExpander");
        var folderTreeRow = this._element("folderTreeRow");
        if (!folderTreeRow.collapsed) {
            expander.className = "expander-down";
            expander.setAttribute("tooltiptext", expander.getAttribute("tooltiptextdown"));
            folderTreeRow.collapsed = true;
            this._element("chooseFolderSeparator").hidden =
                this._element("chooseFolderMenuItem").hidden = false;
        } else {
            expander.className = "expander-up"
            expander.setAttribute("tooltiptext", expander.getAttribute("tooltiptextup"));
            folderTreeRow.collapsed = false;
 
            const FOLDER_TREE_PLACE_URI =
                "place:excludeItems=1&excludeQueries=1&excludeReadOnlyFolders=1&folder=" +
                PlacesUIUtils.allBookmarksFolderId;
            this._folderTree.place = FOLDER_TREE_PLACE_URI;
 
            this._element("chooseFolderSeparator").hidden =
                this._element("chooseFolderMenuItem").hidden = true;
            this._folderTree.selectItems([this.id]);
            this._folderTree.focus();
        }
        window.sizeToContent();
    },
 
    onFolderTreeSelect : function PFP_onFolderTreeSelect() {
        var selectedNode = this._folderTree.selectedNode;
 
        // Disable the "New Folder" button if we cannot create a new folder
        this._element("newFolderButton")
            .disabled = !this._folderTree.insertionPoint || !selectedNode;
 
        if (!selectedNode)
            return;
 
        var folderId = PlacesUtils.getConcreteItemId(selectedNode);
        if (this.id == folderId)
            return;
 
        var folderItem = this._getFolderMenuItem(folderId);
        this._folderMenuList.selectedItem = folderItem;
        folderItem.doCommand();
    },
 
    _markFolderAsRecentlyUsed : function PFP__markFolderAsRecentlyUsed(aFolderId) {
        var txns = [];
 
        // Expire old unused recent folders
        var anno = this._getLastUsedAnnotationObject(false);
        while (this._recentFolders.length > MAX_FOLDER_ITEM_IN_MENU_LIST) {
            var folderId = this._recentFolders.pop().folderId;
            let annoTxn = new PlacesSetItemAnnotationTransaction(folderId, anno);
            txns.push(annoTxn);
        }
 
        // Mark folder as recently used
        anno = this._getLastUsedAnnotationObject(true);
        let annoTxn = new PlacesSetItemAnnotationTransaction(aFolderId, anno);
        txns.push(annoTxn);
 
        let aggregate = new PlacesAggregatedTransaction("Update last used folders", txns);
        PlacesUtils.transactionManager.doTransaction(aggregate);
    },
 
    /**
     * Returns an object which could then be used to set/unset the
     * LAST_USED_ANNO annotation for a folder.
     *
     * @param aLastUsed
     *        Whether to set or unset the LAST_USED_ANNO annotation.
     * @returns an object representing the annotation which could then be used
     *          with the transaction manager.
     */
    _getLastUsedAnnotationObject : function PFP__getLastUsedAnnotationObject(aLastUsed) {
        return {
            name: LAST_USED_ANNO,
            type: Ci.nsIAnnotationService.TYPE_INT32,
            flags: 0,
            value: aLastUsed ? new Date().getTime() : null,
            expires: Ci.nsIAnnotationService.EXPIRE_NEVER,
        };
    },
 
    newFolder: function PFP_newFolder() {
        var ip = this._folderTree.insertionPoint;
 
        // default to the bookmarks menu folder
        if (!ip || ip.itemId == PlacesUIUtils.allBookmarksFolderId) {
                ip = new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
                    PlacesUtils.bookmarks.DEFAULT_INDEX,
                    Ci.nsITreeView.DROP_ON);
        }
 
        // XXXmano: add a separate "New Folder" string at some point...
        var defaultLabel = this._element("newFolderButton").label;
        var txn = new PlacesCreateFolderTransaction(defaultLabel, ip.itemId, ip.index);
        PlacesUtils.transactionManager.doTransaction(txn);
        this._folderTree.focus();
        this._folderTree.selectItems([ip.itemId]);
        PlacesUtils.asContainer(this._folderTree.selectedNode).containerOpen = true;
        this._folderTree.selectItems([this._lastNewItem]);
        this._folderTree.startEditing(this._folderTree.view.selection.currentIndex,
            this._folderTree.columns.getFirstColumn());
    },
 
    // nsINavBookmarkObserver
    onItemChanged : function PFP_onItemChanged(aItemId, aProperty,
                                               aIsAnnotationProperty, aValue,
                                               aLastModified, aItemType) {
        if (aProperty == "title" && aItemType == PlacesUtils.bookmarks.TYPE_FOLDER) {
            // If the title of a folder which is listed within the folders
            // menulist has been changed, we need to update the label of its
            // representing element.
            var menupopup = this._folderMenuList.menupopup;
            for (let i = 0; i < menupopup.childNodes.length; i++) {
                if ("folderId" in menupopup.childNodes[i] &&
                    menupopup.childNodes[i].folderId == aItemId) {
                    menupopup.childNodes[i].label = aValue;
                    break;
                }
            }
        }
        return;
    },
 
    onItemAdded: function PFP_onItemAdded(aItemId) {
        this._lastNewItem = aItemId;
    },
 
    onItemMoved: function() { },
    onItemRemoved: function() { },
    onBeginUpdateBatch: function() { },
    onEndUpdateBatch: function() { },
    onItemVisited: function() { },
}; 

PlaceFolderPicker.css

/* browser/omni.ja/chrome/browser/skin/classic/browser/places/editBookmarkOverlay.css */
 
/**** folder menulist ****/
.folder-icon > .menulist-label-box > .menulist-icon {
  width: 16px;
  height: 16px;
}
 
.folder-icon > .menu-iconic-left {
  display: -moz-box;
}
 
.folder-icon {
  list-style-image: url("chrome://global/skin/icons/folder-item.png") !important;
  -moz-image-region: rect(0px, 32px, 16px, 16px) !important;
}
 
 
/**** expanders ****/
 
.expander-up,
.expander-down {
  min-width: 0;
  margin: 0;
  -moz-margin-end: 4px;
}
 
.expander-up > .button-box,
.expander-down > .button-box {
  padding: 0;
}
 
.expander-up {
  list-style-image: url("chrome://global/skin/icons/collapse.png");
}
 
.expander-down {
  list-style-image: url("chrome://global/skin/icons/expand.png");
}
 
#PFP_folderTree {
  margin-top: 2px;
  margin-bottom: 2px;
}
 
/* editBookmarkOverlay.js で javascript で処理していた
   セパレーターの非表示処理はCSSで可能                  */
#PFP_foldersSeparator:last-child {
  display: none;
}
 
/* ::::: dropdown icons ::::: */
 
#PFP_folderMenuList[selectedId="PFP_toolbarFolderItem"],
#PFP_toolbarFolderItem {
  list-style-image: url("chrome://browser/skin/places/bookmarksToolbar.png") !important;
  -moz-image-region: auto !important;
}
 
#PFP_folderMenuList[selectedId="PFP_bmRootItem"],
#PFP_bmRootItem {
  list-style-image: url("chrome://browser/skin/places/bookmarksMenu.png") !important;
  -moz-image-region: auto !important;
}
 
#PFP_folderMenuList[selectedId="PFP_unfiledRootItem"],
#PFP_unfiledRootItem {
  list-style-image: url("chrome://browser/skin/places/unsortedBookmarks.png") !important;
  -moz-image-region: auto !important;
}