1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/places/content/controller.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1725 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite", 1.10 + "resource://gre/modules/ForgetAboutSite.jsm"); 1.11 +XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", 1.12 + "resource://gre/modules/NetUtil.jsm"); 1.13 + 1.14 +// XXXmano: we should move most/all of these constants to PlacesUtils 1.15 +const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1"; 1.16 + 1.17 +// No change to the view, preserve current selection 1.18 +const RELOAD_ACTION_NOTHING = 0; 1.19 +// Inserting items new to the view, select the inserted rows 1.20 +const RELOAD_ACTION_INSERT = 1; 1.21 +// Removing items from the view, select the first item after the last selected 1.22 +const RELOAD_ACTION_REMOVE = 2; 1.23 +// Moving items within a view, don't treat the dropped items as additional 1.24 +// rows. 1.25 +const RELOAD_ACTION_MOVE = 3; 1.26 + 1.27 +// When removing a bunch of pages we split them in chunks to give some breath 1.28 +// to the main-thread. 1.29 +const REMOVE_PAGES_CHUNKLEN = 300; 1.30 + 1.31 +/** 1.32 + * Represents an insertion point within a container where we can insert 1.33 + * items. 1.34 + * @param aItemId 1.35 + * The identifier of the parent container 1.36 + * @param aIndex 1.37 + * The index within the container where we should insert 1.38 + * @param aOrientation 1.39 + * The orientation of the insertion. NOTE: the adjustments to the 1.40 + * insertion point to accommodate the orientation should be done by 1.41 + * the person who constructs the IP, not the user. The orientation 1.42 + * is provided for informational purposes only! 1.43 + * @param [optional] aIsTag 1.44 + * Indicates if parent container is a tag 1.45 + * @param [optional] aDropNearItemId 1.46 + * When defined we will calculate index based on this itemId 1.47 + * @constructor 1.48 + */ 1.49 +function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag, 1.50 + aDropNearItemId) { 1.51 + this.itemId = aItemId; 1.52 + this._index = aIndex; 1.53 + this.orientation = aOrientation; 1.54 + this.isTag = aIsTag; 1.55 + this.dropNearItemId = aDropNearItemId; 1.56 +} 1.57 + 1.58 +InsertionPoint.prototype = { 1.59 + set index(val) { 1.60 + return this._index = val; 1.61 + }, 1.62 + 1.63 + promiseGUID: function () PlacesUtils.promiseItemGUID(this.itemId), 1.64 + 1.65 + get index() { 1.66 + if (this.dropNearItemId > 0) { 1.67 + // If dropNearItemId is set up we must calculate the real index of 1.68 + // the item near which we will drop. 1.69 + var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId); 1.70 + return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1; 1.71 + } 1.72 + return this._index; 1.73 + } 1.74 +}; 1.75 + 1.76 +/** 1.77 + * Places Controller 1.78 + */ 1.79 + 1.80 +function PlacesController(aView) { 1.81 + this._view = aView; 1.82 + XPCOMUtils.defineLazyServiceGetter(this, "clipboard", 1.83 + "@mozilla.org/widget/clipboard;1", 1.84 + "nsIClipboard"); 1.85 + XPCOMUtils.defineLazyGetter(this, "profileName", function () { 1.86 + return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName; 1.87 + }); 1.88 + 1.89 + this._cachedLivemarkInfoObjects = new Map(); 1.90 +} 1.91 + 1.92 +PlacesController.prototype = { 1.93 + /** 1.94 + * The places view. 1.95 + */ 1.96 + _view: null, 1.97 + 1.98 + QueryInterface: XPCOMUtils.generateQI([ 1.99 + Ci.nsIClipboardOwner 1.100 + ]), 1.101 + 1.102 + // nsIClipboardOwner 1.103 + LosingOwnership: function PC_LosingOwnership (aXferable) { 1.104 + this.cutNodes = []; 1.105 + }, 1.106 + 1.107 + terminate: function PC_terminate() { 1.108 + this._releaseClipboardOwnership(); 1.109 + }, 1.110 + 1.111 + supportsCommand: function PC_supportsCommand(aCommand) { 1.112 + // Non-Places specific commands that we also support 1.113 + switch (aCommand) { 1.114 + case "cmd_undo": 1.115 + case "cmd_redo": 1.116 + case "cmd_cut": 1.117 + case "cmd_copy": 1.118 + case "cmd_paste": 1.119 + case "cmd_delete": 1.120 + case "cmd_selectAll": 1.121 + return true; 1.122 + } 1.123 + 1.124 + // All other Places Commands are prefixed with "placesCmd_" ... this 1.125 + // filters out other commands that we do _not_ support (see 329587). 1.126 + const CMD_PREFIX = "placesCmd_"; 1.127 + return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX); 1.128 + }, 1.129 + 1.130 + isCommandEnabled: function PC_isCommandEnabled(aCommand) { 1.131 + if (PlacesUIUtils.useAsyncTransactions) { 1.132 + switch (aCommand) { 1.133 + case "cmd_cut": 1.134 + case "placesCmd_cut": 1.135 + case "cmd_copy": 1.136 + case "cmd_paste": 1.137 + case "cmd_delete": 1.138 + case "placesCmd_delete": 1.139 + case "cmd_paste": 1.140 + case "placesCmd_paste": 1.141 + case "placesCmd_new:folder": 1.142 + case "placesCmd_new:bookmark": 1.143 + case "placesCmd_createBookmark": 1.144 + return false; 1.145 + } 1.146 + } 1.147 + 1.148 + switch (aCommand) { 1.149 + case "cmd_undo": 1.150 + if (!PlacesUIUtils.useAsyncTransactions) 1.151 + return PlacesUtils.transactionManager.numberOfUndoItems > 0; 1.152 + 1.153 + return PlacesTransactions.topUndoEntry != null; 1.154 + case "cmd_redo": 1.155 + if (!PlacesUIUtils.useAsyncTransactions) 1.156 + return PlacesUtils.transactionManager.numberOfRedoItems > 0; 1.157 + 1.158 + return PlacesTransactions.topRedoEntry != null; 1.159 + case "cmd_cut": 1.160 + case "placesCmd_cut": 1.161 + var nodes = this._view.selectedNodes; 1.162 + // If selection includes history nodes there's no reason to allow cut. 1.163 + for (var i = 0; i < nodes.length; i++) { 1.164 + if (nodes[i].itemId == -1) 1.165 + return false; 1.166 + } 1.167 + // Otherwise fallback to cmd_delete check. 1.168 + case "cmd_delete": 1.169 + case "placesCmd_delete": 1.170 + case "placesCmd_deleteDataHost": 1.171 + return this._hasRemovableSelection(false); 1.172 + case "placesCmd_moveBookmarks": 1.173 + return this._hasRemovableSelection(true); 1.174 + case "cmd_copy": 1.175 + case "placesCmd_copy": 1.176 + return this._view.hasSelection; 1.177 + case "cmd_paste": 1.178 + case "placesCmd_paste": 1.179 + return this._canInsert(true) && this._isClipboardDataPasteable(); 1.180 + case "cmd_selectAll": 1.181 + if (this._view.selType != "single") { 1.182 + let rootNode = this._view.result.root; 1.183 + if (rootNode.containerOpen && rootNode.childCount > 0) 1.184 + return true; 1.185 + } 1.186 + return false; 1.187 + case "placesCmd_open": 1.188 + case "placesCmd_open:window": 1.189 + case "placesCmd_open:tab": 1.190 + var selectedNode = this._view.selectedNode; 1.191 + return selectedNode && PlacesUtils.nodeIsURI(selectedNode); 1.192 + case "placesCmd_new:folder": 1.193 + return this._canInsert(); 1.194 + case "placesCmd_new:bookmark": 1.195 + return this._canInsert(); 1.196 + case "placesCmd_new:separator": 1.197 + return this._canInsert() && 1.198 + !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems && 1.199 + this._view.result.sortingMode == 1.200 + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; 1.201 + case "placesCmd_show:info": 1.202 + var selectedNode = this._view.selectedNode; 1.203 + return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1 1.204 + case "placesCmd_reload": 1.205 + // Livemark containers 1.206 + var selectedNode = this._view.selectedNode; 1.207 + return selectedNode && this.hasCachedLivemarkInfo(selectedNode); 1.208 + case "placesCmd_sortBy:name": 1.209 + var selectedNode = this._view.selectedNode; 1.210 + return selectedNode && 1.211 + PlacesUtils.nodeIsFolder(selectedNode) && 1.212 + !PlacesUtils.nodeIsReadOnly(selectedNode) && 1.213 + this._view.result.sortingMode == 1.214 + Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; 1.215 + case "placesCmd_createBookmark": 1.216 + var node = this._view.selectedNode; 1.217 + return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1; 1.218 + default: 1.219 + return false; 1.220 + } 1.221 + }, 1.222 + 1.223 + doCommand: function PC_doCommand(aCommand) { 1.224 + switch (aCommand) { 1.225 + case "cmd_undo": 1.226 + if (!PlacesUIUtils.useAsyncTransactions) { 1.227 + PlacesUtils.transactionManager.undoTransaction(); 1.228 + return; 1.229 + } 1.230 + PlacesTransactions.undo().then(null, Components.utils.reportError); 1.231 + break; 1.232 + case "cmd_redo": 1.233 + if (!PlacesUIUtils.useAsyncTransactions) { 1.234 + PlacesUtils.transactionManager.redoTransaction(); 1.235 + return; 1.236 + } 1.237 + PlacesTransactions.redo().then(null, Components.utils.reportError); 1.238 + break; 1.239 + case "cmd_cut": 1.240 + case "placesCmd_cut": 1.241 + this.cut(); 1.242 + break; 1.243 + case "cmd_copy": 1.244 + case "placesCmd_copy": 1.245 + this.copy(); 1.246 + break; 1.247 + case "cmd_paste": 1.248 + case "placesCmd_paste": 1.249 + this.paste(); 1.250 + break; 1.251 + case "cmd_delete": 1.252 + case "placesCmd_delete": 1.253 + this.remove("Remove Selection"); 1.254 + break; 1.255 + case "placesCmd_deleteDataHost": 1.256 + var host; 1.257 + if (PlacesUtils.nodeIsHost(this._view.selectedNode)) { 1.258 + var queries = this._view.selectedNode.getQueries(); 1.259 + host = queries[0].domain; 1.260 + } 1.261 + else 1.262 + host = NetUtil.newURI(this._view.selectedNode.uri).host; 1.263 + ForgetAboutSite.removeDataFromDomain(host); 1.264 + break; 1.265 + case "cmd_selectAll": 1.266 + this.selectAll(); 1.267 + break; 1.268 + case "placesCmd_open": 1.269 + PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view); 1.270 + break; 1.271 + case "placesCmd_open:window": 1.272 + PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view); 1.273 + break; 1.274 + case "placesCmd_open:tab": 1.275 + PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view); 1.276 + break; 1.277 + case "placesCmd_new:folder": 1.278 + this.newItem("folder"); 1.279 + break; 1.280 + case "placesCmd_new:bookmark": 1.281 + this.newItem("bookmark"); 1.282 + break; 1.283 + case "placesCmd_new:separator": 1.284 + this.newSeparator().then(null, Components.utils.reportError); 1.285 + break; 1.286 + case "placesCmd_show:info": 1.287 + this.showBookmarkPropertiesForSelection(); 1.288 + break; 1.289 + case "placesCmd_moveBookmarks": 1.290 + this.moveSelectedBookmarks(); 1.291 + break; 1.292 + case "placesCmd_reload": 1.293 + this.reloadSelectedLivemark(); 1.294 + break; 1.295 + case "placesCmd_sortBy:name": 1.296 + this.sortFolderByName().then(null, Components.utils.reportError); 1.297 + break; 1.298 + case "placesCmd_createBookmark": 1.299 + let node = this._view.selectedNode; 1.300 + PlacesUIUtils.showBookmarkDialog({ action: "add" 1.301 + , type: "bookmark" 1.302 + , hiddenRows: [ "description" 1.303 + , "keyword" 1.304 + , "location" 1.305 + , "loadInSidebar" ] 1.306 + , uri: NetUtil.newURI(node.uri) 1.307 + , title: node.title 1.308 + }, window.top); 1.309 + break; 1.310 + } 1.311 + }, 1.312 + 1.313 + onEvent: function PC_onEvent(eventName) { }, 1.314 + 1.315 + 1.316 + /** 1.317 + * Determine whether or not the selection can be removed, either by the 1.318 + * delete or cut operations based on whether or not any of its contents 1.319 + * are non-removable. We don't need to worry about recursion here since it 1.320 + * is a policy decision that a removable item not be placed inside a non- 1.321 + * removable item. 1.322 + * @param aIsMoveCommand 1.323 + * True if the command for which this method is called only moves the 1.324 + * selected items to another container, false otherwise. 1.325 + * @return true if all nodes in the selection can be removed, 1.326 + * false otherwise. 1.327 + */ 1.328 + _hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) { 1.329 + var ranges = this._view.removableSelectionRanges; 1.330 + if (!ranges.length) 1.331 + return false; 1.332 + 1.333 + var root = this._view.result.root; 1.334 + 1.335 + for (var j = 0; j < ranges.length; j++) { 1.336 + var nodes = ranges[j]; 1.337 + for (var i = 0; i < nodes.length; ++i) { 1.338 + // Disallow removing the view's root node 1.339 + if (nodes[i] == root) 1.340 + return false; 1.341 + 1.342 + if (PlacesUtils.nodeIsFolder(nodes[i]) && 1.343 + !PlacesControllerDragHelper.canMoveNode(nodes[i])) 1.344 + return false; 1.345 + 1.346 + // We don't call nodeIsReadOnly here, because nodeIsReadOnly means that 1.347 + // a node has children that cannot be edited, reordered or removed. Here, 1.348 + // we don't care if a node's children can't be reordered or edited, just 1.349 + // that they're removable. All history results have removable children 1.350 + // (based on the principle that any URL in the history table should be 1.351 + // removable), but some special bookmark folders may have non-removable 1.352 + // children, e.g. live bookmark folder children. It doesn't make sense 1.353 + // to delete a child of a live bookmark folder, since when the folder 1.354 + // refreshes, the child will return. 1.355 + var parent = nodes[i].parent || root; 1.356 + if (PlacesUtils.isReadonlyFolder(parent)) 1.357 + return false; 1.358 + } 1.359 + } 1.360 + 1.361 + return true; 1.362 + }, 1.363 + 1.364 + /** 1.365 + * Determines whether or not nodes can be inserted relative to the selection. 1.366 + */ 1.367 + _canInsert: function PC__canInsert(isPaste) { 1.368 + var ip = this._view.insertionPoint; 1.369 + return ip != null && (isPaste || ip.isTag != true); 1.370 + }, 1.371 + 1.372 + /** 1.373 + * Determines whether or not the root node for the view is selected 1.374 + */ 1.375 + rootNodeIsSelected: function PC_rootNodeIsSelected() { 1.376 + var nodes = this._view.selectedNodes; 1.377 + var root = this._view.result.root; 1.378 + for (var i = 0; i < nodes.length; ++i) { 1.379 + if (nodes[i] == root) 1.380 + return true; 1.381 + } 1.382 + 1.383 + return false; 1.384 + }, 1.385 + 1.386 + /** 1.387 + * Looks at the data on the clipboard to see if it is paste-able. 1.388 + * Paste-able data is: 1.389 + * - in a format that the view can receive 1.390 + * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor, 1.391 + * - clipboard data is of type TEXT_UNICODE and 1.392 + * is a valid URI. 1.393 + */ 1.394 + _isClipboardDataPasteable: function PC__isClipboardDataPasteable() { 1.395 + // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely 1.396 + // pasteable, with no need to unwrap all the nodes. 1.397 + 1.398 + var flavors = PlacesControllerDragHelper.placesFlavors; 1.399 + var clipboard = this.clipboard; 1.400 + var hasPlacesData = 1.401 + clipboard.hasDataMatchingFlavors(flavors, flavors.length, 1.402 + Ci.nsIClipboard.kGlobalClipboard); 1.403 + if (hasPlacesData) 1.404 + return this._view.insertionPoint != null; 1.405 + 1.406 + // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow 1.407 + // pasting of valid "text/unicode" and "text/x-moz-url" data 1.408 + var xferable = Cc["@mozilla.org/widget/transferable;1"]. 1.409 + createInstance(Ci.nsITransferable); 1.410 + xferable.init(null); 1.411 + 1.412 + xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL); 1.413 + xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE); 1.414 + clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); 1.415 + 1.416 + try { 1.417 + // getAnyTransferData will throw if no data is available. 1.418 + var data = { }, type = { }; 1.419 + xferable.getAnyTransferData(type, data, { }); 1.420 + data = data.value.QueryInterface(Ci.nsISupportsString).data; 1.421 + if (type.value != PlacesUtils.TYPE_X_MOZ_URL && 1.422 + type.value != PlacesUtils.TYPE_UNICODE) 1.423 + return false; 1.424 + 1.425 + // unwrapNodes() will throw if the data blob is malformed. 1.426 + var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value); 1.427 + return this._view.insertionPoint != null; 1.428 + } 1.429 + catch (e) { 1.430 + // getAnyTransferData or unwrapNodes failed 1.431 + return false; 1.432 + } 1.433 + }, 1.434 + 1.435 + /** 1.436 + * Gathers information about the selected nodes according to the following 1.437 + * rules: 1.438 + * "link" node is a URI 1.439 + * "bookmark" node is a bookamrk 1.440 + * "livemarkChild" node is a child of a livemark 1.441 + * "tagChild" node is a child of a tag 1.442 + * "folder" node is a folder 1.443 + * "query" node is a query 1.444 + * "separator" node is a separator line 1.445 + * "host" node is a host 1.446 + * 1.447 + * @return an array of objects corresponding the selected nodes. Each 1.448 + * object has each of the properties above set if its corresponding 1.449 + * node matches the rule. In addition, the annotations names for each 1.450 + * node are set on its corresponding object as properties. 1.451 + * Notes: 1.452 + * 1) This can be slow, so don't call it anywhere performance critical! 1.453 + * 2) A single-object array corresponding the root node is returned if 1.454 + * there's no selection. 1.455 + */ 1.456 + _buildSelectionMetadata: function PC__buildSelectionMetadata() { 1.457 + var metadata = []; 1.458 + var root = this._view.result.root; 1.459 + var nodes = this._view.selectedNodes; 1.460 + if (nodes.length == 0) 1.461 + nodes.push(root); // See the second note above 1.462 + 1.463 + for (var i = 0; i < nodes.length; i++) { 1.464 + var nodeData = {}; 1.465 + var node = nodes[i]; 1.466 + var nodeType = node.type; 1.467 + var uri = null; 1.468 + 1.469 + // We don't use the nodeIs* methods here to avoid going through the type 1.470 + // property way too often 1.471 + switch (nodeType) { 1.472 + case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY: 1.473 + nodeData["query"] = true; 1.474 + if (node.parent) { 1.475 + switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) { 1.476 + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY: 1.477 + nodeData["host"] = true; 1.478 + break; 1.479 + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY: 1.480 + case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY: 1.481 + nodeData["day"] = true; 1.482 + break; 1.483 + } 1.484 + } 1.485 + break; 1.486 + case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER: 1.487 + case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT: 1.488 + nodeData["folder"] = true; 1.489 + break; 1.490 + case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR: 1.491 + nodeData["separator"] = true; 1.492 + break; 1.493 + case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI: 1.494 + nodeData["link"] = true; 1.495 + uri = NetUtil.newURI(node.uri); 1.496 + if (PlacesUtils.nodeIsBookmark(node)) { 1.497 + nodeData["bookmark"] = true; 1.498 + PlacesUtils.nodeIsTagQuery(node.parent) 1.499 + 1.500 + var parentNode = node.parent; 1.501 + if (parentNode) { 1.502 + if (PlacesUtils.nodeIsTagQuery(parentNode)) 1.503 + nodeData["tagChild"] = true; 1.504 + else if (this.hasCachedLivemarkInfo(parentNode)) 1.505 + nodeData["livemarkChild"] = true; 1.506 + } 1.507 + } 1.508 + break; 1.509 + } 1.510 + 1.511 + // annotations 1.512 + if (uri) { 1.513 + let names = PlacesUtils.annotations.getPageAnnotationNames(uri); 1.514 + for (let j = 0; j < names.length; ++j) 1.515 + nodeData[names[j]] = true; 1.516 + } 1.517 + 1.518 + // For items also include the item-specific annotations 1.519 + if (node.itemId != -1) { 1.520 + let names = PlacesUtils.annotations 1.521 + .getItemAnnotationNames(node.itemId); 1.522 + for (let j = 0; j < names.length; ++j) 1.523 + nodeData[names[j]] = true; 1.524 + } 1.525 + metadata.push(nodeData); 1.526 + } 1.527 + 1.528 + return metadata; 1.529 + }, 1.530 + 1.531 + /** 1.532 + * Determines if a context-menu item should be shown 1.533 + * @param aMenuItem 1.534 + * the context menu item 1.535 + * @param aMetaData 1.536 + * meta data about the selection 1.537 + * @return true if the conditions (see buildContextMenu) are satisfied 1.538 + * and the item can be displayed, false otherwise. 1.539 + */ 1.540 + _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) { 1.541 + var selectiontype = aMenuItem.getAttribute("selectiontype"); 1.542 + if (selectiontype == "multiple" && aMetaData.length == 1) 1.543 + return false; 1.544 + if (selectiontype == "single" && aMetaData.length != 1) 1.545 + return false; 1.546 + 1.547 + var forceHideAttr = aMenuItem.getAttribute("forcehideselection"); 1.548 + if (forceHideAttr) { 1.549 + var forceHideRules = forceHideAttr.split("|"); 1.550 + for (let i = 0; i < aMetaData.length; ++i) { 1.551 + for (let j = 0; j < forceHideRules.length; ++j) { 1.552 + if (forceHideRules[j] in aMetaData[i]) 1.553 + return false; 1.554 + } 1.555 + } 1.556 + } 1.557 + 1.558 + var selectionAttr = aMenuItem.getAttribute("selection"); 1.559 + if (!selectionAttr) { 1.560 + return !aMenuItem.hidden; 1.561 + } 1.562 + 1.563 + if (selectionAttr == "any") 1.564 + return true; 1.565 + 1.566 + var showRules = selectionAttr.split("|"); 1.567 + var anyMatched = false; 1.568 + function metaDataNodeMatches(metaDataNode, rules) { 1.569 + for (var i = 0; i < rules.length; i++) { 1.570 + if (rules[i] in metaDataNode) 1.571 + return true; 1.572 + } 1.573 + return false; 1.574 + } 1.575 + 1.576 + for (var i = 0; i < aMetaData.length; ++i) { 1.577 + if (metaDataNodeMatches(aMetaData[i], showRules)) 1.578 + anyMatched = true; 1.579 + else 1.580 + return false; 1.581 + } 1.582 + return anyMatched; 1.583 + }, 1.584 + 1.585 + /** 1.586 + * Detects information (meta-data rules) about the current selection in the 1.587 + * view (see _buildSelectionMetadata) and sets the visibility state for each 1.588 + * of the menu-items in the given popup with the following rules applied: 1.589 + * 1) The "selectiontype" attribute may be set on a menu-item to "single" 1.590 + * if the menu-item should be visible only if there is a single node 1.591 + * selected, or to "multiple" if the menu-item should be visible only if 1.592 + * multiple nodes are selected. If the attribute is not set or if it is 1.593 + * set to an invalid value, the menu-item may be visible for both types of 1.594 + * selection. 1.595 + * 2) The "selection" attribute may be set on a menu-item to the various 1.596 + * meta-data rules for which it may be visible. The rules should be 1.597 + * separated with the | character. 1.598 + * 3) A menu-item may be visible only if at least one of the rules set in 1.599 + * its selection attribute apply to each of the selected nodes in the 1.600 + * view. 1.601 + * 4) The "forcehideselection" attribute may be set on a menu-item to rules 1.602 + * for which it should be hidden. This attribute takes priority over the 1.603 + * selection attribute. A menu-item would be hidden if at least one of the 1.604 + * given rules apply to one of the selected nodes. The rules should be 1.605 + * separated with the | character. 1.606 + * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to 1.607 + * true if it should be hidden when there's no insertion point 1.608 + * 6) The visibility state of a menu-item is unchanged if none of these 1.609 + * attribute are set. 1.610 + * 7) These attributes should not be set on separators for which the 1.611 + * visibility state is "auto-detected." 1.612 + * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to 1.613 + * true if it should be hidden inside the private browsing mode 1.614 + * @param aPopup 1.615 + * The menupopup to build children into. 1.616 + * @return true if at least one item is visible, false otherwise. 1.617 + */ 1.618 + buildContextMenu: function PC_buildContextMenu(aPopup) { 1.619 + var metadata = this._buildSelectionMetadata(); 1.620 + var ip = this._view.insertionPoint; 1.621 + var noIp = !ip || ip.isTag; 1.622 + 1.623 + var separator = null; 1.624 + var visibleItemsBeforeSep = false; 1.625 + var anyVisible = false; 1.626 + for (var i = 0; i < aPopup.childNodes.length; ++i) { 1.627 + var item = aPopup.childNodes[i]; 1.628 + if (item.localName != "menuseparator") { 1.629 + // We allow pasting into tag containers, so special case that. 1.630 + var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" && 1.631 + noIp && !(ip && ip.isTag && item.id == "placesContext_paste"); 1.632 + item.hidden = hideIfNoIP || !this._shouldShowMenuItem(item, metadata); 1.633 + 1.634 + if (!item.hidden) { 1.635 + visibleItemsBeforeSep = true; 1.636 + anyVisible = true; 1.637 + 1.638 + // Show the separator above the menu-item if any 1.639 + if (separator) { 1.640 + separator.hidden = false; 1.641 + separator = null; 1.642 + } 1.643 + } 1.644 + } 1.645 + else { // menuseparator 1.646 + // Initially hide it. It will be unhidden if there will be at least one 1.647 + // visible menu-item above and below it. 1.648 + item.hidden = true; 1.649 + 1.650 + // We won't show the separator at all if no items are visible above it 1.651 + if (visibleItemsBeforeSep) 1.652 + separator = item; 1.653 + 1.654 + // New separator, count again: 1.655 + visibleItemsBeforeSep = false; 1.656 + } 1.657 + } 1.658 + 1.659 + // Set Open Folder/Links In Tabs items enabled state if they're visible 1.660 + if (anyVisible) { 1.661 + var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs"); 1.662 + if (!openContainerInTabsItem.hidden && this._view.selectedNode && 1.663 + PlacesUtils.nodeIsContainer(this._view.selectedNode)) { 1.664 + openContainerInTabsItem.disabled = 1.665 + !PlacesUtils.hasChildURIs(this._view.selectedNode); 1.666 + } 1.667 + else { 1.668 + // see selectiontype rule in the overlay 1.669 + var openLinksInTabsItem = document.getElementById("placesContext_openLinks:tabs"); 1.670 + openLinksInTabsItem.disabled = openLinksInTabsItem.hidden; 1.671 + } 1.672 + } 1.673 + 1.674 + return anyVisible; 1.675 + }, 1.676 + 1.677 + /** 1.678 + * Select all links in the current view. 1.679 + */ 1.680 + selectAll: function PC_selectAll() { 1.681 + this._view.selectAll(); 1.682 + }, 1.683 + 1.684 + /** 1.685 + * Opens the bookmark properties for the selected URI Node. 1.686 + */ 1.687 + showBookmarkPropertiesForSelection: 1.688 + function PC_showBookmarkPropertiesForSelection() { 1.689 + var node = this._view.selectedNode; 1.690 + if (!node) 1.691 + return; 1.692 + 1.693 + var itemType = PlacesUtils.nodeIsFolder(node) || 1.694 + PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark"; 1.695 + var concreteId = PlacesUtils.getConcreteItemId(node); 1.696 + var isRootItem = PlacesUtils.isRootItem(concreteId); 1.697 + var itemId = node.itemId; 1.698 + if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) { 1.699 + // If this is a root or the Tags query we use the concrete itemId to catch 1.700 + // the correct title for the node. 1.701 + itemId = concreteId; 1.702 + } 1.703 + 1.704 + PlacesUIUtils.showBookmarkDialog({ action: "edit" 1.705 + , type: itemType 1.706 + , itemId: itemId 1.707 + , readOnly: isRootItem 1.708 + , hiddenRows: [ "folderPicker" ] 1.709 + }, window.top); 1.710 + }, 1.711 + 1.712 + /** 1.713 + * This method can be run on a URI parameter to ensure that it didn't 1.714 + * receive a string instead of an nsIURI object. 1.715 + */ 1.716 + _assertURINotString: function PC__assertURINotString(value) { 1.717 + NS_ASSERT((typeof(value) == "object") && !(value instanceof String), 1.718 + "This method should be passed a URI as a nsIURI object, not as a string."); 1.719 + }, 1.720 + 1.721 + /** 1.722 + * Reloads the selected livemark if any. 1.723 + */ 1.724 + reloadSelectedLivemark: function PC_reloadSelectedLivemark() { 1.725 + var selectedNode = this._view.selectedNode; 1.726 + if (selectedNode) { 1.727 + let itemId = selectedNode.itemId; 1.728 + PlacesUtils.livemarks.getLivemark({ id: itemId }) 1.729 + .then(aLivemark => { 1.730 + aLivemark.reload(true); 1.731 + }, Components.utils.reportError); 1.732 + } 1.733 + }, 1.734 + 1.735 + /** 1.736 + * Opens the links in the selected folder, or the selected links in new tabs. 1.737 + */ 1.738 + openSelectionInTabs: function PC_openLinksInTabs(aEvent) { 1.739 + var node = this._view.selectedNode; 1.740 + if (node && PlacesUtils.nodeIsContainer(node)) 1.741 + PlacesUIUtils.openContainerNodeInTabs(this._view.selectedNode, aEvent, this._view); 1.742 + else 1.743 + PlacesUIUtils.openURINodesInTabs(this._view.selectedNodes, aEvent, this._view); 1.744 + }, 1.745 + 1.746 + /** 1.747 + * Shows the Add Bookmark UI for the current insertion point. 1.748 + * 1.749 + * @param aType 1.750 + * the type of the new item (bookmark/livemark/folder) 1.751 + */ 1.752 + newItem: function PC_newItem(aType) { 1.753 + let ip = this._view.insertionPoint; 1.754 + if (!ip) 1.755 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.756 + 1.757 + let performed = 1.758 + PlacesUIUtils.showBookmarkDialog({ action: "add" 1.759 + , type: aType 1.760 + , defaultInsertionPoint: ip 1.761 + , hiddenRows: [ "folderPicker" ] 1.762 + }, window.top); 1.763 + if (performed) { 1.764 + // Select the new item. 1.765 + let insertedNodeId = PlacesUtils.bookmarks 1.766 + .getIdForItemAt(ip.itemId, ip.index); 1.767 + this._view.selectItems([insertedNodeId], false); 1.768 + } 1.769 + }, 1.770 + 1.771 + /** 1.772 + * Create a new Bookmark separator somewhere. 1.773 + */ 1.774 + newSeparator: Task.async(function* () { 1.775 + var ip = this._view.insertionPoint; 1.776 + if (!ip) 1.777 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.778 + 1.779 + if (!PlacesUIUtils.useAsyncTransactions) { 1.780 + let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index); 1.781 + PlacesUtils.transactionManager.doTransaction(txn); 1.782 + // Select the new item. 1.783 + let insertedNodeId = PlacesUtils.bookmarks 1.784 + .getIdForItemAt(ip.itemId, ip.index); 1.785 + this._view.selectItems([insertedNodeId], false); 1.786 + return; 1.787 + } 1.788 + 1.789 + let txn = PlacesTransactions.NewSeparator({ parentGUID: yield ip.promiseGUID() 1.790 + , index: ip.index }); 1.791 + let guid = yield PlacesTransactions.transact(txn); 1.792 + let itemId = yield PlacesUtils.promiseItemId(guid); 1.793 + // Select the new item. 1.794 + this._view.selectItems([itemId], false); 1.795 + }), 1.796 + 1.797 + /** 1.798 + * Opens a dialog for moving the selected nodes. 1.799 + */ 1.800 + moveSelectedBookmarks: function PC_moveBookmarks() { 1.801 + window.openDialog("chrome://browser/content/places/moveBookmarks.xul", 1.802 + "", "chrome, modal", 1.803 + this._view.selectedNodes); 1.804 + }, 1.805 + 1.806 + /** 1.807 + * Sort the selected folder by name 1.808 + */ 1.809 + sortFolderByName: Task.async(function* () { 1.810 + let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode); 1.811 + if (!PlacesUIUtils.useAsyncTransactions) { 1.812 + var txn = new PlacesSortFolderByNameTransaction(itemId); 1.813 + PlacesUtils.transactionManager.doTransaction(txn); 1.814 + return; 1.815 + } 1.816 + let guid = yield PlacesUtils.promiseItemGUID(itemId); 1.817 + yield PlacesTransactions.transact(PlacesTransactions.SortByName(guid)); 1.818 + }), 1.819 + 1.820 + /** 1.821 + * Walk the list of folders we're removing in this delete operation, and 1.822 + * see if the selected node specified is already implicitly being removed 1.823 + * because it is a child of that folder. 1.824 + * @param node 1.825 + * Node to check for containment. 1.826 + * @param pastFolders 1.827 + * List of folders the calling function has already traversed 1.828 + * @return true if the node should be skipped, false otherwise. 1.829 + */ 1.830 + _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) { 1.831 + /** 1.832 + * Determines if a node is contained by another node within a resultset. 1.833 + * @param node 1.834 + * The node to check for containment for 1.835 + * @param parent 1.836 + * The parent container to check for containment in 1.837 + * @return true if node is a member of parent's children, false otherwise. 1.838 + */ 1.839 + function isContainedBy(node, parent) { 1.840 + var cursor = node.parent; 1.841 + while (cursor) { 1.842 + if (cursor == parent) 1.843 + return true; 1.844 + cursor = cursor.parent; 1.845 + } 1.846 + return false; 1.847 + } 1.848 + 1.849 + for (var j = 0; j < pastFolders.length; ++j) { 1.850 + if (isContainedBy(node, pastFolders[j])) 1.851 + return true; 1.852 + } 1.853 + return false; 1.854 + }, 1.855 + 1.856 + /** 1.857 + * Creates a set of transactions for the removal of a range of items. 1.858 + * A range is an array of adjacent nodes in a view. 1.859 + * @param [in] range 1.860 + * An array of nodes to remove. Should all be adjacent. 1.861 + * @param [out] transactions 1.862 + * An array of transactions. 1.863 + * @param [optional] removedFolders 1.864 + * An array of folder nodes that have already been removed. 1.865 + */ 1.866 + _removeRange: function PC__removeRange(range, transactions, removedFolders) { 1.867 + NS_ASSERT(transactions instanceof Array, "Must pass a transactions array"); 1.868 + if (!removedFolders) 1.869 + removedFolders = []; 1.870 + 1.871 + for (var i = 0; i < range.length; ++i) { 1.872 + var node = range[i]; 1.873 + if (this._shouldSkipNode(node, removedFolders)) 1.874 + continue; 1.875 + 1.876 + if (PlacesUtils.nodeIsTagQuery(node.parent)) { 1.877 + // This is a uri node inside a tag container. It needs a special 1.878 + // untag transaction. 1.879 + var tagItemId = PlacesUtils.getConcreteItemId(node.parent); 1.880 + var uri = NetUtil.newURI(node.uri); 1.881 + let txn = new PlacesUntagURITransaction(uri, [tagItemId]); 1.882 + transactions.push(txn); 1.883 + } 1.884 + else if (PlacesUtils.nodeIsTagQuery(node) && node.parent && 1.885 + PlacesUtils.nodeIsQuery(node.parent) && 1.886 + PlacesUtils.asQuery(node.parent).queryOptions.resultType == 1.887 + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) { 1.888 + // This is a tag container. 1.889 + // Untag all URIs tagged with this tag only if the tag container is 1.890 + // child of the "Tags" query in the library, in all other places we 1.891 + // must only remove the query node. 1.892 + var tag = node.title; 1.893 + var URIs = PlacesUtils.tagging.getURIsForTag(tag); 1.894 + for (var j = 0; j < URIs.length; j++) { 1.895 + let txn = new PlacesUntagURITransaction(URIs[j], [tag]); 1.896 + transactions.push(txn); 1.897 + } 1.898 + } 1.899 + else if (PlacesUtils.nodeIsURI(node) && 1.900 + PlacesUtils.nodeIsQuery(node.parent) && 1.901 + PlacesUtils.asQuery(node.parent).queryOptions.queryType == 1.902 + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { 1.903 + // This is a uri node inside an history query. 1.904 + PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri)); 1.905 + // History deletes are not undoable, so we don't have a transaction. 1.906 + } 1.907 + else if (node.itemId == -1 && 1.908 + PlacesUtils.nodeIsQuery(node) && 1.909 + PlacesUtils.asQuery(node).queryOptions.queryType == 1.910 + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { 1.911 + // This is a dynamically generated history query, like queries 1.912 + // grouped by site, time or both. Dynamically generated queries don't 1.913 + // have an itemId even if they are descendants of a bookmark. 1.914 + this._removeHistoryContainer(node); 1.915 + // History deletes are not undoable, so we don't have a transaction. 1.916 + } 1.917 + else { 1.918 + // This is a common bookmark item. 1.919 + if (PlacesUtils.nodeIsFolder(node)) { 1.920 + // If this is a folder we add it to our array of folders, used 1.921 + // to skip nodes that are children of an already removed folder. 1.922 + removedFolders.push(node); 1.923 + } 1.924 + let txn = new PlacesRemoveItemTransaction(node.itemId); 1.925 + transactions.push(txn); 1.926 + } 1.927 + } 1.928 + }, 1.929 + 1.930 + /** 1.931 + * Removes the set of selected ranges from bookmarks. 1.932 + * @param txnName 1.933 + * See |remove|. 1.934 + */ 1.935 + _removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) { 1.936 + var ranges = this._view.removableSelectionRanges; 1.937 + var transactions = []; 1.938 + var removedFolders = []; 1.939 + 1.940 + for (var i = 0; i < ranges.length; i++) 1.941 + this._removeRange(ranges[i], transactions, removedFolders); 1.942 + 1.943 + if (transactions.length > 0) { 1.944 + var txn = new PlacesAggregatedTransaction(txnName, transactions); 1.945 + PlacesUtils.transactionManager.doTransaction(txn); 1.946 + } 1.947 + }, 1.948 + 1.949 + /** 1.950 + * Removes the set of selected ranges from history. 1.951 + * 1.952 + * @note history deletes are not undoable. 1.953 + */ 1.954 + _removeRowsFromHistory: function PC__removeRowsFromHistory() { 1.955 + let nodes = this._view.selectedNodes; 1.956 + let URIs = []; 1.957 + for (let i = 0; i < nodes.length; ++i) { 1.958 + let node = nodes[i]; 1.959 + if (PlacesUtils.nodeIsURI(node)) { 1.960 + let uri = NetUtil.newURI(node.uri); 1.961 + // Avoid duplicates. 1.962 + if (URIs.indexOf(uri) < 0) { 1.963 + URIs.push(uri); 1.964 + } 1.965 + } 1.966 + else if (PlacesUtils.nodeIsQuery(node) && 1.967 + PlacesUtils.asQuery(node).queryOptions.queryType == 1.968 + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { 1.969 + this._removeHistoryContainer(node); 1.970 + } 1.971 + } 1.972 + 1.973 + // Do removal in chunks to give some breath to main-thread. 1.974 + function pagesChunkGenerator(aURIs) { 1.975 + while (aURIs.length) { 1.976 + let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN); 1.977 + PlacesUtils.bhistory.removePages(URIslice, URIslice.length); 1.978 + Services.tm.mainThread.dispatch(function() { 1.979 + try { 1.980 + gen.next(); 1.981 + } catch (ex if ex instanceof StopIteration) {} 1.982 + }, Ci.nsIThread.DISPATCH_NORMAL); 1.983 + yield undefined; 1.984 + } 1.985 + } 1.986 + let gen = pagesChunkGenerator(URIs); 1.987 + gen.next(); 1.988 + }, 1.989 + 1.990 + /** 1.991 + * Removes history visits for an history container node. 1.992 + * @param [in] aContainerNode 1.993 + * The container node to remove. 1.994 + * 1.995 + * @note history deletes are not undoable. 1.996 + */ 1.997 + _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) { 1.998 + if (PlacesUtils.nodeIsHost(aContainerNode)) { 1.999 + // Site container. 1.1000 + PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true); 1.1001 + } 1.1002 + else if (PlacesUtils.nodeIsDay(aContainerNode)) { 1.1003 + // Day container. 1.1004 + let query = aContainerNode.getQueries()[0]; 1.1005 + let beginTime = query.beginTime; 1.1006 + let endTime = query.endTime; 1.1007 + NS_ASSERT(query && beginTime && endTime, 1.1008 + "A valid date container query should exist!"); 1.1009 + // We want to exclude beginTime from the removal because 1.1010 + // removePagesByTimeframe includes both extremes, while date containers 1.1011 + // exclude the lower extreme. So, if we would not exclude it, we would 1.1012 + // end up removing more history than requested. 1.1013 + PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime); 1.1014 + } 1.1015 + }, 1.1016 + 1.1017 + /** 1.1018 + * Removes the selection 1.1019 + * @param aTxnName 1.1020 + * A name for the transaction if this is being performed 1.1021 + * as part of another operation. 1.1022 + */ 1.1023 + remove: function PC_remove(aTxnName) { 1.1024 + if (!this._hasRemovableSelection(false)) 1.1025 + return; 1.1026 + 1.1027 + NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name"); 1.1028 + 1.1029 + var root = this._view.result.root; 1.1030 + 1.1031 + if (PlacesUtils.nodeIsFolder(root)) 1.1032 + this._removeRowsFromBookmarks(aTxnName); 1.1033 + else if (PlacesUtils.nodeIsQuery(root)) { 1.1034 + var queryType = PlacesUtils.asQuery(root).queryOptions.queryType; 1.1035 + if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS) 1.1036 + this._removeRowsFromBookmarks(aTxnName); 1.1037 + else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) 1.1038 + this._removeRowsFromHistory(); 1.1039 + else 1.1040 + NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED"); 1.1041 + } 1.1042 + else 1.1043 + NS_ASSERT(false, "unexpected root"); 1.1044 + }, 1.1045 + 1.1046 + /** 1.1047 + * Fills a DataTransfer object with the content of the selection that can be 1.1048 + * dropped elsewhere. 1.1049 + * @param aEvent 1.1050 + * The dragstart event. 1.1051 + */ 1.1052 + setDataTransfer: function PC_setDataTransfer(aEvent) { 1.1053 + let dt = aEvent.dataTransfer; 1.1054 + 1.1055 + let result = this._view.result; 1.1056 + let didSuppressNotifications = result.suppressNotifications; 1.1057 + if (!didSuppressNotifications) 1.1058 + result.suppressNotifications = true; 1.1059 + 1.1060 + function addData(type, index, overrideURI) { 1.1061 + let wrapNode = PlacesUtils.wrapNode(node, type, overrideURI); 1.1062 + dt.mozSetDataAt(type, wrapNode, index); 1.1063 + } 1.1064 + 1.1065 + function addURIData(index, overrideURI) { 1.1066 + addData(PlacesUtils.TYPE_X_MOZ_URL, index, overrideURI); 1.1067 + addData(PlacesUtils.TYPE_UNICODE, index, overrideURI); 1.1068 + addData(PlacesUtils.TYPE_HTML, index, overrideURI); 1.1069 + } 1.1070 + 1.1071 + try { 1.1072 + let nodes = this._view.draggableSelection; 1.1073 + for (let i = 0; i < nodes.length; ++i) { 1.1074 + var node = nodes[i]; 1.1075 + 1.1076 + // This order is _important_! It controls how this and other 1.1077 + // applications select data to be inserted based on type. 1.1078 + addData(PlacesUtils.TYPE_X_MOZ_PLACE, i); 1.1079 + 1.1080 + // Drop the feed uri for livemark containers 1.1081 + let livemarkInfo = this.getCachedLivemarkInfo(node); 1.1082 + if (livemarkInfo) { 1.1083 + addURIData(i, livemarkInfo.feedURI.spec); 1.1084 + } 1.1085 + else if (node.uri) { 1.1086 + addURIData(i); 1.1087 + } 1.1088 + } 1.1089 + } 1.1090 + finally { 1.1091 + if (!didSuppressNotifications) 1.1092 + result.suppressNotifications = false; 1.1093 + } 1.1094 + }, 1.1095 + 1.1096 + get clipboardAction () { 1.1097 + let action = {}; 1.1098 + let actionOwner; 1.1099 + try { 1.1100 + let xferable = Cc["@mozilla.org/widget/transferable;1"]. 1.1101 + createInstance(Ci.nsITransferable); 1.1102 + xferable.init(null); 1.1103 + xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION) 1.1104 + this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); 1.1105 + xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {}); 1.1106 + [action, actionOwner] = 1.1107 + action.value.QueryInterface(Ci.nsISupportsString).data.split(","); 1.1108 + } catch(ex) { 1.1109 + // Paste from external sources don't have any associated action, just 1.1110 + // fallback to a copy action. 1.1111 + return "copy"; 1.1112 + } 1.1113 + // For cuts also check who inited the action, since cuts across different 1.1114 + // instances should instead be handled as copies (The sources are not 1.1115 + // available for this instance). 1.1116 + if (action == "cut" && actionOwner != this.profileName) 1.1117 + action = "copy"; 1.1118 + 1.1119 + return action; 1.1120 + }, 1.1121 + 1.1122 + _releaseClipboardOwnership: function PC__releaseClipboardOwnership() { 1.1123 + if (this.cutNodes.length > 0) { 1.1124 + // This clears the logical clipboard, doesn't remove data. 1.1125 + this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard); 1.1126 + } 1.1127 + }, 1.1128 + 1.1129 + _clearClipboard: function PC__clearClipboard() { 1.1130 + let xferable = Cc["@mozilla.org/widget/transferable;1"]. 1.1131 + createInstance(Ci.nsITransferable); 1.1132 + xferable.init(null); 1.1133 + // Empty transferables may cause crashes, so just add an unknown type. 1.1134 + const TYPE = "text/x-moz-place-empty"; 1.1135 + xferable.addDataFlavor(TYPE); 1.1136 + xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0); 1.1137 + this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard); 1.1138 + }, 1.1139 + 1.1140 + _populateClipboard: function PC__populateClipboard(aNodes, aAction) { 1.1141 + // This order is _important_! It controls how this and other applications 1.1142 + // select data to be inserted based on type. 1.1143 + let contents = [ 1.1144 + { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] }, 1.1145 + { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] }, 1.1146 + { type: PlacesUtils.TYPE_HTML, entries: [] }, 1.1147 + { type: PlacesUtils.TYPE_UNICODE, entries: [] }, 1.1148 + ]; 1.1149 + 1.1150 + // Avoid handling descendants of a copied node, the transactions take care 1.1151 + // of them automatically. 1.1152 + let copiedFolders = []; 1.1153 + aNodes.forEach(function (node) { 1.1154 + if (this._shouldSkipNode(node, copiedFolders)) 1.1155 + return; 1.1156 + if (PlacesUtils.nodeIsFolder(node)) 1.1157 + copiedFolders.push(node); 1.1158 + 1.1159 + let livemarkInfo = this.getCachedLivemarkInfo(node); 1.1160 + let overrideURI = livemarkInfo ? livemarkInfo.feedURI.spec : null; 1.1161 + 1.1162 + contents.forEach(function (content) { 1.1163 + content.entries.push( 1.1164 + PlacesUtils.wrapNode(node, content.type, overrideURI) 1.1165 + ); 1.1166 + }); 1.1167 + }, this); 1.1168 + 1.1169 + function addData(type, data) { 1.1170 + xferable.addDataFlavor(type); 1.1171 + xferable.setTransferData(type, PlacesUtils.toISupportsString(data), 1.1172 + data.length * 2); 1.1173 + } 1.1174 + 1.1175 + let xferable = Cc["@mozilla.org/widget/transferable;1"]. 1.1176 + createInstance(Ci.nsITransferable); 1.1177 + xferable.init(null); 1.1178 + let hasData = false; 1.1179 + // This order matters here! It controls how this and other applications 1.1180 + // select data to be inserted based on type. 1.1181 + contents.forEach(function (content) { 1.1182 + if (content.entries.length > 0) { 1.1183 + hasData = true; 1.1184 + let glue = 1.1185 + content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl; 1.1186 + addData(content.type, content.entries.join(glue)); 1.1187 + } 1.1188 + }); 1.1189 + 1.1190 + // Track the exected action in the xferable. This must be the last flavor 1.1191 + // since it's the least preferred one. 1.1192 + // Enqueue a unique instance identifier to distinguish operations across 1.1193 + // concurrent instances of the application. 1.1194 + addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName); 1.1195 + 1.1196 + if (hasData) { 1.1197 + this.clipboard.setData(xferable, 1.1198 + this.cutNodes.length > 0 ? this : null, 1.1199 + Ci.nsIClipboard.kGlobalClipboard); 1.1200 + } 1.1201 + }, 1.1202 + 1.1203 + _cutNodes: [], 1.1204 + get cutNodes() this._cutNodes, 1.1205 + set cutNodes(aNodes) { 1.1206 + let self = this; 1.1207 + function updateCutNodes(aValue) { 1.1208 + self._cutNodes.forEach(function (aNode) { 1.1209 + self._view.toggleCutNode(aNode, aValue); 1.1210 + }); 1.1211 + } 1.1212 + 1.1213 + updateCutNodes(false); 1.1214 + this._cutNodes = aNodes; 1.1215 + updateCutNodes(true); 1.1216 + return aNodes; 1.1217 + }, 1.1218 + 1.1219 + /** 1.1220 + * Copy Bookmarks and Folders to the clipboard 1.1221 + */ 1.1222 + copy: function PC_copy() { 1.1223 + let result = this._view.result; 1.1224 + let didSuppressNotifications = result.suppressNotifications; 1.1225 + if (!didSuppressNotifications) 1.1226 + result.suppressNotifications = true; 1.1227 + try { 1.1228 + this._populateClipboard(this._view.selectedNodes, "copy"); 1.1229 + } 1.1230 + finally { 1.1231 + if (!didSuppressNotifications) 1.1232 + result.suppressNotifications = false; 1.1233 + } 1.1234 + }, 1.1235 + 1.1236 + /** 1.1237 + * Cut Bookmarks and Folders to the clipboard 1.1238 + */ 1.1239 + cut: function PC_cut() { 1.1240 + let result = this._view.result; 1.1241 + let didSuppressNotifications = result.suppressNotifications; 1.1242 + if (!didSuppressNotifications) 1.1243 + result.suppressNotifications = true; 1.1244 + try { 1.1245 + this._populateClipboard(this._view.selectedNodes, "cut"); 1.1246 + this.cutNodes = this._view.selectedNodes; 1.1247 + } 1.1248 + finally { 1.1249 + if (!didSuppressNotifications) 1.1250 + result.suppressNotifications = false; 1.1251 + } 1.1252 + }, 1.1253 + 1.1254 + /** 1.1255 + * Paste Bookmarks and Folders from the clipboard 1.1256 + */ 1.1257 + paste: function PC_paste() { 1.1258 + // No reason to proceed if there isn't a valid insertion point. 1.1259 + let ip = this._view.insertionPoint; 1.1260 + if (!ip) 1.1261 + throw Cr.NS_ERROR_NOT_AVAILABLE; 1.1262 + 1.1263 + let action = this.clipboardAction; 1.1264 + 1.1265 + let xferable = Cc["@mozilla.org/widget/transferable;1"]. 1.1266 + createInstance(Ci.nsITransferable); 1.1267 + xferable.init(null); 1.1268 + // This order matters here! It controls the preferred flavors for this 1.1269 + // paste operation. 1.1270 + [ PlacesUtils.TYPE_X_MOZ_PLACE, 1.1271 + PlacesUtils.TYPE_X_MOZ_URL, 1.1272 + PlacesUtils.TYPE_UNICODE, 1.1273 + ].forEach(function (type) xferable.addDataFlavor(type)); 1.1274 + 1.1275 + this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); 1.1276 + 1.1277 + // Now get the clipboard contents, in the best available flavor. 1.1278 + let data = {}, type = {}, items = []; 1.1279 + try { 1.1280 + xferable.getAnyTransferData(type, data, {}); 1.1281 + data = data.value.QueryInterface(Ci.nsISupportsString).data; 1.1282 + type = type.value; 1.1283 + items = PlacesUtils.unwrapNodes(data, type); 1.1284 + } catch(ex) { 1.1285 + // No supported data exists or nodes unwrap failed, just bail out. 1.1286 + return; 1.1287 + } 1.1288 + 1.1289 + let transactions = []; 1.1290 + let insertionIndex = ip.index; 1.1291 + for (let i = 0; i < items.length; ++i) { 1.1292 + if (ip.isTag) { 1.1293 + // Pasting into a tag container means tagging the item, regardless of 1.1294 + // the requested action. 1.1295 + let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri), 1.1296 + [ip.itemId]); 1.1297 + transactions.push(tagTxn); 1.1298 + continue; 1.1299 + } 1.1300 + 1.1301 + // Adjust index to make sure items are pasted in the correct position. 1.1302 + // If index is DEFAULT_INDEX, items are just appended. 1.1303 + if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX) 1.1304 + insertionIndex = ip.index + i; 1.1305 + 1.1306 + // If this is not a copy, check for safety that we can move the source, 1.1307 + // otherwise report an error and fallback to a copy. 1.1308 + if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) { 1.1309 + Components.utils.reportError("Tried to move an unmovable Places node, " + 1.1310 + "reverting to a copy operation."); 1.1311 + action = "copy"; 1.1312 + } 1.1313 + transactions.push( 1.1314 + PlacesUIUtils.makeTransaction(items[i], type, ip.itemId, 1.1315 + insertionIndex, action == "copy") 1.1316 + ); 1.1317 + } 1.1318 + 1.1319 + let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions); 1.1320 + PlacesUtils.transactionManager.doTransaction(aggregatedTxn); 1.1321 + 1.1322 + // Cut/past operations are not repeatable, so clear the clipboard. 1.1323 + if (action == "cut") { 1.1324 + this._clearClipboard(); 1.1325 + } 1.1326 + 1.1327 + // Select the pasted items, they should be consecutive. 1.1328 + let insertedNodeIds = []; 1.1329 + for (let i = 0; i < transactions.length; ++i) { 1.1330 + insertedNodeIds.push( 1.1331 + PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i) 1.1332 + ); 1.1333 + } 1.1334 + if (insertedNodeIds.length > 0) 1.1335 + this._view.selectItems(insertedNodeIds, false); 1.1336 + }, 1.1337 + 1.1338 + /** 1.1339 + * Cache the livemark info for a node. This allows the controller and the 1.1340 + * views to treat the given node as a livemark. 1.1341 + * @param aNode 1.1342 + * a places result node. 1.1343 + * @param aLivemarkInfo 1.1344 + * a mozILivemarkInfo object. 1.1345 + */ 1.1346 + cacheLivemarkInfo: function PC_cacheLivemarkInfo(aNode, aLivemarkInfo) { 1.1347 + this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo); 1.1348 + }, 1.1349 + 1.1350 + /** 1.1351 + * Returns whether or not there's cached mozILivemarkInfo object for a node. 1.1352 + * @param aNode 1.1353 + * a places result node. 1.1354 + * @return true if there's a cached mozILivemarkInfo object for 1.1355 + * aNode, false otherwise. 1.1356 + */ 1.1357 + hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode) 1.1358 + this._cachedLivemarkInfoObjects.has(aNode), 1.1359 + 1.1360 + /** 1.1361 + * Returns the cached livemark info for a node, if set by cacheLivemarkInfo, 1.1362 + * null otherwise. 1.1363 + * @param aNode 1.1364 + * a places result node. 1.1365 + * @return the mozILivemarkInfo object for aNode, if set, null otherwise. 1.1366 + */ 1.1367 + getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode) 1.1368 + this._cachedLivemarkInfoObjects.get(aNode, null) 1.1369 +}; 1.1370 + 1.1371 +/** 1.1372 + * Handles drag and drop operations for views. Note that this is view agnostic! 1.1373 + * You should not use PlacesController._view within these methods, since 1.1374 + * the view that the item(s) have been dropped on was not necessarily active. 1.1375 + * Drop functions are passed the view that is being dropped on. 1.1376 + */ 1.1377 +let PlacesControllerDragHelper = { 1.1378 + /** 1.1379 + * DOM Element currently being dragged over 1.1380 + */ 1.1381 + currentDropTarget: null, 1.1382 + 1.1383 + /** 1.1384 + * Determines if the mouse is currently being dragged over a child node of 1.1385 + * this menu. This is necessary so that the menu doesn't close while the 1.1386 + * mouse is dragging over one of its submenus 1.1387 + * @param node 1.1388 + * The container node 1.1389 + * @return true if the user is dragging over a node within the hierarchy of 1.1390 + * the container, false otherwise. 1.1391 + */ 1.1392 + draggingOverChildNode: function PCDH_draggingOverChildNode(node) { 1.1393 + let currentNode = this.currentDropTarget; 1.1394 + while (currentNode) { 1.1395 + if (currentNode == node) 1.1396 + return true; 1.1397 + currentNode = currentNode.parentNode; 1.1398 + } 1.1399 + return false; 1.1400 + }, 1.1401 + 1.1402 + /** 1.1403 + * @return The current active drag session. Returns null if there is none. 1.1404 + */ 1.1405 + getSession: function PCDH__getSession() { 1.1406 + return this.dragService.getCurrentSession(); 1.1407 + }, 1.1408 + 1.1409 + /** 1.1410 + * Extract the first accepted flavor from a list of flavors. 1.1411 + * @param aFlavors 1.1412 + * The flavors list of type DOMStringList. 1.1413 + */ 1.1414 + getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) { 1.1415 + for (let i = 0; i < aFlavors.length; i++) { 1.1416 + if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1) 1.1417 + return aFlavors[i]; 1.1418 + } 1.1419 + 1.1420 + // If no supported flavor is found, check if data includes text/plain 1.1421 + // contents. If so, request them as text/unicode, a conversion will happen 1.1422 + // automatically. 1.1423 + if (aFlavors.contains("text/plain")) { 1.1424 + return PlacesUtils.TYPE_UNICODE; 1.1425 + } 1.1426 + 1.1427 + return null; 1.1428 + }, 1.1429 + 1.1430 + /** 1.1431 + * Determines whether or not the data currently being dragged can be dropped 1.1432 + * on a places view. 1.1433 + * @param ip 1.1434 + * The insertion point where the items should be dropped. 1.1435 + */ 1.1436 + canDrop: function PCDH_canDrop(ip, dt) { 1.1437 + let dropCount = dt.mozItemCount; 1.1438 + 1.1439 + // Check every dragged item. 1.1440 + for (let i = 0; i < dropCount; i++) { 1.1441 + let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i)); 1.1442 + if (!flavor) 1.1443 + return false; 1.1444 + 1.1445 + // Urls can be dropped on any insertionpoint. 1.1446 + // XXXmano: remember that this method is called for each dragover event! 1.1447 + // Thus we shouldn't use unwrapNodes here at all if possible. 1.1448 + // I think it would be OK to accept bogus data here (e.g. text which was 1.1449 + // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and 1.1450 + // will just case the actual drop to be a no-op), and only rule out valid 1.1451 + // expected cases, which are either unsupported flavors, or items which 1.1452 + // cannot be dropped in the current insertionpoint. The last case will 1.1453 + // likely force us to use unwrapNodes for the private data types of 1.1454 + // places. 1.1455 + if (flavor == TAB_DROP_TYPE) 1.1456 + continue; 1.1457 + 1.1458 + let data = dt.mozGetDataAt(flavor, i); 1.1459 + let dragged; 1.1460 + try { 1.1461 + dragged = PlacesUtils.unwrapNodes(data, flavor)[0]; 1.1462 + } 1.1463 + catch (e) { 1.1464 + return false; 1.1465 + } 1.1466 + 1.1467 + // Only bookmarks and urls can be dropped into tag containers. 1.1468 + if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON && 1.1469 + dragged.type != PlacesUtils.TYPE_X_MOZ_URL && 1.1470 + (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE || 1.1471 + (dragged.uri && dragged.uri.startsWith("place:")) )) 1.1472 + return false; 1.1473 + 1.1474 + // The following loop disallows the dropping of a folder on itself or 1.1475 + // on any of its descendants. 1.1476 + if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER || 1.1477 + (dragged.uri && dragged.uri.startsWith("place:")) ) { 1.1478 + let parentId = ip.itemId; 1.1479 + while (parentId != PlacesUtils.placesRootId) { 1.1480 + if (dragged.concreteId == parentId || dragged.id == parentId) 1.1481 + return false; 1.1482 + parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId); 1.1483 + } 1.1484 + } 1.1485 + } 1.1486 + return true; 1.1487 + }, 1.1488 + 1.1489 + /** 1.1490 + * Determines if an unwrapped node can be moved. 1.1491 + * 1.1492 + * @param aUnwrappedNode 1.1493 + * A node unwrapped by PlacesUtils.unwrapNodes(). 1.1494 + * @return True if the node can be moved, false otherwise. 1.1495 + */ 1.1496 + canMoveUnwrappedNode: function (aUnwrappedNode) { 1.1497 + return aUnwrappedNode.id > 0 && 1.1498 + !PlacesUtils.isRootItem(aUnwrappedNode.id) && 1.1499 + aUnwrappedNode.parent != PlacesUtils.placesRootId && 1.1500 + aUnwrappedNode.parent != PlacesUtils.tagsFolderId && 1.1501 + aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId && 1.1502 + !aUnwrappedNode.parentReadOnly; 1.1503 + }, 1.1504 + 1.1505 + /** 1.1506 + * Determines if a node can be moved. 1.1507 + * 1.1508 + * @param aNode 1.1509 + * A nsINavHistoryResultNode node. 1.1510 + * @return True if the node can be moved, false otherwise. 1.1511 + */ 1.1512 + canMoveNode: 1.1513 + function PCDH_canMoveNode(aNode) { 1.1514 + // Can't move query root. 1.1515 + if (!aNode.parent) 1.1516 + return false; 1.1517 + 1.1518 + let parentId = PlacesUtils.getConcreteItemId(aNode.parent); 1.1519 + let concreteId = PlacesUtils.getConcreteItemId(aNode); 1.1520 + 1.1521 + // Can't move children of tag containers. 1.1522 + if (PlacesUtils.nodeIsTagQuery(aNode.parent)) 1.1523 + return false; 1.1524 + 1.1525 + // Can't move children of read-only containers. 1.1526 + if (PlacesUtils.nodeIsReadOnly(aNode.parent)) 1.1527 + return false; 1.1528 + 1.1529 + // Check for special folders, etc. 1.1530 + if (PlacesUtils.nodeIsContainer(aNode) && 1.1531 + !this.canMoveContainer(aNode.itemId, parentId)) 1.1532 + return false; 1.1533 + 1.1534 + return true; 1.1535 + }, 1.1536 + 1.1537 + /** 1.1538 + * Determines if a container node can be moved. 1.1539 + * 1.1540 + * @param aId 1.1541 + * A bookmark folder id. 1.1542 + * @param [optional] aParentId 1.1543 + * The parent id of the folder. 1.1544 + * @return True if the container can be moved to the target. 1.1545 + */ 1.1546 + canMoveContainer: 1.1547 + function PCDH_canMoveContainer(aId, aParentId) { 1.1548 + if (aId == -1) 1.1549 + return false; 1.1550 + 1.1551 + // Disallow moving of roots and special folders. 1.1552 + const ROOTS = [PlacesUtils.placesRootId, PlacesUtils.bookmarksMenuFolderId, 1.1553 + PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId, 1.1554 + PlacesUtils.toolbarFolderId]; 1.1555 + if (ROOTS.indexOf(aId) != -1) 1.1556 + return false; 1.1557 + 1.1558 + // Get parent id if necessary. 1.1559 + if (aParentId == null || aParentId == -1) 1.1560 + aParentId = PlacesUtils.bookmarks.getFolderIdForItem(aId); 1.1561 + 1.1562 + if (PlacesUtils.bookmarks.getFolderReadonly(aParentId)) 1.1563 + return false; 1.1564 + 1.1565 + return true; 1.1566 + }, 1.1567 + 1.1568 + /** 1.1569 + * Handles the drop of one or more items onto a view. 1.1570 + * @param insertionPoint 1.1571 + * The insertion point where the items should be dropped 1.1572 + */ 1.1573 + onDrop: function PCDH_onDrop(insertionPoint, dt) { 1.1574 + let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1; 1.1575 + 1.1576 + let transactions = []; 1.1577 + let dropCount = dt.mozItemCount; 1.1578 + let movedCount = 0; 1.1579 + for (let i = 0; i < dropCount; ++i) { 1.1580 + let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i)); 1.1581 + if (!flavor) 1.1582 + return; 1.1583 + 1.1584 + let data = dt.mozGetDataAt(flavor, i); 1.1585 + let unwrapped; 1.1586 + if (flavor != TAB_DROP_TYPE) { 1.1587 + // There's only ever one in the D&D case. 1.1588 + unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0]; 1.1589 + } 1.1590 + else if (data instanceof XULElement && data.localName == "tab" && 1.1591 + data.ownerDocument.defaultView instanceof ChromeWindow) { 1.1592 + let uri = data.linkedBrowser.currentURI; 1.1593 + let spec = uri ? uri.spec : "about:blank"; 1.1594 + let title = data.label; 1.1595 + unwrapped = { uri: spec, 1.1596 + title: data.label, 1.1597 + type: PlacesUtils.TYPE_X_MOZ_URL}; 1.1598 + } 1.1599 + else 1.1600 + throw("bogus data was passed as a tab") 1.1601 + 1.1602 + let index = insertionPoint.index; 1.1603 + 1.1604 + // Adjust insertion index to prevent reversal of dragged items. When you 1.1605 + // drag multiple elts upward: need to increment index or each successive 1.1606 + // elt will be inserted at the same index, each above the previous. 1.1607 + let dragginUp = insertionPoint.itemId == unwrapped.parent && 1.1608 + index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id); 1.1609 + if (index != -1 && dragginUp) 1.1610 + index+= movedCount++; 1.1611 + 1.1612 + // If dragging over a tag container we should tag the item. 1.1613 + if (insertionPoint.isTag && 1.1614 + insertionPoint.orientation == Ci.nsITreeView.DROP_ON) { 1.1615 + let uri = NetUtil.newURI(unwrapped.uri); 1.1616 + let tagItemId = insertionPoint.itemId; 1.1617 + let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]); 1.1618 + transactions.push(tagTxn); 1.1619 + } 1.1620 + else { 1.1621 + // If this is not a copy, check for safety that we can move the source, 1.1622 + // otherwise report an error and fallback to a copy. 1.1623 + if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) { 1.1624 + Components.utils.reportError("Tried to move an unmovable Places node, " + 1.1625 + "reverting to a copy operation."); 1.1626 + doCopy = true; 1.1627 + } 1.1628 + transactions.push(PlacesUIUtils.makeTransaction(unwrapped, 1.1629 + flavor, insertionPoint.itemId, 1.1630 + index, doCopy)); 1.1631 + } 1.1632 + } 1.1633 + 1.1634 + let txn = new PlacesAggregatedTransaction("DropItems", transactions); 1.1635 + PlacesUtils.transactionManager.doTransaction(txn); 1.1636 + }, 1.1637 + 1.1638 + /** 1.1639 + * Checks if we can insert into a container. 1.1640 + * @param aContainer 1.1641 + * The container were we are want to drop 1.1642 + */ 1.1643 + disallowInsertion: function(aContainer) { 1.1644 + NS_ASSERT(aContainer, "empty container"); 1.1645 + // Allow dropping into Tag containers. 1.1646 + if (PlacesUtils.nodeIsTagQuery(aContainer)) 1.1647 + return false; 1.1648 + // Disallow insertion of items under readonly folders. 1.1649 + return (!PlacesUtils.nodeIsFolder(aContainer) || 1.1650 + PlacesUtils.nodeIsReadOnly(aContainer)); 1.1651 + }, 1.1652 + 1.1653 + placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, 1.1654 + PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, 1.1655 + PlacesUtils.TYPE_X_MOZ_PLACE], 1.1656 + 1.1657 + // The order matters. 1.1658 + GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, 1.1659 + PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, 1.1660 + PlacesUtils.TYPE_X_MOZ_PLACE, 1.1661 + PlacesUtils.TYPE_X_MOZ_URL, 1.1662 + TAB_DROP_TYPE, 1.1663 + PlacesUtils.TYPE_UNICODE], 1.1664 +}; 1.1665 + 1.1666 + 1.1667 +XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService", 1.1668 + "@mozilla.org/widget/dragservice;1", 1.1669 + "nsIDragService"); 1.1670 + 1.1671 +function goUpdatePlacesCommands() { 1.1672 + // Get the controller for one of the places commands. 1.1673 + var placesController = doGetPlacesControllerForCommand("placesCmd_open"); 1.1674 + function updatePlacesCommand(aCommand) { 1.1675 + goSetCommandEnabled(aCommand, placesController && 1.1676 + placesController.isCommandEnabled(aCommand)); 1.1677 + } 1.1678 + 1.1679 + updatePlacesCommand("placesCmd_open"); 1.1680 + updatePlacesCommand("placesCmd_open:window"); 1.1681 + updatePlacesCommand("placesCmd_open:tab"); 1.1682 + updatePlacesCommand("placesCmd_new:folder"); 1.1683 + updatePlacesCommand("placesCmd_new:bookmark"); 1.1684 + updatePlacesCommand("placesCmd_new:separator"); 1.1685 + updatePlacesCommand("placesCmd_show:info"); 1.1686 + updatePlacesCommand("placesCmd_moveBookmarks"); 1.1687 + updatePlacesCommand("placesCmd_reload"); 1.1688 + updatePlacesCommand("placesCmd_sortBy:name"); 1.1689 + updatePlacesCommand("placesCmd_cut"); 1.1690 + updatePlacesCommand("placesCmd_copy"); 1.1691 + updatePlacesCommand("placesCmd_paste"); 1.1692 + updatePlacesCommand("placesCmd_delete"); 1.1693 +} 1.1694 + 1.1695 +function doGetPlacesControllerForCommand(aCommand) 1.1696 +{ 1.1697 + // A context menu may be built for non-focusable views. Thus, we first try 1.1698 + // to look for a view associated with document.popupNode 1.1699 + let popupNode; 1.1700 + try { 1.1701 + popupNode = document.popupNode; 1.1702 + } catch (e) { 1.1703 + // The document went away (bug 797307). 1.1704 + return null; 1.1705 + } 1.1706 + if (popupNode) { 1.1707 + let view = PlacesUIUtils.getViewForNode(popupNode); 1.1708 + if (view && view._contextMenuShown) 1.1709 + return view.controllers.getControllerForCommand(aCommand); 1.1710 + } 1.1711 + 1.1712 + // When we're not building a context menu, only focusable views 1.1713 + // are possible. Thus, we can safely use the command dispatcher. 1.1714 + let controller = top.document.commandDispatcher 1.1715 + .getControllerForCommand(aCommand); 1.1716 + if (controller) 1.1717 + return controller; 1.1718 + 1.1719 + return null; 1.1720 +} 1.1721 + 1.1722 +function goDoPlacesCommand(aCommand) 1.1723 +{ 1.1724 + let controller = doGetPlacesControllerForCommand(aCommand); 1.1725 + if (controller && controller.isCommandEnabled(aCommand)) 1.1726 + controller.doCommand(aCommand); 1.1727 +} 1.1728 +