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