browser/components/places/content/controller.js

changeset 0
6474c204b198
     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 +

mercurial