- ブックマーク編集パネルにあるPlaceフォルダ選択リストメニュー/ツリービューが使いたかったので。
- editBookmarkOverlay.xul と editBookmarkOverlay.js と editBookmarkOverlay.css のコピーみたいな感じに…。
- editBookmarkOverlay.dtd はコピーせずそのまま使うことにした。
- jsコードは editBookmarkOverlay.js を読み込んで gEditItemOverlay のメソッドのいくつかを改造/置き換えた方が早くて楽なんだけど、中の動きを理解するためにあえて必要な部分をまるまるコピーしながら勉強。
- 機能としては、メニューからダイアログを開いて、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;
}
最終更新:2015年06月02日 20:39