mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 17:16:12 +00:00
2236 lines
76 KiB
JavaScript
Executable File
2236 lines
76 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 ***** */
|
|
|
|
const NHRVO = Ci.nsINavHistoryResultViewObserver;
|
|
|
|
// XXXmano: we should move most/all of these constants to PlacesUtils
|
|
|
|
// These need to be kept in sync with the meaning of these roots in
|
|
// default_places.html!
|
|
const ORGANIZER_ROOT_HISTORY_UNSORTED = "place:beginTime=-2592000000000&beginTimeRef=1&endTime=7200000000&endTimeRef=2&type=1"
|
|
const ORGANIZER_ROOT_HISTORY = ORGANIZER_ROOT_HISTORY_UNSORTED + "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
|
|
const ORGANIZER_ROOT_BOOKMARKS = "place:folder=2&group=3&excludeItems=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;
|
|
|
|
#ifdef XP_MACOSX
|
|
// On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we
|
|
// really just want "\n".
|
|
const NEWLINE= "\n";
|
|
#else
|
|
// On other platforms, the transferable system converts "\r\n" to "\n".
|
|
const NEWLINE = "\r\n";
|
|
#endif
|
|
|
|
/**
|
|
* 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.tm.numberOfUndoItems > 0;
|
|
case "cmd_redo":
|
|
return PlacesUtils.tm.numberOfRedoItems > 0;
|
|
case "cmd_cut":
|
|
case "cmd_delete":
|
|
case "placesCmd_moveBookmarks":
|
|
return this._hasRemovableSelection();
|
|
case "cmd_copy":
|
|
return this._view.hasSelection;
|
|
case "cmd_paste":
|
|
return this._canInsert() &&
|
|
this._hasClipboardData() && this._canPaste();
|
|
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_open:tabs":
|
|
// We can open multiple links if the current selection is either:
|
|
// a) a single folder which contains at least one link
|
|
// b) multiple links
|
|
var node = this._view.selectedNode;
|
|
if (!node)
|
|
return false;
|
|
|
|
if (this._view.hasSingleSelection && PlacesUtils.nodeIsFolder(node)) {
|
|
var contents = PlacesUtils.getFolderContents(node.itemId, false, false);
|
|
for (var i = 0; i < contents.childCount; ++i) {
|
|
var child = contents.getChild(i);
|
|
if (PlacesUtils.nodeIsURI(child))
|
|
return true;
|
|
}
|
|
}
|
|
else {
|
|
var oneLinkIsSelected = false;
|
|
var nodes = this._view.getSelectionNodes();
|
|
for (var i = 0; i < nodes.length; ++i) {
|
|
if (PlacesUtils.nodeIsURI(nodes[i])) {
|
|
if (oneLinkIsSelected)
|
|
return true;
|
|
oneLinkIsSelected = true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
#ifdef MOZ_PLACES_BOOKMARKS
|
|
case "placesCmd_new:folder":
|
|
case "placesCmd_new:livemark":
|
|
return this._canInsert() &&
|
|
this._view.peerDropTypes
|
|
.indexOf(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER) != -1;
|
|
case "placesCmd_new:bookmark":
|
|
return this._canInsert() &&
|
|
this._view.peerDropTypes.indexOf(PlacesUtils.TYPE_X_MOZ_URL) != -1;
|
|
case "placesCmd_new:separator":
|
|
return this._canInsert() &&
|
|
this._view.peerDropTypes
|
|
.indexOf(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR) != -1;
|
|
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_reload":
|
|
if (this._view.hasSingleSelection) {
|
|
var selectedNode = this._view.selectedNode;
|
|
|
|
// Livemark containers
|
|
if (PlacesUtils.nodeIsLivemarkContainer(selectedNode))
|
|
return true;
|
|
|
|
#ifdef EXTENDED_LIVEBOOKMARKS_UI
|
|
// Subscriptions View - not yet exposed anywhere
|
|
if (selectedNode.uri.indexOf("livemark%2F") != -1)
|
|
return true;
|
|
|
|
// children of a live bookmark (legacy bookmarks UI doesn't support
|
|
// this)
|
|
if (PlacesUtils.nodeIsURI() &&
|
|
PlacesUtils.nodeIsLivemarkItem(selectedNode))
|
|
return true;
|
|
#endif
|
|
}
|
|
return false;
|
|
case "placesCmd_sortBy:name":
|
|
var selectedNode = this._view.selectedNode;
|
|
return selectedNode &&
|
|
PlacesUtils.nodeIsFolder(selectedNode) &&
|
|
!PlacesUtils.nodeIsReadOnly(selectedNode);
|
|
case "placesCmd_setAsBookmarksToolbarFolder":
|
|
if (this._view.hasSingleSelection) {
|
|
var selectedNode = this._view.selectedNode;
|
|
if (PlacesUtils.nodeIsFolder(selectedNode) &&
|
|
selectedNode.itemId != PlacesUtils.bookmarks.toolbarFolder) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
#endif
|
|
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.tm.undoTransaction();
|
|
break;
|
|
case "cmd_redo":
|
|
PlacesUtils.tm.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_open:tabs":
|
|
this.openLinksInTabs();
|
|
break;
|
|
#ifdef MOZ_PLACES_BOOKMARKS
|
|
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.reloadSelectedLivemarks();
|
|
break;
|
|
case "placesCmd_sortBy:name":
|
|
this.sortFolderByName();
|
|
break;
|
|
case "placesCmd_setAsBookmarksToolbarFolder":
|
|
this.setBookmarksToolbarFolder();
|
|
break;
|
|
#endif
|
|
}
|
|
},
|
|
|
|
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.
|
|
* @returns true if the there's a selection which has no nodes that cannot be removed,
|
|
* false otherwise.
|
|
*/
|
|
_hasRemovableSelection: function PC__hasRemovableSelection() {
|
|
if (!this._view.hasSelection)
|
|
return false;
|
|
|
|
var nodes = this._view.getSelectionNodes();
|
|
var root = this._view.getResultNode();
|
|
|
|
var btFolderId = PlacesUtils.toolbarFolderId;
|
|
for (var i = 0; i < nodes.length; ++i) {
|
|
// Disallow removing the view's root node
|
|
if (nodes[i] == root)
|
|
return false;
|
|
|
|
// Disallow removing the toolbar folder
|
|
if (PlacesUtils.nodeIsFolder(nodes[i]) && nodes[i].itemId == btFolderId)
|
|
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 the clipboard contains data that the active
|
|
* view can support in a paste operation.
|
|
* @returns true if the clipboard contains data compatible with the active
|
|
* view, false otherwise.
|
|
*/
|
|
_hasClipboardData: function PC__hasClipboardData() {
|
|
var types = this._view.peerDropTypes;
|
|
var flavors =
|
|
Cc["@mozilla.org/supports-array;1"].
|
|
createInstance(Ci.nsISupportsArray);
|
|
for (var i = 0; i < types.length; ++i) {
|
|
var cstring =
|
|
Cc["@mozilla.org/supports-cstring;1"].
|
|
createInstance(Ci.nsISupportsCString);
|
|
cstring.data = types[i];
|
|
flavors.AppendElement(cstring);
|
|
}
|
|
|
|
var clipboard =
|
|
Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
|
|
return clipboard.hasDataMatchingFlavors(flavors,
|
|
Ci.nsIClipboard.kGlobalClipboard);
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
* - not a set of URIs that is entirely already present in the view,
|
|
* since we can only have one instance of a URI per container.
|
|
* @returns true if the data is paste-able, false if the clipboard data
|
|
* cannot be pasted
|
|
*/
|
|
_canPaste: function PC__canPaste() {
|
|
var xferable =
|
|
Cc["@mozilla.org/widget/transferable;1"].
|
|
createInstance(Ci.nsITransferable);
|
|
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
|
|
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
|
|
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE);
|
|
xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_URL);
|
|
|
|
var clipboard = Cc["@mozilla.org/widget/clipboard;1"].
|
|
getService(Ci.nsIClipboard);
|
|
clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard);
|
|
|
|
try {
|
|
// getAnyTransferData can throw if no data is available.
|
|
var data = { }, type = { };
|
|
xferable.getAnyTransferData(type, data, { });
|
|
data = data.value.QueryInterface(Ci.nsISupportsString).data;
|
|
if (this._view.peerDropTypes.indexOf(type.value) == -1)
|
|
return false;
|
|
|
|
// unwrapNodes will throw if the data blob is malformed.
|
|
var nodes = PlacesUtils.unwrapNodes(data, type.value);
|
|
|
|
var ip = this._view.insertionPoint;
|
|
return ip != null;
|
|
}
|
|
catch (e) {
|
|
// Unwrap nodes failed, possibly because a field that should have
|
|
// contained a URI did not actually contain something that is
|
|
// parse-able as a URI.
|
|
return false;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Gathers information about the selected nodes according to the following
|
|
* rules:
|
|
* "link" node is a URI
|
|
* "bookmark" node is a bookamrk
|
|
* "folder" node is a folder
|
|
* "query" node is a query
|
|
* "remotecontainer" node is a remote container
|
|
* "separator" node is a separator line
|
|
* "host" node is a host
|
|
* "mutable" node can have items inserted or reordered
|
|
* "allLivemarks" node is a query containing every livemark
|
|
*
|
|
* @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_REMOTE_CONTAINER:
|
|
nodeData["remotecontainer"] = true;
|
|
break;
|
|
case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
|
|
nodeData["folder"] = true;
|
|
uri = PlacesUtils.bookmarks.getFolderURI(node.itemId);
|
|
|
|
// See nodeIsRemoteContainer
|
|
if (asContainer(node).remoteContainerType != "")
|
|
nodeData["remotecontainer"] = 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;
|
|
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 ("bookmark" in nodeData || "folder" in nodeData) {
|
|
names = PlacesUtils.annotations
|
|
.getItemAnnotationNames(node.itemId, {});
|
|
for (j = 0; j < names.length; ++j)
|
|
nodeData[names[j]] = true;
|
|
}
|
|
}
|
|
#ifdef EXTENDED_LIVEBOOKMARKS_UI
|
|
else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
|
|
// Various queries might live in the left-hand side of the organizer
|
|
// window. If this one happens to have collected all the livemark feeds,
|
|
// allow its context menu to contain "Reload All Livemarks". That will
|
|
// usually only mean the Subscriptions folder, but if some other folder
|
|
// happens to use the same query, it's fine too. Queries have very
|
|
// limited data (no annotations), so we're left checking the query URI
|
|
// directly.
|
|
uri = PlacesUtils._uri(nodes[i].uri);
|
|
if (uri.spec == ORGANIZER_SUBSCRIPTIONS_QUERY)
|
|
nodeData["allLivemarks"] = true;
|
|
}
|
|
#endif
|
|
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;
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
// Check whether the node is a bookmark which should be opened as
|
|
// a web panel
|
|
if (aWhere == "current" && PlacesUtils.nodeIsBookmark(node)) {
|
|
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 livemarks associated with the selection. For the
|
|
* "Subscriptions" folder, reloads all livemarks; for a livemark folder,
|
|
* reloads its children; for a single livemark, reloads its siblings (the
|
|
* children of its parent).
|
|
*/
|
|
reloadSelectedLivemarks: function PC_reloadSelectedLivemarks() {
|
|
var selectedNode = this._view.selectedNode;
|
|
if (this._view.hasSingleSelection) {
|
|
#ifdef EXTENDED_LIVEBOOKMARKS_UI
|
|
if (selectedNode.uri.indexOf("livemark%2F") != -1) {
|
|
PlacesUtils.livemarks.reloadAllLivemarks();
|
|
return;
|
|
}
|
|
#endif
|
|
var folder = null;
|
|
if (PlacesUtils.nodeIsLivemarkContainer(selectedNode)) {
|
|
folder = selectedNode;
|
|
}
|
|
#ifdef EXTENDED_LIVEBOOKMARKS_UI
|
|
else if (PlacesUtils.nodeIsURI()) {
|
|
if (PlacesUtils.nodeIsLivemarkItem(selectedNode))
|
|
folder = selectedNode.parent;
|
|
}
|
|
#endif
|
|
if (folder)
|
|
PlacesUtils.livemarks.reloadLivemarkFolder(folder.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.
|
|
* XXXben this needs to handle the case when there are no open browser windows
|
|
* XXXben this function is really long, should be split apart. The codepaths
|
|
* seem different between load folder in tabs and load selection in
|
|
* tabs, too.
|
|
* See: https://bugzilla.mozilla.org/show_bug.cgi?id=331908
|
|
*/
|
|
openLinksInTabs: function PC_openLinksInTabs() {
|
|
var node = this._view.selectedNode;
|
|
if (this._view.hasSingleSelection && PlacesUtils.nodeIsFolder(node)) {
|
|
// Check prefs to see whether to open over existing tabs.
|
|
var doReplace = getBoolPref("browser.tabs.loadFolderAndReplace");
|
|
var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground");
|
|
// Get the start index to open tabs at
|
|
|
|
// XXX todo: no-browser-window-case
|
|
var browserWindow = getTopWin();
|
|
var browser = browserWindow.getBrowser();
|
|
var tabPanels = browser.browsers;
|
|
var tabCount = tabPanels.length;
|
|
var firstIndex;
|
|
// If browser.tabs.loadFolderAndReplace pref is set, load over all the
|
|
// tabs starting with the first one.
|
|
if (doReplace)
|
|
firstIndex = 0;
|
|
// If the pref is not set, only load over the blank tabs at the end, if any.
|
|
else {
|
|
for (firstIndex = tabCount - 1; firstIndex >= 0; --firstIndex) {
|
|
var br = browser.browsers[firstIndex];
|
|
if (br.currentURI.spec != "about:blank" ||
|
|
br.webProgress.isLoadingDocument)
|
|
break;
|
|
}
|
|
++firstIndex;
|
|
}
|
|
|
|
// Open each uri in the folder in a tab.
|
|
var index = firstIndex;
|
|
var urlsToOpen = [];
|
|
var contents = PlacesUtils.getFolderContents(node.itemId, false, false);
|
|
for (var i = 0; i < contents.childCount; ++i) {
|
|
var child = contents.getChild(i);
|
|
if (PlacesUtils.nodeIsURI(child))
|
|
urlsToOpen.push(child.uri);
|
|
}
|
|
|
|
if (!this._confirmOpenTabs(urlsToOpen.length))
|
|
return;
|
|
|
|
for (var i = 0; i < urlsToOpen.length; ++i) {
|
|
if (index < tabCount)
|
|
tabPanels[index].loadURI(urlsToOpen[i]);
|
|
// Otherwise, create a new tab to load the uri into.
|
|
else
|
|
browser.addTab(urlsToOpen[i]);
|
|
++index;
|
|
}
|
|
|
|
// If no bookmarks were loaded, just bail.
|
|
if (index == firstIndex)
|
|
return;
|
|
|
|
// focus the first tab if prefs say to
|
|
if (!loadInBackground || doReplace) {
|
|
// Select the first tab in the group.
|
|
// Set newly selected tab after quick timeout, otherwise hideous focus problems
|
|
// can occur because new presshell is not ready to handle events
|
|
function selectNewForegroundTab(browser, tab) {
|
|
browser.selectedTab = tab;
|
|
}
|
|
var tabs = browser.mTabContainer.childNodes;
|
|
setTimeout(selectNewForegroundTab, 0, browser, tabs[firstIndex]);
|
|
}
|
|
|
|
// Close any remaining open tabs that are left over.
|
|
// (Always skipped when we append tabs)
|
|
for (var i = tabCount - 1; i >= index; --i)
|
|
browser.removeTab(tabs[i]);
|
|
|
|
// and focus the content
|
|
browserWindow.content.focus();
|
|
}
|
|
else {
|
|
var urlsToOpen = [];
|
|
var nodes = this._view.getSelectionNodes();
|
|
|
|
for (var i = 0; i < nodes.length; ++i) {
|
|
if (PlacesUtils.nodeIsURI(nodes[i]))
|
|
urlsToOpen.push(nodes[i].uri);
|
|
}
|
|
|
|
if (!this._confirmOpenTabs(urlsToOpen.length))
|
|
return;
|
|
|
|
for (var i = 0; i < urlsToOpen.length; ++i) {
|
|
getTopWin().openNewTabWith(urlsToOpen[i], null, null);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
|
|
this._view.saveSelection(this._view.SAVE_SELECTION_INSERT);
|
|
|
|
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)
|
|
this._view.restoreSelection();
|
|
},
|
|
|
|
|
|
/**
|
|
* 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;
|
|
|
|
this._view.saveSelection(this._view.SAVE_SELECTION_INSERT);
|
|
if (PlacesUtils.showAddFolderUI(null, ip))
|
|
this._view.restoreSelection();
|
|
},
|
|
|
|
/**
|
|
* 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 = new PlacesCreateSeparatorTransaction(ip.itemId, ip.index);
|
|
PlacesUtils.tm.doTransaction(txn);
|
|
},
|
|
|
|
/**
|
|
* 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(), PlacesUtils.tm);
|
|
},
|
|
|
|
/**
|
|
* Sort the selected folder by name
|
|
*/
|
|
sortFolderByName: function PC_sortFolderByName() {
|
|
var selectedNode = this._view.selectedNode;
|
|
var txn = new PlacesSortFolderByNameTransaction(selectedNode.itemId,
|
|
selectedNode.bookmarkIndex);
|
|
PlacesUtils.tm.doTransaction(txn);
|
|
},
|
|
|
|
/**
|
|
* Makes the selected node the bookmarks toolbar folder.
|
|
*/
|
|
setBookmarksToolbarFolder: function PC_setBookmarksToolbarFolder() {
|
|
if (!this._view.hasSingleSelection)
|
|
return false;
|
|
var selectedNode = this._view.selectedNode;
|
|
var txn = new PlacesSetBookmarksToolbarTransaction(selectedNode.itemId);
|
|
PlacesUtils.tm.doTransaction(txn);
|
|
return true;
|
|
},
|
|
|
|
|
|
/**
|
|
* 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 range
|
|
* An array of nodes to remove. Should all be adjacent.
|
|
* @param transactions
|
|
* An array of transactions.
|
|
*/
|
|
_removeRange: function PC__removeRange(range, transactions) {
|
|
NS_ASSERT(transactions instanceof Array, "Must pass a transactions array");
|
|
var index = PlacesUtils.getIndexOfNode(range[0]);
|
|
|
|
var removedFolders = [];
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @returns true if the node should be skipped, false otherwise.
|
|
*/
|
|
function shouldSkipNode(node) {
|
|
for (var j = 0; j < removedFolders.length; ++j) {
|
|
if (isContainedBy(node, removedFolders[j]))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < range.length; ++i) {
|
|
var node = range[i];
|
|
if (shouldSkipNode(node))
|
|
continue;
|
|
|
|
if (PlacesUtils.nodeIsFolder(node)) {
|
|
// TODO -- node.parent might be a query and not a folder. See bug 324948
|
|
var folder = node;
|
|
removedFolders.push(folder);
|
|
transactions.push(new PlacesRemoveFolderTransaction(folder.itemId));
|
|
}
|
|
else if (PlacesUtils.nodeIsSeparator(node)) {
|
|
// A Bookmark separator.
|
|
transactions.push(new PlacesRemoveSeparatorTransaction(
|
|
node.parent.itemId, index));
|
|
}
|
|
else if (PlacesUtils.nodeIsFolder(node.parent)) {
|
|
// A Bookmark in a Bookmark Folder.
|
|
transactions.push(new PlacesRemoveItemTransaction(node.itemId,
|
|
PlacesUtils._uri(node.uri), node.parent.itemId, index));
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Removes the set of selected ranges from bookmarks.
|
|
* @param txnName
|
|
* See |remove|.
|
|
*/
|
|
_removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
|
|
var ranges = this._view.getRemovableSelectionRanges();
|
|
var transactions = [];
|
|
for (var i = ranges.length - 1; i >= 0 ; --i)
|
|
this._removeRange(ranges[i], transactions);
|
|
if (transactions.length > 0) {
|
|
var txn = new PlacesAggregateTransaction(txnName, transactions);
|
|
PlacesUtils.tm.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();
|
|
for (var i = 0; i < nodes.length; ++i) {
|
|
var node = nodes[i];
|
|
var bhist = PlacesUtils.history.QueryInterface(Ci.nsIBrowserHistory);
|
|
if (PlacesUtils.nodeIsHost(node))
|
|
bhist.removePagesFromHost(node.title, true);
|
|
else if (PlacesUtils.nodeIsURI(node))
|
|
bhist.removePage(PlacesUtils._uri(node.uri));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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");
|
|
this._view.saveSelection(this._view.SAVE_SELECTION_REMOVE);
|
|
|
|
// 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.
|
|
var type = this._view.getResult().root.type;
|
|
if (PlacesUtils.nodeIsFolder(this._view.getResult().root))
|
|
this._removeRowsFromBookmarks(aTxnName);
|
|
else
|
|
this._removeRowsFromHistory();
|
|
|
|
this._view.restoreSelection();
|
|
},
|
|
|
|
/**
|
|
* 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 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);
|
|
}
|
|
|
|
if (PlacesUtils.nodeIsFolder(node) || PlacesUtils.nodeIsQuery(node)) {
|
|
// Look up this node's place: URI in the annotation service to see if
|
|
// it is a special, non-movable folder.
|
|
// XXXben: TODO
|
|
|
|
addData(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
|
|
|
|
// Allow dropping the feed uri of live-bookmark folders
|
|
if (PlacesUtils.nodeIsLivemarkContainer(node)) {
|
|
var uri = PlacesUtils.livemarks.getFeedURI(node.itemId);
|
|
addURIData(uri.spec);
|
|
}
|
|
|
|
}
|
|
else if (PlacesUtils.nodeIsSeparator(node)) {
|
|
addData(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
|
|
}
|
|
else {
|
|
// 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);
|
|
addURIData();
|
|
}
|
|
dataSet.push(data);
|
|
}
|
|
return dataSet;
|
|
},
|
|
|
|
/**
|
|
* Copy Bookmarks and Folders to the clipboard
|
|
*/
|
|
copy: function PC_copy() {
|
|
var nodes = this._view.getCopyableSelection();
|
|
|
|
var xferable =
|
|
Cc["@mozilla.org/widget/transferable;1"].
|
|
createInstance(Ci.nsITransferable);
|
|
var foundFolder = false, foundLink = false;
|
|
var pcString = psString = placeString = mozURLString = htmlString = unicodeString = "";
|
|
for (var i = 0; i < nodes.length; ++i) {
|
|
var node = nodes[i];
|
|
function generateChunk(type, overrideURI) {
|
|
var suffix = i < (nodes.length - 1) ? NEWLINE : "";
|
|
return PlacesUtils.wrapNode(node, type, overrideURI) + suffix;
|
|
}
|
|
|
|
function generateURIChunks(overrideURI) {
|
|
mozURLString += generateChunk(PlacesUtils.TYPE_X_MOZ_URL, overrideURI);
|
|
htmlString += generateChunk(PlacesUtils.TYPE_HTML, overrideURI);
|
|
unicodeString += generateChunk(PlacesUtils.TYPE_UNICODE, overrideURI);
|
|
}
|
|
|
|
if (PlacesUtils.nodeIsFolder(node) || PlacesUtils.nodeIsQuery(node)) {
|
|
pcString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
|
|
|
|
// Also copy the feed URI for live-bookmark folders
|
|
if (PlacesUtils.nodeIsLivemarkContainer(node)) {
|
|
var uri = PlacesUtils.livemarks.getFeedURI(node.itemId);
|
|
generateURIChunks(uri.spec);
|
|
}
|
|
}
|
|
else if (PlacesUtils.nodeIsSeparator(node))
|
|
psString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
|
|
else {
|
|
placeString += generateChunk(PlacesUtils.TYPE_X_MOZ_PLACE);
|
|
generateURIChunks();
|
|
}
|
|
}
|
|
|
|
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 (pcString)
|
|
addData(PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER, pcString);
|
|
if (psString)
|
|
addData(PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR, psString);
|
|
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 (pcString || psString || placeString || unicodeString || htmlString ||
|
|
mozURLString) {
|
|
var clipboard =
|
|
Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
|
|
clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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 = Cc["@mozilla.org/widget/clipboard;1"].
|
|
getService(Ci.nsIClipboard);
|
|
|
|
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 = [];
|
|
for (var i = 0; i < items.length; ++i) {
|
|
transactions.push(PlacesUtils.makeTransaction(items[i], type.value,
|
|
ip.itemId, ip.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 =
|
|
[].concat(getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER]),
|
|
getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR]),
|
|
getTransactions([PlacesUtils.TYPE_X_MOZ_PLACE,
|
|
PlacesUtils.TYPE_X_MOZ_URL,
|
|
PlacesUtils.TYPE_UNICODE]));
|
|
var txn = new PlacesAggregateTransaction("Paste", transactions);
|
|
PlacesUtils.tm.doTransaction(txn);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* An object implementing the AVI
|
|
* @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 parent = view.getResult().root;
|
|
if (PlacesUtils.nodeIsReadOnly(parent) ||
|
|
!PlacesUtils.nodeIsFolder(parent))
|
|
return false;
|
|
|
|
var session = this.getSession();
|
|
if (session) {
|
|
if (orientation != NHRVO.DROP_ON)
|
|
var types = view.peerDropTypes;
|
|
else
|
|
types = view.childDropTypes;
|
|
for (var i = 0; i < types.length; ++i) {
|
|
if (session.isDataFlavorSupported(types[i]))
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Creates a Transeferable object that can be filled with data of types
|
|
* supported by a view.
|
|
* @param session
|
|
* The active drag session
|
|
* @param view
|
|
* An object implementing the AVI that supplies a list of
|
|
* supported droppable content types
|
|
* @param orientation
|
|
* The orientation of the drop
|
|
* @returns An object implementing nsITransferable that can receive data
|
|
* dropped onto a view.
|
|
*/
|
|
_initTransferable: function PCDH__initTransferable(session, view, orientation) {
|
|
var xferable = Cc["@mozilla.org/widget/transferable;1"].
|
|
createInstance(Ci.nsITransferable);
|
|
if (orientation != NHRVO.DROP_ON)
|
|
var types = view.peerDropTypes;
|
|
else
|
|
types = view.childDropTypes;
|
|
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 sourceView
|
|
* The AVI-implementing object that started the drop.
|
|
* @param targetView
|
|
* The AVI-implementing object that received the drop.
|
|
* @param insertionPoint
|
|
* The insertion point where the items should be dropped
|
|
*/
|
|
onDrop: function PCDH_onDrop(sourceView, targetView, insertionPoint) {
|
|
var session = this.getSession();
|
|
var copy = session.dragAction & Ci.nsIDragService.DRAGDROP_ACTION_COPY;
|
|
var transactions = [];
|
|
var xferable = this._initTransferable(session, targetView,
|
|
insertionPoint.orientation);
|
|
var dropCount = session.numDropItems;
|
|
for (var i = dropCount - 1; i >= 0; --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];
|
|
transactions.push(PlacesUtils.makeTransaction(unwrapped,
|
|
flavor.value, insertionPoint.itemId,
|
|
insertionPoint.index, copy));
|
|
}
|
|
|
|
var txn = new PlacesAggregateTransaction("DropItems", transactions);
|
|
PlacesUtils.tm.doTransaction(txn);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Method and utility stubs for Place Edit Transactions
|
|
*/
|
|
function PlacesBaseTransaction() {
|
|
}
|
|
PlacesBaseTransaction.prototype = {
|
|
utils: PlacesUtils,
|
|
|
|
bookmarks: Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
|
getService(Ci.nsINavBookmarksService),
|
|
_livemarks: null,
|
|
get livemarks() {
|
|
if (!this._livemarks) {
|
|
this._livemarks =
|
|
Cc["@mozilla.org/browser/livemark-service;2"].
|
|
getService(Ci.nsILivemarkService);
|
|
}
|
|
return this._livemarks;
|
|
},
|
|
|
|
// The minimum amount of transactions we should tell our observers to begin
|
|
// batching (rather than letting them do incremental drawing).
|
|
MIN_TRANSACTIONS_FOR_BATCH: 5,
|
|
|
|
LOG: LOG,
|
|
redoTransaction: function PIT_redoTransaction() {
|
|
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
get isTransient() {
|
|
return false;
|
|
},
|
|
|
|
merge: function PIT_merge(transaction) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Performs several Places Transactions in a single batch.
|
|
*/
|
|
function PlacesAggregateTransaction(name, transactions) {
|
|
this._transactions = transactions;
|
|
this._name = name;
|
|
this.container = -1;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesAggregateTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function() {
|
|
this.LOG("== " + this._name + " (Aggregate) ==============");
|
|
if (this._transactions.length >= this.MIN_TRANSACTIONS_FOR_BATCH)
|
|
this.bookmarks.beginUpdateBatch();
|
|
for (var i = 0; i < this._transactions.length; ++i) {
|
|
var txn = this._transactions[i];
|
|
if (this.container > -1)
|
|
txn.container = this.container;
|
|
txn.doTransaction();
|
|
}
|
|
if (this._transactions.length >= this.MIN_TRANSACTIONS_FOR_BATCH)
|
|
this.bookmarks.endUpdateBatch();
|
|
this.LOG("== " + this._name + " (Aggregate Ends) =========");
|
|
},
|
|
|
|
undoTransaction: function() {
|
|
this.LOG("== UN" + this._name + " (UNAggregate) ============");
|
|
if (this._transactions.length >= this.MIN_TRANSACTIONS_FOR_BATCH)
|
|
this.bookmarks.beginUpdateBatch();
|
|
for (var i = this._transactions.length - 1; i >= 0; --i) {
|
|
var txn = this._transactions[i];
|
|
if (this.container > -1)
|
|
txn.container = this.container;
|
|
txn.undoTransaction();
|
|
}
|
|
if (this._transactions.length >= this.MIN_TRANSACTIONS_FOR_BATCH)
|
|
this.bookmarks.endUpdateBatch();
|
|
this.LOG("== UN" + this._name + " (UNAggregate Ends) =======");
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Transaction for creating a new folder item.
|
|
*
|
|
* @param aName
|
|
* the name of the new folder
|
|
* @param aContainer
|
|
* the identifier of the folder in which the new folder should be
|
|
* added.
|
|
* @param [optional] aIndex
|
|
* the index of the item in aContainer, pass -1 or nothing to create
|
|
* the item at the end of aContainer.
|
|
* @param [optional] aAnnotations
|
|
* the annotations to set for the new folder.
|
|
* @param [optional] aChildItemsTransactions
|
|
* array of transactions for items to be created under the new folder.
|
|
*/
|
|
function PlacesCreateFolderTransaction(aName, aContainer, aIndex,
|
|
aAnnotations, aChildItemsTransactions) {
|
|
this._name = aName;
|
|
this._container = aContainer;
|
|
this._index = typeof(aIndex) == "number" ? aIndex : -1;
|
|
this._annotations = aAnnotations;
|
|
this._id = null;
|
|
this._childItemsTransactions = aChildItemsTransactions || [];
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesCreateFolderTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
// childItemsTransaction support
|
|
get container() { return this._container; },
|
|
set container(val) { return this._container = val; },
|
|
|
|
doTransaction: function PCFT_doTransaction() {
|
|
var bookmarks = this.utils.bookmarks;
|
|
this._id = bookmarks.createFolder(this._container, this._name, this._index);
|
|
if (this._annotations.length > 0)
|
|
this.utils.setAnnotationsForItem(this._id, this._annotations);
|
|
|
|
for (var i = 0; i < this._childItemsTransactions.length; ++i) {
|
|
var txn = this._childItemsTransactions[i];
|
|
txn.container = this._id;
|
|
txn.doTransaction();
|
|
}
|
|
},
|
|
|
|
undoTransaction: function PCFT_undoTransaction() {
|
|
this.bookmarks.removeFolder(this._id);
|
|
for (var i = 0; i < this._childItemsTransactions.length; ++i) {
|
|
var txn = this.childItemsTransactions[i];
|
|
txn.undoTransaction();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Transaction for creating a new bookmark item
|
|
*
|
|
* @param aURI
|
|
* the uri of the new bookmark (nsIURI)
|
|
* @param aContainer
|
|
* the identifier of the folder in which the bookmark should be added.
|
|
* @param [optional] aIndex
|
|
* the index of the item in aContainer, pass -1 or nothing to create
|
|
* the item at the end of aContainer.
|
|
* @param [optional] aTitle
|
|
* the title of the new bookmark.
|
|
* @param [optional] aKeyword
|
|
* the keyword of the new bookmark.
|
|
* @param [optional] aAnnotations
|
|
* the annotations to set for the new bookmark.
|
|
* @param [optional] aChildTransactions
|
|
* child transactions to commit after creating the bookmark. Prefer
|
|
* using any of the arguments above if possible. In general, a child
|
|
* transations should be used only if the change it does has to be
|
|
* reverted manually when removing the bookmark item.
|
|
* a child transaction must support setting its bookmark-item
|
|
* identifier via an "id" js setter.
|
|
*/
|
|
function PlacesCreateItemTransaction(aURI, aContainer, aIndex, aTitle,
|
|
aKeyword, aAnnotations,
|
|
aChildTransactions) {
|
|
this._uri = aURI;
|
|
this._container = aContainer;
|
|
this._index = typeof(aIndex) == "number" ? aIndex : -1;
|
|
this._title = aTitle;
|
|
this._keyword = aKeyword;
|
|
this._annotations = aAnnotations;
|
|
this._childTransactions = aChildTransactions || [];
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesCreateItemTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
// childItemsTransactions support for the create-folder transaction
|
|
get container() { return this._container; },
|
|
set container(val) { return this._container = val; },
|
|
|
|
doTransaction: function PCIT_doTransaction() {
|
|
var bookmarks = this.utils.bookmarks;
|
|
this._id = bookmarks.insertBookmark(this.container, this._uri, this._index,
|
|
this._title);
|
|
if (this._keyword)
|
|
bookmarks.setKeywordForBookmark(this._id, this._keyword);
|
|
if (this._annotations && this._annotations.length > 0)
|
|
this.utils.setAnnotationsForItem(this._id, this._annotations);
|
|
|
|
for (var i = 0; i < this._childTransactions.length; ++i) {
|
|
var txn = this._childTransactions[i];
|
|
txn.id = this._id;
|
|
txn.doTransaction();
|
|
}
|
|
},
|
|
|
|
undoTransaction: function PCIT_undoTransaction() {
|
|
this.utils.bookmarks.removeItem(this._id);
|
|
for (var i = 0; i < this._childTransactions.length; ++i) {
|
|
var txn = this._childTransactions[i];
|
|
txn.undoTransaction();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Transaction for creating a new separator item
|
|
*
|
|
* @param aContainer
|
|
* the identifier of the folder in which the separator should be
|
|
* added.
|
|
* @param [optional] aIndex
|
|
* the index of the item in aContainer, pass -1 or nothing to create
|
|
* the separator at the end of aContainer.
|
|
*/
|
|
function PlacesCreateSeparatorTransaction(aContainer, aIndex) {
|
|
this._container = aContainer;
|
|
this._index = typeof(aIndex) == "number" ? aIndex : -1;
|
|
this._id = null;
|
|
}
|
|
PlacesCreateSeparatorTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
// childItemsTransaction support
|
|
get container() { return this._container; },
|
|
set container(val) { return this._container = val; },
|
|
|
|
doTransaction: function PCST_doTransaction() {
|
|
this.LOG("Create separator in: " + this.container + "," + this._index);
|
|
this._id = this.bookmarks.insertSeparator(this.container, this._index);
|
|
},
|
|
|
|
undoTransaction: function PCST_undoTransaction() {
|
|
this.LOG("UNCreate separator from: " + this.container + "," + this._index);
|
|
this.bookmarks.removeChildAt(this.container, this._index);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Transaction for creating a new live-bookmark item.
|
|
*
|
|
* @see nsILivemarksService::createLivemark for documentation regarding the
|
|
* first three arguments.
|
|
*
|
|
* @param aContainer
|
|
* the identifier of the folder in which the live-bookmark should be
|
|
* added.
|
|
* @param [optional] aIndex
|
|
* the index of the item in aContainer, pass -1 or nothing to create
|
|
* the item at the end of aContainer.
|
|
* @param [optional] aAnnotations
|
|
* the annotations to set for the new live-bookmark.
|
|
*/
|
|
function PlacesCreateLivemarkTransaction(aFeedURI, aSiteURI, aName,
|
|
aContainer, aIndex, aAnnotations) {
|
|
this._feedURI = aFeedURI;
|
|
this._siteURI = aSiteURI;
|
|
this._name = aName;
|
|
this._container = aContainer;
|
|
this._index = typeof(aIndex) == "number" ? aIndex : -1;
|
|
this._annotations = aAnnotations;
|
|
}
|
|
PlacesCreateLivemarkTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
// childItemsTransaction support
|
|
get container() { return this._container; },
|
|
set container(val) { return this._container = val; },
|
|
|
|
doTransaction: function PCLT_doTransaction() {
|
|
this._id = this.utils.livemarks
|
|
.createLivemark(this._container, this._name, this._siteURI,
|
|
this._feedURI, this._index);
|
|
if (this._annotations)
|
|
this.utils.setAnnotationsForItem(this._id, this._annotations);
|
|
},
|
|
|
|
undoTransaction: function PCLT_undoTransaction() {
|
|
this.bookmarks.removeFolder(this._id);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Move an Item
|
|
*/
|
|
function PlacesMoveItemTransaction(aItemId, aNewContainer, aNewIndex) {
|
|
NS_ASSERT(!isNaN(aItemId + aNewContainer + aNewIndex), "Parameter is NaN!");
|
|
NS_ASSERT(aNewIndex >= -1, "invalid insertion index");
|
|
this._id = aItemId;
|
|
this._oldContainer = this.utils.bookmarks.getFolderIdForItem(this._id);
|
|
this._oldIndex = this.utils.bookmarks.getItemIndex(this._id);
|
|
NS_ASSERT(this._oldContainer > 0 && this._oldIndex >= 0, "invalid item");
|
|
this._newContainer = aNewContainer;
|
|
this._newIndex = aNewIndex;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesMoveItemTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PMIT_doTransaction() {
|
|
this.bookmarks.moveItem(this._id, this._newContainer, this._newIndex);
|
|
},
|
|
|
|
undoTransaction: function PMIT_undoTransaction() {
|
|
this.bookmarks.moveItem(this._id, this._oldContainer, this._oldIndex);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove a Folder
|
|
* This is a little complicated. When we remove a container we need to remove
|
|
* all of its children. We can't just repurpose our existing transactions for
|
|
* this since they cache their parent container id. Since the folder structure
|
|
* is being removed, this id is being destroyed and when it is re-created will
|
|
* likely have a different id.
|
|
*/
|
|
|
|
function PlacesRemoveFolderTransaction(id) {
|
|
this._removeTxn = this.bookmarks.getRemoveFolderTransaction(id);
|
|
this._id = id;
|
|
this._transactions = []; // A set of transactions to remove content.
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesRemoveFolderTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
/**
|
|
* Create a flat, ordered list of transactions for a depth-first recreation
|
|
* of items within this folder.
|
|
*/
|
|
_saveFolderContents: function PRFT__saveFolderContents() {
|
|
this._transactions = [];
|
|
var contents = this.utils.getFolderContents(this._id, false, false);
|
|
var ios = Cc["@mozilla.org/network/io-service;1"].
|
|
getService(Ci.nsIIOService);
|
|
for (var i = 0; i < contents.childCount; ++i) {
|
|
var child = contents.getChild(i);
|
|
var txn;
|
|
if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER) {
|
|
txn = new PlacesRemoveFolderTransaction(child.itemId);
|
|
}
|
|
else if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
|
|
txn = new PlacesRemoveSeparatorTransaction(this._id, i);
|
|
}
|
|
else {
|
|
txn = new PlacesRemoveItemTransaction(child.itemId,
|
|
ios.newURI(child.uri, null, null),
|
|
this._id, i);
|
|
}
|
|
this._transactions.push(txn);
|
|
}
|
|
},
|
|
|
|
doTransaction: function PRFT_doTransaction() {
|
|
var title = this.bookmarks.getItemTitle(this._id);
|
|
this.LOG("Remove Folder: " + title);
|
|
|
|
this._saveFolderContents();
|
|
|
|
// Remove children backwards to preserve parent-child relationships.
|
|
for (var i = this._transactions.length - 1; i >= 0; --i)
|
|
this._transactions[i].doTransaction();
|
|
|
|
// Remove this folder itself.
|
|
this._removeTxn.doTransaction();
|
|
},
|
|
|
|
undoTransaction: function PRFT_undoTransaction() {
|
|
this._removeTxn.undoTransaction();
|
|
|
|
var title = this.bookmarks.getItemTitle(this._id);
|
|
this.LOG("UNRemove Folder: " + title);
|
|
|
|
// Create children forwards to preserve parent-child relationships.
|
|
for (var i = 0; i < this._transactions.length; ++i)
|
|
this._transactions[i].undoTransaction();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove an Item
|
|
*/
|
|
function PlacesRemoveItemTransaction(id, uri, oldContainer, oldIndex) {
|
|
this._id = id;
|
|
this._uri = uri;
|
|
this._oldContainer = oldContainer;
|
|
this._oldIndex = oldIndex;
|
|
this._annotations = [];
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesRemoveItemTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PRIT_doTransaction() {
|
|
this.LOG("Remove Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex);
|
|
this._title = this.bookmarks.getItemTitle(this._id);
|
|
this._annotations = this.utils.getAnnotationsForItem(this._id);
|
|
this.utils.bookmarks.removeItem(this._id);
|
|
},
|
|
|
|
undoTransaction: function PRIT_undoTransaction() {
|
|
this.LOG("UNRemove Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex);
|
|
this._id = this.bookmarks.insertBookmark(this._oldContainer, this._uri,
|
|
this._oldIndex, this._title);
|
|
this.utils.setAnnotationsForItem(this._id, this._annotations);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove a separator
|
|
*/
|
|
function PlacesRemoveSeparatorTransaction(oldContainer, oldIndex) {
|
|
this._oldContainer = oldContainer;
|
|
this._oldIndex = oldIndex;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesRemoveSeparatorTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PRST_doTransaction() {
|
|
this.LOG("Remove Separator from: " + this._oldContainer + "," + this._oldIndex);
|
|
this.bookmarks.removeChildAt(this._oldContainer, this._oldIndex);
|
|
},
|
|
|
|
undoTransaction: function PRST_undoTransaction() {
|
|
this.LOG("UNRemove Separator from: " + this._oldContainer + "," + this._oldIndex);
|
|
this.bookmarks.insertSeparator(this._oldContainer, this._oldIndex);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a bookmark's title.
|
|
*/
|
|
function PlacesEditItemTitleTransaction(id, newTitle) {
|
|
this._id = id;
|
|
this._newTitle = newTitle;
|
|
this._oldTitle = "";
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditItemTitleTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PEITT_doTransaction() {
|
|
this._oldTitle = this.bookmarks.getItemTitle(this._id);
|
|
this.bookmarks.setItemTitle(this._id, this._newTitle);
|
|
},
|
|
|
|
undoTransaction: function PEITT_undoTransaction() {
|
|
this.bookmarks.setItemTitle(this._id, this._oldTitle);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a bookmark's uri.
|
|
*/
|
|
function PlacesEditBookmarkURITransaction(aBookmarkId, aNewURI) {
|
|
this._id = aBookmarkId;
|
|
this._newURI = aNewURI;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditBookmarkURITransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PEBUT_doTransaction() {
|
|
this._oldURI = this.bookmarks.getBookmarkURI(this._id);
|
|
this.bookmarks.changeBookmarkURI(this._id, this._newURI);
|
|
},
|
|
|
|
undoTransaction: function PEBUT_undoTransaction() {
|
|
this.bookmarks.changeBookmarkURI(this._id, this._oldURI);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set/Unset Load-in-sidebar annotation
|
|
*/
|
|
function PlacesSetLoadInSidebarTransaction(aBookmarkId, aLoadInSidebar) {
|
|
this.id = aBookmarkId;
|
|
this._loadInSidebar = aLoadInSidebar;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesSetLoadInSidebarTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
_anno: {
|
|
name: LOAD_IN_SIDEBAR_ANNO,
|
|
type: Ci.nsIAnnotationService.TYPE_INT32,
|
|
value: 1,
|
|
flags: 0,
|
|
expires: Ci.nsIAnnotationService.EXPIRE_NEVER
|
|
},
|
|
|
|
doTransaction: function PSLIST_doTransaction() {
|
|
this._wasSet = this.utils.annotations
|
|
.itemHasAnnotation(this.id, this._anno.name);
|
|
if (this._loadInSidebar) {
|
|
this.utils.setAnnotationsForItem(this.id, [this._anno]);
|
|
}
|
|
else {
|
|
try {
|
|
this.utils.annotations.removeItemAnnotation(this.id, this._anno.name);
|
|
} catch(ex) { }
|
|
}
|
|
},
|
|
|
|
undoTransaction: function PSLIST_undoTransaction() {
|
|
if (this._wasSet != this._loadInSidebar) {
|
|
this._loadInSidebar = !this._loadInSidebar;
|
|
this.doTransaction();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a the description of a bookmark or a folder
|
|
*
|
|
*/
|
|
function PlacesEditItemDescriptionTransaction(aItemId, aDescription) {
|
|
this.id = aItemId;
|
|
this._newDescription = aDescription;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditItemDescriptionTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
_oldDescription: "",
|
|
DESCRIPTION_ANNO: DESCRIPTION_ANNO,
|
|
nsIAnnotationService: Components.interfaces.nsIAnnotationService,
|
|
|
|
doTransaction: function PSLIST_doTransaction() {
|
|
const annos = this.utils.annotations;
|
|
|
|
if (annos.itemHasAnnotation(this.id, this.DESCRIPTION_ANNO)) {
|
|
this._oldDescription =
|
|
annos.getItemAnnotationString(this.id, this.DESCRIPTION_ANNO);
|
|
}
|
|
|
|
if (this._newDescription) {
|
|
annos.setItemAnnotationString(this.id, this.DESCRIPTION_ANNO,
|
|
this._newDescription, 0,
|
|
this.nsIAnnotationService.EXPIRE_NEVER);
|
|
}
|
|
else if (this._oldDescription)
|
|
annos.removeItemAnnotation(this.id, this.DESCRIPTION_ANNO);
|
|
},
|
|
|
|
undoTransaction: function PSLIST_undoTransaction() {
|
|
const annos = this.utils.annotations;
|
|
|
|
if (this._oldDescription) {
|
|
annos.setItemAnnotationString(this.id, this.DESCRIPTION_ANNO,
|
|
this._oldDescription, 0,
|
|
this.nsIAnnotationService.EXPIRE_NEVER);
|
|
}
|
|
else if (annos.itemHasAnnotation(this.id, this.DESCRIPTION_ANNO))
|
|
annos.removeItemAnnotation(this.id, this.DESCRIPTION_ANNO);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a bookmark's keyword.
|
|
*/
|
|
function PlacesEditBookmarkKeywordTransaction(id, newKeyword) {
|
|
this.id = id;
|
|
this._newKeyword = newKeyword;
|
|
this._oldKeyword = "";
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditBookmarkKeywordTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PEBKT_doTransaction() {
|
|
this._oldKeyword = this.bookmarks.getKeywordForBookmark(this.id);
|
|
this.bookmarks.setKeywordForBookmark(this.id, this._newKeyword);
|
|
},
|
|
|
|
undoTransaction: function PEBKT_undoTransaction() {
|
|
this.bookmarks.setKeywordForBookmark(this.id, this._oldKeyword);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit the post data associated with a URI
|
|
*/
|
|
function PlacesEditURIPostDataTransaction(aURI, aPostData) {
|
|
this._uri = aURI;
|
|
this._newPostData = aPostData;
|
|
this._oldPostData = null;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditURIPostDataTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PEUPDT_doTransaction() {
|
|
this._oldPostData = this.utils.getPostDataForURI(this._uri);
|
|
this.utils.setPostDataForURI(this._uri, this._newPostData);
|
|
},
|
|
|
|
undoTransaction: function PEUPDT_undoTransaction() {
|
|
this.utils.setPostDataForURI(this._uri, this._oldPostData);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a live bookmark's site URI.
|
|
*/
|
|
function PlacesEditLivemarkSiteURITransaction(folderId, uri) {
|
|
this._folderId = folderId;
|
|
this._newURI = uri;
|
|
this._oldURI = null;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditLivemarkSiteURITransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PELSUT_doTransaction() {
|
|
this._oldURI = this.livemarks.getSiteURI(this._folderId);
|
|
this.livemarks.setSiteURI(this._folderId, this._newURI);
|
|
},
|
|
|
|
undoTransaction: function PELSUT_undoTransaction() {
|
|
this.livemarks.setSiteURI(this._folderId, this._oldURI);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a live bookmark's feed URI.
|
|
*/
|
|
function PlacesEditLivemarkFeedURITransaction(folderId, uri) {
|
|
this._folderId = folderId;
|
|
this._newURI = uri;
|
|
this._oldURI = null;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditLivemarkFeedURITransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PELFUT_doTransaction() {
|
|
this._oldURI = this.livemarks.getFeedURI(this._folderId);
|
|
this.livemarks.setFeedURI(this._folderId, this._newURI);
|
|
this.livemarks.reloadLivemarkFolder(this._folderId);
|
|
},
|
|
|
|
undoTransaction: function PELFUT_undoTransaction() {
|
|
this.livemarks.setFeedURI(this._folderId, this._oldURI);
|
|
this.livemarks.reloadLivemarkFolder(this._folderId);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a bookmark's microsummary.
|
|
*/
|
|
function PlacesEditBookmarkMicrosummaryTransaction(aID, newMicrosummary) {
|
|
this.id = aID;
|
|
this._newMicrosummary = newMicrosummary;
|
|
this._oldMicrosummary = null;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesEditBookmarkMicrosummaryTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
mss: Cc["@mozilla.org/microsummary/service;1"].
|
|
getService(Ci.nsIMicrosummaryService),
|
|
|
|
doTransaction: function PEBMT_doTransaction() {
|
|
this._oldMicrosummary = this.mss.getMicrosummary(this.id);
|
|
if (this._newMicrosummary)
|
|
this.mss.setMicrosummary(this.id, this._newMicrosummary);
|
|
else
|
|
this.mss.removeMicrosummary(this.id);
|
|
},
|
|
|
|
undoTransaction: function PEBMT_undoTransaction() {
|
|
if (this._oldMicrosummary)
|
|
this.mss.setMicrosummary(this.id, this._oldMicrosummary);
|
|
else
|
|
this.mss.removeMicrosummary(this.id);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sort a folder by name
|
|
*/
|
|
function PlacesSortFolderByNameTransaction(aFolderId, aFolderIndex) {
|
|
this._folderId = aFolderId;
|
|
this._folderIndex = aFolderIndex;
|
|
this._oldOrder = null,
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesSortFolderByNameTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PSSFBN_doTransaction() {
|
|
this._oldOrder = [];
|
|
|
|
var items = [];
|
|
var contents = this.utils.getFolderContents(this._folderId, false, false);
|
|
var count = contents.childCount;
|
|
for (var i = 0; i < count; ++i) {
|
|
var item = contents.getChild(i);
|
|
this._oldOrder[item.itemId] = i;
|
|
items.push(item);
|
|
}
|
|
|
|
function sortItems(a, b) {
|
|
var atitle = a.title;
|
|
var btitle = b.title;
|
|
return (atitle == btitle) ? 0 : ((atitle < btitle) ? -1 : 1);
|
|
}
|
|
items.sort(sortItems);
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
this.bookmarks.setItemIndex(items[i].itemId, i);
|
|
},
|
|
|
|
undoTransaction: function PSSFBN_undoTransaction() {
|
|
for (item in this._oldOrder)
|
|
this.bookmarks.setItemIndex(item, this._oldOrder[item]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set the bookmarks toolbar folder.
|
|
*/
|
|
function PlacesSetBookmarksToolbarTransaction(aFolderId) {
|
|
this._folderId = aFolderId;
|
|
this._oldFolderId = this.utils.toolbarFolder;
|
|
this.redoTransaction = this.doTransaction;
|
|
}
|
|
PlacesSetBookmarksToolbarTransaction.prototype = {
|
|
__proto__: PlacesBaseTransaction.prototype,
|
|
|
|
doTransaction: function PSBTT_doTransaction() {
|
|
this.utils.bookmarks.toolbarFolder = this._folderId;
|
|
},
|
|
|
|
undoTransaction: function PSBTT_undoTransaction() {
|
|
this.utils.bookmarks.toolbarFolder = this._oldFolderId;
|
|
}
|
|
};
|
|
|
|
function goUpdatePlacesCommands() {
|
|
goUpdateCommand("placesCmd_open");
|
|
goUpdateCommand("placesCmd_open:window");
|
|
goUpdateCommand("placesCmd_open:tab");
|
|
goUpdateCommand("placesCmd_open:tabs");
|
|
#ifdef MOZ_PLACES_BOOKMARKS
|
|
goUpdateCommand("placesCmd_new:folder");
|
|
goUpdateCommand("placesCmd_new:bookmark");
|
|
goUpdateCommand("placesCmd_new:livemark");
|
|
goUpdateCommand("placesCmd_new:separator");
|
|
goUpdateCommand("placesCmd_show:info");
|
|
goUpdateCommand("placesCmd_moveBookmarks");
|
|
goUpdateCommand("placesCmd_setAsBookmarksToolbarFolder");
|
|
goUpdateCommand("placesCmd_reload");
|
|
goUpdateCommand("placesCmd_sortBy:name");
|
|
#endif
|
|
}
|