/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* The base view implements everything that's common to the toolbar and
* menu views.
*/
function PlacesViewBase(aPlace) {
this.place = aPlace;
this._controller = new PlacesController(this);
this._viewElt.controllers.appendController(this._controller);
}
PlacesViewBase.prototype = {
// The xul element that holds the entire view.
_viewElt: null,
get viewElt() this._viewElt,
get associatedElement() this._viewElt,
get controllers() this._viewElt.controllers,
// The xul element that represents the root container.
_rootElt: null,
// Set to true for views that are represented by native widgets (i.e.
// the native mac menu).
_nativeView: false,
QueryInterface: XPCOMUtils.generateQI(
[Components.interfaces.nsINavHistoryResultObserver,
Components.interfaces.nsISupportsWeakReference]),
_place: "",
get place() this._place,
set place(val) {
this._place = val;
let history = PlacesUtils.history;
let queries = { }, options = { };
history.queryStringToQueries(val, queries, { }, options);
if (!queries.value.length)
queries.value = [history.getNewQuery()];
let result = history.executeQueries(queries.value, queries.value.length,
options.value);
result.addObserver(this, false);
return val;
},
_result: null,
get result() this._result,
set result(val) {
if (this._result == val)
return val;
if (this._result) {
this._result.removeObserver(this);
this._resultNode.containerOpen = false;
}
if (this._rootElt.localName == "menupopup")
this._rootElt._built = false;
this._result = val;
if (val) {
this._resultNode = val.root;
this._rootElt._placesNode = this._resultNode;
this._domNodes = new Map();
this._domNodes.set(this._resultNode, this._rootElt);
// This calls _rebuild through invalidateContainer.
this._resultNode.containerOpen = true;
}
else {
this._resultNode = null;
delete this._domNodes;
}
return val;
},
/**
* Gets the DOM node used for the given places node.
*
* @param aPlacesNode
* a places result node.
* @throws if there is no DOM node set for aPlacesNode.
*/
_getDOMNodeForPlacesNode:
function PVB__getDOMNodeForPlacesNode(aPlacesNode) {
let node = this._domNodes.get(aPlacesNode, null);
if (!node) {
throw new Error("No DOM node set for aPlacesNode.\nnode.type: " +
aPlacesNode.type + ". node.parent: " + aPlacesNode);
}
return node;
},
get controller() this._controller,
get selType() "single",
selectItems: function() { },
selectAll: function() { },
get selectedNode() {
if (this._contextMenuShown) {
let anchor = this._contextMenuShown.triggerNode;
if (!anchor)
return null;
if (anchor._placesNode)
return this._rootElt == anchor ? null : anchor._placesNode;
anchor = anchor.parentNode;
return this._rootElt == anchor ? null : (anchor._placesNode || null);
}
return null;
},
get hasSelection() this.selectedNode != null,
get selectedNodes() {
let selectedNode = this.selectedNode;
return selectedNode ? [selectedNode] : [];
},
get removableSelectionRanges() {
// On static content the current selectedNode would be the selection's
// parent node. We don't want to allow removing a node when the
// selection is not explicit.
if (document.popupNode &&
(document.popupNode == "menupopup" || !document.popupNode._placesNode))
return [];
return [this.selectedNodes];
},
get draggableSelection() [this._draggedElt],
get insertionPoint() {
// There is no insertion point for history queries, so bail out now and
// save a lot of work when updating commands.
let resultNode = this._resultNode;
if (PlacesUtils.nodeIsQuery(resultNode) &&
PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
return null;
// By default, the insertion point is at the top level, at the end.
let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
let container = this._resultNode;
let orientation = Ci.nsITreeView.DROP_BEFORE;
let isTag = false;
let selectedNode = this.selectedNode;
if (selectedNode) {
let popup = document.popupNode;
if (!popup._placesNode || popup._placesNode == this._resultNode ||
popup._placesNode.itemId == -1) {
// If a static menuitem is selected, or if the root node is selected,
// the insertion point is inside the folder, at the end.
container = selectedNode;
orientation = Ci.nsITreeView.DROP_ON;
}
else {
// In all other cases the insertion point is before that node.
container = selectedNode.parent;
index = container.getChildIndex(selectedNode);
isTag = PlacesUtils.nodeIsTagQuery(container);
}
}
if (PlacesControllerDragHelper.disallowInsertion(container))
return null;
return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
index, orientation, isTag);
},
buildContextMenu: function PVB_buildContextMenu(aPopup) {
this._contextMenuShown = aPopup;
window.updateCommands("places");
return this.controller.buildContextMenu(aPopup);
},
destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
this._contextMenuShown = null;
},
_cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
// Remove Places nodes from the popup.
let child = aPopup._startMarker;
while (child.nextSibling != aPopup._endMarker) {
let sibling = child.nextSibling;
if (sibling._placesNode && !aDelay) {
aPopup.removeChild(sibling);
}
else if (sibling._placesNode && aDelay) {
// HACK (bug 733419): the popups originating from the OS X native
// menubar don't live-update while open, thus we don't clean it
// until the next popupshowing, to avoid zombie menuitems.
if (!aPopup._delayedRemovals)
aPopup._delayedRemovals = [];
aPopup._delayedRemovals.push(sibling);
child = child.nextSibling;
}
else {
child = child.nextSibling;
}
}
},
_rebuildPopup: function PVB__rebuildPopup(aPopup) {
let resultNode = aPopup._placesNode;
if (!resultNode.containerOpen)
return;
if (this.controller.hasCachedLivemarkInfo(resultNode)) {
this._setEmptyPopupStatus(aPopup, false);
aPopup._built = true;
this._populateLivemarkPopup(aPopup);
return;
}
this._cleanPopup(aPopup);
let cc = resultNode.childCount;
if (cc > 0) {
this._setEmptyPopupStatus(aPopup, false);
for (let i = 0; i < cc; ++i) {
let child = resultNode.getChild(i);
this._insertNewItemToPopup(child, aPopup, null);
}
}
else {
this._setEmptyPopupStatus(aPopup, true);
}
aPopup._built = true;
},
_removeChild: function PVB__removeChild(aChild) {
// If document.popupNode pointed to this child, null it out,
// otherwise controller's command-updating may rely on the removed
// item still being "selected".
if (document.popupNode == aChild)
document.popupNode = null;
aChild.parentNode.removeChild(aChild);
},
_setEmptyPopupStatus:
function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
if (!aPopup._emptyMenuitem) {
let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
aPopup._emptyMenuitem = document.createElement("menuitem");
aPopup._emptyMenuitem.setAttribute("label", label);
aPopup._emptyMenuitem.setAttribute("disabled", true);
}
if (aEmpty) {
aPopup.setAttribute("emptyplacesresult", "true");
// Don't add the menuitem if there is static content.
if (!aPopup._startMarker.previousSibling &&
!aPopup._endMarker.nextSibling)
aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
}
else {
aPopup.removeAttribute("emptyplacesresult");
try {
aPopup.removeChild(aPopup._emptyMenuitem);
} catch (ex) {}
}
},
_createMenuItemForPlacesNode:
function PVB__createMenuItemForPlacesNode(aPlacesNode) {
this._domNodes.delete(aPlacesNode);
let element;
let type = aPlacesNode.type;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
element = document.createElement("menuseparator");
}
else {
let itemId = aPlacesNode.itemId;
if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
element = document.createElement("menuitem");
element.className = "menuitem-iconic bookmark-item menuitem-with-favicon";
element.setAttribute("scheme",
PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri));
}
else if (PlacesUtils.containerTypes.indexOf(type) != -1) {
element = document.createElement("menu");
element.setAttribute("container", "true");
if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
element.setAttribute("query", "true");
if (PlacesUtils.nodeIsTagQuery(aPlacesNode))
element.setAttribute("tagContainer", "true");
else if (PlacesUtils.nodeIsDay(aPlacesNode))
element.setAttribute("dayContainer", "true");
else if (PlacesUtils.nodeIsHost(aPlacesNode))
element.setAttribute("hostContainer", "true");
}
else if (itemId != -1) {
PlacesUtils.livemarks.getLivemark({ id: itemId })
.then(aLivemark => {
element.setAttribute("livemark", "true");
#ifdef XP_MACOSX
// OS X native menubar doesn't track list-style-images since
// it doesn't have a frame (bug 733415). Thus enforce updating.
element.setAttribute("image", "");
element.removeAttribute("image");
#endif
this.controller.cacheLivemarkInfo(aPlacesNode, aLivemark);
}, () => undefined);
}
let popup = document.createElement("menupopup");
popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
if (!this._nativeView) {
popup.setAttribute("placespopup", "true");
}
#ifdef XP_MACOSX
// No context menu on mac.
popup.setAttribute("context", "placesContext");
#endif
element.appendChild(popup);
element.className = "menu-iconic bookmark-item";
this._domNodes.set(aPlacesNode, popup);
}
else
throw "Unexpected node";
element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
let icon = aPlacesNode.icon;
if (icon)
element.setAttribute("image",
PlacesUIUtils.getImageURLForResolution(window, icon));
}
element._placesNode = aPlacesNode;
if (!this._domNodes.has(aPlacesNode))
this._domNodes.set(aPlacesNode, element);
return element;
},
_insertNewItemToPopup:
function PVB__insertNewItemToPopup(aNewChild, aPopup, aBefore) {
let element = this._createMenuItemForPlacesNode(aNewChild);
let before = aBefore || aPopup._endMarker;
aPopup.insertBefore(element, before);
return element;
},
_setLivemarkSiteURIMenuItem:
function PVB__setLivemarkSiteURIMenuItem(aPopup) {
let livemarkInfo = this.controller.getCachedLivemarkInfo(aPopup._placesNode);
let siteUrl = livemarkInfo && livemarkInfo.siteURI ?
livemarkInfo.siteURI.spec : null;
if (!siteUrl && aPopup._siteURIMenuitem) {
aPopup.removeChild(aPopup._siteURIMenuitem);
aPopup._siteURIMenuitem = null;
aPopup.removeChild(aPopup._siteURIMenuseparator);
aPopup._siteURIMenuseparator = null;
}
else if (siteUrl && !aPopup._siteURIMenuitem) {
// Add "Open (Feed Name)" menuitem.
aPopup._siteURIMenuitem = document.createElement("menuitem");
aPopup._siteURIMenuitem.className = "openlivemarksite-menuitem";
aPopup._siteURIMenuitem.setAttribute("targetURI", siteUrl);
aPopup._siteURIMenuitem.setAttribute("oncommand",
"openUILink(this.getAttribute('targetURI'), event);");
// If a user middle-clicks this item we serve the oncommand event.
// We are using checkForMiddleClick because of Bug 246720.
// Note: stopPropagation is needed to avoid serving middle-click
// with BT_onClick that would open all items in tabs.
aPopup._siteURIMenuitem.setAttribute("onclick",
"checkForMiddleClick(this, event); event.stopPropagation();");
let label =
PlacesUIUtils.getFormattedString("menuOpenLivemarkOrigin.label",
[aPopup.parentNode.getAttribute("label")])
aPopup._siteURIMenuitem.setAttribute("label", label);
aPopup.insertBefore(aPopup._siteURIMenuitem, aPopup._startMarker);
aPopup._siteURIMenuseparator = document.createElement("menuseparator");
aPopup.insertBefore(aPopup._siteURIMenuseparator, aPopup._startMarker);
}
},
/**
* Add, update or remove the livemark status menuitem.
* @param aPopup
* The livemark container popup
* @param aStatus
* The livemark status
*/
_setLivemarkStatusMenuItem:
function PVB_setLivemarkStatusMenuItem(aPopup, aStatus) {
let statusMenuitem = aPopup._statusMenuitem;
if (!statusMenuitem) {
// Create the status menuitem and cache it in the popup object.
statusMenuitem = document.createElement("menuitem");
statusMenuitem.className = "livemarkstatus-menuitem";
statusMenuitem.setAttribute("disabled", true);
aPopup._statusMenuitem = statusMenuitem;
}
if (aStatus == Ci.mozILivemark.STATUS_LOADING ||
aStatus == Ci.mozILivemark.STATUS_FAILED) {
// Status has changed, update the cached status menuitem.
let stringId = aStatus == Ci.mozILivemark.STATUS_LOADING ?
"bookmarksLivemarkLoading" : "bookmarksLivemarkFailed";
statusMenuitem.setAttribute("label", PlacesUIUtils.getString(stringId));
if (aPopup._startMarker.nextSibling != statusMenuitem)
aPopup.insertBefore(statusMenuitem, aPopup._startMarker.nextSibling);
}
else {
// The livemark has finished loading.
if (aPopup._statusMenuitem.parentNode == aPopup)
aPopup.removeChild(aPopup._statusMenuitem);
}
},
toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// We may get the popup for menus, but we need the menu itself.
if (elt.localName == "menupopup")
elt = elt.parentNode;
if (aValue)
elt.setAttribute("cutting", "true");
else
elt.removeAttribute("cutting");
},
nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
// Here we need the