forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			1666 lines
		
	
	
	
		
			60 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable file
		
	
	
	
	
			
		
		
	
	
			1666 lines
		
	
	
	
		
			60 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable file
		
	
	
	
	
| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 | |
| /* ***** BEGIN LICENSE BLOCK *****
 | |
|  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 | |
|  *
 | |
|  * The contents of this file are subject to the Mozilla Public License Version
 | |
|  * 1.1 (the "License"); you may not use this file except in compliance with
 | |
|  * the License. You may obtain a copy of the License at
 | |
|  * http://www.mozilla.org/MPL/
 | |
|  *
 | |
|  * Software distributed under the License is distributed on an "AS IS" basis,
 | |
|  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 | |
|  * for the specific language governing rights and limitations under the
 | |
|  * License.
 | |
|  *
 | |
|  * The Original Code is the Places Command Controller.
 | |
|  *
 | |
|  * The Initial Developer of the Original Code is Google Inc.
 | |
|  * Portions created by the Initial Developer are Copyright (C) 2005
 | |
|  * the Initial Developer. All Rights Reserved.
 | |
|  *
 | |
|  * Contributor(s):
 | |
|  *   Ben Goodger <beng@google.com>
 | |
|  *   Myk Melez <myk@mozilla.org>
 | |
|  *   Asaf Romano <mano@mozilla.com>
 | |
|  *
 | |
|  * Alternatively, the contents of this file may be used under the terms of
 | |
|  * either the GNU General Public License Version 2 or later (the "GPL"), or
 | |
|  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 | |
|  * in which case the provisions of the GPL or the LGPL are applicable instead
 | |
|  * of those above. If you wish to allow use of your version of this file only
 | |
|  * under the terms of either the GPL or the LGPL, and not to allow others to
 | |
|  * use your version of this file under the terms of the MPL, indicate your
 | |
|  * decision by deleting the provisions above and replace them with the notice
 | |
|  * and other provisions required by the GPL or the LGPL. If you do not delete
 | |
|  * the provisions above, a recipient may use your version of this file under
 | |
|  * the terms of any one of the MPL, the GPL or the LGPL.
 | |
|  *
 | |
|  * ***** END LICENSE BLOCK ***** */
 | |
| 
 | |
| // XXXmano: we should move most/all of these constants to PlacesUtils
 | |
| const ORGANIZER_ROOT_BOOKMARKS = "place:folder=2&excludeItems=1&queryType=1";
 | |
| const ORGANIZER_SUBSCRIPTIONS_QUERY = "place:annotation=livemark%2FfeedURI";
 | |
| 
 | |
| // No change to the view, preserve current selection
 | |
| const RELOAD_ACTION_NOTHING = 0;
 | |
| // Inserting items new to the view, select the inserted rows
 | |
| const RELOAD_ACTION_INSERT = 1;
 | |
| // Removing items from the view, select the first item after the last selected
 | |
| const RELOAD_ACTION_REMOVE = 2;
 | |
| // Moving items within a view, don't treat the dropped items as additional 
 | |
| // rows.
 | |
| const RELOAD_ACTION_MOVE = 3;
 | |
| 
 | |
| // when removing a bunch of pages we split them in chunks to avoid passing
 | |
| // a too big array to RemovePages
 | |
| // 300 is the best choice with an history of about 150000 visits
 | |
| // smaller chunks could cause a Slow Script warning with a huge history
 | |
| const REMOVE_PAGES_CHUNKLEN = 300;
 | |
| // if we are removing less than this pages we will remove them one by one
 | |
| // since it will be reflected faster on the UI
 | |
| // 10 is a good compromise, since allows the user to delete a little amount of
 | |
| // urls for privacy reasons, but does not cause heavy disk access
 | |
| const REMOVE_PAGES_MAX_SINGLEREMOVES = 10;
 | |
| 
 | |
| /**
 | |
|  * Represents an insertion point within a container where we can insert
 | |
|  * items. 
 | |
|  * @param   aItemId
 | |
|  *          The identifier of the parent container
 | |
|  * @param   aIndex
 | |
|  *          The index within the container where we should insert
 | |
|  * @param   aOrientation
 | |
|  *          The orientation of the insertion. NOTE: the adjustments to the
 | |
|  *          insertion point to accommodate the orientation should be done by
 | |
|  *          the person who constructs the IP, not the user. The orientation
 | |
|  *          is provided for informational purposes only!
 | |
|  * @constructor
 | |
|  */
 | |
| function InsertionPoint(aItemId, aIndex, aOrientation) {
 | |
|   this.itemId = aItemId;
 | |
|   this.index = aIndex;
 | |
|   this.orientation = aOrientation;
 | |
| }
 | |
| InsertionPoint.prototype.toString = function IP_toString() {
 | |
|   return "[object InsertionPoint(folder:" + this.itemId + ",index:" + this.index + ",orientation:" + this.orientation + ")]";
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Places Controller
 | |
|  */
 | |
| 
 | |
| function PlacesController(aView) {
 | |
|   this._view = aView;
 | |
| }
 | |
| 
 | |
| PlacesController.prototype = {
 | |
|   /**
 | |
|    * The places view.
 | |
|    */
 | |
|   _view: null,
 | |
| 
 | |
|   isCommandEnabled: function PC_isCommandEnabled(aCommand) {
 | |
|     switch (aCommand) {
 | |
|     case "cmd_undo":
 | |
|       return PlacesUtils.ptm.numberOfUndoItems > 0;
 | |
|     case "cmd_redo":
 | |
|       return PlacesUtils.ptm.numberOfRedoItems > 0;
 | |
|     case "cmd_cut":
 | |
|     case "cmd_delete":
 | |
|       return this._hasRemovableSelection(false);
 | |
|     case "placesCmd_moveBookmarks":
 | |
|       return this._hasRemovableSelection(true);
 | |
|     case "cmd_copy":
 | |
|       return this._view.hasSelection;
 | |
|     case "cmd_paste":
 | |
|       return this._canInsert() && this._isClipboardDataPasteable();
 | |
|     case "cmd_selectAll":
 | |
|       if (this._view.selType != "single") {
 | |
|         var result = this._view.getResult();
 | |
|         if (result) {
 | |
|           var container = asContainer(result.root);
 | |
|           if (container.childCount > 0);
 | |
|             return true;
 | |
|         }
 | |
|       }
 | |
|       return false;
 | |
|     case "placesCmd_open":
 | |
|     case "placesCmd_open:window":
 | |
|     case "placesCmd_open:tab":
 | |
|       return this._view.selectedURINode;
 | |
|     case "placesCmd_new:folder":
 | |
|     case "placesCmd_new:livemark":
 | |
|       return this._canInsert();
 | |
|     case "placesCmd_new:bookmark":
 | |
|       return this._canInsert();
 | |
|     case "placesCmd_new:separator":
 | |
|       return this._canInsert() &&
 | |
|              !asQuery(this._view.getResult().root).queryOptions.excludeItems &&
 | |
|              this._view.getResult().sortingMode ==
 | |
|                  Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
 | |
|     case "placesCmd_show:info":
 | |
|       if (this._view.hasSingleSelection) {
 | |
|         var selectedNode = this._view.selectedNode;
 | |
|         if (PlacesUtils.nodeIsFolder(selectedNode) ||
 | |
|             (PlacesUtils.nodeIsBookmark(selectedNode) &&
 | |
|             !PlacesUtils.nodeIsLivemarkItem(selectedNode)))
 | |
|           return true;
 | |
|       }
 | |
|       return false;
 | |
|     case "placesCmd_reloadMicrosummary":
 | |
|       if (this._view.hasSingleSelection) {
 | |
|         var selectedNode = this._view.selectedNode;
 | |
|         if (PlacesUtils.nodeIsBookmark(selectedNode)) {
 | |
|           var mss = PlacesUtils.microsummaries;
 | |
|           if (mss.hasMicrosummary(selectedNode.itemId))
 | |
|             return true;
 | |
|         }
 | |
|       }
 | |
|       return false;
 | |
|     case "placesCmd_reload":
 | |
|       if (this._view.hasSingleSelection) {
 | |
|         var selectedNode = this._view.selectedNode;
 | |
| 
 | |
|         // Livemark containers
 | |
|         if (PlacesUtils.nodeIsLivemarkContainer(selectedNode))
 | |
|           return true;
 | |
|       }
 | |
|       return false;
 | |
|     case "placesCmd_sortBy:name":
 | |
|       var selectedNode = this._view.selectedNode;
 | |
|       return selectedNode &&
 | |
|              PlacesUtils.nodeIsFolder(selectedNode) &&
 | |
|              !PlacesUtils.nodeIsReadOnly(selectedNode) &&
 | |
|              this._view.getResult().sortingMode ==
 | |
|                  Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
 | |
|     default:
 | |
|       return false;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   supportsCommand: function PC_supportsCommand(aCommand) {
 | |
|     //LOG("supportsCommand: " + command);
 | |
|     // Non-Places specific commands that we also support
 | |
|     switch (aCommand) {
 | |
|     case "cmd_undo":
 | |
|     case "cmd_redo":
 | |
|     case "cmd_cut":
 | |
|     case "cmd_copy":
 | |
|     case "cmd_paste":
 | |
|     case "cmd_delete":
 | |
|     case "cmd_selectAll":
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     // All other Places Commands are prefixed with "placesCmd_" ... this 
 | |
|     // filters out other commands that we do _not_ support (see 329587).
 | |
|     const CMD_PREFIX = "placesCmd_";
 | |
|     return (aCommand.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
 | |
|   },
 | |
| 
 | |
|   doCommand: function PC_doCommand(aCommand) {
 | |
|     switch (aCommand) {
 | |
|     case "cmd_undo":
 | |
|       PlacesUtils.ptm.undoTransaction();
 | |
|       break;
 | |
|     case "cmd_redo":
 | |
|       PlacesUtils.ptm.redoTransaction();
 | |
|       break;
 | |
|     case "cmd_cut":
 | |
|       this.cut();
 | |
|       break;
 | |
|     case "cmd_copy":
 | |
|       this.copy();
 | |
|       break;
 | |
|     case "cmd_paste":
 | |
|       this.paste();
 | |
|       break;
 | |
|     case "cmd_delete":
 | |
|       this.remove("Remove Selection");
 | |
|       break;
 | |
|     case "cmd_selectAll":
 | |
|       this.selectAll();
 | |
|       break;
 | |
|     case "placesCmd_open":
 | |
|       this.openSelectedNodeIn("current");
 | |
|       break;
 | |
|     case "placesCmd_open:window":
 | |
|       this.openSelectedNodeIn("window");
 | |
|       break;
 | |
|     case "placesCmd_open:tab":
 | |
|       this.openSelectedNodeIn("tab");
 | |
|       break;
 | |
|     case "placesCmd_new:folder":
 | |
|       this.newItem("folder");
 | |
|       break;
 | |
|     case "placesCmd_new:bookmark":
 | |
|       this.newItem("bookmark");
 | |
|       break;
 | |
|     case "placesCmd_new:livemark":
 | |
|       this.newItem("livemark");
 | |
|       break;
 | |
|     case "placesCmd_new:separator":
 | |
|       this.newSeparator();
 | |
|       break;
 | |
|     case "placesCmd_show:info":
 | |
|       this.showBookmarkPropertiesForSelection();
 | |
|       break;
 | |
|     case "placesCmd_moveBookmarks":
 | |
|       this.moveSelectedBookmarks();
 | |
|       break;
 | |
|     case "placesCmd_reload":
 | |
|       this.reloadSelectedLivemark();
 | |
|       break;
 | |
|     case "placesCmd_reloadMicrosummary":
 | |
|       this.reloadSelectedMicrosummary();
 | |
|       break;
 | |
|     case "placesCmd_sortBy:name":
 | |
|       this.sortFolderByName();
 | |
|       break;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onEvent: function PC_onEvent(eventName) { },
 | |
| 
 | |
|   
 | |
|   /**
 | |
|    * Determine whether or not the selection can be removed, either by the 
 | |
|    * delete or cut operations based on whether or not any of its contents
 | |
|    * are non-removable. We don't need to worry about recursion here since it
 | |
|    * is a policy decision that a removable item not be placed inside a non-
 | |
|    * removable item.
 | |
|    * @param aIsMoveCommand
 | |
|    *        True if thecommand for which this method is called only moves the
 | |
|    *        selected items to another container, false otherwise.
 | |
|    * @returns true if the there's a selection which has no nodes that cannot be removed,
 | |
|    *          false otherwise.
 | |
|    */
 | |
|   _hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) {
 | |
|     if (!this._view.hasSelection)
 | |
|       return false;
 | |
| 
 | |
|     var nodes = this._view.getSelectionNodes();
 | |
|     var root = this._view.getResultNode();
 | |
| 
 | |
|     for (var i = 0; i < nodes.length; ++i) {
 | |
|       // Disallow removing the view's root node
 | |
|       if (nodes[i] == root)
 | |
|         return false;
 | |
| 
 | |
|       // Disallow removing shortcuts from the left pane
 | |
|       var nodeItemId = nodes[i].itemId;
 | |
|       if (PlacesUtils.annotations
 | |
|                      .itemHasAnnotation(nodeItemId, ORGANIZER_QUERY_ANNO))
 | |
|         return false;
 | |
| 
 | |
|       // Disallow removing the toolbar, menu and unfiled-bookmarks folders
 | |
|       if (!aIsMoveCommand &&
 | |
|            (nodeItemId == PlacesUtils.toolbarFolderId ||
 | |
|             nodeItemId == PlacesUtils.unfiledBookmarksFolderId ||
 | |
|             nodeItemId == PlacesUtils.bookmarksMenuFolderId))
 | |
|         return false;
 | |
| 
 | |
|       // We don't call nodeIsReadOnly here, because nodeIsReadOnly means that
 | |
|       // a node has children that cannot be edited, reordered or removed. Here,
 | |
|       // we don't care if a node's children can't be reordered or edited, just
 | |
|       // that they're removable. All history results have removable children
 | |
|       // (based on the principle that any URL in the history table should be
 | |
|       // removable), but some special bookmark folders may have non-removable
 | |
|       // children, e.g. live bookmark folder children. It doesn't make sense
 | |
|       // to delete a child of a live bookmark folder, since when the folder
 | |
|       // refreshes, the child will return.
 | |
|       var parent = nodes[i].parent || root;
 | |
|       if (PlacesUtils.isReadonlyFolder(parent))
 | |
|         return false;
 | |
|     }
 | |
|     return true;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines whether or not nodes can be inserted relative to the selection.
 | |
|    */
 | |
|   _canInsert: function PC__canInsert() {
 | |
|     return this._view.insertionPoint != null;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines whether or not the root node for the view is selected
 | |
|    */
 | |
|   rootNodeIsSelected: function PC_rootNodeIsSelected() {
 | |
|     if (this._view.hasSelection) {
 | |
|       var nodes = this._view.getSelectionNodes();
 | |
|       var root = this._view.getResultNode();
 | |
|       for (var i = 0; i < nodes.length; ++i) {
 | |
|         if (nodes[i] == root)
 | |
|           return true;      
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Looks at the data on the clipboard to see if it is paste-able. 
 | |
|    * Paste-able data is:
 | |
|    *   - in a format that the view can receive
 | |
|    * @returns true if: - clipboard data is of a TYPE_X_MOZ_PLACE_* flavor,
 | |
|                        - clipboard data is of type TEXT_UNICODE and
 | |
|                          is a valid URI.
 | |
|    */
 | |
|   _isClipboardDataPasteable: function PC__isClipboardDataPasteable() {
 | |
|     // if the clipboard contains TYPE_X_MOZ_PLACE_* data, it is definitely
 | |
|     // pasteable, with no need to unwrap all the nodes.
 | |
| 
 | |
|     var flavors = PlacesUtils.placesFlavors;
 | |
|     var clipboard = PlacesUtils.clipboard;
 | |
|     var hasPlacesData =
 | |
|       clipboard.hasDataMatchingFlavors(flavors, flavors.length,
 | |
|                                        Ci.nsIClipboard.kGlobalClipboard);
 | |
|     if (hasPlacesData)
 | |
|       return this._view.insertionPoint != null;
 | |
| 
 | |
|     // if the clipboard doesn't have TYPE_X_MOZ_PLACE_* data, we also allow
 | |
|     // pasting of valid "text/unicode" and "text/x-moz-url" data
 | |
|     var xferable = Cc["@mozilla.org/widget/transferable;1"].
 | |
|                    createInstance(Ci.nsITransferable);
 | |
| 
 | |
|     xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
 | |
|     xferable.addDataFlavor(PlacesUtils.TYPE_UNICODE);
 | |
|     clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
 | |
| 
 | |
|     try {
 | |
|       // getAnyTransferData will throw if no data is available.
 | |
|       var data = { }, type = { };
 | |
|       xferable.getAnyTransferData(type, data, { });
 | |
|       data = data.value.QueryInterface(Ci.nsISupportsString).data;
 | |
|       if (type.value != PlacesUtils.TYPE_X_MOZ_URL &&
 | |
|           type.value != PlacesUtils.TYPE_UNICODE)
 | |
|         return false;
 | |
| 
 | |
|       // unwrapNodes() will throw if the data blob is malformed.
 | |
|       var unwrappedNodes = PlacesUtils.unwrapNodes(data, type.value);
 | |
|       return this._view.insertionPoint != null;
 | |
|     }
 | |
|     catch (e) {
 | |
|       // getAnyTransferData or unwrapNodes failed
 | |
|       return false;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /** 
 | |
|    * Gathers information about the selected nodes according to the following
 | |
|    * rules:
 | |
|    *    "link"              node is a URI
 | |
|    *    "bookmark"          node is a bookamrk
 | |
|    *    "livemarkChild"     node is a child of a livemark
 | |
|    *    "folder"            node is a folder
 | |
|    *    "query"             node is a query
 | |
|    *    "dynamiccontainer"  node is a dynamic container
 | |
|    *    "separator"         node is a separator line
 | |
|    *    "host"              node is a host
 | |
|    *    "mutable"           node can have items inserted or reordered
 | |
|    *    
 | |
|    * @returns an array of objects corresponding the selected nodes. Each
 | |
|    *          object has each of the properties above set if its corresponding
 | |
|    *          node matches the rule. In addition, the annotations names for each 
 | |
|    *          node are set on its corresponding object as properties.
 | |
|    * Notes:
 | |
|    *   1) This can be slow, so don't call it anywhere performance critical!
 | |
|    *   2) A single-object array corresponding the root node is returned if
 | |
|    *      there's no selection.
 | |
|    */
 | |
|   _buildSelectionMetadata: function PC__buildSelectionMetadata() {
 | |
|     var metadata = [];
 | |
|     var nodes = [];
 | |
|     var root = this._view.getResult().root;
 | |
|     if (this._view.hasSelection)
 | |
|       nodes = this._view.getSelectionNodes();
 | |
|     else // See the second note above
 | |
|       nodes = [root];
 | |
| 
 | |
|     for (var i=0; i < nodes.length; i++) {
 | |
|       var nodeData = {};
 | |
|       var node = nodes[i];
 | |
|       var nodeType = node.type;
 | |
|       var uri = null;
 | |
| 
 | |
|       // We don't use the nodeIs* methods here to avoid going through the type
 | |
|       // property way too often
 | |
|       switch(nodeType) {
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY:
 | |
|           nodeData["query"] = true;
 | |
|           break;
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_DYNAMIC_CONTAINER:
 | |
|           nodeData["dynamiccontainer"] = true;
 | |
|           break;
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT:
 | |
|           nodeData["folder"] = true;
 | |
|           break;
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_HOST:
 | |
|           nodeData["host"] = true;
 | |
|           break;
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
 | |
|           nodeData["separator"] = true;
 | |
|           break;
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_VISIT:
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_FULL_VISIT:
 | |
|           nodeData["link"] = true;
 | |
|           uri = PlacesUtils._uri(node.uri);
 | |
|           if (PlacesUtils.nodeIsBookmark(node)) {
 | |
|             nodeData["bookmark"] = true;
 | |
|             var mss = PlacesUtils.microsummaries;
 | |
|             if (mss.hasMicrosummary(node.itemId))
 | |
|               nodeData["microsummary"] = true;
 | |
|             else if (node.parent &&
 | |
|                      PlacesUtils.nodeIsLivemarkContainer(node.parent))
 | |
|               nodeData["livemarkChild"] = true;
 | |
|           }
 | |
|           break;
 | |
|         case Ci.nsINavHistoryResultNode.RESULT_TYPE_DAY:
 | |
|           nodeData["day"] = true;
 | |
|       }
 | |
| 
 | |
|       // Mutability is whether or not a container can have selected items
 | |
|       // inserted or reordered. It does _not_ dictate whether or not the 
 | |
|       // container can have items removed from it, since some containers that
 | |
|       // aren't  reorderable can have items removed from them, e.g. a history
 | |
|       // list. 
 | |
|       if (!PlacesUtils.nodeIsReadOnly(node) &&
 | |
|           !PlacesUtils.isReadonlyFolder(node.parent || root))
 | |
|         nodeData["mutable"] = true;
 | |
| 
 | |
|       // annotations
 | |
|       if (uri) {
 | |
|         var names = PlacesUtils.annotations.getPageAnnotationNames(uri, {});
 | |
|         for (var j = 0; j < names.length; ++j)
 | |
|           nodeData[names[j]] = true;
 | |
|       }
 | |
| 
 | |
|       // For items also include the item-specific annotations
 | |
|       if (node.itemId != -1) {
 | |
|         names = PlacesUtils.annotations
 | |
|                            .getItemAnnotationNames(node.itemId, {});
 | |
|         for (j = 0; j < names.length; ++j)
 | |
|           nodeData[names[j]] = true;
 | |
|       }
 | |
|       metadata.push(nodeData);
 | |
|     }
 | |
| 
 | |
|     return metadata;
 | |
|   },
 | |
| 
 | |
|   /** 
 | |
|    * Determines if a context-menu item should be shown
 | |
|    * @param   aMenuItem
 | |
|    *          the context menu item 
 | |
|    * @param   aMetaData
 | |
|    *          meta data about the selection
 | |
|    * @returns true if the conditions (see buildContextMenu) are satisfied
 | |
|    *          and the item can be displayed, false otherwise. 
 | |
|    */
 | |
|   _shouldShowMenuItem: function PC__shouldShowMenuItem(aMenuItem, aMetaData) {
 | |
|     var selectiontype = aMenuItem.getAttribute("selectiontype");
 | |
|     if (selectiontype == "multiple" && aMetaData.length == 1)
 | |
|       return false;
 | |
|     if (selectiontype == "single" && aMetaData.length != 1)
 | |
|       return false;
 | |
| 
 | |
|     var forceHideRules = aMenuItem.getAttribute("forcehideselection").split("|");
 | |
|     for (var i = 0; i < aMetaData.length; ++i) {
 | |
|       for (var j=0; j < forceHideRules.length; ++j) {
 | |
|         if (forceHideRules[j] in aMetaData[i])
 | |
|           return false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (aMenuItem.hasAttribute("selection")) {
 | |
|       var showRules = aMenuItem.getAttribute("selection").split("|");
 | |
|       var anyMatched = false;
 | |
|       function metaDataNodeMatches(metaDataNode, rules) {
 | |
|         for (var i=0; i < rules.length; i++) {
 | |
|           if (rules[i] in metaDataNode)
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         return false;
 | |
|       }
 | |
|       for (var i = 0; i < aMetaData.length; ++i) {
 | |
|         if (metaDataNodeMatches(aMetaData[i], showRules))
 | |
|           anyMatched = true;
 | |
|         else
 | |
|           return false;
 | |
|       }
 | |
|       return anyMatched;
 | |
|     }
 | |
| 
 | |
|     return !aMenuItem.hidden;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Detects information (meta-data rules) about the current selection in the
 | |
|    * view (see _buildSelectionMetadata) and sets the visibility state for each
 | |
|    * of the menu-items in the given popup with the following rules applied:
 | |
|    *  1) The "selectiontype" attribute may be set on a menu-item to "single"
 | |
|    *     if the menu-item should be visible only if there is a single node
 | |
|    *     selected, or to "multiple" if the menu-item should be visible only if
 | |
|    *     multiple nodes are selected. If the attribute is not set or if it is
 | |
|    *     set to an invalid value, the menu-item may be visible for both types of
 | |
|    *     selection.
 | |
|    *  2) The "selection" attribute may be set on a menu-item to the various
 | |
|    *     meta-data rules for which it may be visible. The rules should be
 | |
|    *     separated with the | character.
 | |
|    *  3) A menu-item may be visible only if at least one of the rules set in
 | |
|    *     its selection attribute apply to each of the selected nodes in the
 | |
|    *     view.
 | |
|    *  4) The "forcehideselection" attribute may be set on a menu-item to rules
 | |
|    *     for which it should be hidden. This attribute takes priority over the
 | |
|    *     selection attribute. A menu-item would be hidden if at least one of the
 | |
|    *     given rules apply to one of the selected nodes. The rules should be
 | |
|    *     separated with the | character.
 | |
|    *  5) The visibility state of a menu-item is unchanged if none of these
 | |
|    *     attribute are set.
 | |
|    *  6) These attributes should not be set on separators for which the
 | |
|    *     visibility state is "auto-detected."
 | |
|    * @param   aPopup
 | |
|    *          The menupopup to build children into.
 | |
|    * @return true if at least one item is visible, false otherwise.
 | |
|    */
 | |
|   buildContextMenu: function PC_buildContextMenu(aPopup) {
 | |
|     var metadata = this._buildSelectionMetadata();
 | |
| 
 | |
|     var separator = null;
 | |
|     var visibleItemsBeforeSep = false;
 | |
|     var anyVisible = false;
 | |
|     for (var i = 0; i < aPopup.childNodes.length; ++i) {
 | |
|       var item = aPopup.childNodes[i];
 | |
|       if (item.localName != "menuseparator") {
 | |
|         item.hidden = !this._shouldShowMenuItem(item, metadata);
 | |
|         if (!item.hidden) {
 | |
|           visibleItemsBeforeSep = true;
 | |
|           anyVisible = true;
 | |
| 
 | |
|           // Show the separator above the menu-item if any
 | |
|           if (separator) {
 | |
|             separator.hidden = false;
 | |
|             separator = null;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       else { // menuseparator
 | |
|         // Initially hide it. It will be unhidden if there will be at least one
 | |
|         // visible menu-item above and below it.
 | |
|         item.hidden = true;
 | |
| 
 | |
|         // We won't show the separator at all if no items are visible above it
 | |
|         if (visibleItemsBeforeSep)
 | |
|           separator = item;
 | |
| 
 | |
|         // New separator, count again:
 | |
|         visibleItemsBeforeSep = false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Set Open Folder/Links In Tabs items enabled state if they're visible
 | |
|     if (anyVisible) {
 | |
|       var openContainerInTabsItem = document.getElementById("placesContext_openContainer:tabs");
 | |
|       if (!openContainerInTabsItem.hidden && this._view.selectedNode &&
 | |
|           PlacesUtils.nodeIsContainer(this._view.selectedNode)) {
 | |
|         openContainerInTabsItem.disabled =
 | |
|           PlacesUtils.getURLsForContainerNode(this._view.selectedNode)
 | |
|                      .length == 0;
 | |
|       }
 | |
|       else {
 | |
|         // see selectiontype rule in the overlay
 | |
|         var openLinksInTabsItem = document.getElementById("placesContext_openLinks:tabs");
 | |
|         openLinksInTabsItem.disabled = openLinksInTabsItem.hidden;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return anyVisible;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Select all links in the current view. 
 | |
|    */
 | |
|   selectAll: function PC_selectAll() {
 | |
|     this._view.selectAll();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Loads the selected node's URL in the appropriate tab or window or as a web
 | |
|    * panel given the user's preference specified by modifier keys tracked by a
 | |
|    * DOM mouse/key event.
 | |
|    * @param   aEvent
 | |
|    *          The DOM mouse/key event with modifier keys set that track the
 | |
|    *          user's preferred destination window or tab.
 | |
|    */
 | |
|   openSelectedNodeWithEvent: function PC_openSelectedNodeWithEvent(aEvent) {
 | |
|     this.openSelectedNodeIn(whereToOpenLink(aEvent));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Loads the selected node's URL in the appropriate tab or window or as a
 | |
|    * web panel.
 | |
|    * see also openUILinkIn
 | |
|    */
 | |
|   openSelectedNodeIn: function PC_openSelectedNodeIn(aWhere) {
 | |
|     var node = this._view.selectedURINode;
 | |
|     if (node && PlacesUtils.checkURLSecurity(node)) {
 | |
|       var isBookmark = PlacesUtils.nodeIsBookmark(node);
 | |
| 
 | |
|       if (isBookmark)
 | |
|         PlacesUtils.markPageAsFollowedBookmark(node.uri);
 | |
|       else
 | |
|         PlacesUtils.markPageAsTyped(node.uri);
 | |
| 
 | |
|       // Check whether the node is a bookmark which should be opened as
 | |
|       // a web panel
 | |
|       if (aWhere == "current" && isBookmark) {
 | |
|         if (PlacesUtils.annotations
 | |
|                        .itemHasAnnotation(node.itemId, LOAD_IN_SIDEBAR_ANNO)) {
 | |
|           var w = getTopWin();
 | |
|           if (w) {
 | |
|             w.openWebPanel(node.title, node.uri);
 | |
|             return;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|       openUILinkIn(node.uri, aWhere);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the bookmark properties for the selected URI Node.
 | |
|    */
 | |
|   showBookmarkPropertiesForSelection: 
 | |
|   function PC_showBookmarkPropertiesForSelection() {
 | |
|     var node = this._view.selectedNode;
 | |
|     if (!node)
 | |
|       return;
 | |
| 
 | |
|     if (PlacesUtils.nodeIsFolder(node))
 | |
|       PlacesUtils.showFolderProperties(node.itemId);
 | |
|     else if (PlacesUtils.nodeIsBookmark(node))
 | |
|       PlacesUtils.showBookmarkProperties(node.itemId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * This method can be run on a URI parameter to ensure that it didn't
 | |
|    * receive a string instead of an nsIURI object.
 | |
|    */
 | |
|   _assertURINotString: function PC__assertURINotString(value) {
 | |
|     NS_ASSERT((typeof(value) == "object") && !(value instanceof String), 
 | |
|            "This method should be passed a URI as a nsIURI object, not as a string.");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Reloads the selected livemark if any.
 | |
|    */
 | |
|   reloadSelectedLivemark: function PC_reloadSelectedLivemark() {
 | |
|     if (this._view.hasSingleSelection) {
 | |
|       var selectedNode = this._view.selectedNode;
 | |
|       if (PlacesUtils.nodeIsLivemarkContainer(selectedNode))
 | |
|         PlacesUtils.livemarks.reloadLivemarkFolder(selectedNode.itemId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Reload the microsummary associated with the selection
 | |
|    */
 | |
|   reloadSelectedMicrosummary: function PC_reloadSelectedMicrosummary() {
 | |
|     var selectedNode = this._view.selectedNode;
 | |
|     var mss = PlacesUtils.microsummaries;
 | |
|     if (mss.hasMicrosummary(selectedNode.itemId))
 | |
|       mss.refreshMicrosummary(selectedNode.itemId);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Gives the user a chance to cancel loading lots of tabs at once
 | |
|    */
 | |
|   _confirmOpenTabs: function(numTabsToOpen) {
 | |
|     var pref = Cc["@mozilla.org/preferences-service;1"].
 | |
|                getService(Ci.nsIPrefBranch);
 | |
| 
 | |
|     const kWarnOnOpenPref = "browser.tabs.warnOnOpen";
 | |
|     var reallyOpen = true;
 | |
|     if (pref.getBoolPref(kWarnOnOpenPref)) {
 | |
|       if (numTabsToOpen >= pref.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
 | |
|         var promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].
 | |
|                             getService(Ci.nsIPromptService);
 | |
| 
 | |
|         // default to true: if it were false, we wouldn't get this far
 | |
|         var warnOnOpen = { value: true };
 | |
| 
 | |
|         var messageKey = "tabs.openWarningMultipleBranded";
 | |
|         var openKey = "tabs.openButtonMultiple";
 | |
|         var strings = document.getElementById("placeBundle");
 | |
|         const BRANDING_BUNDLE_URI = "chrome://branding/locale/brand.properties";
 | |
|         var brandShortName = Cc["@mozilla.org/intl/stringbundle;1"].
 | |
|                              getService(Ci.nsIStringBundleService).
 | |
|                              createBundle(BRANDING_BUNDLE_URI).
 | |
|                              GetStringFromName("brandShortName");
 | |
|        
 | |
|         var buttonPressed = promptService.confirmEx(window,
 | |
|           PlacesUtils.getString("tabs.openWarningTitle"),
 | |
|           PlacesUtils.getFormattedString(messageKey, 
 | |
|             [numTabsToOpen, brandShortName]),
 | |
|           (promptService.BUTTON_TITLE_IS_STRING * promptService.BUTTON_POS_0)
 | |
|           + (promptService.BUTTON_TITLE_CANCEL * promptService.BUTTON_POS_1),
 | |
|           PlacesUtils.getString(openKey),
 | |
|           null, null,
 | |
|           PlacesUtils.getFormattedString("tabs.openWarningPromptMeBranded",
 | |
|             [brandShortName]),
 | |
|           warnOnOpen);
 | |
| 
 | |
|          reallyOpen = (buttonPressed == 0);
 | |
|          // don't set the pref unless they press OK and it's false
 | |
|          if (reallyOpen && !warnOnOpen.value)
 | |
|            pref.setBoolPref(kWarnOnOpenPref, false);
 | |
|       }
 | |
|     }
 | |
|     return reallyOpen;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens the links in the selected folder, or the selected links in new tabs. 
 | |
|    */
 | |
|   openSelectionInTabs: function PC_openLinksInTabs(aEvent) {
 | |
|     var node = this._view.selectedNode;
 | |
|     if (this._view.hasSingleSelection && PlacesUtils.nodeIsContainer(node))
 | |
|       PlacesUtils.openContainerNodeInTabs(this._view.selectedNode, aEvent);
 | |
|     else
 | |
|       PlacesUtils.openURINodesInTabs(this._view.getSelectionNodes(), aEvent);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Shows the Add Bookmark UI for the current insertion point.
 | |
|    *
 | |
|    * @param aType
 | |
|    *        the type of the new item (bookmark/livemark/folder)
 | |
|    */
 | |
|   newItem: function PC_newItem(aType) {
 | |
|     var ip = this._view.insertionPoint;
 | |
|     if (!ip)
 | |
|       throw Cr.NS_ERROR_NOT_AVAILABLE;
 | |
| 
 | |
|     var performed = false;
 | |
|     if (aType == "bookmark")
 | |
|       performed = PlacesUtils.showAddBookmarkUI(null, null, null, ip);
 | |
|     else if (aType == "livemark")
 | |
|       performed = PlacesUtils.showAddLivemarkUI(null, null, null, null, ip);
 | |
|     else // folder
 | |
|       performed = PlacesUtils.showAddFolderUI(null, ip);
 | |
| 
 | |
|     if (performed) {
 | |
|       // select the new item
 | |
|       var insertedNodeId = PlacesUtils.bookmarks
 | |
|                                       .getIdForItemAt(ip.itemId, ip.index);
 | |
|       this._view.selectItems([insertedNodeId], ip.itemId);
 | |
|     }
 | |
|   },
 | |
| 
 | |
| 
 | |
|   /**
 | |
|    * Create a new Bookmark folder somewhere. Prompts the user for the name
 | |
|    * of the folder. 
 | |
|    */
 | |
|   newFolder: function PC_newFolder() {
 | |
|     var ip = this._view.insertionPoint;
 | |
|     if (!ip)
 | |
|       throw Cr.NS_ERROR_NOT_AVAILABLE;
 | |
| 
 | |
|     var performed = false;
 | |
|     performed = PlacesUtils.showAddFolderUI(null, ip);
 | |
|     if (performed) {
 | |
|       // select the new item
 | |
|       var insertedNodeId = PlacesUtils.bookmarks
 | |
|                                       .getIdForItemAt(ip.itemId, ip.index);
 | |
|       this._view.selectItems([insertedNodeId]);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Create a new Bookmark separator somewhere.
 | |
|    */
 | |
|   newSeparator: function PC_newSeparator() {
 | |
|     var ip = this._view.insertionPoint;
 | |
|     if (!ip)
 | |
|       throw Cr.NS_ERROR_NOT_AVAILABLE;
 | |
|     var txn = PlacesUtils.ptm.createSeparator(ip.itemId, ip.index);
 | |
|     PlacesUtils.ptm.doTransaction(txn);
 | |
|     // select the new item
 | |
|     var insertedNodeId = PlacesUtils.bookmarks
 | |
|                                     .getIdForItemAt(ip.itemId, ip.index);
 | |
|     this._view.selectItems([insertedNodeId]);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Opens a dialog for moving the selected nodes.
 | |
|    */
 | |
|   moveSelectedBookmarks: function PC_moveBookmarks() {
 | |
|     window.openDialog("chrome://browser/content/places/moveBookmarks.xul",
 | |
|                       "", "chrome, modal",
 | |
|                       this._view.getSelectionNodes());
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Sort the selected folder by name
 | |
|    */
 | |
|   sortFolderByName: function PC_sortFolderByName() {
 | |
|     var itemId = PlacesUtils.getConcreteItemId(this._view.selectedNode);
 | |
|     var txn = PlacesUtils.ptm.sortFolderByName(itemId);
 | |
|     PlacesUtils.ptm.doTransaction(txn);
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Walk the list of folders we're removing in this delete operation, and
 | |
|    * see if the selected node specified is already implicitly being removed 
 | |
|    * because it is a child of that folder. 
 | |
|    * @param   node
 | |
|    *          Node to check for containment. 
 | |
|    * @param   pastFolders
 | |
|    *          List of folders the calling function has already traversed
 | |
|    * @returns true if the node should be skipped, false otherwise. 
 | |
|    */
 | |
|   _shouldSkipNode: function PC_shouldSkipNode(node, pastFolders) {
 | |
|     /**
 | |
|      * Determines if a node is contained by another node within a resultset. 
 | |
|      * @param   node
 | |
|      *          The node to check for containment for
 | |
|      * @param   parent
 | |
|      *          The parent container to check for containment in
 | |
|      * @returns true if node is a member of parent's children, false otherwise.
 | |
|      */
 | |
|     function isContainedBy(node, parent) {
 | |
|       var cursor = node.parent;
 | |
|       while (cursor) {
 | |
|         if (cursor == parent)
 | |
|           return true;
 | |
|         cursor = cursor.parent;
 | |
|       }
 | |
|       return false;
 | |
|     }
 | |
|   
 | |
|       for (var j = 0; j < pastFolders.length; ++j) {
 | |
|         if (isContainedBy(node, pastFolders[j]))
 | |
|           return true;
 | |
|       }
 | |
|       return false;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Creates a set of transactions for the removal of a range of items. 
 | |
|    * A range is an array of adjacent nodes in a view.
 | |
|    * @param   [in] range
 | |
|    *          An array of nodes to remove. Should all be adjacent. 
 | |
|    * @param   [out] transactions
 | |
|    *          An array of transactions.
 | |
|    */
 | |
|   _removeRange: function PC__removeRange(range, transactions) {
 | |
|     NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
 | |
| 
 | |
|     var removedFolders = [];
 | |
| 
 | |
|     for (var i = 0; i < range.length; ++i) {
 | |
|       var node = range[i];
 | |
|       if (this._shouldSkipNode(node, removedFolders))
 | |
|         continue;
 | |
| 
 | |
|       if (PlacesUtils.nodeIsFolder(node))
 | |
|         removedFolders.push(node);
 | |
| 
 | |
|       transactions.push(PlacesUtils.ptm.removeItem(node.itemId));
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the set of selected ranges from bookmarks.
 | |
|    * @param   txnName
 | |
|    *          See |remove|.
 | |
|    */
 | |
|   _removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
 | |
|     var ranges = this._view.getRemovableSelectionRanges();
 | |
|     var transactions = [];
 | |
|     // Delete the selected rows. Do this by walking the selection backward, so
 | |
|     // that when undo is performed they are re-inserted in the correct order.
 | |
|     for (var i = ranges.length - 1; i >= 0 ; --i)
 | |
|       this._removeRange(ranges[i], transactions);
 | |
|     if (transactions.length > 0) {
 | |
|       var txn = PlacesUtils.ptm.aggregateTransactions(txnName, transactions);
 | |
|       PlacesUtils.ptm.doTransaction(txn);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the set of selected ranges from history.
 | |
|    */
 | |
|   _removeRowsFromHistory: function PC__removeRowsFromHistory() {
 | |
|     // Other containers are history queries, just delete from history
 | |
|     // history deletes are not undoable.
 | |
|     var nodes = this._view.getSelectionNodes();
 | |
|     var URIs = [];
 | |
|     var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
 | |
|     var resultView = this._view.getResultView();
 | |
|     for (var i = 0; i < nodes.length; ++i) {
 | |
|       var node = nodes[i];
 | |
|       if (PlacesUtils.nodeIsHost(node))
 | |
|         bhist.removePagesFromHost(node.title, true);
 | |
|       else if (PlacesUtils.nodeIsURI(node)) {
 | |
|         var uri = PlacesUtils._uri(node.uri);
 | |
|         // avoid trying to delete the same url twice
 | |
|         if (URIs.indexOf(uri) < 0) {
 | |
|           URIs.push(uri);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // if we have to delete a lot of urls RemovePage will be slow, it's better
 | |
|     // to delete them in bunch and rebuild the full treeView
 | |
|     if (URIs.length > REMOVE_PAGES_MAX_SINGLEREMOVES) {
 | |
|       // do removal in chunks to avoid passing a too big array to removePages
 | |
|       for (var i = 0; i < URIs.length; i += REMOVE_PAGES_CHUNKLEN) {
 | |
|         var URIslice = URIs.slice(i, Math.max(i + REMOVE_PAGES_CHUNKLEN, URIs.length));
 | |
|         // set DoBatchNotify only on the last chunk
 | |
|         bhist.removePages(URIslice, URIslice.length,
 | |
|                           (i + REMOVE_PAGES_CHUNKLEN) >= URIs.length);
 | |
|       }
 | |
|     }
 | |
|     else {
 | |
|       // if we have to delete fewer urls, removepage will allow us to avoid
 | |
|       // rebuilding the full treeView
 | |
|       for (var i = 0; i < URIs.length; ++i)
 | |
|         bhist.removePage(URIs[i]);
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Removes the selection
 | |
|    * @param   aTxnName
 | |
|    *          A name for the transaction if this is being performed
 | |
|    *          as part of another operation.
 | |
|    */
 | |
|   remove: function PC_remove(aTxnName) {
 | |
|     NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
 | |
| 
 | |
|     var root = this._view.getResult().root;
 | |
| 
 | |
|     if (PlacesUtils.nodeIsFolder(root)) 
 | |
|       this._removeRowsFromBookmarks(aTxnName);
 | |
|     else if (PlacesUtils.nodeIsQuery(root)) {
 | |
|       var queryType = asQuery(root).queryOptions.queryType;
 | |
|       if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS)
 | |
|         this._removeRowsFromBookmarks(aTxnName);
 | |
|       else if (queryType == Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY)
 | |
|         this._removeRowsFromHistory();
 | |
|       else
 | |
|         NS_ASSERT(false, "implement support for QUERY_TYPE_UNIFIED");
 | |
|     }
 | |
|     else
 | |
|       NS_ASSERT(false, "unexpected root");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Get a TransferDataSet containing the content of the selection that can be
 | |
|    * dropped elsewhere. 
 | |
|    * @param   dragAction
 | |
|    *          The action to happen when dragging, i.e. copy
 | |
|    * @returns A TransferDataSet object that can be dragged and dropped 
 | |
|    *          elsewhere.
 | |
|    */
 | |
|   getTransferData: function PC_getTransferData(dragAction) {
 | |
|     var result = this._view.getResult();
 | |
|     var oldViewer = result.viewer;
 | |
|     try {
 | |
|       result.viewer = null;
 | |
|       var nodes = null;
 | |
|       if (dragAction == Ci.nsIDragService.DRAGDROP_ACTION_COPY)
 | |
|         nodes = this._view.getCopyableSelection();
 | |
|       else
 | |
|         nodes = this._view.getDragableSelection();
 | |
|       var dataSet = new TransferDataSet();
 | |
|       for (var i = 0; i < nodes.length; ++i) {
 | |
|         var node = nodes[i];
 | |
| 
 | |
|         var data = new TransferData();
 | |
|         function addData(type, overrideURI) {
 | |
|           data.addDataForFlavour(type, PlacesUtils._wrapString(
 | |
|                                  PlacesUtils.wrapNode(node, type, overrideURI)));
 | |
|         }
 | |
| 
 | |
|         function addURIData(overrideURI) {
 | |
|           addData(PlacesUtils.TYPE_X_MOZ_URL, overrideURI);
 | |
|           addData(PlacesUtils.TYPE_UNICODE, overrideURI);
 | |
|           addData(PlacesUtils.TYPE_HTML, overrideURI);
 | |
|         }
 | |
| 
 | |
|         // This order is _important_! It controls how this and other 
 | |
|         // applications select data to be inserted based on type.
 | |
|         addData(PlacesUtils.TYPE_X_MOZ_PLACE);
 | |
|       
 | |
|         var uri;
 | |
|       
 | |
|         // Allow dropping the feed uri of live-bookmark folders
 | |
|         if (PlacesUtils.nodeIsLivemarkContainer(node))
 | |
|           uri = PlacesUtils.livemarks.getFeedURI(node.itemId).spec;
 | |
|       
 | |
|         addURIData(uri);
 | |
|         dataSet.push(data);
 | |
|       }
 | |
|     }
 | |
|     finally {
 | |
|       if (oldViewer)
 | |
|         result.viewer = oldViewer;
 | |
|     }
 | |
|     return dataSet;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Copy Bookmarks and Folders to the clipboard
 | |
|    */
 | |
|   copy: function PC_copy() {
 | |
|     var result = this._view.getResult();
 | |
|     var oldViewer = result.viewer;
 | |
|     try {
 | |
|       result.viewer = null;
 | |
|       var nodes = this._view.getCopyableSelection();
 | |
| 
 | |
|       var xferable =  Cc["@mozilla.org/widget/transferable;1"].
 | |
|                       createInstance(Ci.nsITransferable);
 | |
|       var foundFolder = false, foundLink = false;
 | |
|       var copiedFolders = [];
 | |
|       var placeString = mozURLString = htmlString = unicodeString = "";
 | |
|     
 | |
|       for (var i = 0; i < nodes.length; ++i) {
 | |
|         var node = nodes[i];
 | |
|         if (this._shouldSkipNode(node, copiedFolders))
 | |
|           continue;
 | |
|         if (PlacesUtils.nodeIsFolder(node))
 | |
|           copiedFolders.push(node);
 | |
|         
 | |
|         function generateChunk(type, overrideURI) {
 | |
|           var suffix = i < (nodes.length - 1) ? NEWLINE : "";
 | |
|           var uri = overrideURI;
 | |
|         
 | |
|           if (PlacesUtils.nodeIsLivemarkContainer(node))
 | |
|             uri = PlacesUtils.livemarks.getFeedURI(node.itemId).spec
 | |
| 
 | |
|           mozURLString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_X_MOZ_URL,
 | |
|                                                  uri) + suffix);
 | |
|           unicodeString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_UNICODE,
 | |
|                                                  uri) + suffix);
 | |
|           htmlString += (PlacesUtils.wrapNode(node, PlacesUtils.TYPE_HTML,
 | |
|                                                  uri) + suffix);
 | |
|         
 | |
|           var placeSuffix = i < (nodes.length - 1) ? "," : "";
 | |
|           return PlacesUtils.wrapNode(node, type, overrideURI) + placeSuffix;
 | |
|         }
 | |
| 
 | |
|         // all items wrapped as TYPE_X_MOZ_PLACE
 | |
|         placeString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE);
 | |
|       }
 | |
| 
 | |
|       function addData(type, data) {
 | |
|         xferable.addDataFlavor(type);
 | |
|         xferable.setTransferData(type, PlacesUtils._wrapString(data), data.length * 2);
 | |
|       }
 | |
|       // This order is _important_! It controls how this and other applications 
 | |
|       // select data to be inserted based on type.
 | |
|       if (placeString)
 | |
|         addData(PlacesUtils.TYPE_X_MOZ_PLACE, placeString);
 | |
|       if (mozURLString)
 | |
|         addData(PlacesUtils.TYPE_X_MOZ_URL, mozURLString);
 | |
|       if (unicodeString)
 | |
|         addData(PlacesUtils.TYPE_UNICODE, unicodeString);
 | |
|       if (htmlString)
 | |
|         addData(PlacesUtils.TYPE_HTML, htmlString);
 | |
| 
 | |
|       if (placeString || unicodeString || htmlString || mozURLString) {
 | |
|         PlacesUtils.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
 | |
|       }
 | |
|     }
 | |
|     finally {
 | |
|       if (oldViewer)
 | |
|         result.viewer = oldViewer;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Cut Bookmarks and Folders to the clipboard
 | |
|    */
 | |
|   cut: function PC_cut() {
 | |
|     this.copy();
 | |
|     this.remove("Cut Selection");
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Paste Bookmarks and Folders from the clipboard
 | |
|    */
 | |
|   paste: function PC_paste() {
 | |
|     // Strategy:
 | |
|     // 
 | |
|     // There can be data of various types (folder, separator, link) on the 
 | |
|     // clipboard. We need to get all of that data and build edit transactions
 | |
|     // for them. This means asking the clipboard once for each type and 
 | |
|     // aggregating the results. 
 | |
| 
 | |
|     /**
 | |
|      * Constructs a transferable that can receive data of specific types.
 | |
|      * @param   types
 | |
|      *          The types of data the transferable can hold, in order of
 | |
|      *          preference.
 | |
|      * @returns The transferable.
 | |
|      */
 | |
|     function makeXferable(types) {
 | |
|       var xferable = 
 | |
|           Cc["@mozilla.org/widget/transferable;1"].
 | |
|           createInstance(Ci.nsITransferable);
 | |
|       for (var i = 0; i < types.length; ++i) 
 | |
|         xferable.addDataFlavor(types[i]);
 | |
|       return xferable;
 | |
|     }
 | |
| 
 | |
|     var clipboard = PlacesUtils.clipboard;
 | |
| 
 | |
|     var ip = this._view.insertionPoint;
 | |
|     if (!ip)
 | |
|       throw Cr.NS_ERROR_NOT_AVAILABLE;
 | |
| 
 | |
|     /**
 | |
|      * Gets a list of transactions to perform the paste of specific types.
 | |
|      * @param   types
 | |
|      *          The types of data to form paste transactions for
 | |
|      * @returns An array of transactions that perform the paste.
 | |
|      */
 | |
|     function getTransactions(types) {
 | |
|       var xferable = makeXferable(types);
 | |
|       clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
 | |
| 
 | |
|       var data = { }, type = { };
 | |
|       try {
 | |
|         xferable.getAnyTransferData(type, data, { });
 | |
|         data = data.value.QueryInterface(Ci.nsISupportsString).data;
 | |
|         var items = PlacesUtils.unwrapNodes(data, type.value);
 | |
|         var transactions = [];
 | |
|         var index = ip.index;
 | |
|         for (var i = 0; i < items.length; ++i) {
 | |
|           // adjusted to make sure that items are given the correct index -
 | |
|           // transactions insert differently if index == -1
 | |
|           if (ip.index > -1)
 | |
|             index = ip.index + i;
 | |
|           transactions.push(PlacesUtils.makeTransaction(items[i], type.value, 
 | |
|                                                         ip.itemId, index,
 | |
|                                                         true));
 | |
|         }
 | |
|         return transactions;
 | |
|       }
 | |
|       catch (e) {
 | |
|         // getAnyTransferData will throw if there is no data of the specified
 | |
|         // type on the clipboard. 
 | |
|         // unwrapNodes will throw if the data that is present is malformed in
 | |
|         // some way. 
 | |
|         // In either case, don't fail horribly, just return no data.
 | |
|       }
 | |
|       return [];
 | |
|     }
 | |
| 
 | |
|     // Get transactions to paste any folders, separators or links that might
 | |
|     // be on the clipboard, aggregate them and execute them. 
 | |
|     var transactions = getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE,
 | |
|                                         PlacesUtils.TYPE_X_MOZ_URL, 
 | |
|                                         PlacesUtils.TYPE_UNICODE]);
 | |
|     var txn = PlacesUtils.ptm.aggregateTransactions("Paste", transactions);
 | |
|     PlacesUtils.ptm.doTransaction(txn);
 | |
| 
 | |
|     // select the pasted items, they should be consecutive
 | |
|     var insertedNodeIds = [];
 | |
|     for (var i = 0; i < transactions.length; ++i)
 | |
|       insertedNodeIds.push(PlacesUtils.bookmarks
 | |
|                                       .getIdForItemAt(ip.itemId, ip.index + i));
 | |
|     if (insertedNodeIds.length > 0)
 | |
|       this._view.selectItems(insertedNodeIds);
 | |
|   }
 | |
| };
 | |
| 
 | |
| function PlacesMenuDNDObserver(aView, aPopup) {
 | |
|   this._view = aView;
 | |
|   this._popup = aPopup;
 | |
|   this._popup.addEventListener("draggesture", this, false);
 | |
|   this._popup.addEventListener("dragover", this, false);
 | |
|   this._popup.addEventListener("dragdrop", this, false);
 | |
|   this._popup.addEventListener("dragexit", this, false);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * XXXmano-please-rewrite-me: This code was ported over from menu.xul in bug 399729.
 | |
|  * Unsurprisngly it's still mostly broken due to bug 337761, thus I didn't bother
 | |
|  * trying to cleaning up this extremely buggy  over-folder detection code yet.
 | |
|  */
 | |
| PlacesMenuDNDObserver.prototype = {
 | |
|   _view: null,
 | |
|   _popup: null,
 | |
| 
 | |
|   // Sub-menus should be opened when the mouse drags over them, and closed
 | |
|   // when the mouse drags off.  The overFolder object manages opening and closing
 | |
|   // of folders when the mouse hovers.
 | |
|   _overFolder: {node: null, openTimer: null, hoverTime: 350, closeTimer: null},
 | |
| 
 | |
|   // If this menu's parent auto-opened it because it was dragged over, but didn't
 | |
|   // close it because the mouse dragged into it, the menu should close itself
 | |
|   // onDragExit.  This timer is set in dragExit to close the menu.
 | |
|   _closeMenuTimer: null,
 | |
| 
 | |
|   _setTimer: function TBV_DO_setTimer(time) {
 | |
|     var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
 | |
|     timer.initWithCallback(this, time, timer.TYPE_ONE_SHOT);
 | |
|     return timer;
 | |
|   },
 | |
| 
 | |
|   // Function to process all timer notifications.
 | |
|   notify: function TBV_DO_notify(timer) {
 | |
|     // Timer to open a submenu that's being dragged over.
 | |
|     if (timer == this._overFolder.openTimer) {
 | |
|       this._overFolder.node.lastChild.setAttribute("autoopened", "true");
 | |
|       this._overFolder.node.lastChild.showPopup(this._overFolder.node);
 | |
|       this._overFolder.openTimer = null;
 | |
|     }
 | |
| 
 | |
|     // Timer to close a submenu that's been dragged off of.
 | |
|     if (timer == this._overFolder.closeTimer) {
 | |
|       // Only close the submenu if the mouse isn't being dragged over any
 | |
|       // of its child menus.
 | |
|       var draggingOverChild =
 | |
|         PlacesControllerDragHelper.draggingOverChildNode(this._overFolder.node);
 | |
|       if (draggingOverChild)
 | |
|         this._overFolder.node = null;
 | |
|       this._clearOverFolder();
 | |
| 
 | |
|       // Close any parent folders which aren't being dragged over.
 | |
|       // (This is necessary because of the above code that keeps a folder
 | |
|       // open while its children are being dragged over.)
 | |
|       if (!draggingOverChild)
 | |
|         this._closeParentMenus();
 | |
|     }
 | |
| 
 | |
|     // Timer to close this menu after the drag exit.
 | |
|     if (timer == this._closeMenuTimer) {
 | |
|       if (!PlacesControllerDragHelper.draggingOverChildNode(this._popup)) {
 | |
|         this._popup.hidePopup();
 | |
|         // Close any parent menus that aren't being dragged over;
 | |
|         // otherwise they'll stay open because they couldn't close
 | |
|         // while this menu was being dragged over.
 | |
|         this._closeParentMenus();
 | |
|       }
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // Helper function to close all parent menus of this menu,
 | |
|   // as long as none of the parent's children are currently being
 | |
|   // dragged over.
 | |
|   _closeParentMenus: function TBV_DO_closeParentMenus() {
 | |
|     var parent = this._popup.parentNode;
 | |
|     while (parent) {
 | |
|       if (parent.nodeName == "menupopup" && parent._resultNode) {
 | |
|         if (PlacesControllerDragHelper.draggingOverChildNode(parent.parentNode))
 | |
|           break;
 | |
|         parent.hidePopup();
 | |
|       }
 | |
|       parent = parent.parentNode;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // The mouse is no longer dragging over the stored menubutton.
 | |
|   // Close the menubutton, clear out drag styles, and clear all
 | |
|   // timers for opening/closing it.
 | |
|   _clearOverFolder: function TBV_DO_clearOverFolder() {
 | |
|     if (this._overFolder.node && this._overFolder.node.lastChild) {
 | |
|       if (!this._overFolder.node.lastChild.hasAttribute("dragover"))
 | |
|         this._overFolder.node.lastChild.hidePopup();
 | |
|       this._overFolder.node = null;
 | |
|     }
 | |
|     if (this._overFolder.openTimer) {
 | |
|       this._overFolder.openTimer.cancel();
 | |
|       this._overFolder.openTimer = null;
 | |
|     }
 | |
|     if (this._overFolder.closeTimer) {
 | |
|       this._overFolder.closeTimer.cancel();
 | |
|       this._overFolder.closeTimer = null;
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   // This function returns information about where to drop when
 | |
|   // dragging over this menu--insertion point, child index to drop
 | |
|   // before, and folder to drop into.
 | |
|   _getDropPoint: function TBV_DO_getDropPoint(event) {
 | |
|     // Can't drop if the menu isn't a folder
 | |
|     var resultNode = this._popup._resultNode;
 | |
|     if (!PlacesUtils.nodeIsFolder(resultNode))
 | |
|       return null;
 | |
| 
 | |
|     var dropPoint = { ip: null, beforeIndex: null, folderNode: null };
 | |
|     // Loop through all the nodes to see which one this should
 | |
|     // get dropped in/above/below.
 | |
|     var start = 0;
 | |
|     var end = this._popup.childNodes.length;
 | |
|     if (this._popup == this._view && this._view.localName == "menupopup") {
 | |
|       // Ignore static content at the top and bottom of the menu.
 | |
|       start = this._view._startMarker + 1;
 | |
|       if (this._view._endMarker != -1)
 | |
|         end = this._view._endMarker;
 | |
|     }
 | |
| 
 | |
|     var popupFirstChildY = this._popup.firstChild.boxObject.y;
 | |
|     for (var i = start; i < end; i++) {
 | |
|       var xulNode = this._popup.childNodes[i];
 | |
|       var nodeY = xulNode.boxObject.y - popupFirstChildY;
 | |
|       var nodeHeight = xulNode.boxObject.height;
 | |
|       if (xulNode.node &&
 | |
|           PlacesUtils.nodeIsFolder(xulNode.node) &&
 | |
|           !PlacesUtils.nodeIsReadOnly(xulNode.node)) {
 | |
|         // This is a folder. If the mouse is in the top 25% of the
 | |
|         // node, drop above the folder.  If it's in the middle
 | |
|         // 50%, drop into the folder.  If it's past that, drop below.
 | |
|         if (event.layerY < nodeY + (nodeHeight * 0.25)) {
 | |
|           // Drop above this folder.
 | |
|           dropPoint.ip = new InsertionPoint(resultNode.itemId, i - start,
 | |
|                                             -1);
 | |
|           dropPoint.beforeIndex = i;
 | |
|           return dropPoint;
 | |
|         }
 | |
|         else if (event.layerY < nodeY + (nodeHeight * 0.75)) {
 | |
|           // Drop inside this folder.
 | |
|           dropPoint.ip = new InsertionPoint(xulNode.node.itemId, -1, 1);
 | |
|           dropPoint.beforeIndex = i;
 | |
|           dropPoint.folderNode = xulNode;
 | |
|           return dropPoint;
 | |
|         }
 | |
|       } else {
 | |
|         // This is a non-folder node. If the mouse is above the middle,
 | |
|         // drop above the folder.  Otherwise, drop below.
 | |
|         if (event.layerY < nodeY + (nodeHeight / 2)) {
 | |
|           // Drop above this bookmark.
 | |
|           dropPoint.ip = new InsertionPoint(resultNode.itemId, i - start, -1);
 | |
|           dropPoint.beforeIndex = i;
 | |
|           return dropPoint;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     // Should drop below the last node.
 | |
|     dropPoint.ip = new InsertionPoint(resultNode.itemId, -1, 1);
 | |
|     dropPoint.beforeIndex = -1;
 | |
|     return dropPoint;
 | |
|   },
 | |
| 
 | |
|   // This function clears all of the dragover styles that were set when
 | |
|   // a menuitem was dragged over.
 | |
|   _clearStyles: function TBV_DO_clearStyles() {
 | |
|     this._popup.removeAttribute("dragover");
 | |
|     for (var i = 0; i < this._popup.childNodes.length; i++) {
 | |
|       this._popup.childNodes[i].removeAttribute("dragover-top");
 | |
|       this._popup.childNodes[i].removeAttribute("dragover-bottom");
 | |
|       this._popup.childNodes[i].removeAttribute("dragover-into");
 | |
|     }
 | |
|   },
 | |
| 
 | |
|   onDragStart: function TBV_DO_onDragStart(event, xferData, dragAction) {
 | |
|     this._view._selection = event.target.node;
 | |
|     this._view._cachedInsertionPoint = undefined;
 | |
|     if (event.ctrlKey)
 | |
|       dragAction.action = Ci.nsIDragService.DRAGDROP_ACTION_COPY;
 | |
|     xferData.data = this._view.controller.getTransferData(dragAction.action);
 | |
|   },
 | |
| 
 | |
|   canDrop: function TBV_DO_canDrop(event, session) {
 | |
|     return PlacesControllerDragHelper.canDrop(this._view._viewer, -1);
 | |
|   },
 | |
| 
 | |
|   onDragOver: function TBV_DO_onDragOver(event, flavor, session) {
 | |
|     PlacesControllerDragHelper.currentDropTarget = event.target;
 | |
|     var dropPoint = this._getDropPoint(event);
 | |
|     if (dropPoint == null)
 | |
|       return;
 | |
| 
 | |
|     this._clearStyles();
 | |
|     if (dropPoint.folderNode) {
 | |
|       // Dragging over a folder; set the appropriate styles.
 | |
|       if (this._overFolder.node != dropPoint.folderNode) {
 | |
|         this._clearOverFolder();
 | |
|         this._overFolder.node = dropPoint.folderNode;
 | |
|         this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
 | |
|       }
 | |
|       dropPoint.folderNode.setAttribute("dragover-into", "true");
 | |
|     }
 | |
|     else {
 | |
|       // Dragging over a menuitem, set dragover-top/bottom to show where
 | |
|       // the item will be dropped and clear out any old folder info.
 | |
|       if (dropPoint.beforeIndex == -1) {
 | |
|         if (this._popup == this._view && this._view.localName == "menupopup" &&
 | |
|             this._popup._endMarker != -1) {
 | |
|           this._popup.childNodes[this._popup._endMarker]
 | |
|                      .setAttribute("dragover-top", "true");
 | |
|         }
 | |
|         else
 | |
|           this._popup.lastChild.setAttribute("dragover-bottom", "true");
 | |
|       }
 | |
|       else {
 | |
|         this._popup.childNodes[dropPoint.beforeIndex]
 | |
|             .setAttribute("dragover-top", "true");
 | |
|       }
 | |
| 
 | |
|       // Clear out old folder information
 | |
|       this._clearOverFolder();
 | |
|     }
 | |
|     this._popup.setAttribute("dragover", "true");
 | |
|   },
 | |
| 
 | |
|   onDrop: function TBV_DO_onDrop(event, dropData, session) {
 | |
|     var dropPoint = this._getDropPoint(event);
 | |
|     if (!dropPoint)
 | |
|       return;
 | |
| 
 | |
|     PlacesControllerDragHelper.onDrop(dropPoint.ip);
 | |
|   },
 | |
| 
 | |
|   onDragExit: function TBV_DO_onDragExit(event, session) {
 | |
|     PlacesControllerDragHelper.currentDropTarget = null;
 | |
|     this._clearStyles();
 | |
|     // Close any folder being hovered over
 | |
|     if (this._overFolder.node)
 | |
|       this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
 | |
|     // The autoopened attribute is set when this folder was automatically
 | |
|     // opened after the user dragged over it.  If this attribute is set,
 | |
|     // auto-close the folder on drag exit.
 | |
|     if (this._popup.hasAttribute("autoopened"))
 | |
|       this._closeMenuTimer = this._setTimer(this._overFolder.hoverTime);
 | |
|   },
 | |
| 
 | |
|   getSupportedFlavours: function TBV_DO_getSupportedFlavours() {
 | |
|     var flavorSet = new FlavourSet();
 | |
|     var types = PlacesUtils.GENERIC_VIEW_DROP_TYPES;
 | |
|     for (var i = 0; i < types.length; ++i)
 | |
|       flavorSet.appendFlavour(types[i]);
 | |
|     return flavorSet;
 | |
|   },
 | |
| 
 | |
|   handleEvent: function(aEvent) {
 | |
|     switch (aEvent.type) {
 | |
|     case "draggesture":
 | |
|       if (aEvent.target.localName != "menu" && aEvent.target.node) {
 | |
|         // TODO--allow menu drag if shift (or alt??) key is down
 | |
|         nsDragAndDrop.startDrag(aEvent, this);
 | |
|       }
 | |
|       break;
 | |
|     case "dragover":
 | |
|       nsDragAndDrop.dragOver(aEvent, this);
 | |
|       break;
 | |
|     case "dragdrop":
 | |
|       nsDragAndDrop.drop(aEvent, this);
 | |
|       break;
 | |
|     case "dragexit":
 | |
|       nsDragAndDrop.dragExit(aEvent, this);
 | |
|       break;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handles drag and drop operations for views. Note that this is view agnostic!
 | |
|  * You should not use PlacesController._view within these methods, since
 | |
|  * the view that the item(s) have been dropped on was not necessarily active. 
 | |
|  * Drop functions are passed the view that is being dropped on. 
 | |
|  */
 | |
| var PlacesControllerDragHelper = {
 | |
| 
 | |
|   /**
 | |
|    * Determines if the mouse is currently being dragged over a child node of
 | |
|    * this menu. This is necessary so that the menu doesn't close while the
 | |
|    * mouse is dragging over one of its submenus
 | |
|    * @param   node
 | |
|    *          The container node
 | |
|    * @returns true if the user is dragging over a node within the hierarchy of
 | |
|    *          the container, false otherwise.
 | |
|    */
 | |
|   draggingOverChildNode: function PCDH_draggingOverChildNode(node) {
 | |
|     var currentNode = this.currentDropTarget;
 | |
|     while (currentNode) {
 | |
|       if (currentNode == node)
 | |
|         return true;
 | |
|       currentNode = currentNode.parentNode;
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * DOM Element currently being dragged over
 | |
|    */
 | |
|   currentDropTarget: null,
 | |
| 
 | |
|   /**
 | |
|    * @returns The current active drag session. Returns null if there is none.
 | |
|    */
 | |
|   getSession: function VO__getSession() {
 | |
|     var dragService = Cc["@mozilla.org/widget/dragservice;1"].
 | |
|                       getService(Ci.nsIDragService);
 | |
|     return dragService.getCurrentSession();
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Determines whether or not the data currently being dragged can be dropped
 | |
|    * on the specified view. 
 | |
|    * @param   view
 | |
|    *          A places view object (nsINavHistoryResultViewer)
 | |
|    * @param   orientation
 | |
|    *          The orientation of the drop
 | |
|    * @returns true if the data being dragged is of a type supported by the view
 | |
|    *          it is being dragged over, false otherwise. 
 | |
|    */
 | |
|   canDrop: function PCDH_canDrop(view, orientation) {
 | |
|     var root = view.result.root;
 | |
|     if (PlacesUtils.nodeIsReadOnly(root) || 
 | |
|         !PlacesUtils.nodeIsFolder(root))
 | |
|       return false;
 | |
| 
 | |
|     var session = this.getSession();
 | |
|     if (session) {
 | |
|       var types = PlacesUtils.GENERIC_VIEW_DROP_TYPES;
 | |
|       for (var i = 0; i < types.length; ++i) {
 | |
|         if (session.isDataFlavorSupported(types[i]))
 | |
|           return true;
 | |
|       }
 | |
|     }
 | |
|     return false;
 | |
|   },
 | |
| 
 | |
|   /** 
 | |
|    * Creates a Transferable object that can be filled with data of types
 | |
|    * supported by a view. 
 | |
|    * @param   session
 | |
|    *          The active drag session
 | |
|    * @returns An object implementing nsITransferable that can receive data
 | |
|    *          dropped onto a view. 
 | |
|    */
 | |
|   _initTransferable: function PCDH__initTransferable(session) {
 | |
|     var xferable = Cc["@mozilla.org/widget/transferable;1"].
 | |
|                    createInstance(Ci.nsITransferable);
 | |
|     var types = PlacesUtils.GENERIC_VIEW_DROP_TYPES;
 | |
|     for (var i = 0; i < types.length; ++i) {
 | |
|       if (session.isDataFlavorSupported(types[i]))
 | |
|         xferable.addDataFlavor(types[i]);
 | |
|     }
 | |
|     return xferable;
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Handles the drop of one or more items onto a view.
 | |
|    * @param   insertionPoint
 | |
|    *          The insertion point where the items should be dropped
 | |
|    */
 | |
|   onDrop: function PCDH_onDrop(insertionPoint) {
 | |
|     var session = this.getSession();
 | |
|     var copy = session.dragAction & Ci.nsIDragService.DRAGDROP_ACTION_COPY;
 | |
|     var transactions = [];
 | |
|     var xferable = this._initTransferable(session);
 | |
|     var dropCount = session.numDropItems;
 | |
| 
 | |
|     var movedCount = 0;
 | |
| 
 | |
|     for (var i = 0; i < dropCount; ++i) {
 | |
|       session.getData(xferable, i);
 | |
| 
 | |
|       var data = { }, flavor = { };
 | |
|       xferable.getAnyTransferData(flavor, data, { });
 | |
|       data.value.QueryInterface(Ci.nsISupportsString);
 | |
| 
 | |
|       // There's only ever one in the D&D case. 
 | |
|       var unwrapped = PlacesUtils.unwrapNodes(data.value.data, 
 | |
|                                               flavor.value)[0];
 | |
|       var index = insertionPoint.index;
 | |
| 
 | |
|       // Adjust insertion index to prevent reversal of dragged items. When you
 | |
|       // drag multiple elts upward: need to increment index or each successive
 | |
|       // elt will be inserted at the same index, each above the previous.
 | |
|       if ((index != -1) && ((index < unwrapped.index) ||
 | |
|                            (unwrapped.folder && (index < unwrapped.folder.index)))) {
 | |
|         index = index + movedCount;
 | |
|         movedCount++;
 | |
|       }
 | |
| 
 | |
|       transactions.push(PlacesUtils.makeTransaction(unwrapped,
 | |
|                         flavor.value, insertionPoint.itemId,
 | |
|                         index, copy));
 | |
|     }
 | |
| 
 | |
|     var txn = PlacesUtils.ptm.aggregateTransactions("DropItems", transactions);
 | |
|     PlacesUtils.ptm.doTransaction(txn);
 | |
|   }
 | |
| };
 | |
| 
 | |
| function goUpdatePlacesCommands() {
 | |
|   var placesController;
 | |
|   try {
 | |
|     // Or any other command...
 | |
|     placesController = top.document.commandDispatcher
 | |
|                           .getControllerForCommand("placesCmd_open");
 | |
|   }
 | |
|   catch(ex) { return; }
 | |
| 
 | |
|   function updatePlacesCommand(aCommand) {
 | |
|     var enabled = false;
 | |
|     if (placesController)
 | |
|       enabled = placesController.isCommandEnabled(aCommand);
 | |
|     goSetCommandEnabled(aCommand, enabled);
 | |
|   }
 | |
| 
 | |
|   updatePlacesCommand("placesCmd_open");
 | |
|   updatePlacesCommand("placesCmd_open:window");
 | |
|   updatePlacesCommand("placesCmd_open:tab");
 | |
|   updatePlacesCommand("placesCmd_new:folder");
 | |
|   updatePlacesCommand("placesCmd_new:bookmark");
 | |
|   updatePlacesCommand("placesCmd_new:livemark");
 | |
|   updatePlacesCommand("placesCmd_new:separator");
 | |
|   updatePlacesCommand("placesCmd_show:info");
 | |
|   updatePlacesCommand("placesCmd_moveBookmarks");
 | |
|   updatePlacesCommand("placesCmd_reload");
 | |
|   updatePlacesCommand("placesCmd_reloadMicrosummary");
 | |
|   updatePlacesCommand("placesCmd_sortBy:name");
 | |
| }
 | 
