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