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.

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

mercurial