browser/components/places/content/controller.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite",
michael@0 7 "resource://gre/modules/ForgetAboutSite.jsm");
michael@0 8 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 9 "resource://gre/modules/NetUtil.jsm");
michael@0 10
michael@0 11 // XXXmano: we should move most/all of these constants to PlacesUtils
michael@0 12 const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
michael@0 13
michael@0 14 // No change to the view, preserve current selection
michael@0 15 const RELOAD_ACTION_NOTHING = 0;
michael@0 16 // Inserting items new to the view, select the inserted rows
michael@0 17 const RELOAD_ACTION_INSERT = 1;
michael@0 18 // Removing items from the view, select the first item after the last selected
michael@0 19 const RELOAD_ACTION_REMOVE = 2;
michael@0 20 // Moving items within a view, don't treat the dropped items as additional
michael@0 21 // rows.
michael@0 22 const RELOAD_ACTION_MOVE = 3;
michael@0 23
michael@0 24 // When removing a bunch of pages we split them in chunks to give some breath
michael@0 25 // to the main-thread.
michael@0 26 const REMOVE_PAGES_CHUNKLEN = 300;
michael@0 27
michael@0 28 /**
michael@0 29 * Represents an insertion point within a container where we can insert
michael@0 30 * items.
michael@0 31 * @param aItemId
michael@0 32 * The identifier of the parent container
michael@0 33 * @param aIndex
michael@0 34 * The index within the container where we should insert
michael@0 35 * @param aOrientation
michael@0 36 * The orientation of the insertion. NOTE: the adjustments to the
michael@0 37 * insertion point to accommodate the orientation should be done by
michael@0 38 * the person who constructs the IP, not the user. The orientation
michael@0 39 * is provided for informational purposes only!
michael@0 40 * @param [optional] aIsTag
michael@0 41 * Indicates if parent container is a tag
michael@0 42 * @param [optional] aDropNearItemId
michael@0 43 * When defined we will calculate index based on this itemId
michael@0 44 * @constructor
michael@0 45 */
michael@0 46 function InsertionPoint(aItemId, aIndex, aOrientation, aIsTag,
michael@0 47 aDropNearItemId) {
michael@0 48 this.itemId = aItemId;
michael@0 49 this._index = aIndex;
michael@0 50 this.orientation = aOrientation;
michael@0 51 this.isTag = aIsTag;
michael@0 52 this.dropNearItemId = aDropNearItemId;
michael@0 53 }
michael@0 54
michael@0 55 InsertionPoint.prototype = {
michael@0 56 set index(val) {
michael@0 57 return this._index = val;
michael@0 58 },
michael@0 59
michael@0 60 promiseGUID: function () PlacesUtils.promiseItemGUID(this.itemId),
michael@0 61
michael@0 62 get index() {
michael@0 63 if (this.dropNearItemId > 0) {
michael@0 64 // If dropNearItemId is set up we must calculate the real index of
michael@0 65 // the item near which we will drop.
michael@0 66 var index = PlacesUtils.bookmarks.getItemIndex(this.dropNearItemId);
michael@0 67 return this.orientation == Ci.nsITreeView.DROP_BEFORE ? index : index + 1;
michael@0 68 }
michael@0 69 return this._index;
michael@0 70 }
michael@0 71 };
michael@0 72
michael@0 73 /**
michael@0 74 * Places Controller
michael@0 75 */
michael@0 76
michael@0 77 function PlacesController(aView) {
michael@0 78 this._view = aView;
michael@0 79 XPCOMUtils.defineLazyServiceGetter(this, "clipboard",
michael@0 80 "@mozilla.org/widget/clipboard;1",
michael@0 81 "nsIClipboard");
michael@0 82 XPCOMUtils.defineLazyGetter(this, "profileName", function () {
michael@0 83 return Services.dirsvc.get("ProfD", Ci.nsIFile).leafName;
michael@0 84 });
michael@0 85
michael@0 86 this._cachedLivemarkInfoObjects = new Map();
michael@0 87 }
michael@0 88
michael@0 89 PlacesController.prototype = {
michael@0 90 /**
michael@0 91 * The places view.
michael@0 92 */
michael@0 93 _view: null,
michael@0 94
michael@0 95 QueryInterface: XPCOMUtils.generateQI([
michael@0 96 Ci.nsIClipboardOwner
michael@0 97 ]),
michael@0 98
michael@0 99 // nsIClipboardOwner
michael@0 100 LosingOwnership: function PC_LosingOwnership (aXferable) {
michael@0 101 this.cutNodes = [];
michael@0 102 },
michael@0 103
michael@0 104 terminate: function PC_terminate() {
michael@0 105 this._releaseClipboardOwnership();
michael@0 106 },
michael@0 107
michael@0 108 supportsCommand: function PC_supportsCommand(aCommand) {
michael@0 109 // Non-Places specific commands that we also support
michael@0 110 switch (aCommand) {
michael@0 111 case "cmd_undo":
michael@0 112 case "cmd_redo":
michael@0 113 case "cmd_cut":
michael@0 114 case "cmd_copy":
michael@0 115 case "cmd_paste":
michael@0 116 case "cmd_delete":
michael@0 117 case "cmd_selectAll":
michael@0 118 return true;
michael@0 119 }
michael@0 120
michael@0 121 // All other Places Commands are prefixed with "placesCmd_" ... this
michael@0 122 // filters out other commands that we do _not_ support (see 329587).
michael@0 123 const CMD_PREFIX = "placesCmd_";
michael@0 124 return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
michael@0 125 },
michael@0 126
michael@0 127 isCommandEnabled: function PC_isCommandEnabled(aCommand) {
michael@0 128 if (PlacesUIUtils.useAsyncTransactions) {
michael@0 129 switch (aCommand) {
michael@0 130 case "cmd_cut":
michael@0 131 case "placesCmd_cut":
michael@0 132 case "cmd_copy":
michael@0 133 case "cmd_paste":
michael@0 134 case "cmd_delete":
michael@0 135 case "placesCmd_delete":
michael@0 136 case "cmd_paste":
michael@0 137 case "placesCmd_paste":
michael@0 138 case "placesCmd_new:folder":
michael@0 139 case "placesCmd_new:bookmark":
michael@0 140 case "placesCmd_createBookmark":
michael@0 141 return false;
michael@0 142 }
michael@0 143 }
michael@0 144
michael@0 145 switch (aCommand) {
michael@0 146 case "cmd_undo":
michael@0 147 if (!PlacesUIUtils.useAsyncTransactions)
michael@0 148 return PlacesUtils.transactionManager.numberOfUndoItems > 0;
michael@0 149
michael@0 150 return PlacesTransactions.topUndoEntry != null;
michael@0 151 case "cmd_redo":
michael@0 152 if (!PlacesUIUtils.useAsyncTransactions)
michael@0 153 return PlacesUtils.transactionManager.numberOfRedoItems > 0;
michael@0 154
michael@0 155 return PlacesTransactions.topRedoEntry != null;
michael@0 156 case "cmd_cut":
michael@0 157 case "placesCmd_cut":
michael@0 158 var nodes = this._view.selectedNodes;
michael@0 159 // If selection includes history nodes there's no reason to allow cut.
michael@0 160 for (var i = 0; i < nodes.length; i++) {
michael@0 161 if (nodes[i].itemId == -1)
michael@0 162 return false;
michael@0 163 }
michael@0 164 // Otherwise fallback to cmd_delete check.
michael@0 165 case "cmd_delete":
michael@0 166 case "placesCmd_delete":
michael@0 167 case "placesCmd_deleteDataHost":
michael@0 168 return this._hasRemovableSelection(false);
michael@0 169 case "placesCmd_moveBookmarks":
michael@0 170 return this._hasRemovableSelection(true);
michael@0 171 case "cmd_copy":
michael@0 172 case "placesCmd_copy":
michael@0 173 return this._view.hasSelection;
michael@0 174 case "cmd_paste":
michael@0 175 case "placesCmd_paste":
michael@0 176 return this._canInsert(true) && this._isClipboardDataPasteable();
michael@0 177 case "cmd_selectAll":
michael@0 178 if (this._view.selType != "single") {
michael@0 179 let rootNode = this._view.result.root;
michael@0 180 if (rootNode.containerOpen && rootNode.childCount > 0)
michael@0 181 return true;
michael@0 182 }
michael@0 183 return false;
michael@0 184 case "placesCmd_open":
michael@0 185 case "placesCmd_open:window":
michael@0 186 case "placesCmd_open:tab":
michael@0 187 var selectedNode = this._view.selectedNode;
michael@0 188 return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
michael@0 189 case "placesCmd_new:folder":
michael@0 190 return this._canInsert();
michael@0 191 case "placesCmd_new:bookmark":
michael@0 192 return this._canInsert();
michael@0 193 case "placesCmd_new:separator":
michael@0 194 return this._canInsert() &&
michael@0 195 !PlacesUtils.asQuery(this._view.result.root).queryOptions.excludeItems &&
michael@0 196 this._view.result.sortingMode ==
michael@0 197 Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
michael@0 198 case "placesCmd_show:info":
michael@0 199 var selectedNode = this._view.selectedNode;
michael@0 200 return selectedNode && PlacesUtils.getConcreteItemId(selectedNode) != -1
michael@0 201 case "placesCmd_reload":
michael@0 202 // Livemark containers
michael@0 203 var selectedNode = this._view.selectedNode;
michael@0 204 return selectedNode && this.hasCachedLivemarkInfo(selectedNode);
michael@0 205 case "placesCmd_sortBy:name":
michael@0 206 var selectedNode = this._view.selectedNode;
michael@0 207 return selectedNode &&
michael@0 208 PlacesUtils.nodeIsFolder(selectedNode) &&
michael@0 209 !PlacesUtils.nodeIsReadOnly(selectedNode) &&
michael@0 210 this._view.result.sortingMode ==
michael@0 211 Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
michael@0 212 case "placesCmd_createBookmark":
michael@0 213 var node = this._view.selectedNode;
michael@0 214 return node && PlacesUtils.nodeIsURI(node) && node.itemId == -1;
michael@0 215 default:
michael@0 216 return false;
michael@0 217 }
michael@0 218 },
michael@0 219
michael@0 220 doCommand: function PC_doCommand(aCommand) {
michael@0 221 switch (aCommand) {
michael@0 222 case "cmd_undo":
michael@0 223 if (!PlacesUIUtils.useAsyncTransactions) {
michael@0 224 PlacesUtils.transactionManager.undoTransaction();
michael@0 225 return;
michael@0 226 }
michael@0 227 PlacesTransactions.undo().then(null, Components.utils.reportError);
michael@0 228 break;
michael@0 229 case "cmd_redo":
michael@0 230 if (!PlacesUIUtils.useAsyncTransactions) {
michael@0 231 PlacesUtils.transactionManager.redoTransaction();
michael@0 232 return;
michael@0 233 }
michael@0 234 PlacesTransactions.redo().then(null, Components.utils.reportError);
michael@0 235 break;
michael@0 236 case "cmd_cut":
michael@0 237 case "placesCmd_cut":
michael@0 238 this.cut();
michael@0 239 break;
michael@0 240 case "cmd_copy":
michael@0 241 case "placesCmd_copy":
michael@0 242 this.copy();
michael@0 243 break;
michael@0 244 case "cmd_paste":
michael@0 245 case "placesCmd_paste":
michael@0 246 this.paste();
michael@0 247 break;
michael@0 248 case "cmd_delete":
michael@0 249 case "placesCmd_delete":
michael@0 250 this.remove("Remove Selection");
michael@0 251 break;
michael@0 252 case "placesCmd_deleteDataHost":
michael@0 253 var host;
michael@0 254 if (PlacesUtils.nodeIsHost(this._view.selectedNode)) {
michael@0 255 var queries = this._view.selectedNode.getQueries();
michael@0 256 host = queries[0].domain;
michael@0 257 }
michael@0 258 else
michael@0 259 host = NetUtil.newURI(this._view.selectedNode.uri).host;
michael@0 260 ForgetAboutSite.removeDataFromDomain(host);
michael@0 261 break;
michael@0 262 case "cmd_selectAll":
michael@0 263 this.selectAll();
michael@0 264 break;
michael@0 265 case "placesCmd_open":
michael@0 266 PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
michael@0 267 break;
michael@0 268 case "placesCmd_open:window":
michael@0 269 PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
michael@0 270 break;
michael@0 271 case "placesCmd_open:tab":
michael@0 272 PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
michael@0 273 break;
michael@0 274 case "placesCmd_new:folder":
michael@0 275 this.newItem("folder");
michael@0 276 break;
michael@0 277 case "placesCmd_new:bookmark":
michael@0 278 this.newItem("bookmark");
michael@0 279 break;
michael@0 280 case "placesCmd_new:separator":
michael@0 281 this.newSeparator().then(null, Components.utils.reportError);
michael@0 282 break;
michael@0 283 case "placesCmd_show:info":
michael@0 284 this.showBookmarkPropertiesForSelection();
michael@0 285 break;
michael@0 286 case "placesCmd_moveBookmarks":
michael@0 287 this.moveSelectedBookmarks();
michael@0 288 break;
michael@0 289 case "placesCmd_reload":
michael@0 290 this.reloadSelectedLivemark();
michael@0 291 break;
michael@0 292 case "placesCmd_sortBy:name":
michael@0 293 this.sortFolderByName().then(null, Components.utils.reportError);
michael@0 294 break;
michael@0 295 case "placesCmd_createBookmark":
michael@0 296 let node = this._view.selectedNode;
michael@0 297 PlacesUIUtils.showBookmarkDialog({ action: "add"
michael@0 298 , type: "bookmark"
michael@0 299 , hiddenRows: [ "description"
michael@0 300 , "keyword"
michael@0 301 , "location"
michael@0 302 , "loadInSidebar" ]
michael@0 303 , uri: NetUtil.newURI(node.uri)
michael@0 304 , title: node.title
michael@0 305 }, window.top);
michael@0 306 break;
michael@0 307 }
michael@0 308 },
michael@0 309
michael@0 310 onEvent: function PC_onEvent(eventName) { },
michael@0 311
michael@0 312
michael@0 313 /**
michael@0 314 * Determine whether or not the selection can be removed, either by the
michael@0 315 * delete or cut operations based on whether or not any of its contents
michael@0 316 * are non-removable. We don't need to worry about recursion here since it
michael@0 317 * is a policy decision that a removable item not be placed inside a non-
michael@0 318 * removable item.
michael@0 319 * @param aIsMoveCommand
michael@0 320 * True if the command for which this method is called only moves the
michael@0 321 * selected items to another container, false otherwise.
michael@0 322 * @return true if all nodes in the selection can be removed,
michael@0 323 * false otherwise.
michael@0 324 */
michael@0 325 _hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) {
michael@0 326 var ranges = this._view.removableSelectionRanges;
michael@0 327 if (!ranges.length)
michael@0 328 return false;
michael@0 329
michael@0 330 var root = this._view.result.root;
michael@0 331
michael@0 332 for (var j = 0; j < ranges.length; j++) {
michael@0 333 var nodes = ranges[j];
michael@0 334 for (var i = 0; i < nodes.length; ++i) {
michael@0 335 // Disallow removing the view's root node
michael@0 336 if (nodes[i] == root)
michael@0 337 return false;
michael@0 338
michael@0 339 if (PlacesUtils.nodeIsFolder(nodes[i]) &&
michael@0 340 !PlacesControllerDragHelper.canMoveNode(nodes[i]))
michael@0 341 return false;
michael@0 342
michael@0 343 // We don't call nodeIsReadOnly here, because nodeIsReadOnly means that
michael@0 344 // a node has children that cannot be edited, reordered or removed. Here,
michael@0 345 // we don't care if a node's children can't be reordered or edited, just
michael@0 346 // that they're removable. All history results have removable children
michael@0 347 // (based on the principle that any URL in the history table should be
michael@0 348 // removable), but some special bookmark folders may have non-removable
michael@0 349 // children, e.g. live bookmark folder children. It doesn't make sense
michael@0 350 // to delete a child of a live bookmark folder, since when the folder
michael@0 351 // refreshes, the child will return.
michael@0 352 var parent = nodes[i].parent || root;
michael@0 353 if (PlacesUtils.isReadonlyFolder(parent))
michael@0 354 return false;
michael@0 355 }
michael@0 356 }
michael@0 357
michael@0 358 return true;
michael@0 359 },
michael@0 360
michael@0 361 /**
michael@0 362 * Determines whether or not nodes can be inserted relative to the selection.
michael@0 363 */
michael@0 364 _canInsert: function PC__canInsert(isPaste) {
michael@0 365 var ip = this._view.insertionPoint;
michael@0 366 return ip != null && (isPaste || ip.isTag != true);
michael@0 367 },
michael@0 368
michael@0 369 /**
michael@0 370 * Determines whether or not the root node for the view is selected
michael@0 371 */
michael@0 372 rootNodeIsSelected: function PC_rootNodeIsSelected() {
michael@0 373 var nodes = this._view.selectedNodes;
michael@0 374 var root = this._view.result.root;
michael@0 375 for (var i = 0; i < nodes.length; ++i) {
michael@0 376 if (nodes[i] == root)
michael@0 377 return true;
michael@0 378 }
michael@0 379
michael@0 380 return false;
michael@0 381 },
michael@0 382
michael@0 383 /**
michael@0 384 * Looks at the data on the clipboard to see if it is paste-able.
michael@0 385 * Paste-able data is:
michael@0 386 * - in a format that the view can receive
michael@0 387 * @return true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
michael@0 388 * - clipboard data is of type TEXT_UNICODE and
michael@0 389 * is a valid URI.
michael@0 390 */
michael@0 391 _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
michael@0 392 // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
michael@0 393 // pasteable, with no need to unwrap all the nodes.
michael@0 394
michael@0 395 var flavors = PlacesControllerDragHelper.placesFlavors;
michael@0 396 var clipboard = this.clipboard;
michael@0 397 var hasPlacesData =
michael@0 398 clipboard.hasDataMatchingFlavors(flavors, flavors.length,
michael@0 399 Ci.nsIClipboard.kGlobalClipboard);
michael@0 400 if (hasPlacesData)
michael@0 401 return this._view.insertionPoint != null;
michael@0 402
michael@0 403 // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
michael@0 404 // pasting of valid "text/unicode" and "text/x-moz-url" data
michael@0 405 var xferable = Cc["@mozilla.org/widget/transferable;1"].
michael@0 406 createInstance(Ci.nsITransferable);
michael@0 407 xferable.init(null);
michael@0 408
michael@0 409 xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
michael@0 410 xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
michael@0 411 clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
michael@0 412
michael@0 413 try {
michael@0 414 // getAnyTransferData will throw if no data is available.
michael@0 415 var data = { }, type = { };
michael@0 416 xferable.getAnyTransferData(type, data, { });
michael@0 417 data = data.value.QueryInterface(Ci.nsISupportsString).data;
michael@0 418 if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
michael@0 419 type.value != PlacesUtils.TYPE_UNICODE)
michael@0 420 return false;
michael@0 421
michael@0 422 // unwrapNodes() will throw if the data blob is malformed.
michael@0 423 var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
michael@0 424 return this._view.insertionPoint != null;
michael@0 425 }
michael@0 426 catch (e) {
michael@0 427 // getAnyTransferData or unwrapNodes failed
michael@0 428 return false;
michael@0 429 }
michael@0 430 },
michael@0 431
michael@0 432 /**
michael@0 433 * Gathers information about the selected nodes according to the following
michael@0 434 * rules:
michael@0 435 * "link" node is a URI
michael@0 436 * "bookmark" node is a bookamrk
michael@0 437 * "livemarkChild" node is a child of a livemark
michael@0 438 * "tagChild" node is a child of a tag
michael@0 439 * "folder" node is a folder
michael@0 440 * "query" node is a query
michael@0 441 * "separator" node is a separator line
michael@0 442 * "host" node is a host
michael@0 443 *
michael@0 444 * @return an array of objects corresponding the selected nodes. Each
michael@0 445 * object has each of the properties above set if its corresponding
michael@0 446 * node matches the rule. In addition, the annotations names for each
michael@0 447 * node are set on its corresponding object as properties.
michael@0 448 * Notes:
michael@0 449 * 1) This can be slow, so don't call it anywhere performance critical!
michael@0 450 * 2) A single-object array corresponding the root node is returned if
michael@0 451 * there's no selection.
michael@0 452 */
michael@0 453 _buildSelectionMetadata: function PC__buildSelectionMetadata() {
michael@0 454 var metadata = [];
michael@0 455 var root = this._view.result.root;
michael@0 456 var nodes = this._view.selectedNodes;
michael@0 457 if (nodes.length == 0)
michael@0 458 nodes.push(root); // See the second note above
michael@0 459
michael@0 460 for (var i = 0; i < nodes.length; i++) {
michael@0 461 var nodeData = {};
michael@0 462 var node = nodes[i];
michael@0 463 var nodeType = node.type;
michael@0 464 var uri = null;
michael@0 465
michael@0 466 // We don't use the nodeIs* methods here to avoid going through the type
michael@0 467 // property way too often
michael@0 468 switch (nodeType) {
michael@0 469 case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
michael@0 470 nodeData["query"] = true;
michael@0 471 if (node.parent) {
michael@0 472 switch (PlacesUtils.asQuery(node.parent).queryOptions.resultType) {
michael@0 473 case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
michael@0 474 nodeData["host"] = true;
michael@0 475 break;
michael@0 476 case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
michael@0 477 case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
michael@0 478 nodeData["day"] = true;
michael@0 479 break;
michael@0 480 }
michael@0 481 }
michael@0 482 break;
michael@0 483 case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
michael@0 484 case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
michael@0 485 nodeData["folder"] = true;
michael@0 486 break;
michael@0 487 case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
michael@0 488 nodeData["separator"] = true;
michael@0 489 break;
michael@0 490 case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
michael@0 491 nodeData["link"] = true;
michael@0 492 uri = NetUtil.newURI(node.uri);
michael@0 493 if (PlacesUtils.nodeIsBookmark(node)) {
michael@0 494 nodeData["bookmark"] = true;
michael@0 495 PlacesUtils.nodeIsTagQuery(node.parent)
michael@0 496
michael@0 497 var parentNode = node.parent;
michael@0 498 if (parentNode) {
michael@0 499 if (PlacesUtils.nodeIsTagQuery(parentNode))
michael@0 500 nodeData["tagChild"] = true;
michael@0 501 else if (this.hasCachedLivemarkInfo(parentNode))
michael@0 502 nodeData["livemarkChild"] = true;
michael@0 503 }
michael@0 504 }
michael@0 505 break;
michael@0 506 }
michael@0 507
michael@0 508 // annotations
michael@0 509 if (uri) {
michael@0 510 let names = PlacesUtils.annotations.getPageAnnotationNames(uri);
michael@0 511 for (let j = 0; j < names.length; ++j)
michael@0 512 nodeData[names[j]] = true;
michael@0 513 }
michael@0 514
michael@0 515 // For items also include the item-specific annotations
michael@0 516 if (node.itemId != -1) {
michael@0 517 let names = PlacesUtils.annotations
michael@0 518 .getItemAnnotationNames(node.itemId);
michael@0 519 for (let j = 0; j < names.length; ++j)
michael@0 520 nodeData[names[j]] = true;
michael@0 521 }
michael@0 522 metadata.push(nodeData);
michael@0 523 }
michael@0 524
michael@0 525 return metadata;
michael@0 526 },
michael@0 527
michael@0 528 /**
michael@0 529 * Determines if a context-menu item should be shown
michael@0 530 * @param aMenuItem
michael@0 531 * the context menu item
michael@0 532 * @param aMetaData
michael@0 533 * meta data about the selection
michael@0 534 * @return true if the conditions (see buildContextMenu) are satisfied
michael@0 535 * and the item can be displayed, false otherwise.
michael@0 536 */
michael@0 537 _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
michael@0 538 var selectiontype = aMenuItem.getAttribute("selectiontype");
michael@0 539 if (selectiontype == "multiple" && aMetaData.length == 1)
michael@0 540 return false;
michael@0 541 if (selectiontype == "single" && aMetaData.length != 1)
michael@0 542 return false;
michael@0 543
michael@0 544 var forceHideAttr = aMenuItem.getAttribute("forcehideselection");
michael@0 545 if (forceHideAttr) {
michael@0 546 var forceHideRules = forceHideAttr.split("|");
michael@0 547 for (let i = 0; i < aMetaData.length; ++i) {
michael@0 548 for (let j = 0; j < forceHideRules.length; ++j) {
michael@0 549 if (forceHideRules[j] in aMetaData[i])
michael@0 550 return false;
michael@0 551 }
michael@0 552 }
michael@0 553 }
michael@0 554
michael@0 555 var selectionAttr = aMenuItem.getAttribute("selection");
michael@0 556 if (!selectionAttr) {
michael@0 557 return !aMenuItem.hidden;
michael@0 558 }
michael@0 559
michael@0 560 if (selectionAttr == "any")
michael@0 561 return true;
michael@0 562
michael@0 563 var showRules = selectionAttr.split("|");
michael@0 564 var anyMatched = false;
michael@0 565 function metaDataNodeMatches(metaDataNode, rules) {
michael@0 566 for (var i = 0; i < rules.length; i++) {
michael@0 567 if (rules[i] in metaDataNode)
michael@0 568 return true;
michael@0 569 }
michael@0 570 return false;
michael@0 571 }
michael@0 572
michael@0 573 for (var i = 0; i < aMetaData.length; ++i) {
michael@0 574 if (metaDataNodeMatches(aMetaData[i], showRules))
michael@0 575 anyMatched = true;
michael@0 576 else
michael@0 577 return false;
michael@0 578 }
michael@0 579 return anyMatched;
michael@0 580 },
michael@0 581
michael@0 582 /**
michael@0 583 * Detects information (meta-data rules) about the current selection in the
michael@0 584 * view (see _buildSelectionMetadata) and sets the visibility state for each
michael@0 585 * of the menu-items in the given popup with the following rules applied:
michael@0 586 * 1) The "selectiontype" attribute may be set on a menu-item to "single"
michael@0 587 * if the menu-item should be visible only if there is a single node
michael@0 588 * selected, or to "multiple" if the menu-item should be visible only if
michael@0 589 * multiple nodes are selected. If the attribute is not set or if it is
michael@0 590 * set to an invalid value, the menu-item may be visible for both types of
michael@0 591 * selection.
michael@0 592 * 2) The "selection" attribute may be set on a menu-item to the various
michael@0 593 * meta-data rules for which it may be visible. The rules should be
michael@0 594 * separated with the | character.
michael@0 595 * 3) A menu-item may be visible only if at least one of the rules set in
michael@0 596 * its selection attribute apply to each of the selected nodes in the
michael@0 597 * view.
michael@0 598 * 4) The "forcehideselection" attribute may be set on a menu-item to rules
michael@0 599 * for which it should be hidden. This attribute takes priority over the
michael@0 600 * selection attribute. A menu-item would be hidden if at least one of the
michael@0 601 * given rules apply to one of the selected nodes. The rules should be
michael@0 602 * separated with the | character.
michael@0 603 * 5) The "hideifnoinsertionpoint" attribute may be set on a menu-item to
michael@0 604 * true if it should be hidden when there's no insertion point
michael@0 605 * 6) The visibility state of a menu-item is unchanged if none of these
michael@0 606 * attribute are set.
michael@0 607 * 7) These attributes should not be set on separators for which the
michael@0 608 * visibility state is "auto-detected."
michael@0 609 * 8) The "hideifprivatebrowsing" attribute may be set on a menu-item to
michael@0 610 * true if it should be hidden inside the private browsing mode
michael@0 611 * @param aPopup
michael@0 612 * The menupopup to build children into.
michael@0 613 * @return true if at least one item is visible, false otherwise.
michael@0 614 */
michael@0 615 buildContextMenu: function PC_buildContextMenu(aPopup) {
michael@0 616 var metadata = this._buildSelectionMetadata();
michael@0 617 var ip = this._view.insertionPoint;
michael@0 618 var noIp = !ip || ip.isTag;
michael@0 619
michael@0 620 var separator = null;
michael@0 621 var visibleItemsBeforeSep = false;
michael@0 622 var anyVisible = false;
michael@0 623 for (var i = 0; i < aPopup.childNodes.length; ++i) {
michael@0 624 var item = aPopup.childNodes[i];
michael@0 625 if (item.localName != "menuseparator") {
michael@0 626 // We allow pasting into tag containers, so special case that.
michael@0 627 var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
michael@0 628 noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
michael@0 629 item.hidden = hideIfNoIP || !this._shouldShowMenuItem(item, metadata);
michael@0 630
michael@0 631 if (!item.hidden) {
michael@0 632 visibleItemsBeforeSep = true;
michael@0 633 anyVisible = true;
michael@0 634
michael@0 635 // Show the separator above the menu-item if any
michael@0 636 if (separator) {
michael@0 637 separator.hidden = false;
michael@0 638 separator = null;
michael@0 639 }
michael@0 640 }
michael@0 641 }
michael@0 642 else { // menuseparator
michael@0 643 // Initially hide it. It will be unhidden if there will be at least one
michael@0 644 // visible menu-item above and below it.
michael@0 645 item.hidden = true;
michael@0 646
michael@0 647 // We won't show the separator at all if no items are visible above it
michael@0 648 if (visibleItemsBeforeSep)
michael@0 649 separator = item;
michael@0 650
michael@0 651 // New separator, count again:
michael@0 652 visibleItemsBeforeSep = false;
michael@0 653 }
michael@0 654 }
michael@0 655
michael@0 656 // Set Open Folder/Links In Tabs items enabled state if they're visible
michael@0 657 if (anyVisible) {
michael@0 658 var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
michael@0 659 if (!openContainerInTabsItem.hidden && this._view.selectedNode &&
michael@0 660 PlacesUtils.nodeIsContainer(this._view.selectedNode)) {
michael@0 661 openContainerInTabsItem.disabled =
michael@0 662 !PlacesUtils.hasChildURIs(this._view.selectedNode);
michael@0 663 }
michael@0 664 else {
michael@0 665 // see selectiontype rule in the overlay
michael@0 666 var openLinksInTabsItem = document.getElementById("placesContext_openLinks:tabs");
michael@0 667 openLinksInTabsItem.disabled = openLinksInTabsItem.hidden;
michael@0 668 }
michael@0 669 }
michael@0 670
michael@0 671 return anyVisible;
michael@0 672 },
michael@0 673
michael@0 674 /**
michael@0 675 * Select all links in the current view.
michael@0 676 */
michael@0 677 selectAll: function PC_selectAll() {
michael@0 678 this._view.selectAll();
michael@0 679 },
michael@0 680
michael@0 681 /**
michael@0 682 * Opens the bookmark properties for the selected URI Node.
michael@0 683 */
michael@0 684 showBookmarkPropertiesForSelection:
michael@0 685 function PC_showBookmarkPropertiesForSelection() {
michael@0 686 var node = this._view.selectedNode;
michael@0 687 if (!node)
michael@0 688 return;
michael@0 689
michael@0 690 var itemType = PlacesUtils.nodeIsFolder(node) ||
michael@0 691 PlacesUtils.nodeIsTagQuery(node) ? "folder" : "bookmark";
michael@0 692 var concreteId = PlacesUtils.getConcreteItemId(node);
michael@0 693 var isRootItem = PlacesUtils.isRootItem(concreteId);
michael@0 694 var itemId = node.itemId;
michael@0 695 if (isRootItem || PlacesUtils.nodeIsTagQuery(node)) {
michael@0 696 // If this is a root or the Tags query we use the concrete itemId to catch
michael@0 697 // the correct title for the node.
michael@0 698 itemId = concreteId;
michael@0 699 }
michael@0 700
michael@0 701 PlacesUIUtils.showBookmarkDialog({ action: "edit"
michael@0 702 , type: itemType
michael@0 703 , itemId: itemId
michael@0 704 , readOnly: isRootItem
michael@0 705 , hiddenRows: [ "folderPicker" ]
michael@0 706 }, window.top);
michael@0 707 },
michael@0 708
michael@0 709 /**
michael@0 710 * This method can be run on a URI parameter to ensure that it didn't
michael@0 711 * receive a string instead of an nsIURI object.
michael@0 712 */
michael@0 713 _assertURINotString: function PC__assertURINotString(value) {
michael@0 714 NS_ASSERT((typeof(value) == "object") && !(value instanceof String),
michael@0 715 "This method should be passed a URI as a nsIURI object, not as a string.");
michael@0 716 },
michael@0 717
michael@0 718 /**
michael@0 719 * Reloads the selected livemark if any.
michael@0 720 */
michael@0 721 reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
michael@0 722 var selectedNode = this._view.selectedNode;
michael@0 723 if (selectedNode) {
michael@0 724 let itemId = selectedNode.itemId;
michael@0 725 PlacesUtils.livemarks.getLivemark({ id: itemId })
michael@0 726 .then(aLivemark => {
michael@0 727 aLivemark.reload(true);
michael@0 728 }, Components.utils.reportError);
michael@0 729 }
michael@0 730 },
michael@0 731
michael@0 732 /**
michael@0 733 * Opens the links in the selected folder, or the selected links in new tabs.
michael@0 734 */
michael@0 735 openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
michael@0 736 var node = this._view.selectedNode;
michael@0 737 if (node && PlacesUtils.nodeIsContainer(node))
michael@0 738 PlacesUIUtils.openContainerNodeInTabs(this._view.selectedNode, aEvent, this._view);
michael@0 739 else
michael@0 740 PlacesUIUtils.openURINodesInTabs(this._view.selectedNodes, aEvent, this._view);
michael@0 741 },
michael@0 742
michael@0 743 /**
michael@0 744 * Shows the Add Bookmark UI for the current insertion point.
michael@0 745 *
michael@0 746 * @param aType
michael@0 747 * the type of the new item (bookmark/livemark/folder)
michael@0 748 */
michael@0 749 newItem: function PC_newItem(aType) {
michael@0 750 let ip = this._view.insertionPoint;
michael@0 751 if (!ip)
michael@0 752 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 753
michael@0 754 let performed =
michael@0 755 PlacesUIUtils.showBookmarkDialog({ action: "add"
michael@0 756 , type: aType
michael@0 757 , defaultInsertionPoint: ip
michael@0 758 , hiddenRows: [ "folderPicker" ]
michael@0 759 }, window.top);
michael@0 760 if (performed) {
michael@0 761 // Select the new item.
michael@0 762 let insertedNodeId = PlacesUtils.bookmarks
michael@0 763 .getIdForItemAt(ip.itemId, ip.index);
michael@0 764 this._view.selectItems([insertedNodeId], false);
michael@0 765 }
michael@0 766 },
michael@0 767
michael@0 768 /**
michael@0 769 * Create a new Bookmark separator somewhere.
michael@0 770 */
michael@0 771 newSeparator: Task.async(function* () {
michael@0 772 var ip = this._view.insertionPoint;
michael@0 773 if (!ip)
michael@0 774 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 775
michael@0 776 if (!PlacesUIUtils.useAsyncTransactions) {
michael@0 777 let txn = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
michael@0 778 PlacesUtils.transactionManager.doTransaction(txn);
michael@0 779 // Select the new item.
michael@0 780 let insertedNodeId = PlacesUtils.bookmarks
michael@0 781 .getIdForItemAt(ip.itemId, ip.index);
michael@0 782 this._view.selectItems([insertedNodeId], false);
michael@0 783 return;
michael@0 784 }
michael@0 785
michael@0 786 let txn = PlacesTransactions.NewSeparator({ parentGUID: yield ip.promiseGUID()
michael@0 787 , index: ip.index });
michael@0 788 let guid = yield PlacesTransactions.transact(txn);
michael@0 789 let itemId = yield PlacesUtils.promiseItemId(guid);
michael@0 790 // Select the new item.
michael@0 791 this._view.selectItems([itemId], false);
michael@0 792 }),
michael@0 793
michael@0 794 /**
michael@0 795 * Opens a dialog for moving the selected nodes.
michael@0 796 */
michael@0 797 moveSelectedBookmarks: function PC_moveBookmarks() {
michael@0 798 window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
michael@0 799 "", "chrome, modal",
michael@0 800 this._view.selectedNodes);
michael@0 801 },
michael@0 802
michael@0 803 /**
michael@0 804 * Sort the selected folder by name
michael@0 805 */
michael@0 806 sortFolderByName: Task.async(function* () {
michael@0 807 let itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
michael@0 808 if (!PlacesUIUtils.useAsyncTransactions) {
michael@0 809 var txn = new PlacesSortFolderByNameTransaction(itemId);
michael@0 810 PlacesUtils.transactionManager.doTransaction(txn);
michael@0 811 return;
michael@0 812 }
michael@0 813 let guid = yield PlacesUtils.promiseItemGUID(itemId);
michael@0 814 yield PlacesTransactions.transact(PlacesTransactions.SortByName(guid));
michael@0 815 }),
michael@0 816
michael@0 817 /**
michael@0 818 * Walk the list of folders we're removing in this delete operation, and
michael@0 819 * see if the selected node specified is already implicitly being removed
michael@0 820 * because it is a child of that folder.
michael@0 821 * @param node
michael@0 822 * Node to check for containment.
michael@0 823 * @param pastFolders
michael@0 824 * List of folders the calling function has already traversed
michael@0 825 * @return true if the node should be skipped, false otherwise.
michael@0 826 */
michael@0 827 _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
michael@0 828 /**
michael@0 829 * Determines if a node is contained by another node within a resultset.
michael@0 830 * @param node
michael@0 831 * The node to check for containment for
michael@0 832 * @param parent
michael@0 833 * The parent container to check for containment in
michael@0 834 * @return true if node is a member of parent's children, false otherwise.
michael@0 835 */
michael@0 836 function isContainedBy(node, parent) {
michael@0 837 var cursor = node.parent;
michael@0 838 while (cursor) {
michael@0 839 if (cursor == parent)
michael@0 840 return true;
michael@0 841 cursor = cursor.parent;
michael@0 842 }
michael@0 843 return false;
michael@0 844 }
michael@0 845
michael@0 846 for (var j = 0; j < pastFolders.length; ++j) {
michael@0 847 if (isContainedBy(node, pastFolders[j]))
michael@0 848 return true;
michael@0 849 }
michael@0 850 return false;
michael@0 851 },
michael@0 852
michael@0 853 /**
michael@0 854 * Creates a set of transactions for the removal of a range of items.
michael@0 855 * A range is an array of adjacent nodes in a view.
michael@0 856 * @param [in] range
michael@0 857 * An array of nodes to remove. Should all be adjacent.
michael@0 858 * @param [out] transactions
michael@0 859 * An array of transactions.
michael@0 860 * @param [optional] removedFolders
michael@0 861 * An array of folder nodes that have already been removed.
michael@0 862 */
michael@0 863 _removeRange: function PC__removeRange(range, transactions, removedFolders) {
michael@0 864 NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
michael@0 865 if (!removedFolders)
michael@0 866 removedFolders = [];
michael@0 867
michael@0 868 for (var i = 0; i < range.length; ++i) {
michael@0 869 var node = range[i];
michael@0 870 if (this._shouldSkipNode(node, removedFolders))
michael@0 871 continue;
michael@0 872
michael@0 873 if (PlacesUtils.nodeIsTagQuery(node.parent)) {
michael@0 874 // This is a uri node inside a tag container. It needs a special
michael@0 875 // untag transaction.
michael@0 876 var tagItemId = PlacesUtils.getConcreteItemId(node.parent);
michael@0 877 var uri = NetUtil.newURI(node.uri);
michael@0 878 let txn = new PlacesUntagURITransaction(uri, [tagItemId]);
michael@0 879 transactions.push(txn);
michael@0 880 }
michael@0 881 else if (PlacesUtils.nodeIsTagQuery(node) && node.parent &&
michael@0 882 PlacesUtils.nodeIsQuery(node.parent) &&
michael@0 883 PlacesUtils.asQuery(node.parent).queryOptions.resultType ==
michael@0 884 Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY) {
michael@0 885 // This is a tag container.
michael@0 886 // Untag all URIs tagged with this tag only if the tag container is
michael@0 887 // child of the "Tags" query in the library, in all other places we
michael@0 888 // must only remove the query node.
michael@0 889 var tag = node.title;
michael@0 890 var URIs = PlacesUtils.tagging.getURIsForTag(tag);
michael@0 891 for (var j = 0; j < URIs.length; j++) {
michael@0 892 let txn = new PlacesUntagURITransaction(URIs[j], [tag]);
michael@0 893 transactions.push(txn);
michael@0 894 }
michael@0 895 }
michael@0 896 else if (PlacesUtils.nodeIsURI(node) &&
michael@0 897 PlacesUtils.nodeIsQuery(node.parent) &&
michael@0 898 PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
michael@0 899 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
michael@0 900 // This is a uri node inside an history query.
michael@0 901 PlacesUtils.bhistory.removePage(NetUtil.newURI(node.uri));
michael@0 902 // History deletes are not undoable, so we don't have a transaction.
michael@0 903 }
michael@0 904 else if (node.itemId == -1 &&
michael@0 905 PlacesUtils.nodeIsQuery(node) &&
michael@0 906 PlacesUtils.asQuery(node).queryOptions.queryType ==
michael@0 907 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
michael@0 908 // This is a dynamically generated history query, like queries
michael@0 909 // grouped by site, time or both. Dynamically generated queries don't
michael@0 910 // have an itemId even if they are descendants of a bookmark.
michael@0 911 this._removeHistoryContainer(node);
michael@0 912 // History deletes are not undoable, so we don't have a transaction.
michael@0 913 }
michael@0 914 else {
michael@0 915 // This is a common bookmark item.
michael@0 916 if (PlacesUtils.nodeIsFolder(node)) {
michael@0 917 // If this is a folder we add it to our array of folders, used
michael@0 918 // to skip nodes that are children of an already removed folder.
michael@0 919 removedFolders.push(node);
michael@0 920 }
michael@0 921 let txn = new PlacesRemoveItemTransaction(node.itemId);
michael@0 922 transactions.push(txn);
michael@0 923 }
michael@0 924 }
michael@0 925 },
michael@0 926
michael@0 927 /**
michael@0 928 * Removes the set of selected ranges from bookmarks.
michael@0 929 * @param txnName
michael@0 930 * See |remove|.
michael@0 931 */
michael@0 932 _removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
michael@0 933 var ranges = this._view.removableSelectionRanges;
michael@0 934 var transactions = [];
michael@0 935 var removedFolders = [];
michael@0 936
michael@0 937 for (var i = 0; i < ranges.length; i++)
michael@0 938 this._removeRange(ranges[i], transactions, removedFolders);
michael@0 939
michael@0 940 if (transactions.length > 0) {
michael@0 941 var txn = new PlacesAggregatedTransaction(txnName, transactions);
michael@0 942 PlacesUtils.transactionManager.doTransaction(txn);
michael@0 943 }
michael@0 944 },
michael@0 945
michael@0 946 /**
michael@0 947 * Removes the set of selected ranges from history.
michael@0 948 *
michael@0 949 * @note history deletes are not undoable.
michael@0 950 */
michael@0 951 _removeRowsFromHistory: function PC__removeRowsFromHistory() {
michael@0 952 let nodes = this._view.selectedNodes;
michael@0 953 let URIs = [];
michael@0 954 for (let i = 0; i < nodes.length; ++i) {
michael@0 955 let node = nodes[i];
michael@0 956 if (PlacesUtils.nodeIsURI(node)) {
michael@0 957 let uri = NetUtil.newURI(node.uri);
michael@0 958 // Avoid duplicates.
michael@0 959 if (URIs.indexOf(uri) < 0) {
michael@0 960 URIs.push(uri);
michael@0 961 }
michael@0 962 }
michael@0 963 else if (PlacesUtils.nodeIsQuery(node) &&
michael@0 964 PlacesUtils.asQuery(node).queryOptions.queryType ==
michael@0 965 Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
michael@0 966 this._removeHistoryContainer(node);
michael@0 967 }
michael@0 968 }
michael@0 969
michael@0 970 // Do removal in chunks to give some breath to main-thread.
michael@0 971 function pagesChunkGenerator(aURIs) {
michael@0 972 while (aURIs.length) {
michael@0 973 let URIslice = aURIs.splice(0, REMOVE_PAGES_CHUNKLEN);
michael@0 974 PlacesUtils.bhistory.removePages(URIslice, URIslice.length);
michael@0 975 Services.tm.mainThread.dispatch(function() {
michael@0 976 try {
michael@0 977 gen.next();
michael@0 978 } catch (ex if ex instanceof StopIteration) {}
michael@0 979 }, Ci.nsIThread.DISPATCH_NORMAL);
michael@0 980 yield undefined;
michael@0 981 }
michael@0 982 }
michael@0 983 let gen = pagesChunkGenerator(URIs);
michael@0 984 gen.next();
michael@0 985 },
michael@0 986
michael@0 987 /**
michael@0 988 * Removes history visits for an history container node.
michael@0 989 * @param [in] aContainerNode
michael@0 990 * The container node to remove.
michael@0 991 *
michael@0 992 * @note history deletes are not undoable.
michael@0 993 */
michael@0 994 _removeHistoryContainer: function PC__removeHistoryContainer(aContainerNode) {
michael@0 995 if (PlacesUtils.nodeIsHost(aContainerNode)) {
michael@0 996 // Site container.
michael@0 997 PlacesUtils.bhistory.removePagesFromHost(aContainerNode.title, true);
michael@0 998 }
michael@0 999 else if (PlacesUtils.nodeIsDay(aContainerNode)) {
michael@0 1000 // Day container.
michael@0 1001 let query = aContainerNode.getQueries()[0];
michael@0 1002 let beginTime = query.beginTime;
michael@0 1003 let endTime = query.endTime;
michael@0 1004 NS_ASSERT(query && beginTime && endTime,
michael@0 1005 "A valid date container query should exist!");
michael@0 1006 // We want to exclude beginTime from the removal because
michael@0 1007 // removePagesByTimeframe includes both extremes, while date containers
michael@0 1008 // exclude the lower extreme. So, if we would not exclude it, we would
michael@0 1009 // end up removing more history than requested.
michael@0 1010 PlacesUtils.bhistory.removePagesByTimeframe(beginTime + 1, endTime);
michael@0 1011 }
michael@0 1012 },
michael@0 1013
michael@0 1014 /**
michael@0 1015 * Removes the selection
michael@0 1016 * @param aTxnName
michael@0 1017 * A name for the transaction if this is being performed
michael@0 1018 * as part of another operation.
michael@0 1019 */
michael@0 1020 remove: function PC_remove(aTxnName) {
michael@0 1021 if (!this._hasRemovableSelection(false))
michael@0 1022 return;
michael@0 1023
michael@0 1024 NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
michael@0 1025
michael@0 1026 var root = this._view.result.root;
michael@0 1027
michael@0 1028 if (PlacesUtils.nodeIsFolder(root))
michael@0 1029 this._removeRowsFromBookmarks(aTxnName);
michael@0 1030 else if (PlacesUtils.nodeIsQuery(root)) {
michael@0 1031 var queryType = PlacesUtils.asQuery(root).queryOptions.queryType;
michael@0 1032 if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
michael@0 1033 this._removeRowsFromBookmarks(aTxnName);
michael@0 1034 else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
michael@0 1035 this._removeRowsFromHistory();
michael@0 1036 else
michael@0 1037 NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
michael@0 1038 }
michael@0 1039 else
michael@0 1040 NS_ASSERT(false, "unexpected root");
michael@0 1041 },
michael@0 1042
michael@0 1043 /**
michael@0 1044 * Fills a DataTransfer object with the content of the selection that can be
michael@0 1045 * dropped elsewhere.
michael@0 1046 * @param aEvent
michael@0 1047 * The dragstart event.
michael@0 1048 */
michael@0 1049 setDataTransfer: function PC_setDataTransfer(aEvent) {
michael@0 1050 let dt = aEvent.dataTransfer;
michael@0 1051
michael@0 1052 let result = this._view.result;
michael@0 1053 let didSuppressNotifications = result.suppressNotifications;
michael@0 1054 if (!didSuppressNotifications)
michael@0 1055 result.suppressNotifications = true;
michael@0 1056
michael@0 1057 function addData(type, index, overrideURI) {
michael@0 1058 let wrapNode = PlacesUtils.wrapNode(node, type, overrideURI);
michael@0 1059 dt.mozSetDataAt(type, wrapNode, index);
michael@0 1060 }
michael@0 1061
michael@0 1062 function addURIData(index, overrideURI) {
michael@0 1063 addData(PlacesUtils.TYPE_X_MOZ_URL, index, overrideURI);
michael@0 1064 addData(PlacesUtils.TYPE_UNICODE, index, overrideURI);
michael@0 1065 addData(PlacesUtils.TYPE_HTML, index, overrideURI);
michael@0 1066 }
michael@0 1067
michael@0 1068 try {
michael@0 1069 let nodes = this._view.draggableSelection;
michael@0 1070 for (let i = 0; i < nodes.length; ++i) {
michael@0 1071 var node = nodes[i];
michael@0 1072
michael@0 1073 // This order is _important_! It controls how this and other
michael@0 1074 // applications select data to be inserted based on type.
michael@0 1075 addData(PlacesUtils.TYPE_X_MOZ_PLACE, i);
michael@0 1076
michael@0 1077 // Drop the feed uri for livemark containers
michael@0 1078 let livemarkInfo = this.getCachedLivemarkInfo(node);
michael@0 1079 if (livemarkInfo) {
michael@0 1080 addURIData(i, livemarkInfo.feedURI.spec);
michael@0 1081 }
michael@0 1082 else if (node.uri) {
michael@0 1083 addURIData(i);
michael@0 1084 }
michael@0 1085 }
michael@0 1086 }
michael@0 1087 finally {
michael@0 1088 if (!didSuppressNotifications)
michael@0 1089 result.suppressNotifications = false;
michael@0 1090 }
michael@0 1091 },
michael@0 1092
michael@0 1093 get clipboardAction () {
michael@0 1094 let action = {};
michael@0 1095 let actionOwner;
michael@0 1096 try {
michael@0 1097 let xferable = Cc["@mozilla.org/widget/transferable;1"].
michael@0 1098 createInstance(Ci.nsITransferable);
michael@0 1099 xferable.init(null);
michael@0 1100 xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION)
michael@0 1101 this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
michael@0 1102 xferable.getTransferData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, action, {});
michael@0 1103 [action, actionOwner] =
michael@0 1104 action.value.QueryInterface(Ci.nsISupportsString).data.split(",");
michael@0 1105 } catch(ex) {
michael@0 1106 // Paste from external sources don't have any associated action, just
michael@0 1107 // fallback to a copy action.
michael@0 1108 return "copy";
michael@0 1109 }
michael@0 1110 // For cuts also check who inited the action, since cuts across different
michael@0 1111 // instances should instead be handled as copies (The sources are not
michael@0 1112 // available for this instance).
michael@0 1113 if (action == "cut" && actionOwner != this.profileName)
michael@0 1114 action = "copy";
michael@0 1115
michael@0 1116 return action;
michael@0 1117 },
michael@0 1118
michael@0 1119 _releaseClipboardOwnership: function PC__releaseClipboardOwnership() {
michael@0 1120 if (this.cutNodes.length > 0) {
michael@0 1121 // This clears the logical clipboard, doesn't remove data.
michael@0 1122 this.clipboard.emptyClipboard(Ci.nsIClipboard.kGlobalClipboard);
michael@0 1123 }
michael@0 1124 },
michael@0 1125
michael@0 1126 _clearClipboard: function PC__clearClipboard() {
michael@0 1127 let xferable = Cc["@mozilla.org/widget/transferable;1"].
michael@0 1128 createInstance(Ci.nsITransferable);
michael@0 1129 xferable.init(null);
michael@0 1130 // Empty transferables may cause crashes, so just add an unknown type.
michael@0 1131 const TYPE = "text/x-moz-place-empty";
michael@0 1132 xferable.addDataFlavor(TYPE);
michael@0 1133 xferable.setTransferData(TYPE, PlacesUtils.toISupportsString(""), 0);
michael@0 1134 this.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
michael@0 1135 },
michael@0 1136
michael@0 1137 _populateClipboard: function PC__populateClipboard(aNodes, aAction) {
michael@0 1138 // This order is _important_! It controls how this and other applications
michael@0 1139 // select data to be inserted based on type.
michael@0 1140 let contents = [
michael@0 1141 { type: PlacesUtils.TYPE_X_MOZ_PLACE, entries: [] },
michael@0 1142 { type: PlacesUtils.TYPE_X_MOZ_URL, entries: [] },
michael@0 1143 { type: PlacesUtils.TYPE_HTML, entries: [] },
michael@0 1144 { type: PlacesUtils.TYPE_UNICODE, entries: [] },
michael@0 1145 ];
michael@0 1146
michael@0 1147 // Avoid handling descendants of a copied node, the transactions take care
michael@0 1148 // of them automatically.
michael@0 1149 let copiedFolders = [];
michael@0 1150 aNodes.forEach(function (node) {
michael@0 1151 if (this._shouldSkipNode(node, copiedFolders))
michael@0 1152 return;
michael@0 1153 if (PlacesUtils.nodeIsFolder(node))
michael@0 1154 copiedFolders.push(node);
michael@0 1155
michael@0 1156 let livemarkInfo = this.getCachedLivemarkInfo(node);
michael@0 1157 let overrideURI = livemarkInfo ? livemarkInfo.feedURI.spec : null;
michael@0 1158
michael@0 1159 contents.forEach(function (content) {
michael@0 1160 content.entries.push(
michael@0 1161 PlacesUtils.wrapNode(node, content.type, overrideURI)
michael@0 1162 );
michael@0 1163 });
michael@0 1164 }, this);
michael@0 1165
michael@0 1166 function addData(type, data) {
michael@0 1167 xferable.addDataFlavor(type);
michael@0 1168 xferable.setTransferData(type, PlacesUtils.toISupportsString(data),
michael@0 1169 data.length * 2);
michael@0 1170 }
michael@0 1171
michael@0 1172 let xferable = Cc["@mozilla.org/widget/transferable;1"].
michael@0 1173 createInstance(Ci.nsITransferable);
michael@0 1174 xferable.init(null);
michael@0 1175 let hasData = false;
michael@0 1176 // This order matters here! It controls how this and other applications
michael@0 1177 // select data to be inserted based on type.
michael@0 1178 contents.forEach(function (content) {
michael@0 1179 if (content.entries.length > 0) {
michael@0 1180 hasData = true;
michael@0 1181 let glue =
michael@0 1182 content.type == PlacesUtils.TYPE_X_MOZ_PLACE ? "," : PlacesUtils.endl;
michael@0 1183 addData(content.type, content.entries.join(glue));
michael@0 1184 }
michael@0 1185 });
michael@0 1186
michael@0 1187 // Track the exected action in the xferable. This must be the last flavor
michael@0 1188 // since it's the least preferred one.
michael@0 1189 // Enqueue a unique instance identifier to distinguish operations across
michael@0 1190 // concurrent instances of the application.
michael@0 1191 addData(PlacesUtils.TYPE_X_MOZ_PLACE_ACTION, aAction + "," + this.profileName);
michael@0 1192
michael@0 1193 if (hasData) {
michael@0 1194 this.clipboard.setData(xferable,
michael@0 1195 this.cutNodes.length > 0 ? this : null,
michael@0 1196 Ci.nsIClipboard.kGlobalClipboard);
michael@0 1197 }
michael@0 1198 },
michael@0 1199
michael@0 1200 _cutNodes: [],
michael@0 1201 get cutNodes() this._cutNodes,
michael@0 1202 set cutNodes(aNodes) {
michael@0 1203 let self = this;
michael@0 1204 function updateCutNodes(aValue) {
michael@0 1205 self._cutNodes.forEach(function (aNode) {
michael@0 1206 self._view.toggleCutNode(aNode, aValue);
michael@0 1207 });
michael@0 1208 }
michael@0 1209
michael@0 1210 updateCutNodes(false);
michael@0 1211 this._cutNodes = aNodes;
michael@0 1212 updateCutNodes(true);
michael@0 1213 return aNodes;
michael@0 1214 },
michael@0 1215
michael@0 1216 /**
michael@0 1217 * Copy Bookmarks and Folders to the clipboard
michael@0 1218 */
michael@0 1219 copy: function PC_copy() {
michael@0 1220 let result = this._view.result;
michael@0 1221 let didSuppressNotifications = result.suppressNotifications;
michael@0 1222 if (!didSuppressNotifications)
michael@0 1223 result.suppressNotifications = true;
michael@0 1224 try {
michael@0 1225 this._populateClipboard(this._view.selectedNodes, "copy");
michael@0 1226 }
michael@0 1227 finally {
michael@0 1228 if (!didSuppressNotifications)
michael@0 1229 result.suppressNotifications = false;
michael@0 1230 }
michael@0 1231 },
michael@0 1232
michael@0 1233 /**
michael@0 1234 * Cut Bookmarks and Folders to the clipboard
michael@0 1235 */
michael@0 1236 cut: function PC_cut() {
michael@0 1237 let result = this._view.result;
michael@0 1238 let didSuppressNotifications = result.suppressNotifications;
michael@0 1239 if (!didSuppressNotifications)
michael@0 1240 result.suppressNotifications = true;
michael@0 1241 try {
michael@0 1242 this._populateClipboard(this._view.selectedNodes, "cut");
michael@0 1243 this.cutNodes = this._view.selectedNodes;
michael@0 1244 }
michael@0 1245 finally {
michael@0 1246 if (!didSuppressNotifications)
michael@0 1247 result.suppressNotifications = false;
michael@0 1248 }
michael@0 1249 },
michael@0 1250
michael@0 1251 /**
michael@0 1252 * Paste Bookmarks and Folders from the clipboard
michael@0 1253 */
michael@0 1254 paste: function PC_paste() {
michael@0 1255 // No reason to proceed if there isn't a valid insertion point.
michael@0 1256 let ip = this._view.insertionPoint;
michael@0 1257 if (!ip)
michael@0 1258 throw Cr.NS_ERROR_NOT_AVAILABLE;
michael@0 1259
michael@0 1260 let action = this.clipboardAction;
michael@0 1261
michael@0 1262 let xferable = Cc["@mozilla.org/widget/transferable;1"].
michael@0 1263 createInstance(Ci.nsITransferable);
michael@0 1264 xferable.init(null);
michael@0 1265 // This order matters here! It controls the preferred flavors for this
michael@0 1266 // paste operation.
michael@0 1267 [ PlacesUtils.TYPE_X_MOZ_PLACE,
michael@0 1268 PlacesUtils.TYPE_X_MOZ_URL,
michael@0 1269 PlacesUtils.TYPE_UNICODE,
michael@0 1270 ].forEach(function (type) xferable.addDataFlavor(type));
michael@0 1271
michael@0 1272 this.clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
michael@0 1273
michael@0 1274 // Now get the clipboard contents, in the best available flavor.
michael@0 1275 let data = {}, type = {}, items = [];
michael@0 1276 try {
michael@0 1277 xferable.getAnyTransferData(type, data, {});
michael@0 1278 data = data.value.QueryInterface(Ci.nsISupportsString).data;
michael@0 1279 type = type.value;
michael@0 1280 items = PlacesUtils.unwrapNodes(data, type);
michael@0 1281 } catch(ex) {
michael@0 1282 // No supported data exists or nodes unwrap failed, just bail out.
michael@0 1283 return;
michael@0 1284 }
michael@0 1285
michael@0 1286 let transactions = [];
michael@0 1287 let insertionIndex = ip.index;
michael@0 1288 for (let i = 0; i < items.length; ++i) {
michael@0 1289 if (ip.isTag) {
michael@0 1290 // Pasting into a tag container means tagging the item, regardless of
michael@0 1291 // the requested action.
michael@0 1292 let tagTxn = new PlacesTagURITransaction(NetUtil.newURI(items[i].uri),
michael@0 1293 [ip.itemId]);
michael@0 1294 transactions.push(tagTxn);
michael@0 1295 continue;
michael@0 1296 }
michael@0 1297
michael@0 1298 // Adjust index to make sure items are pasted in the correct position.
michael@0 1299 // If index is DEFAULT_INDEX, items are just appended.
michael@0 1300 if (ip.index != PlacesUtils.bookmarks.DEFAULT_INDEX)
michael@0 1301 insertionIndex = ip.index + i;
michael@0 1302
michael@0 1303 // If this is not a copy, check for safety that we can move the source,
michael@0 1304 // otherwise report an error and fallback to a copy.
michael@0 1305 if (action != "copy" && !PlacesControllerDragHelper.canMoveUnwrappedNode(items[i])) {
michael@0 1306 Components.utils.reportError("Tried to move an unmovable Places node, " +
michael@0 1307 "reverting to a copy operation.");
michael@0 1308 action = "copy";
michael@0 1309 }
michael@0 1310 transactions.push(
michael@0 1311 PlacesUIUtils.makeTransaction(items[i], type, ip.itemId,
michael@0 1312 insertionIndex, action == "copy")
michael@0 1313 );
michael@0 1314 }
michael@0 1315
michael@0 1316 let aggregatedTxn = new PlacesAggregatedTransaction("Paste", transactions);
michael@0 1317 PlacesUtils.transactionManager.doTransaction(aggregatedTxn);
michael@0 1318
michael@0 1319 // Cut/past operations are not repeatable, so clear the clipboard.
michael@0 1320 if (action == "cut") {
michael@0 1321 this._clearClipboard();
michael@0 1322 }
michael@0 1323
michael@0 1324 // Select the pasted items, they should be consecutive.
michael@0 1325 let insertedNodeIds = [];
michael@0 1326 for (let i = 0; i < transactions.length; ++i) {
michael@0 1327 insertedNodeIds.push(
michael@0 1328 PlacesUtils.bookmarks.getIdForItemAt(ip.itemId, ip.index + i)
michael@0 1329 );
michael@0 1330 }
michael@0 1331 if (insertedNodeIds.length > 0)
michael@0 1332 this._view.selectItems(insertedNodeIds, false);
michael@0 1333 },
michael@0 1334
michael@0 1335 /**
michael@0 1336 * Cache the livemark info for a node. This allows the controller and the
michael@0 1337 * views to treat the given node as a livemark.
michael@0 1338 * @param aNode
michael@0 1339 * a places result node.
michael@0 1340 * @param aLivemarkInfo
michael@0 1341 * a mozILivemarkInfo object.
michael@0 1342 */
michael@0 1343 cacheLivemarkInfo: function PC_cacheLivemarkInfo(aNode, aLivemarkInfo) {
michael@0 1344 this._cachedLivemarkInfoObjects.set(aNode, aLivemarkInfo);
michael@0 1345 },
michael@0 1346
michael@0 1347 /**
michael@0 1348 * Returns whether or not there's cached mozILivemarkInfo object for a node.
michael@0 1349 * @param aNode
michael@0 1350 * a places result node.
michael@0 1351 * @return true if there's a cached mozILivemarkInfo object for
michael@0 1352 * aNode, false otherwise.
michael@0 1353 */
michael@0 1354 hasCachedLivemarkInfo: function PC_hasCachedLivemarkInfo(aNode)
michael@0 1355 this._cachedLivemarkInfoObjects.has(aNode),
michael@0 1356
michael@0 1357 /**
michael@0 1358 * Returns the cached livemark info for a node, if set by cacheLivemarkInfo,
michael@0 1359 * null otherwise.
michael@0 1360 * @param aNode
michael@0 1361 * a places result node.
michael@0 1362 * @return the mozILivemarkInfo object for aNode, if set, null otherwise.
michael@0 1363 */
michael@0 1364 getCachedLivemarkInfo: function PC_getCachedLivemarkInfo(aNode)
michael@0 1365 this._cachedLivemarkInfoObjects.get(aNode, null)
michael@0 1366 };
michael@0 1367
michael@0 1368 /**
michael@0 1369 * Handles drag and drop operations for views. Note that this is view agnostic!
michael@0 1370 * You should not use PlacesController._view within these methods, since
michael@0 1371 * the view that the item(s) have been dropped on was not necessarily active.
michael@0 1372 * Drop functions are passed the view that is being dropped on.
michael@0 1373 */
michael@0 1374 let PlacesControllerDragHelper = {
michael@0 1375 /**
michael@0 1376 * DOM Element currently being dragged over
michael@0 1377 */
michael@0 1378 currentDropTarget: null,
michael@0 1379
michael@0 1380 /**
michael@0 1381 * Determines if the mouse is currently being dragged over a child node of
michael@0 1382 * this menu. This is necessary so that the menu doesn't close while the
michael@0 1383 * mouse is dragging over one of its submenus
michael@0 1384 * @param node
michael@0 1385 * The container node
michael@0 1386 * @return true if the user is dragging over a node within the hierarchy of
michael@0 1387 * the container, false otherwise.
michael@0 1388 */
michael@0 1389 draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
michael@0 1390 let currentNode = this.currentDropTarget;
michael@0 1391 while (currentNode) {
michael@0 1392 if (currentNode == node)
michael@0 1393 return true;
michael@0 1394 currentNode = currentNode.parentNode;
michael@0 1395 }
michael@0 1396 return false;
michael@0 1397 },
michael@0 1398
michael@0 1399 /**
michael@0 1400 * @return The current active drag session. Returns null if there is none.
michael@0 1401 */
michael@0 1402 getSession: function PCDH__getSession() {
michael@0 1403 return this.dragService.getCurrentSession();
michael@0 1404 },
michael@0 1405
michael@0 1406 /**
michael@0 1407 * Extract the first accepted flavor from a list of flavors.
michael@0 1408 * @param aFlavors
michael@0 1409 * The flavors list of type DOMStringList.
michael@0 1410 */
michael@0 1411 getFirstValidFlavor: function PCDH_getFirstValidFlavor(aFlavors) {
michael@0 1412 for (let i = 0; i < aFlavors.length; i++) {
michael@0 1413 if (this.GENERIC_VIEW_DROP_TYPES.indexOf(aFlavors[i]) != -1)
michael@0 1414 return aFlavors[i];
michael@0 1415 }
michael@0 1416
michael@0 1417 // If no supported flavor is found, check if data includes text/plain
michael@0 1418 // contents. If so, request them as text/unicode, a conversion will happen
michael@0 1419 // automatically.
michael@0 1420 if (aFlavors.contains("text/plain")) {
michael@0 1421 return PlacesUtils.TYPE_UNICODE;
michael@0 1422 }
michael@0 1423
michael@0 1424 return null;
michael@0 1425 },
michael@0 1426
michael@0 1427 /**
michael@0 1428 * Determines whether or not the data currently being dragged can be dropped
michael@0 1429 * on a places view.
michael@0 1430 * @param ip
michael@0 1431 * The insertion point where the items should be dropped.
michael@0 1432 */
michael@0 1433 canDrop: function PCDH_canDrop(ip, dt) {
michael@0 1434 let dropCount = dt.mozItemCount;
michael@0 1435
michael@0 1436 // Check every dragged item.
michael@0 1437 for (let i = 0; i < dropCount; i++) {
michael@0 1438 let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
michael@0 1439 if (!flavor)
michael@0 1440 return false;
michael@0 1441
michael@0 1442 // Urls can be dropped on any insertionpoint.
michael@0 1443 // XXXmano: remember that this method is called for each dragover event!
michael@0 1444 // Thus we shouldn't use unwrapNodes here at all if possible.
michael@0 1445 // I think it would be OK to accept bogus data here (e.g. text which was
michael@0 1446 // somehow wrapped as TAB_DROP_TYPE, this is not in our control, and
michael@0 1447 // will just case the actual drop to be a no-op), and only rule out valid
michael@0 1448 // expected cases, which are either unsupported flavors, or items which
michael@0 1449 // cannot be dropped in the current insertionpoint. The last case will
michael@0 1450 // likely force us to use unwrapNodes for the private data types of
michael@0 1451 // places.
michael@0 1452 if (flavor == TAB_DROP_TYPE)
michael@0 1453 continue;
michael@0 1454
michael@0 1455 let data = dt.mozGetDataAt(flavor, i);
michael@0 1456 let dragged;
michael@0 1457 try {
michael@0 1458 dragged = PlacesUtils.unwrapNodes(data, flavor)[0];
michael@0 1459 }
michael@0 1460 catch (e) {
michael@0 1461 return false;
michael@0 1462 }
michael@0 1463
michael@0 1464 // Only bookmarks and urls can be dropped into tag containers.
michael@0 1465 if (ip.isTag && ip.orientation == Ci.nsITreeView.DROP_ON &&
michael@0 1466 dragged.type != PlacesUtils.TYPE_X_MOZ_URL &&
michael@0 1467 (dragged.type != PlacesUtils.TYPE_X_MOZ_PLACE ||
michael@0 1468 (dragged.uri && dragged.uri.startsWith("place:")) ))
michael@0 1469 return false;
michael@0 1470
michael@0 1471 // The following loop disallows the dropping of a folder on itself or
michael@0 1472 // on any of its descendants.
michael@0 1473 if (dragged.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER ||
michael@0 1474 (dragged.uri && dragged.uri.startsWith("place:")) ) {
michael@0 1475 let parentId = ip.itemId;
michael@0 1476 while (parentId != PlacesUtils.placesRootId) {
michael@0 1477 if (dragged.concreteId == parentId || dragged.id == parentId)
michael@0 1478 return false;
michael@0 1479 parentId = PlacesUtils.bookmarks.getFolderIdForItem(parentId);
michael@0 1480 }
michael@0 1481 }
michael@0 1482 }
michael@0 1483 return true;
michael@0 1484 },
michael@0 1485
michael@0 1486 /**
michael@0 1487 * Determines if an unwrapped node can be moved.
michael@0 1488 *
michael@0 1489 * @param aUnwrappedNode
michael@0 1490 * A node unwrapped by PlacesUtils.unwrapNodes().
michael@0 1491 * @return True if the node can be moved, false otherwise.
michael@0 1492 */
michael@0 1493 canMoveUnwrappedNode: function (aUnwrappedNode) {
michael@0 1494 return aUnwrappedNode.id > 0 &&
michael@0 1495 !PlacesUtils.isRootItem(aUnwrappedNode.id) &&
michael@0 1496 aUnwrappedNode.parent != PlacesUtils.placesRootId &&
michael@0 1497 aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
michael@0 1498 aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId &&
michael@0 1499 !aUnwrappedNode.parentReadOnly;
michael@0 1500 },
michael@0 1501
michael@0 1502 /**
michael@0 1503 * Determines if a node can be moved.
michael@0 1504 *
michael@0 1505 * @param aNode
michael@0 1506 * A nsINavHistoryResultNode node.
michael@0 1507 * @return True if the node can be moved, false otherwise.
michael@0 1508 */
michael@0 1509 canMoveNode:
michael@0 1510 function PCDH_canMoveNode(aNode) {
michael@0 1511 // Can't move query root.
michael@0 1512 if (!aNode.parent)
michael@0 1513 return false;
michael@0 1514
michael@0 1515 let parentId = PlacesUtils.getConcreteItemId(aNode.parent);
michael@0 1516 let concreteId = PlacesUtils.getConcreteItemId(aNode);
michael@0 1517
michael@0 1518 // Can't move children of tag containers.
michael@0 1519 if (PlacesUtils.nodeIsTagQuery(aNode.parent))
michael@0 1520 return false;
michael@0 1521
michael@0 1522 // Can't move children of read-only containers.
michael@0 1523 if (PlacesUtils.nodeIsReadOnly(aNode.parent))
michael@0 1524 return false;
michael@0 1525
michael@0 1526 // Check for special folders, etc.
michael@0 1527 if (PlacesUtils.nodeIsContainer(aNode) &&
michael@0 1528 !this.canMoveContainer(aNode.itemId, parentId))
michael@0 1529 return false;
michael@0 1530
michael@0 1531 return true;
michael@0 1532 },
michael@0 1533
michael@0 1534 /**
michael@0 1535 * Determines if a container node can be moved.
michael@0 1536 *
michael@0 1537 * @param aId
michael@0 1538 * A bookmark folder id.
michael@0 1539 * @param [optional] aParentId
michael@0 1540 * The parent id of the folder.
michael@0 1541 * @return True if the container can be moved to the target.
michael@0 1542 */
michael@0 1543 canMoveContainer:
michael@0 1544 function PCDH_canMoveContainer(aId, aParentId) {
michael@0 1545 if (aId == -1)
michael@0 1546 return false;
michael@0 1547
michael@0 1548 // Disallow moving of roots and special folders.
michael@0 1549 const ROOTS = [PlacesUtils.placesRootId, PlacesUtils.bookmarksMenuFolderId,
michael@0 1550 PlacesUtils.tagsFolderId, PlacesUtils.unfiledBookmarksFolderId,
michael@0 1551 PlacesUtils.toolbarFolderId];
michael@0 1552 if (ROOTS.indexOf(aId) != -1)
michael@0 1553 return false;
michael@0 1554
michael@0 1555 // Get parent id if necessary.
michael@0 1556 if (aParentId == null || aParentId == -1)
michael@0 1557 aParentId = PlacesUtils.bookmarks.getFolderIdForItem(aId);
michael@0 1558
michael@0 1559 if (PlacesUtils.bookmarks.getFolderReadonly(aParentId))
michael@0 1560 return false;
michael@0 1561
michael@0 1562 return true;
michael@0 1563 },
michael@0 1564
michael@0 1565 /**
michael@0 1566 * Handles the drop of one or more items onto a view.
michael@0 1567 * @param insertionPoint
michael@0 1568 * The insertion point where the items should be dropped
michael@0 1569 */
michael@0 1570 onDrop: function PCDH_onDrop(insertionPoint, dt) {
michael@0 1571 let doCopy = ["copy", "link"].indexOf(dt.dropEffect) != -1;
michael@0 1572
michael@0 1573 let transactions = [];
michael@0 1574 let dropCount = dt.mozItemCount;
michael@0 1575 let movedCount = 0;
michael@0 1576 for (let i = 0; i < dropCount; ++i) {
michael@0 1577 let flavor = this.getFirstValidFlavor(dt.mozTypesAt(i));
michael@0 1578 if (!flavor)
michael@0 1579 return;
michael@0 1580
michael@0 1581 let data = dt.mozGetDataAt(flavor, i);
michael@0 1582 let unwrapped;
michael@0 1583 if (flavor != TAB_DROP_TYPE) {
michael@0 1584 // There's only ever one in the D&D case.
michael@0 1585 unwrapped = PlacesUtils.unwrapNodes(data, flavor)[0];
michael@0 1586 }
michael@0 1587 else if (data instanceof XULElement && data.localName == "tab" &&
michael@0 1588 data.ownerDocument.defaultView instanceof ChromeWindow) {
michael@0 1589 let uri = data.linkedBrowser.currentURI;
michael@0 1590 let spec = uri ? uri.spec : "about:blank";
michael@0 1591 let title = data.label;
michael@0 1592 unwrapped = { uri: spec,
michael@0 1593 title: data.label,
michael@0 1594 type: PlacesUtils.TYPE_X_MOZ_URL};
michael@0 1595 }
michael@0 1596 else
michael@0 1597 throw("bogus data was passed as a tab")
michael@0 1598
michael@0 1599 let index = insertionPoint.index;
michael@0 1600
michael@0 1601 // Adjust insertion index to prevent reversal of dragged items. When you
michael@0 1602 // drag multiple elts upward: need to increment index or each successive
michael@0 1603 // elt will be inserted at the same index, each above the previous.
michael@0 1604 let dragginUp = insertionPoint.itemId == unwrapped.parent &&
michael@0 1605 index < PlacesUtils.bookmarks.getItemIndex(unwrapped.id);
michael@0 1606 if (index != -1 && dragginUp)
michael@0 1607 index+= movedCount++;
michael@0 1608
michael@0 1609 // If dragging over a tag container we should tag the item.
michael@0 1610 if (insertionPoint.isTag &&
michael@0 1611 insertionPoint.orientation == Ci.nsITreeView.DROP_ON) {
michael@0 1612 let uri = NetUtil.newURI(unwrapped.uri);
michael@0 1613 let tagItemId = insertionPoint.itemId;
michael@0 1614 let tagTxn = new PlacesTagURITransaction(uri, [tagItemId]);
michael@0 1615 transactions.push(tagTxn);
michael@0 1616 }
michael@0 1617 else {
michael@0 1618 // If this is not a copy, check for safety that we can move the source,
michael@0 1619 // otherwise report an error and fallback to a copy.
michael@0 1620 if (!doCopy && !PlacesControllerDragHelper.canMoveUnwrappedNode(unwrapped)) {
michael@0 1621 Components.utils.reportError("Tried to move an unmovable Places node, " +
michael@0 1622 "reverting to a copy operation.");
michael@0 1623 doCopy = true;
michael@0 1624 }
michael@0 1625 transactions.push(PlacesUIUtils.makeTransaction(unwrapped,
michael@0 1626 flavor, insertionPoint.itemId,
michael@0 1627 index, doCopy));
michael@0 1628 }
michael@0 1629 }
michael@0 1630
michael@0 1631 let txn = new PlacesAggregatedTransaction("DropItems", transactions);
michael@0 1632 PlacesUtils.transactionManager.doTransaction(txn);
michael@0 1633 },
michael@0 1634
michael@0 1635 /**
michael@0 1636 * Checks if we can insert into a container.
michael@0 1637 * @param aContainer
michael@0 1638 * The container were we are want to drop
michael@0 1639 */
michael@0 1640 disallowInsertion: function(aContainer) {
michael@0 1641 NS_ASSERT(aContainer, "empty container");
michael@0 1642 // Allow dropping into Tag containers.
michael@0 1643 if (PlacesUtils.nodeIsTagQuery(aContainer))
michael@0 1644 return false;
michael@0 1645 // Disallow insertion of items under readonly folders.
michael@0 1646 return (!PlacesUtils.nodeIsFolder(aContainer) ||
michael@0 1647 PlacesUtils.nodeIsReadOnly(aContainer));
michael@0 1648 },
michael@0 1649
michael@0 1650 placesFlavors: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
michael@0 1651 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
michael@0 1652 PlacesUtils.TYPE_X_MOZ_PLACE],
michael@0 1653
michael@0 1654 // The order matters.
michael@0 1655 GENERIC_VIEW_DROP_TYPES: [PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
michael@0 1656 PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
michael@0 1657 PlacesUtils.TYPE_X_MOZ_PLACE,
michael@0 1658 PlacesUtils.TYPE_X_MOZ_URL,
michael@0 1659 TAB_DROP_TYPE,
michael@0 1660 PlacesUtils.TYPE_UNICODE],
michael@0 1661 };
michael@0 1662
michael@0 1663
michael@0 1664 XPCOMUtils.defineLazyServiceGetter(PlacesControllerDragHelper, "dragService",
michael@0 1665 "@mozilla.org/widget/dragservice;1",
michael@0 1666 "nsIDragService");
michael@0 1667
michael@0 1668 function goUpdatePlacesCommands() {
michael@0 1669 // Get the controller for one of the places commands.
michael@0 1670 var placesController = doGetPlacesControllerForCommand("placesCmd_open");
michael@0 1671 function updatePlacesCommand(aCommand) {
michael@0 1672 goSetCommandEnabled(aCommand, placesController &&
michael@0 1673 placesController.isCommandEnabled(aCommand));
michael@0 1674 }
michael@0 1675
michael@0 1676 updatePlacesCommand("placesCmd_open");
michael@0 1677 updatePlacesCommand("placesCmd_open:window");
michael@0 1678 updatePlacesCommand("placesCmd_open:tab");
michael@0 1679 updatePlacesCommand("placesCmd_new:folder");
michael@0 1680 updatePlacesCommand("placesCmd_new:bookmark");
michael@0 1681 updatePlacesCommand("placesCmd_new:separator");
michael@0 1682 updatePlacesCommand("placesCmd_show:info");
michael@0 1683 updatePlacesCommand("placesCmd_moveBookmarks");
michael@0 1684 updatePlacesCommand("placesCmd_reload");
michael@0 1685 updatePlacesCommand("placesCmd_sortBy:name");
michael@0 1686 updatePlacesCommand("placesCmd_cut");
michael@0 1687 updatePlacesCommand("placesCmd_copy");
michael@0 1688 updatePlacesCommand("placesCmd_paste");
michael@0 1689 updatePlacesCommand("placesCmd_delete");
michael@0 1690 }
michael@0 1691
michael@0 1692 function doGetPlacesControllerForCommand(aCommand)
michael@0 1693 {
michael@0 1694 // A context menu may be built for non-focusable views. Thus, we first try
michael@0 1695 // to look for a view associated with document.popupNode
michael@0 1696 let popupNode;
michael@0 1697 try {
michael@0 1698 popupNode = document.popupNode;
michael@0 1699 } catch (e) {
michael@0 1700 // The document went away (bug 797307).
michael@0 1701 return null;
michael@0 1702 }
michael@0 1703 if (popupNode) {
michael@0 1704 let view = PlacesUIUtils.getViewForNode(popupNode);
michael@0 1705 if (view && view._contextMenuShown)
michael@0 1706 return view.controllers.getControllerForCommand(aCommand);
michael@0 1707 }
michael@0 1708
michael@0 1709 // When we're not building a context menu, only focusable views
michael@0 1710 // are possible. Thus, we can safely use the command dispatcher.
michael@0 1711 let controller = top.document.commandDispatcher
michael@0 1712 .getControllerForCommand(aCommand);
michael@0 1713 if (controller)
michael@0 1714 return controller;
michael@0 1715
michael@0 1716 return null;
michael@0 1717 }
michael@0 1718
michael@0 1719 function goDoPlacesCommand(aCommand)
michael@0 1720 {
michael@0 1721 let controller = doGetPlacesControllerForCommand(aCommand);
michael@0 1722 if (controller && controller.isCommandEnabled(aCommand))
michael@0 1723 controller.doCommand(aCommand);
michael@0 1724 }
michael@0 1725

mercurial