summaryrefslogtreecommitdiffstats
path: root/browser/components/places/content/controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/content/controller.js')
-rw-r--r--browser/components/places/content/controller.js1742
1 files changed, 1742 insertions, 0 deletions
diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js
new file mode 100644
index 000000000..0d66fbcaf
--- /dev/null
+++ b/browser/components/places/content/controller.js
@@ -0,0 +1,1742 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite",
+ "resource://gre/modules/ForgetAboutSite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+ "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+// XXXmano: we should move most/all of these constants to PlacesUtils
+const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
+
+// No change to the view, preserve current selection
+const RELOAD_ACTION_NOTHING = 0;
+// Inserting items new to the view, select the inserted rows
+const RELOAD_ACTION_INSERT = 1;
+// Removing items from the view, select the first item after the last selected
+const RELOAD_ACTION_REMOVE = 2;
+// Moving items within a view, don't treat the dropped items as additional
+// rows.
+const RELOAD_ACTION_MOVE = 3;
+
+// When removing a bunch of pages we split them in chunks to give some breath
+// to the main-thread.
+const REMOVE_PAGES_CHUNKLEN = 300;
+
+/**
+ * Represents an insertion point within a container where we can insert
+ * items.
+ * @param aItemId
+ * The identifier of the parent container
+ * @param aIndex
+ * The index within the container where we should insert
+ * @param aOrientation
+ * The orientation of the insertion. NOTE: the adjustments to the
+ * insertion point to accommodate the orientation should be done by
+ * the person who constructs the IP, not the user. The orientation
+ * is provided for informational purposes only!
+ * @param [optional] aTag
+ * The tag name if this IP is set to a tag, null otherwise.
+ * @param [optional] aDropNearItemId
+ * When defined we will calculate index based on this itemId
+ * @constructor
+ */
+function InsertionPoint(aItemId, aIndex, aOrientation, aTagName = null,
+ aDropNearItemId = false) {
+ this.itemId = aItemId;
+ this._index = aIndex;
+ this.orientation = aOrientation;
+ this.tagName = aTagName;
+ this.dropNearItemId = aDropNearItemId;
+}
+
+InsertionPoint.prototype = {
+ set index(val) {
+ return this._index = val;
+ },
+
+ promiseGuid: function () {
+ return PlacesUtils.promiseItemGuid(this.itemId);
+ },
+
+ get index() {
+ if (this.dropNearItemId > 0) {
+ // If dropNearItemId is set up we must calculate the real index of
+ // the item near which we will drop.
+ var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
+ return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
+ }
+ return this._index;
+ },
+
+ get isTag() {
+ return typeof(this.tagName) == "string";
+ }
+};
+
+/**
+ * Places Controller
+ */
+
+function PlacesController(aView) {
+ this._view = aView;
+ XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
+ "@mozilla.org/widget/clipboard;1",
+ "nsIClipboard");
+ XPCOMUtils.defineLazyGetter(this, "profileName", function () {
+ return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
+ });
+
+ this._cachedLivemarkInfoObjects = new Map();
+}
+
+PlacesController.prototype = {
+ /**
+ * The places view.
+ */
+ _view: null,
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIClipboardOwner
+ ]),
+
+ // nsIClipboardOwner
+ LosingOwnership: function PC_LosingOwnership (aXferable) {
+ this.cutNodes = [];
+ },
+
+ terminate: function PC_terminate() {
+ this._releaseClipboardOwnership();
+ },
+
+ supportsCommand: function PC_supportsCommand(aCommand) {
+ // Non-Places specific commands that we also support
+ switch (aCommand) {
+ case "cmd_undo":
+ case "cmd_redo":
+ case "cmd_cut":
+ case "cmd_copy":
+ case "cmd_paste":
+ case "cmd_delete":
+ case "cmd_selectAll":
+ return true;
+ }
+
+ // All other Places Commands are prefixed with "placesCmd_" ... this
+ // filters out other commands that we do _not_ support (see 329587).
+ const CMD_PREFIX = "placesCmd_";
+ return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
+ },
+
+ isCommandEnabled: function PC_isCommandEnabled(aCommand) {
+ switch (aCommand) {
+ case "cmd_undo":
+ if (!PlacesUIUtils.useAsyncTransactions)
+ return PlacesUtils.transactionManager.numberOfUndoItems > 0;
+
+ return PlacesTransactions.topUndoEntry != null;
+ case "cmd_redo":
+ if (!PlacesUIUtils.useAsyncTransactions)
+ return PlacesUtils.transactionManager.numberOfRedoItems > 0;
+
+ return PlacesTransactions.topRedoEntry != null;
+ case "cmd_cut":
+ case "placesCmd_cut":
+ case "placesCmd_moveBookmarks":
+ for (let node of this._view.selectedNodes) {
+ // If selection includes history nodes or tags-as-bookmark, disallow
+ // cutting.
+ if (node.itemId == -1 ||
+ (node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
+ return false;
+ }
+ }
+ // Otherwise fall through the cmd_delete check.
+ case "cmd_delete":
+ case "placesCmd_delete":
+ case "placesCmd_deleteDataHost":
+ return this._hasRemovableSelection();
+ case "cmd_copy":
+ case "placesCmd_copy":
+ return this._view.hasSelection;
+ case "cmd_paste":
+ case "placesCmd_paste":
+ return this._canInsert(true) && this._isClipboardDataPasteable();
+ case "cmd_selectAll":
+ if (this._view.selType != "single") {
+ let rootNode = this._view.result.root;
+ if (rootNode.containerOpen && rootNode.childCount > 0)
+ return true;
+ }
+ return false;
+ case "placesCmd_open":
+ case "placesCmd_open:window":
+ case "placesCmd_open:privatewindow":
+ case "placesCmd_open:tab":
+ var selectedNode = this._view.selectedNode;
+ return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
+ case "placesCmd_new:folder":
+ return this._canInsert();
+ case "placesCmd_new:bookmark":
+ return this._canInsert();
+ case "placesCmd_new:separator":
+ return this._canInsert() &&
+ !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
+ this._view.result.sortingMode ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ case "placesCmd_show:info": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
+ }
+ case "placesCmd_reload": {
+ // Livemark containers
+ let selectedNode = this._view.selectedNode;
+ return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
+ }
+ case "placesCmd_sortBy:name": {
+ let selectedNode = this._view.selectedNode;
+ return selectedNode &&
+ PlacesUtils.nodeIsFolder(selectedNode) &&
+ !PlacesUIUtils.isContentsReadOnly(selectedNode) &&
+ this._view.result.sortingMode ==
+ Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
+ }
+ case "placesCmd_createBookmark":
+ var node = this._view.selectedNode;
+ return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function PC_doCommand(aCommand) {
+ switch (aCommand) {
+ case "cmd_undo":
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.undoTransaction();
+ return;
+ }
+ PlacesTransactions.undo().then(null, Components.utils.reportError);
+ break;
+ case "cmd_redo":
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ PlacesUtils.transactionManager.redoTransaction();
+ return;
+ }
+ PlacesTransactions.redo().then(null, Components.utils.reportError);
+ break;
+ case "cmd_cut":
+ case "placesCmd_cut":
+ this.cut();
+ break;
+ case "cmd_copy":
+ case "placesCmd_copy":
+ this.copy();
+ break;
+ case "cmd_paste":
+ case "placesCmd_paste":
+ this.paste().then(null, Components.utils.reportError);
+ break;
+ case "cmd_delete":
+ case "placesCmd_delete":
+ this.remove("Remove Selection").then(null, Components.utils.reportError);
+ break;
+ case "placesCmd_deleteDataHost":
+ var host;
+ if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
+ var queries = this._view.selectedNode.getQueries();
+ host = queries[0].domain;
+ }
+ else
+ host = NetUtil.newURI(this._view.selectedNode.uri).host;
+ ForgetAboutSite.removeDataFromDomain(host);
+ break;
+ case "cmd_selectAll":
+ this.selectAll();
+ break;
+ case "placesCmd_open":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
+ break;
+ case "placesCmd_open:window":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
+ break;
+ case "placesCmd_open:privatewindow":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
+ break;
+ case "placesCmd_open:tab":
+ PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
+ break;
+ case "placesCmd_new:folder":
+ this.newItem("folder");
+ break;
+ case "placesCmd_new:bookmark":
+ this.newItem("bookmark");
+ break;
+ case "placesCmd_new:separator":
+ this.newSeparator().catch(Components.utils.reportError);
+ break;
+ case "placesCmd_show:info":
+ this.showBookmarkPropertiesForSelection();
+ break;
+ case "placesCmd_moveBookmarks":
+ this.moveSelectedBookmarks();
+ break;
+ case "placesCmd_reload":
+ this.reloadSelectedLivemark();
+ break;
+ case "placesCmd_sortBy:name":
+ this.sortFolderByName().then(null, Components.utils.reportError);
+ break;
+ case "placesCmd_createBookmark":
+ let node = this._view.selectedNode;
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: "bookmark"
+ , hiddenRows: [ "description"
+ , "keyword"
+ , "location"
+ , "loadInSidebar" ]
+ , uri: NetUtil.newURI(node.uri)
+ , title: node.title
+ }, window.top);
+ break;
+ }
+ },
+
+ onEvent: function PC_onEvent(eventName) { },
+
+
+ /**
+ * Determine whether or not the selection can be removed, either by the
+ * delete or cut operations based on whether or not any of its contents
+ * are non-removable. We don't need to worry about recursion here since it
+ * is a policy decision that a removable item not be placed inside a non-
+ * removable item.
+ *
+ * @return true if all nodes in the selection can be removed,
+ * false otherwise.
+ */
+ _hasRemovableSelection() {
+ var ranges = this._view.removableSelectionRanges;
+ if (!ranges.length)
+ return false;
+
+ var root = this._view.result.root;
+
+ for (var j = 0; j < ranges.length; j++) {
+ var nodes = ranges[j];
+ for (var i = 0; i < nodes.length; ++i) {
+ // Disallow removing the view's root node
+ if (nodes[i] == root)
+ return false;
+
+ if (!PlacesUIUtils.canUserRemove(nodes[i]))
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Determines whether or not nodes can be inserted relative to the selection.
+ */
+ _canInsert: function PC__canInsert(isPaste) {
+ var ip = this._view.insertionPoint;
+ return ip != null && (isPaste || ip.isTag != true);
+ },
+
+ /**
+ * Looks at the data on the clipboard to see if it is paste-able.
+ * Paste-able data is:
+ * - in a format that the view can receive
+ * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
+ * - clipboard data is of type TEXT_UNICODE and
+ * is a valid URI.
+ */
+ _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
+ // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
+ // pasteable, with no need to unwrap all the nodes.
+
+ var flavors = PlacesUIUtils.PLACES_FLAVORS;
+ var clipboard = this.clipboard;
+ var hasPlacesData =
+ clipboard.hasDataMatchingFlavors(flavors, flavors.length,
+ Ci.nsIClipboard.kGlobalClipboard);
+ if (hasPlacesData)
+ return this._view.insertionPoint != null;
+
+ // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
+ // pasting of valid "text/unicode" and "text/x-moz-url" data
+ var xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
+ xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
+ clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+ try {
+ // getAnyTransferData will throw if no data is available.
+ var data = { }, type = { };
+ xferable.getAnyTransferData(type, data, { });
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
+ type.value != PlacesUtils.TYPE_UNICODE)
+ return false;
+
+ // unwrapNodes() will throw if the data blob is malformed.
+ PlacesUtils.unwrapNodes(data, type.value);
+ return this._view.insertionPoint != null;
+ }
+ catch (e) {
+ // getAnyTransferData or unwrapNodes failed
+ return false;
+ }
+ },
+
+ /**
+ * Gathers information about the selected nodes according to the following
+ * rules:
+ * "link" node is a URI
+ * "bookmark" node is a bookmark
+ * "livemarkChild" node is a child of a livemark
+ * "tagChild" node is a child of a tag
+ * "folder" node is a folder
+ * "query" node is a query
+ * "separator" node is a separator line
+ * "host" node is a host
+ *
+ * @return an array of objects corresponding the selected nodes. Each
+ * object has each of the properties above set if its corresponding
+ * node matches the rule. In addition, the annotations names for each
+ * node are set on its corresponding object as properties.
+ * Notes:
+ * 1) This can be slow, so don't call it anywhere performance critical!
+ */
+ _buildSelectionMetadata: function PC__buildSelectionMetadata() {
+ var metadata = [];
+ var nodes = this._view.selectedNodes;
+
+ for (var i = 0; i < nodes.length; i++) {
+ var nodeData = {};
+ var node = nodes[i];
+ var nodeType = node.type;
+ var uri = null;
+
+ // We don't use the nodeIs* methods here to avoid going through the type
+ // property way too often
+ switch (nodeType) {
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
+ nodeData["query"] = true;
+ if (node.parent) {
+ switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
+ nodeData["host"] = true;
+ break;
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
+ case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
+ nodeData["day"] = true;
+ break;
+ }
+ }
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
+ nodeData["folder"] = true;
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
+ nodeData["separator"] = true;
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
+ nodeData["link"] = true;
+ uri = NetUtil.newURI(node.uri);
+ if (PlacesUtils.nodeIsBookmark(node)) {
+ nodeData["bookmark"] = true;
+ var parentNode = node.parent;
+ if (parentNode) {
+ if (PlacesUtils.nodeIsTagQuery(parentNode))
+ nodeData["tagChild"] = true;
+ else if (this.hasCachedLivemarkInfo(parentNode))
+ nodeData["livemarkChild"] = true;
+ }
+ }
+ break;
+ }
+
+ // annotations
+ if (uri) {
+ let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
+ for (let j = 0; j < names.length; ++j)
+ nodeData[names[j]] = true;
+ }
+
+ // For items also include the item-specific annotations
+ if (node.itemId != -1) {
+ let names = PlacesUtils.annotations
+ .getItemAnnotationNames(node.itemId);
+ for (let j = 0; j < names.length; ++j)
+ nodeData[names[j]] = true;
+ }
+ metadata.push(nodeData);
+ }
+
+ return metadata;
+ },
+
+ /**
+ * Determines if a context-menu item should be shown
+ * @param aMenuItem
+ * the context menu item
+ * @param aMetaData
+ * meta data about the selection
+ * @return true if the conditions (see buildContextMenu) are satisfied
+ * and the item can be displayed, false otherwise.
+ */
+ _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
+ var selectiontype = aMenuItem.getAttribute("selectiontype");
+ if (!selectiontype) {
+ selectiontype = "single|multiple";
+ }
+ var selectionTypes = selectiontype.split("|");
+ if (selectionTypes.includes("any")) {
+ return true;
+ }
+ var count = aMetaData.length;
+ if (count > 1 && !selectionTypes.includes("multiple"))
+ return false;
+ if (count == 1 && !selectionTypes.includes("single"))
+ return false;
+ // NB: if there is no selection, we show the item if and only if
+ // the selectiontype includes 'none' - the metadata list will be
+ // empty so none of the other criteria will apply anyway.
+ if (count == 0)
+ return selectionTypes.includes("none");
+
+ var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
+ if (forceHideAttr) {
+ var forceHideRules = forceHideAttr.split("|");
+ for (let i = 0; i < aMetaData.length; ++i) {
+ for (let j = 0; j < forceHideRules.length; ++j) {
+ if (forceHideRules[j] in aMetaData[i])
+ return false;
+ }
+ }
+ }
+
+ var selectionAttr = aMenuItem.getAttribute("selection");
+ if (!selectionAttr) {
+ return !aMenuItem.hidden;
+ }
+
+ if (selectionAttr == "any")
+ return true;
+
+ var showRules = selectionAttr.split("|");
+ var anyMatched = false;
+ function metaDataNodeMatches(metaDataNode, rules) {
+ for (var i = 0; i < rules.length; i++) {
+ if (rules[i] in metaDataNode)
+ return true;
+ }
+ return false;
+ }
+
+ for (var i = 0; i < aMetaData.length; ++i) {
+ if (metaDataNodeMatches(aMetaData[i], showRules))
+ anyMatched = true;
+ else
+ return false;
+ }
+ return anyMatched;
+ },
+
+ /**
+ * Detects information (meta-data rules) about the current selection in the
+ * view (see _buildSelectionMetadata) and sets the visibility state for each
+ * of the menu-items in the given popup with the following rules applied:
+ * 0) The "ignoreitem" attribute may be set to "true" for this code not to
+ * handle that menuitem.
+ * 1) The "selectiontype" attribute may be set on a menu-item to "single"
+ * if the menu-item should be visible only if there is a single node
+ * selected, or to "multiple" if the menu-item should be visible only if
+ * multiple nodes are selected, or to "none" if the menuitems should be
+ * visible for if there are no selected nodes, or to a |-separated
+ * combination of these.
+ * If the attribute is not set or set to an invalid value, the menu-item
+ * may be visible irrespective of the selection.
+ * 2) The "selection" attribute may be set on a menu-item to the various
+ * meta-data rules for which it may be visible. The rules should be
+ * separated with the | character.
+ * 3) A menu-item may be visible only if at least one of the rules set in
+ * its selection attribute apply to each of the selected nodes in the
+ * view.
+ * 4) The "forcehideselection" attribute may be set on a menu-item to rules
+ * for which it should be hidden. This attribute takes priority over the
+ * selection attribute. A menu-item would be hidden if at least one of the
+ * given rules apply to one of the selected nodes. The rules should be
+ * separated with the | character.
+ * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
+ * true if it should be hidden when there's no insertion point
+ * 6) The visibility state of a menu-item is unchanged if none of these
+ * attribute are set.
+ * 7) These attributes should not be set on separators for which the
+ * visibility state is "auto-detected."
+ * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
+ * true if it should be hidden inside the private browsing mode
+ * @param aPopup
+ * The menupopup to build children into.
+ * @return true if at least one item is visible, false otherwise.
+ */
+ buildContextMenu: function PC_buildContextMenu(aPopup) {
+ var metadata = this._buildSelectionMetadata();
+ var ip = this._view.insertionPoint;
+ var noIp = !ip || ip.isTag;
+
+ var separator = null;
+ var visibleItemsBeforeSep = false;
+ var usableItemCount = 0;
+ for (var i = 0; i < aPopup.childNodes.length; ++i) {
+ var item = aPopup.childNodes[i];
+ if (item.getAttribute("ignoreitem") == "true") {
+ continue;
+ }
+ if (item.localName != "menuseparator") {
+ // We allow pasting into tag containers, so special case that.
+ var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
+ noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
+ var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
+ PrivateBrowsingUtils.isWindowPrivate(window);
+ var shouldHideItem = hideIfNoIP || hideIfPrivate ||
+ !this._shouldShowMenuItem(item, metadata);
+ item.hidden = item.disabled = shouldHideItem;
+
+ if (!item.hidden) {
+ visibleItemsBeforeSep = true;
+ usableItemCount++;
+
+ // Show the separator above the menu-item if any
+ if (separator) {
+ separator.hidden = false;
+ separator = null;
+ }
+ }
+ }
+ else { // menuseparator
+ // Initially hide it. It will be unhidden if there will be at least one
+ // visible menu-item above and below it.
+ item.hidden = true;
+
+ // We won't show the separator at all if no items are visible above it
+ if (visibleItemsBeforeSep)
+ separator = item;
+
+ // New separator, count again:
+ visibleItemsBeforeSep = false;
+ }
+ }
+
+ // Set Open Folder/Links In Tabs items enabled state if they're visible
+ if (usableItemCount > 0) {
+ var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
+ if (!openContainerInTabsItem.hidden) {
+ var containerToUse = this._view.selectedNode || this._view.result.root;
+ if (PlacesUtils.nodeIsContainer(containerToUse)) {
+ if (!PlacesUtils.hasChildURIs(containerToUse)) {
+ openContainerInTabsItem.disabled = true;
+ // Ensure that we don't display the menu if nothing is enabled:
+ usableItemCount--;
+ }
+ }
+ }
+ }
+
+ return usableItemCount > 0;
+ },
+
+ /**
+ * Select all links in the current view.
+ */
+ selectAll: function PC_selectAll() {
+ this._view.selectAll();
+ },
+
+ /**
+ * Opens the bookmark properties for the selected URI Node.
+ */
+ showBookmarkPropertiesForSelection() {
+ let node = this._view.selectedNode;
+ if (!node)
+ return;
+
+ PlacesUIUtils.showBookmarkDialog({ action: "edit"
+ , node
+ , hiddenRows: [ "folderPicker" ]
+ }, window.top);
+ },
+
+ /**
+ * This method can be run on a URI parameter to ensure that it didn't
+ * receive a string instead of an nsIURI object.
+ */
+ _assertURINotString: function PC__assertURINotString(value) {
+ NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
+ "This method should be passed a URI as a nsIURI object, not as a string.");
+ },
+
+ /**
+ * Reloads the selected livemark if any.
+ */
+ reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
+ var selectedNode = this._view.selectedNode;
+ if (selectedNode) {
+ let itemId = selectedNode.itemId;
+ PlacesUtils.livemarks.getLivemark({ id: itemId })
+ .then(aLivemark => {
+ aLivemark.reload(true);
+ }, Components.utils.reportError);
+ }
+ },
+
+ /**
+ * Opens the links in the selected folder, or the selected links in new tabs.
+ */
+ openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
+ var node = this._view.selectedNode;
+ var nodes = this._view.selectedNodes;
+ // In the case of no selection, open the root node:
+ if (!node && !nodes.length) {
+ node = this._view.result.root;
+ }
+ if (node && PlacesUtils.nodeIsContainer(node))
+ PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this._view);
+ else
+ PlacesUIUtils.openURINodesInTabs(nodes, aEvent, this._view);
+ },
+
+ /**
+ * Shows the Add Bookmark UI for the current insertion point.
+ *
+ * @param aType
+ * the type of the new item (bookmark/livemark/folder)
+ */
+ newItem: function PC_newItem(aType) {
+ let ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let performed =
+ PlacesUIUtils.showBookmarkDialog({ action: "add"
+ , type: aType
+ , defaultInsertionPoint: ip
+ , hiddenRows: [ "folderPicker" ]
+ }, window.top);
+ if (performed) {
+ // Select the new item.
+ let insertedNodeId = PlacesUtils.bookmarks
+ .getIdForItemAt(ip.itemId, ip.index);
+ this._view.selectItems([insertedNodeId], false);
+ }
+ },
+
+ /**
+ * Create a new Bookmark separator somewhere.
+ */
+ newSeparator: Task.async(function* () {
+ var ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ // Select the new item.
+ let insertedNodeId = PlacesUtils.bookmarks
+ .getIdForItemAt(ip.itemId, ip.index);
+ this._view.selectItems([insertedNodeId], false);
+ return;
+ }
+
+ let txn = PlacesTransactions.NewSeparator({ parentGuid: yield ip.promiseGuid()
+ , index: ip.index });
+ let guid = yield txn.transact();
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ // Select the new item.
+ this._view.selectItems([itemId], false);
+ }),
+
+ /**
+ * Opens a dialog for moving the selected nodes.
+ */
+ moveSelectedBookmarks: function PC_moveBookmarks() {
+ window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
+ "", "chrome, modal",
+ this._view.selectedNodes);
+ },
+
+ /**
+ * Sort the selected folder by name
+ */
+ sortFolderByName: Task.async(function* () {
+ let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
+ if (!PlacesUIUtils.useAsyncTransactions) {
+ var txn = new PlacesSortFolderByNameTransaction(itemId);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ return;
+ }
+ let guid = yield PlacesUtils.promiseItemGuid(itemId);
+ yield PlacesTransactions.SortByName(guid).transact();
+ }),
+
+ /**
+ * Walk the list of folders we're removing in this delete operation, and
+ * see if the selected node specified is already implicitly being removed
+ * because it is a child of that folder.
+ * @param node
+ * Node to check for containment.
+ * @param pastFolders
+ * List of folders the calling function has already traversed
+ * @return true if the node should be skipped, false otherwise.
+ */
+ _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
+ /**
+ * Determines if a node is contained by another node within a resultset.
+ * @param node
+ * The node to check for containment for
+ * @param parent
+ * The parent container to check for containment in
+ * @return true if node is a member of parent's children, false otherwise.
+ */
+ function isContainedBy(node, parent) {
+ var cursor = node.parent;
+ while (cursor) {
+ if (cursor == parent)
+ return true;
+ cursor = cursor.parent;
+ }
+ return false;
+ }
+
+ for (var j = 0; j < pastFolders.length; ++j) {
+ if (isContainedBy(node, pastFolders[j]))
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Creates a set of transactions for the removal of a range of items.
+ * A range is an array of adjacent nodes in a view.
+ * @param [in] range
+ * An array of nodes to remove. Should all be adjacent.
+ * @param [out] transactions
+ * An array of transactions.
+ * @param [optional] removedFolders
+ * An array of folder nodes that have already been removed.
+ */
+ _removeRange: function PC__removeRange(range, transactions, removedFolders) {
+ NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
+ if (!removedFolders)
+ removedFolders = [];
+
+ for (var i = 0; i < range.length; ++i) {
+ var node = range[i];
+ if (this._shouldSkipNode(node, removedFolders))
+ continue;
+
+ if (PlacesUtils.nodeIsTagQuery(node.parent)) {
+ // This is a uri node inside a tag container. It needs a special
+ // untag transaction.
+ var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
+ var uri = NetUtil.newURI(node.uri);
+ if (PlacesUIUtils.useAsyncTransactions) {
+ let tag = node.parent.title;
+ if (!tag)
+ tag = PlacesUtils.bookmarks.getItemTitle(tagItemId);
+ transactions.push(PlacesTransactions.Untag({ uri: uri, tag: tag }));
+ }
+ else {
+ let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
+ transactions.push(txn);
+ }
+ }
+ else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
+ PlacesUtils.nodeIsQuery(node.parent) &&
+ PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
+ // This is a tag container.
+ // Untag all URIs tagged with this tag only if the tag container is
+ // child of the "Tags" query in the library, in all other places we
+ // must only remove the query node.
+ let tag = node.title;
+ let URIs = PlacesUtils.tagging.getURIsForTag(tag);
+ if (PlacesUIUtils.useAsyncTransactions) {
+ transactions.push(PlacesTransactions.Untag({ tag: tag, uris: URIs }));
+ }
+ else {
+ for (var j = 0; j < URIs.length; j++) {
+ let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
+ transactions.push(txn);
+ }
+ }
+ }
+ else if (PlacesUtils.nodeIsURI(node) &&
+ PlacesUtils.nodeIsQuery(node.parent) &&
+ PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ // This is a uri node inside an history query.
+ PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
+ // History deletes are not undoable, so we don't have a transaction.
+ }
+ else if (node.itemId == -1 &&
+ PlacesUtils.nodeIsQuery(node) &&
+ PlacesUtils.asQuery(node).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ // This is a dynamically generated history query, like queries
+ // grouped by site, time or both. Dynamically generated queries don't
+ // have an itemId even if they are descendants of a bookmark.
+ this._removeHistoryContainer(node);
+ // History deletes are not undoable, so we don't have a transaction.
+ }
+ else {
+ // This is a common bookmark item.
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // If this is a folder we add it to our array of folders, used
+ // to skip nodes that are children of an already removed folder.
+ removedFolders.push(node);
+ }
+ if (PlacesUIUtils.useAsyncTransactions) {
+ transactions.push(
+ PlacesTransactions.Remove({ guid: node.bookmarkGuid }));
+ }
+ else {
+ let txn = new PlacesRemoveItemTransaction(node.itemId);
+ transactions.push(txn);
+ }
+ }
+ }
+ },
+
+ /**
+ * Removes the set of selected ranges from bookmarks.
+ * @param txnName
+ * See |remove|.
+ */
+ _removeRowsFromBookmarks: Task.async(function* (txnName) {
+ var ranges = this._view.removableSelectionRanges;
+ var transactions = [];
+ var removedFolders = [];
+
+ for (var i = 0; i < ranges.length; i++)
+ this._removeRange(ranges[i], transactions, removedFolders);
+
+ if (transactions.length > 0) {
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield PlacesTransactions.batch(transactions);
+ }
+ else {
+ var txn = new PlacesAggregatedTransaction(txnName, transactions);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ }
+ }),
+
+ /**
+ * Removes the set of selected ranges from history.
+ *
+ * @note history deletes are not undoable.
+ */
+ _removeRowsFromHistory: function PC__removeRowsFromHistory() {
+ let nodes = this._view.selectedNodes;
+ let URIs = [];
+ for (let i = 0; i < nodes.length; ++i) {
+ let node = nodes[i];
+ if (PlacesUtils.nodeIsURI(node)) {
+ let uri = NetUtil.newURI(node.uri);
+ // Avoid duplicates.
+ if (URIs.indexOf(uri) < 0) {
+ URIs.push(uri);
+ }
+ }
+ else if (PlacesUtils.nodeIsQuery(node) &&
+ PlacesUtils.asQuery(node).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ this._removeHistoryContainer(node);
+ }
+ }
+
+ // Do removal in chunks to give some breath to main-thread.
+ function* pagesChunkGenerator(aURIs) {
+ while (aURIs.length) {
+ let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
+ PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
+ Services.tm.mainThread.dispatch(() => gen.next(),
+ Ci.nsIThread.DISPATCH_NORMAL);
+ yield undefined;
+ }
+ }
+ let gen = pagesChunkGenerator(URIs);
+ gen.next();
+ },
+
+ /**
+ * Removes history visits for an history container node.
+ * @param [in] aContainerNode
+ * The container node to remove.
+ *
+ * @note history deletes are not undoable.
+ */
+ _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) {
+ if (PlacesUtils.nodeIsHost(aContainerNode)) {
+ // Site container.
+ PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
+ }
+ else if (PlacesUtils.nodeIsDay(aContainerNode)) {
+ // Day container.
+ let query = aContainerNode.getQueries()[0];
+ let beginTime = query.beginTime;
+ let endTime = query.endTime;
+ NS_ASSERT(query && beginTime && endTime,
+ "A valid date container query should exist!");
+ // We want to exclude beginTime from the removal because
+ // removePagesByTimeframe includes both extremes, while date containers
+ // exclude the lower extreme. So, if we would not exclude it, we would
+ // end up removing more history than requested.
+ PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
+ }
+ },
+
+ /**
+ * Removes the selection
+ * @param aTxnName
+ * A name for the transaction if this is being performed
+ * as part of another operation.
+ */
+ remove: Task.async(function* (aTxnName) {
+ if (!this._hasRemovableSelection())
+ return;
+
+ NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
+
+ var root = this._view.result.root;
+
+ if (PlacesUtils.nodeIsFolder(root)) {
+ if (PlacesUIUtils.useAsyncTransactions)
+ yield this._removeRowsFromBookmarks(aTxnName);
+ else
+ this._removeRowsFromBookmarks(aTxnName);
+ }
+ else if (PlacesUtils.nodeIsQuery(root)) {
+ var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
+ if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) {
+ if (PlacesUIUtils.useAsyncTransactions)
+ yield this._removeRowsFromBookmarks(aTxnName);
+ else
+ this._removeRowsFromBookmarks(aTxnName);
+ }
+ else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
+ this._removeRowsFromHistory();
+ }
+ else {
+ NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
+ }
+ }
+ else
+ NS_ASSERT(false, "unexpected root");
+ }),
+
+ /**
+ * Fills a DataTransfer object with the content of the selection that can be
+ * dropped elsewhere.
+ * @param aEvent
+ * The dragstart event.
+ */
+ setDataTransfer: function PC_setDataTransfer(aEvent) {
+ let dt = aEvent.dataTransfer;
+
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+
+ function addData(type, index, feedURI) {
+ let wrapNode = PlacesUtils.wrapNode(node, type, feedURI);
+ dt.mozSetDataAt(type, wrapNode, index);
+ }
+
+ function addURIData(index, feedURI) {
+ addData(PlacesUtils.TYPE_X_MOZ_URL, index, feedURI);
+ addData(PlacesUtils.TYPE_UNICODE, index, feedURI);
+ addData(PlacesUtils.TYPE_HTML, index, feedURI);
+ }
+
+ try {
+ let nodes = this._view.draggableSelection;
+ for (let i = 0; i < nodes.length; ++i) {
+ var node = nodes[i];
+
+ // This order is _important_! It controls how this and other
+ // applications select data to be inserted based on type.
+ addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
+
+ // Drop the feed uri for livemark containers
+ let livemarkInfo = this.getCachedLivemarkInfo(node);
+ if (livemarkInfo) {
+ addURIData(i, livemarkInfo.feedURI.spec);
+ }
+ else if (node.uri) {
+ addURIData(i);
+ }
+ }
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ get clipboardAction () {
+ let action = {};
+ let actionOwner;
+ try {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
+ this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+ xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
+ [action, actionOwner] =
+ action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
+ } catch (ex) {
+ // Paste from external sources don't have any associated action, just
+ // fallback to a copy action.
+ return "copy";
+ }
+ // For cuts also check who inited the action, since cuts across different
+ // instances should instead be handled as copies (The sources are not
+ // available for this instance).
+ if (action == "cut" && actionOwner != this.profileName)
+ action = "copy";
+
+ return action;
+ },
+
+ _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
+ if (this.cutNodes.length > 0) {
+ // This clears the logical clipboard, doesn't remove data.
+ this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
+ }
+ },
+
+ _clearClipboard: function PC__clearClipboard() {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ // Empty transferables may cause crashes, so just add an unknown type.
+ const TYPE = "text/x-moz-place-empty";
+ xferable.addDataFlavor(TYPE);
+ xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
+ this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
+ },
+
+ _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
+ // This order is _important_! It controls how this and other applications
+ // select data to be inserted based on type.
+ let contents = [
+ { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
+ { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
+ { type: PlacesUtils.TYPE_HTML, entries: [] },
+ { type: PlacesUtils.TYPE_UNICODE, entries: [] },
+ ];
+
+ // Avoid handling descendants of a copied node, the transactions take care
+ // of them automatically.
+ let copiedFolders = [];
+ aNodes.forEach(function (node) {
+ if (this._shouldSkipNode(node, copiedFolders))
+ return;
+ if (PlacesUtils.nodeIsFolder(node))
+ copiedFolders.push(node);
+
+ let livemarkInfo = this.getCachedLivemarkInfo(node);
+ let feedURI = livemarkInfo && livemarkInfo.feedURI.spec;
+
+ contents.forEach(function (content) {
+ content.entries.push(
+ PlacesUtils.wrapNode(node, content.type, feedURI)
+ );
+ });
+ }, this);
+
+ function addData(type, data) {
+ xferable.addDataFlavor(type);
+ xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
+ data.length * 2);
+ }
+
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ let hasData = false;
+ // This order matters here! It controls how this and other applications
+ // select data to be inserted based on type.
+ contents.forEach(function (content) {
+ if (content.entries.length > 0) {
+ hasData = true;
+ let glue =
+ content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
+ addData(content.type, content.entries.join(glue));
+ }
+ });
+
+ // Track the exected action in the xferable. This must be the last flavor
+ // since it's the least preferred one.
+ // Enqueue a unique instance identifier to distinguish operations across
+ // concurrent instances of the application.
+ addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
+
+ if (hasData) {
+ this.clipboard.setData(xferable,
+ this.cutNodes.length > 0 ? this : null,
+ Ci.nsIClipboard.kGlobalClipboard);
+ }
+ },
+
+ _cutNodes: [],
+ get cutNodes() {
+ return this._cutNodes;
+ },
+ set cutNodes(aNodes) {
+ let self = this;
+ function updateCutNodes(aValue) {
+ self._cutNodes.forEach(function (aNode) {
+ self._view.toggleCutNode(aNode, aValue);
+ });
+ }
+
+ updateCutNodes(false);
+ this._cutNodes = aNodes;
+ updateCutNodes(true);
+ return aNodes;
+ },
+
+ /**
+ * Copy Bookmarks and Folders to the clipboard
+ */
+ copy: function PC_copy() {
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ this._populateClipboard(this._view.selectedNodes, "copy");
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ /**
+ * Cut Bookmarks and Folders to the clipboard
+ */
+ cut: function PC_cut() {
+ let result = this._view.result;
+ let didSuppressNotifications = result.suppressNotifications;
+ if (!didSuppressNotifications)
+ result.suppressNotifications = true;
+ try {
+ this._populateClipboard(this._view.selectedNodes, "cut");
+ this.cutNodes = this._view.selectedNodes;
+ }
+ finally {
+ if (!didSuppressNotifications)
+ result.suppressNotifications = false;
+ }
+ },
+
+ /**
+ * Paste Bookmarks and Folders from the clipboard
+ */
+ paste: Task.async(function* () {
+ // No reason to proceed if there isn't a valid insertion point.
+ let ip = this._view.insertionPoint;
+ if (!ip)
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ let action = this.clipboardAction;
+
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ xferable.init(null);
+ // This order matters here! It controls the preferred flavors for this
+ // paste operation.
+ [ PlacesUtils.TYPE_X_MOZ_PLACE,
+ PlacesUtils.TYPE_X_MOZ_URL,
+ PlacesUtils.TYPE_UNICODE,
+ ].forEach(type => xferable.addDataFlavor(type));
+
+ this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
+
+ // Now get the clipboard contents, in the best available flavor.
+ let data = {}, type = {}, items = [];
+ try {
+ xferable.getAnyTransferData(type, data, {});
+ data = data.value.QueryInterface(Ci.nsISupportsString).data;
+ type = type.value;
+ items = PlacesUtils.unwrapNodes(data, type);
+ } catch (ex) {
+ // No supported data exists or nodes unwrap failed, just bail out.
+ return;
+ }
+
+ let itemsToSelect = [];
+ if (PlacesUIUtils.useAsyncTransactions) {
+ if (ip.isTag) {
+ let uris = items.filter(item => "uri" in item).map(item => NetUtil.newURI(item.uri));
+ yield PlacesTransactions.Tag({ uris: uris, tag: ip.tagName }).transact();
+ }
+ else {
+ yield PlacesTransactions.batch(function* () {
+ let insertionIndex = ip.index;
+ let parent = yield ip.promiseGuid();
+
+ for (let item of items) {
+ let doCopy = action == "copy";
+
+ // If this is not a copy, check for safety that we can move the
+ // source, otherwise report an error and fallback to a copy.
+ if (!doCopy &&
+ !PlacesControllerDragHelper.canMoveUnwrappedNode(item)) {
+ Components.utils.reportError("Tried to move an unmovable " +
+ "Places node, reverting to a copy operation.");
+ doCopy = true;
+ }
+ let guid = yield PlacesUIUtils.getTransactionForData(
+ item, type, parent, insertionIndex, doCopy).transact();
+ itemsToSelect.push(yield PlacesUtils.promiseItemId(guid));
+
+ // Adjust index to make sure items are pasted in the correct
+ // position. If index is DEFAULT_INDEX, items are just appended.
+ if (insertionIndex != PlacesUtils.bookmarks.DEFAULT_INDEX)
+ insertionIndex++;
+ }
+ });
+ }
+ }
+ else {
+ let transactions = [];
+ let insertionIndex = ip.index;
+ for (let i = 0; i < items.length; ++i) {
+ if (ip.isTag) {
+ // Pasting into a tag container means tagging the item, regardless of
+ // the requested action.
+ let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
+ [ip.itemId]);
+ transactions.push(tagTxn);
+ continue;
+ }
+
+ // Adjust index to make sure items are pasted in the correct position.
+ // If index is DEFAULT_INDEX, items are just appended.
+ if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
+ insertionIndex = ip.index + i;
+
+ // If this is not a copy, check for safety that we can move the source,
+ // otherwise report an error and fallback to a copy.
+ if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
+ Components.utils.reportError("Tried to move an unmovable Places " +
+ "node, reverting to a copy operation.");
+ action = "copy";
+ }
+ transactions.push(
+ PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
+ insertionIndex, action == "copy")
+ );
+ }
+
+ let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
+ PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
+
+ for (let i = 0; i < transactions.length; ++i) {
+ itemsToSelect.push(
+ PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
+ );
+ }
+ }
+
+ // Cut/past operations are not repeatable, so clear the clipboard.
+ if (action == "cut") {
+ this._clearClipboard();
+ }
+
+ if (itemsToSelect.length > 0)
+ this._view.selectItems(itemsToSelect, false);
+ }),
+
+ /**
+ * Cache the livemark info for a node. This allows the controller and the
+ * views to treat the given node as a livemark.
+ * @param aNode
+ * a places result node.
+ * @param aLivemarkInfo
+ * a mozILivemarkInfo object.
+ */
+ cacheLivemarkInfo: function PC_cacheLivemarkInfo(aNode, aLivemarkInfo) {
+ this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
+ },
+
+ /**
+ * Returns whether or not there's cached mozILivemarkInfo object for a node.
+ * @param aNode
+ * a places result node.
+ * @return true if there's a cached mozILivemarkInfo object for
+ * aNode, false otherwise.
+ */
+ hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode) {
+ return this._cachedLivemarkInfoObjects.has(aNode);
+ },
+
+ /**
+ * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
+ * null otherwise.
+ * @param aNode
+ * a places result node.
+ * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
+ */
+ getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode) {
+ return this._cachedLivemarkInfoObjects.get(aNode, null);
+ }
+};
+
+/**
+ * Handles drag and drop operations for views. Note that this is view agnostic!
+ * You should not use PlacesController._view within these methods, since
+ * the view that the item(s) have been dropped on was not necessarily active.
+ * Drop functions are passed the view that is being dropped on.
+ */
+var PlacesControllerDragHelper = {
+ /**
+ * DOM Element currently being dragged over
+ */
+ currentDropTarget: null,
+
+ /**
+ * Determines if the mouse is currently being dragged over a child node of
+ * this menu. This is necessary so that the menu doesn't close while the
+ * mouse is dragging over one of its submenus
+ * @param node
+ * The container node
+ * @return true if the user is dragging over a node within the hierarchy of
+ * the container, false otherwise.
+ */
+ draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
+ let currentNode = this.currentDropTarget;
+ while (currentNode) {
+ if (currentNode == node)
+ return true;
+ currentNode = currentNode.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * @return The current active drag session. Returns null if there is none.
+ */
+ getSession: function PCDH__getSession() {
+ return this.dragService.getCurrentSession();
+ },
+
+ /**
+ * Extract the first accepted flavor from a list of flavors.
+ * @param aFlavors
+ * The flavors list of type DOMStringList.
+ */
+ getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) {
+ for (let i = 0; i < aFlavors.length; i++) {
+ if (PlacesUIUtils.SUPPORTED_FLAVORS.includes(aFlavors[i]))
+ return aFlavors[i];
+ }
+
+ // If no supported flavor is found, check if data includes text/plain
+ // contents. If so, request them as text/unicode, a conversion will happen
+ // automatically.
+ if (aFlavors.contains("text/plain")) {
+ return PlacesUtils.TYPE_UNICODE;
+ }
+
+ return null;
+ },
+
+ /**
+ * Determines whether or not the data currently being dragged can be dropped
+ * on a places view.
+ * @param ip
+ * The insertion point where the items should be dropped.
+ */
+ canDrop: function PCDH_canDrop(ip, dt) {
+ let dropCount = dt.mozItemCount;
+
+ // Check every dragged item.
+ for (let i = 0; i < dropCount; i++) {
+ let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
+ if (!flavor)
+ return false;
+
+ // Urls can be dropped on any insertionpoint.
+ // XXXmano: remember that this method is called for each dragover event!
+ // Thus we shouldn't use unwrapNodes here at all if possible.
+ // I think it would be OK to accept bogus data here (e.g. text which was
+ // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
+ // will just case the actual drop to be a no-op), and only rule out valid
+ // expected cases, which are either unsupported flavors, or items which
+ // cannot be dropped in the current insertionpoint. The last case will
+ // likely force us to use unwrapNodes for the private data types of
+ // places.
+ if (flavor == TAB_DROP_TYPE)
+ continue;
+
+ let data = dt.mozGetDataAt(flavor, i);
+ let dragged;
+ try {
+ dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
+ }
+ catch (e) {
+ return false;
+ }
+
+ // Only bookmarks and urls can be dropped into tag containers.
+ if (ip.isTag &&
+ dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
+ (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
+ (dragged.uri && dragged.uri.startsWith("place:")) ))
+ return false;
+
+ // The following loop disallows the dropping of a folder on itself or
+ // on any of its descendants.
+ if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
+ (dragged.uri && dragged.uri.startsWith("place:")) ) {
+ let parentId = ip.itemId;
+ while (parentId != PlacesUtils.placesRootId) {
+ if (dragged.concreteId == parentId || dragged.id == parentId)
+ return false;
+ parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
+ }
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Determines if an unwrapped node can be moved.
+ *
+ * @param aUnwrappedNode
+ * A node unwrapped by PlacesUtils.unwrapNodes().
+ * @return True if the node can be moved, false otherwise.
+ */
+ canMoveUnwrappedNode: function (aUnwrappedNode) {
+ return aUnwrappedNode.id > 0 &&
+ !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
+ (!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
+ aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
+ aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
+ },
+
+ /**
+ * Determines if a node can be moved.
+ *
+ * @param aNode
+ * A nsINavHistoryResultNode node.
+ * @param [optional] aDOMNode
+ * A XUL DOM node.
+ * @return True if the node can be moved, false otherwise.
+ */
+ canMoveNode(aNode, aDOMNode) {
+ // Only bookmark items are movable.
+ if (aNode.itemId == -1)
+ return false;
+
+ let parentNode = aNode.parent;
+ if (!parentNode) {
+ // Normally parentless places nodes can not be moved,
+ // but simulated bookmarked URI nodes are special.
+ return !!aDOMNode &&
+ aDOMNode.hasAttribute("simulated-places-node") &&
+ PlacesUtils.nodeIsBookmark(aNode);
+ }
+
+ // Once tags and bookmarked are divorced, the tag-query check should be
+ // removed.
+ return !(PlacesUtils.nodeIsFolder(parentNode) &&
+ PlacesUIUtils.isContentsReadOnly(parentNode)) &&
+ !PlacesUtils.nodeIsTagQuery(parentNode);
+ },
+
+ /**
+ * Handles the drop of one or more items onto a view.
+ * @param insertionPoint
+ * The insertion point where the items should be dropped
+ */
+ onDrop: Task.async(function* (insertionPoint, dt) {
+ let doCopy = ["copy", "link"].includes(dt.dropEffect);
+
+ let transactions = [];
+ let dropCount = dt.mozItemCount;
+ let movedCount = 0;
+ let parentGuid = PlacesUIUtils.useAsyncTransactions ?
+ (yield insertionPoint.promiseGuid()) : null;
+ let tagName = insertionPoint.tagName;
+ for (let i = 0; i < dropCount; ++i) {
+ let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
+ if (!flavor)
+ return;
+
+ let data = dt.mozGetDataAt(flavor, i);
+ let unwrapped;
+ if (flavor != TAB_DROP_TYPE) {
+ // There's only ever one in the D&D case.
+ unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
+ }
+ else if (data instanceof XULElement && data.localName == "tab" &&
+ data.ownerGlobal instanceof ChromeWindow) {
+ let uri = data.linkedBrowser.currentURI;
+ let spec = uri ? uri.spec : "about:blank";
+ unwrapped = { uri: spec,
+ title: data.label,
+ type: PlacesUtils.TYPE_X_MOZ_URL};
+ }
+ else
+ throw new Error("bogus data was passed as a tab");
+
+ let index = insertionPoint.index;
+
+ // Adjust insertion index to prevent reversal of dragged items. When you
+ // drag multiple elts upward: need to increment index or each successive
+ // elt will be inserted at the same index, each above the previous.
+ let dragginUp = insertionPoint.itemId == unwrapped.parent &&
+ index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
+ if (index != -1 && dragginUp)
+ index+= movedCount++;
+
+ // If dragging over a tag container we should tag the item.
+ if (insertionPoint.isTag) {
+ let uri = NetUtil.newURI(unwrapped.uri);
+ let tagItemId = insertionPoint.itemId;
+ if (PlacesUIUtils.useAsyncTransactions)
+ transactions.push(PlacesTransactions.Tag({ uri: uri, tag: tagName }));
+ else
+ transactions.push(new PlacesTagURITransaction(uri, [tagItemId]));
+ }
+ else {
+ // If this is not a copy, check for safety that we can move the source,
+ // otherwise report an error and fallback to a copy.
+ if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) {
+ Components.utils.reportError("Tried to move an unmovable Places " +
+ "node, reverting to a copy operation.");
+ doCopy = true;
+ }
+ if (PlacesUIUtils.useAsyncTransactions) {
+ transactions.push(
+ PlacesUIUtils.getTransactionForData(unwrapped,
+ flavor,
+ parentGuid,
+ index,
+ doCopy));
+ }
+ else {
+ transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
+ flavor, insertionPoint.itemId,
+ index, doCopy));
+ }
+ }
+ }
+
+ if (PlacesUIUtils.useAsyncTransactions) {
+ yield PlacesTransactions.batch(transactions);
+ }
+ else {
+ let txn = new PlacesAggregatedTransaction("DropItems", transactions);
+ PlacesUtils.transactionManager.doTransaction(txn);
+ }
+ }),
+
+ /**
+ * Checks if we can insert into a container.
+ * @param aContainer
+ * The container were we are want to drop
+ */
+ disallowInsertion: function(aContainer) {
+ NS_ASSERT(aContainer, "empty container");
+ // Allow dropping into Tag containers and editable folders.
+ return !PlacesUtils.nodeIsTagQuery(aContainer) &&
+ (!PlacesUtils.nodeIsFolder(aContainer) ||
+ PlacesUIUtils.isContentsReadOnly(aContainer));
+ }
+};
+
+
+XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
+ "@mozilla.org/widget/dragservice;1",
+ "nsIDragService");
+
+function goUpdatePlacesCommands() {
+ // Get the controller for one of the places commands.
+ var placesController = doGetPlacesControllerForCommand("placesCmd_open");
+ function updatePlacesCommand(aCommand) {
+ goSetCommandEnabled(aCommand, placesController &&
+ placesController.isCommandEnabled(aCommand));
+ }
+
+ updatePlacesCommand("placesCmd_open");
+ updatePlacesCommand("placesCmd_open:window");
+ updatePlacesCommand("placesCmd_open:privatewindow");
+ updatePlacesCommand("placesCmd_open:tab");
+ updatePlacesCommand("placesCmd_new:folder");
+ updatePlacesCommand("placesCmd_new:bookmark");
+ updatePlacesCommand("placesCmd_new:separator");
+ updatePlacesCommand("placesCmd_show:info");
+ updatePlacesCommand("placesCmd_moveBookmarks");
+ updatePlacesCommand("placesCmd_reload");
+ updatePlacesCommand("placesCmd_sortBy:name");
+ updatePlacesCommand("placesCmd_cut");
+ updatePlacesCommand("placesCmd_copy");
+ updatePlacesCommand("placesCmd_paste");
+ updatePlacesCommand("placesCmd_delete");
+}
+
+function doGetPlacesControllerForCommand(aCommand)
+{
+ // A context menu may be built for non-focusable views. Thus, we first try
+ // to look for a view associated with document.popupNode
+ let popupNode;
+ try {
+ popupNode = document.popupNode;
+ } catch (e) {
+ // The document went away (bug 797307).
+ return null;
+ }
+ if (popupNode) {
+ let view = PlacesUIUtils.getViewForNode(popupNode);
+ if (view && view._contextMenuShown)
+ return view.controllers.getControllerForCommand(aCommand);
+ }
+
+ // When we're not building a context menu, only focusable views
+ // are possible. Thus, we can safely use the command dispatcher.
+ let controller = top.document.commandDispatcher
+ .getControllerForCommand(aCommand);
+ if (controller)
+ return controller;
+
+ return null;
+}
+
+function goDoPlacesCommand(aCommand)
+{
+ let controller = doGetPlacesControllerForCommand(aCommand);
+ if (controller && controller.isCommandEnabled(aCommand))
+ controller.doCommand(aCommand);
+}