mirror of
https://github.com/rn10950/RetroZilla.git
synced 2024-11-10 01:40:17 +01:00
2624 lines
92 KiB
JavaScript
2624 lines
92 KiB
JavaScript
|
/* -*- 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>
|
||
|
*
|
||
|
* 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 ***** */
|
||
|
|
||
|
function LOG(str) {
|
||
|
dump("*** " + str + "\n");
|
||
|
}
|
||
|
|
||
|
const Ci = Components.interfaces;
|
||
|
const Cc = Components.classes;
|
||
|
const Cr = Components.results;
|
||
|
|
||
|
const NHRVO = Ci.nsINavHistoryResultViewObserver;
|
||
|
|
||
|
// These need to be kept in sync with the meaning of these roots in
|
||
|
// default_places.html!
|
||
|
const ORGANIZER_ROOT_HISTORY = "place:&beginTime=-2592000000000&beginTimeRef=1&endTime=7200000000&endTimeRef=2&sort=4&type=1";
|
||
|
const ORGANIZER_ROOT_BOOKMARKS = "place:&folder=2&group=3&excludeItems=1";
|
||
|
const ORGANIZER_SUBSCRIPTIONS_QUERY = "place:&annotation=livemark%2FfeedURI";
|
||
|
|
||
|
// Place entries that are containers, e.g. bookmark folders or queries.
|
||
|
const TYPE_X_MOZ_PLACE_CONTAINER = "text/x-moz-place-container";
|
||
|
// Place entries that are bookmark separators.
|
||
|
const TYPE_X_MOZ_PLACE_SEPARATOR = "text/x-moz-place-separator";
|
||
|
// Place entries that are not containers or separators
|
||
|
const TYPE_X_MOZ_PLACE = "text/x-moz-place";
|
||
|
// Place entries in shortcut url format (url\ntitle)
|
||
|
const TYPE_X_MOZ_URL = "text/x-moz-url";
|
||
|
// Place entries formatted as HTML anchors
|
||
|
const TYPE_HTML = "text/html";
|
||
|
// Place entries as raw URL text
|
||
|
const TYPE_UNICODE = "text/unicode";
|
||
|
|
||
|
// 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
|
||
|
|
||
|
function STACK(args) {
|
||
|
var temp = arguments.callee.caller;
|
||
|
while (temp) {
|
||
|
LOG("NAME: " + temp.name);
|
||
|
temp = temp.arguments.callee.caller;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents an insertion point within a container where we can insert
|
||
|
* items.
|
||
|
* @param folderId
|
||
|
* The folderId of the parent container
|
||
|
* @param index
|
||
|
* The index within the container where we should insert
|
||
|
* @param orientation
|
||
|
* 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(folderId, index, orientation) {
|
||
|
this.folderId = folderId;
|
||
|
this.index = index;
|
||
|
this.orientation = orientation;
|
||
|
}
|
||
|
InsertionPoint.prototype.toString = function IP_toString() {
|
||
|
return "[object InsertionPoint(folder:" + this.folderId + ",index:" + this.index + ",orientation:" + this.orientation + ")]";
|
||
|
};
|
||
|
|
||
|
function QI_node(node, iid) {
|
||
|
var result = null;
|
||
|
try {
|
||
|
result = node.QueryInterface(iid);
|
||
|
}
|
||
|
catch (e) {
|
||
|
}
|
||
|
NS_ASSERT(result, "Node QI Failed");
|
||
|
return result;
|
||
|
}
|
||
|
function asFolder(node) { return QI_node(node, Ci.nsINavHistoryFolderResultNode); }
|
||
|
function asVisit(node) { return QI_node(node, Ci.nsINavHistoryVisitResultNode); }
|
||
|
function asFullVisit(node){ return QI_node(node, Ci.nsINavHistoryFullVisitResultNode);}
|
||
|
function asContainer(node){ return QI_node(node, Ci.nsINavHistoryContainerResultNode);}
|
||
|
function asQuery(node) { return QI_node(node, Ci.nsINavHistoryQueryResultNode); }
|
||
|
|
||
|
/**
|
||
|
* A View Configuration
|
||
|
*/
|
||
|
function ViewConfig(peerDropTypes, childDropTypes, excludeItems, excludeQueries,
|
||
|
expandQueries, peerDropIndex) {
|
||
|
this.peerDropTypes = peerDropTypes;
|
||
|
this.childDropTypes = childDropTypes;
|
||
|
this.excludeItems = excludeItems;
|
||
|
this.excludeQueries = excludeQueries;
|
||
|
this.expandQueries = expandQueries;
|
||
|
this.peerDropIndex = peerDropIndex;
|
||
|
}
|
||
|
ViewConfig.GENERIC_DROP_TYPES =
|
||
|
[TYPE_X_MOZ_PLACE_CONTAINER,
|
||
|
TYPE_X_MOZ_PLACE_SEPARATOR,
|
||
|
TYPE_X_MOZ_PLACE,
|
||
|
TYPE_X_MOZ_URL];
|
||
|
|
||
|
/**
|
||
|
* Configures Views, applying some meta-model rules. These rules are model-like,
|
||
|
* e.g. must apply everwhere a model is instantiated, but are not actually stored
|
||
|
* in the data model itself. For example, you can't drag leaf items onto the
|
||
|
* places root. This needs to be enforced automatically everywhere a view of
|
||
|
* that model is instantiated.
|
||
|
*/
|
||
|
var ViewConfigurator = {
|
||
|
rules: {
|
||
|
"folder=1": new ViewConfig([TYPE_X_MOZ_PLACE_CONTAINER],
|
||
|
ViewConfig.GENERIC_DROP_TYPES,
|
||
|
true, false, false, 4)
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Applies rules to a specific view.
|
||
|
*/
|
||
|
configure: function PC_configure(view) {
|
||
|
// Determine what place the view is showing.
|
||
|
var place = view.place;
|
||
|
|
||
|
// Find a ruleset that matches the current place.
|
||
|
var rules = null;
|
||
|
for (var test in this.rules) {
|
||
|
if (place.indexOf(test) != -1) {
|
||
|
rules = this.rules[test];
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If rules are found, apply them.
|
||
|
if (rules) {
|
||
|
view.peerDropTypes = rules.peerDropTypes;
|
||
|
view.childDropTypes = rules.childDropTypes;
|
||
|
view.excludeItems = rules.excludeItems;
|
||
|
view.excludeQueries = rules.excludeQueries;
|
||
|
view.expandQueries = rules.expandQueries;
|
||
|
view.peerDropIndex = rules.peerDropIndex;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* The Master Places Controller
|
||
|
*/
|
||
|
var PlacesController = {
|
||
|
/**
|
||
|
* Makes a URI from a spec.
|
||
|
* @param spec
|
||
|
* The string spec of the URI
|
||
|
* @returns A URI object for the spec.
|
||
|
*/
|
||
|
_uri: function PC__uri(spec) {
|
||
|
var ios =
|
||
|
Cc["@mozilla.org/network/io-service;1"].
|
||
|
getService(Ci.nsIIOService);
|
||
|
return ios.newURI(spec, null, null);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The Bookmarks Service.
|
||
|
*/
|
||
|
_bookmarks: null,
|
||
|
get bookmarks() {
|
||
|
if (!this._bookmarks) {
|
||
|
this._bookmarks =
|
||
|
Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||
|
getService(Ci.nsINavBookmarksService);
|
||
|
}
|
||
|
return this._bookmarks;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The Nav History Service.
|
||
|
*/
|
||
|
_history: null,
|
||
|
get history() {
|
||
|
if (!this._history) {
|
||
|
this._history =
|
||
|
Cc["@mozilla.org/browser/nav-history-service;1"].
|
||
|
getService(Ci.nsINavHistoryService);
|
||
|
}
|
||
|
return this._history;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The Live Bookmark Service.
|
||
|
*/
|
||
|
_livemarks: null,
|
||
|
get livemarks() {
|
||
|
if (!this._livemarks) {
|
||
|
this._livemarks =
|
||
|
Cc["@mozilla.org/browser/livemark-service;1"].
|
||
|
getService(Ci.nsILivemarkService);
|
||
|
}
|
||
|
return this._livemarks;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The Annotations Service.
|
||
|
*/
|
||
|
_annotations: null,
|
||
|
get annotations() {
|
||
|
if (!this._annotations) {
|
||
|
this._annotations =
|
||
|
Cc["@mozilla.org/browser/annotation-service;1"].
|
||
|
getService(Ci.nsIAnnotationService);
|
||
|
}
|
||
|
return this._annotations;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Generates a HistoryResultNode for the contents of a folder.
|
||
|
* @param folderId
|
||
|
* The folder to open
|
||
|
* @param excludeItems
|
||
|
* True to hide all items (individual bookmarks). This is used on
|
||
|
* the left places pane so you just get a folder hierarchy.
|
||
|
* @param expandQueries
|
||
|
* True to make query items expand as new containers. For managing,
|
||
|
* you want this to be false, for menus and such, you want this to
|
||
|
* be true.
|
||
|
* @returns A HistoryContainerResultNode containing the contents of the
|
||
|
* folder. This container is guaranteed to be open.
|
||
|
*/
|
||
|
getFolderContents: function PC_getFolderContents(folderId, excludeItems, expandQueries) {
|
||
|
var query = this.history.getNewQuery();
|
||
|
query.setFolders([folderId], 1);
|
||
|
var options = this.history.getNewQueryOptions();
|
||
|
options.setGroupingMode([Ci.nsINavHistoryQueryOptions.GROUP_BY_FOLDER], 1);
|
||
|
options.excludeItems = excludeItems;
|
||
|
options.expandQueries = expandQueries;
|
||
|
|
||
|
var result = this.history.executeQuery(query, options);
|
||
|
result.root.containerOpen = true;
|
||
|
return asContainer(result.root);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The currently active Places view.
|
||
|
*/
|
||
|
_activeView: null,
|
||
|
get activeView() {
|
||
|
return this._activeView;
|
||
|
},
|
||
|
set activeView(activeView) {
|
||
|
this._activeView = activeView;
|
||
|
return this._activeView;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* The Transaction Manager for this instance.
|
||
|
*/
|
||
|
_tm: null,
|
||
|
get tm() {
|
||
|
if (!this._tm) {
|
||
|
this._tm =
|
||
|
Cc["@mozilla.org/transactionmanager;1"].
|
||
|
createInstance(Ci.nsITransactionManager);
|
||
|
}
|
||
|
return this._tm;
|
||
|
},
|
||
|
|
||
|
isCommandEnabled: function PC_isCommandEnabled(command) {
|
||
|
//LOG("isCommandEnabled: " + command);
|
||
|
return document.getElementById(command).getAttribute("disabled") != "true";
|
||
|
},
|
||
|
|
||
|
supportsCommand: function PC_supportsCommand(command) {
|
||
|
//LOG("supportsCommand: " + command);
|
||
|
// Non-Places specific commands that we also support
|
||
|
if (command == "cmd_undo" || command == "cmd_redo")
|
||
|
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 (command.substr(0, CMD_PREFIX.length) == CMD_PREFIX);
|
||
|
},
|
||
|
|
||
|
doCommand: function PC_doCommand(command) {
|
||
|
LOG("doCommand: " + command);
|
||
|
if (command == "cmd_undo")
|
||
|
this.tm.undoTransaction();
|
||
|
else if (command == "cmd_redo")
|
||
|
this.tm.redoTransaction();
|
||
|
},
|
||
|
|
||
|
onEvent: function PC_onEvent(eventName) {
|
||
|
//LOG("onEvent: " + eventName);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates the enabled state of a command element.
|
||
|
* @param commandId
|
||
|
* The id of the command element to update
|
||
|
* @param enabled
|
||
|
* Whether or not the command element should be enabled.
|
||
|
*/
|
||
|
_setEnabled: function PC__setEnabled(commandId, enabled) {
|
||
|
var command = document.getElementById(commandId);
|
||
|
// Prevents excessive setAttributes
|
||
|
var disabled = command.hasAttribute("disabled");
|
||
|
if (enabled && disabled)
|
||
|
command.removeAttribute("disabled");
|
||
|
else if (!enabled && !disabled)
|
||
|
command.setAttribute("disabled", "true");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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 selection contains no nodes that cannot be removed,
|
||
|
* false otherwise.
|
||
|
*/
|
||
|
_hasRemovableSelection: function PC__hasRemovableSelection() {
|
||
|
var v = this.activeView;
|
||
|
NS_ASSERT(v, "No active view - cannot paste!");
|
||
|
if (!v)
|
||
|
return false;
|
||
|
var nodes = v.getSelectionNodes();
|
||
|
var root = v.getResult().root;
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
var parent = nodes[i].parent || root;
|
||
|
// 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.
|
||
|
if (this.nodeIsFolder(parent)) {
|
||
|
var readOnly = this.bookmarks.getFolderReadonly(asFolder(parent).folderId);
|
||
|
if (readOnly)
|
||
|
return !readOnly;
|
||
|
}
|
||
|
}
|
||
|
if (!v.hasSelection)
|
||
|
return !this.nodeIsReadOnly(root);
|
||
|
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._activeView.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() {
|
||
|
var v = this.activeView;
|
||
|
NS_ASSERT(v, "No active view - cannot insert!");
|
||
|
if (!v)
|
||
|
return false;
|
||
|
var nodes = v.getSelectionNodes();
|
||
|
var root = v.getResult().root;
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
var parent = nodes[i].parent || root;
|
||
|
if (this.nodeIsReadOnly(parent))
|
||
|
return false;
|
||
|
}
|
||
|
// Even if there's no selection, we need to check the root. Otherwise
|
||
|
// commands may be enabled for history views when nothing is selected.
|
||
|
return !this.nodeIsReadOnly(root);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a Bookmark folder or not.
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a Bookmark folder, false otherwise
|
||
|
*/
|
||
|
nodeIsFolder: function PC_nodeIsFolder(node) {
|
||
|
NS_ASSERT(node, "null node");
|
||
|
return (node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode represents a bookmarked URI.
|
||
|
*
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
*
|
||
|
* @returns true if the node represents a bookmarked URI, false otherwise
|
||
|
*/
|
||
|
nodeIsBookmark: function PC_nodeIsBookmark(node) {
|
||
|
NS_ASSERT(node, "null node");
|
||
|
|
||
|
if (!this.nodeIsURI(node))
|
||
|
return false;
|
||
|
var uri = this._uri(node.uri);
|
||
|
return this.bookmarks.isBookmarked(uri);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a Bookmark separator.
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a Bookmark separator, false otherwise
|
||
|
*/
|
||
|
nodeIsSeparator: function PC_nodeIsSeparator(node) {
|
||
|
NS_ASSERT(node, "null node");
|
||
|
return (node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a URL item or not
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a URL item, false otherwise
|
||
|
*/
|
||
|
nodeIsURI: function PC_nodeIsURI(node) {
|
||
|
NS_ASSERT(node, "null node");
|
||
|
const NHRN = Ci.nsINavHistoryResultNode;
|
||
|
return node.type == NHRN.RESULT_TYPE_URI ||
|
||
|
node.type == NHRN.RESULT_TYPE_VISIT ||
|
||
|
node.type == NHRN.RESULT_TYPE_FULL_VISIT;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a Query item or not
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a Query item, false otherwise
|
||
|
*/
|
||
|
nodeIsQuery: function PC_nodeIsQuery(node) {
|
||
|
NS_ASSERT(node, "null node");
|
||
|
return node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines if a node is read only (children cannot be inserted, sometimes
|
||
|
* they cannot be removed depending on the circumstance)
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is readonly, false otherwise
|
||
|
*/
|
||
|
nodeIsReadOnly: function PC_nodeIsReadOnly(node) {
|
||
|
NS_ASSERT(node, "null node");
|
||
|
if (this.nodeIsFolder(node))
|
||
|
return this.bookmarks.getFolderReadonly(asFolder(node).folderId);
|
||
|
else if (this.nodeIsQuery(node))
|
||
|
return asQuery(node).childrenReadOnly;
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a host folder or not
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a host item, false otherwise
|
||
|
*/
|
||
|
nodeIsHost: function PC_nodeIsHost(node) {
|
||
|
return node.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_HOST;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a container item or not
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a container item, false otherwise
|
||
|
*/
|
||
|
nodeIsContainer: function PC_nodeIsContainer(node) {
|
||
|
const NHRN = Ci.nsINavHistoryResultNode;
|
||
|
return node.type == NHRN.RESULT_TYPE_HOST ||
|
||
|
node.type == NHRN.RESULT_TYPE_QUERY ||
|
||
|
node.type == NHRN.RESULT_TYPE_FOLDER ||
|
||
|
node.type == NHRN.RESULT_TYPE_REMOTE_CONTAINER;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not a ResultNode is a remotecontainer item.
|
||
|
* ResultNote may be either a remote container result type or a bookmark folder
|
||
|
* with a nonempty remoteContainerType. The remote container result node
|
||
|
* type is for dynamically created remote containers (i.e., for the file
|
||
|
* browser service where you get your folders in bookmark menus). Bookmark
|
||
|
* folders are marked as remote containers when some other component is
|
||
|
* registered as interested in them and providing some operations, in which
|
||
|
* case their remoteContainerType indicates which component is thus registered.
|
||
|
* For exmaple, the livemark service uses this mechanism.
|
||
|
* @param node
|
||
|
* A NavHistoryResultNode
|
||
|
* @returns true if the node is a container item, false otherwise
|
||
|
*/
|
||
|
nodeIsRemoteContainer: function PC_nodeIsRemoteContainer(node) {
|
||
|
const NHRN = Ci.nsINavHistoryResultNode;
|
||
|
if (node.type == NHRN.RESULT_TYPE_REMOTE_CONTAINER)
|
||
|
return true;
|
||
|
if (this.nodeIsFolder(node))
|
||
|
return asContainer(node).remoteContainerType != "";
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether a ResultNode is a remote container registered by the livemark service.
|
||
|
* @param node
|
||
|
* A NavHistory Result Node
|
||
|
* @returns true if the node is a livemark container item
|
||
|
*/
|
||
|
nodeIsLivemarkContainer: function PC_nodeIsLivemarkContainer(node) {
|
||
|
return (this.nodeIsRemoteContainer(node) &&
|
||
|
asContainer(node).remoteContainerType == "@mozilla.org/browser/livemark-service;1");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates undo/redo commands.
|
||
|
*/
|
||
|
updateTMCommands: function PC_updateTMCommands() {
|
||
|
this._setEnabled("cmd_undo", this.tm.numberOfUndoItems > 0);
|
||
|
this._setEnabled("cmd_redo", this.tm.numberOfRedoItems > 0);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines whether or not the selection intersects the read only "system"
|
||
|
* portion of the display.
|
||
|
* @returns true if the selection intersects, false otherwise.
|
||
|
*/
|
||
|
_selectionOverlapsSystemArea: function PC__selectionOverlapsSystemArea() {
|
||
|
var v = this.activeView;
|
||
|
if (!v.hasSelection)
|
||
|
return false;
|
||
|
var nodes = v.getSelectionNodes();
|
||
|
var root = v.getResult().root;
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
// We also don't care about nodes that aren't at the root level.
|
||
|
if (nodes[i].parent != root ||
|
||
|
this.getIndexOfNode(nodes[i]) >= v.peerDropIndex)
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates commands on focus/selection change to reflect the enabled/
|
||
|
* disabledness of commands in relation to the state of the selection.
|
||
|
*/
|
||
|
onCommandUpdate: function PC_onCommandUpdate() {
|
||
|
var v = this._activeView;
|
||
|
if (!v) {
|
||
|
// Initial update, no view is set yet
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var inSysArea = this._selectionOverlapsSystemArea();
|
||
|
var selectedNode = v.selectedNode;
|
||
|
var canInsert = this._canInsert();
|
||
|
|
||
|
// Show Info
|
||
|
var hasSingleSelection = v.hasSingleSelection;
|
||
|
this._setEnabled("placesCmd_show:info", !inSysArea &&
|
||
|
hasSingleSelection &&
|
||
|
(this.nodeIsBookmark(selectedNode) ||
|
||
|
this.nodeIsFolder(selectedNode)));
|
||
|
this._updateSelectCommands();
|
||
|
this._updateEditCommands(inSysArea, canInsert);
|
||
|
this._updateOpenCommands(inSysArea, hasSingleSelection, selectedNode);
|
||
|
this._updateSortCommands(inSysArea, hasSingleSelection, selectedNode, canInsert);
|
||
|
this._updateCreateCommands(inSysArea, canInsert);
|
||
|
this._updateLivemarkCommands(hasSingleSelection, selectedNode);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates commands for selecting.
|
||
|
*/
|
||
|
_updateSelectCommands: function PC__updateSelectCommands() {
|
||
|
var result = this._activeView.getResult();
|
||
|
if (result) {
|
||
|
var container = asContainer(result.root);
|
||
|
this._setEnabled("placesCmd_select:all",
|
||
|
this._activeView.selType != "single" &&
|
||
|
container.childCount > 0);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates commands for persistent sorting
|
||
|
* @param inSysArea
|
||
|
* true if the selection intersects the read only "system" area.
|
||
|
* @param hasSingleSelection
|
||
|
* true if only one item is selected in the view
|
||
|
* @param selectedNode
|
||
|
* The selected nsINavHistoryResultNode
|
||
|
* @param canInsert
|
||
|
* true if the item is a writable container that can be inserted
|
||
|
* into
|
||
|
*/
|
||
|
_updateSortCommands:
|
||
|
function PC__updateSortCommands(inSysArea, hasSingleSelection, selectedNode,
|
||
|
canInsert) {
|
||
|
// Some views, like menupopups, destroy their result as they hide, but they
|
||
|
// are still the "last-active" view. Don't barf.
|
||
|
var result = this.activeView.getResult();
|
||
|
var viewIsFolder = result ? this.nodeIsFolder(result.root) : false;
|
||
|
|
||
|
// Depending on the selection, the persistent sort command sorts the
|
||
|
// contents of the current folder (when the selection is mixed or leaf
|
||
|
// items like individual bookmarks are selected) or the contents of the
|
||
|
// selected folder (if a single folder is selected).
|
||
|
var sortingChildren = false;
|
||
|
var name = result.root.title;
|
||
|
var sortFolder = result.root;
|
||
|
if (selectedNode && selectedNode.parent) {
|
||
|
name = selectedNode.parent.title;
|
||
|
sortFolder = selectedNode.parent;
|
||
|
}
|
||
|
if (hasSingleSelection && this.nodeIsFolder(selectedNode)) {
|
||
|
name = selectedNode.title;
|
||
|
sortFolder = selectedNode;
|
||
|
sortingChildren = true;
|
||
|
}
|
||
|
|
||
|
// Count the children of the container. If there aren't at least two, we
|
||
|
// don't want to enable the command since there's nothing to be sorted.
|
||
|
// We need to get the unfiltered contents of the container to make this
|
||
|
// determination, which means a new query, since the existing query may
|
||
|
// be filtered (e.g. left list).
|
||
|
var enoughChildrenToSort = false;
|
||
|
if (this.nodeIsFolder(sortFolder)) {
|
||
|
var folder = asFolder(sortFolder);
|
||
|
var contents = this.getFolderContents(folder.folderId, false, false);
|
||
|
enoughChildrenToSort = contents.childCount > 1;
|
||
|
}
|
||
|
var metadata = this._buildSelectionMetadata();
|
||
|
this._setEnabled("placesCmd_sortby:name",
|
||
|
(sortingChildren || !inSysArea) && canInsert && viewIsFolder &&
|
||
|
!("mixed" in metadata) && enoughChildrenToSort);
|
||
|
|
||
|
var strings = document.getElementById("placeBundle");
|
||
|
var command = document.getElementById("placesCmd_sortby:name");
|
||
|
|
||
|
if (name) {
|
||
|
command.setAttribute("label",
|
||
|
strings.getFormattedString("sortByName", [name]));
|
||
|
}
|
||
|
else
|
||
|
command.setAttribute("label", strings.getString("sortByNameGeneric"));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates commands for opening links
|
||
|
* @param inSysArea
|
||
|
* true if the selection intersects the read only "system" area.
|
||
|
* @param hasSingleSelection
|
||
|
* true if only one item is selected in the view
|
||
|
* @param selectedNode
|
||
|
* The selected nsINavHistoryResultNode
|
||
|
*/
|
||
|
_updateOpenCommands:
|
||
|
function PC__updateOpenCommands(inSysArea, hasSingleSelection, selectedNode) {
|
||
|
// Open
|
||
|
var hasSelectedURI = this.activeView.selectedURINode != null;
|
||
|
this._setEnabled("placesCmd_open", !inSysArea && hasSelectedURI);
|
||
|
this._setEnabled("placesCmd_open:window", !inSysArea && hasSelectedURI);
|
||
|
this._setEnabled("placesCmd_open:tab", !inSysArea && hasSelectedURI);
|
||
|
|
||
|
// We can open multiple links in tabs if there is either:
|
||
|
// a) a single folder selected
|
||
|
// b) many links or folders selected
|
||
|
// XXXben - inSysArea should be removed, and generally be replaced by
|
||
|
// something that counts the number of selected links or the
|
||
|
// number of links in the folder and enables the command only if
|
||
|
// the number is less than some 'safe' amount.
|
||
|
var singleFolderSelected = hasSingleSelection &&
|
||
|
this.nodeIsFolder(selectedNode);
|
||
|
this._setEnabled("placesCmd_open:tabs",
|
||
|
!inSysArea && (singleFolderSelected ||
|
||
|
!hasSingleSelection));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines if the active view can support inserting items of a certain type.
|
||
|
*/
|
||
|
_viewSupportsInsertingType: function PC__viewSupportsInsertingType(type) {
|
||
|
var types = this.activeView.peerDropTypes;
|
||
|
for (var i = 0; i < types.length; ++i) {
|
||
|
if (types[i] == type.value)
|
||
|
return true;
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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(TYPE_X_MOZ_PLACE_CONTAINER);
|
||
|
xferable.addDataFlavor(TYPE_X_MOZ_PLACE_SEPARATOR);
|
||
|
xferable.addDataFlavor(TYPE_X_MOZ_PLACE);
|
||
|
xferable.addDataFlavor(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._viewSupportsInsertingType(type.value))
|
||
|
return false;
|
||
|
|
||
|
// unwrapNodes will throw if the data blob is malformed.
|
||
|
var nodes = this.unwrapNodes(data, type.value);
|
||
|
|
||
|
var ip = this.activeView.insertionPoint;
|
||
|
if (!ip)
|
||
|
return false;
|
||
|
var contents = this.getFolderContents(ip.folderId);
|
||
|
var cc = contents.childCount;
|
||
|
|
||
|
var self = this;
|
||
|
/**
|
||
|
* Determines whether or not a node is a first-level child of a folder.
|
||
|
* @param node
|
||
|
* The node to check
|
||
|
* @returns true if the node is a child of the container at the top level, false
|
||
|
* otherwise
|
||
|
*/
|
||
|
function nodeIsInList(node) {
|
||
|
// Anything that isn't a URI is paste-able many times (folders,
|
||
|
// separators)
|
||
|
if (!self.nodeIsURI(node))
|
||
|
return false;
|
||
|
for (var i = 0; i < cc; ++i) {
|
||
|
if (contents.getChild(i).uri == node.uri.spec)
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Since the bookmarks data model enforces only one instance of a URI per
|
||
|
// folder, it is not possible to paste a selection into a folder where
|
||
|
// all of the URIs already exist. Thus we need to return false to disable
|
||
|
// the command for this case. If only some of the URIs are present, we
|
||
|
// can still paste the non-present URIs, so it's ok to enable the command.
|
||
|
var nodesInList = 0;
|
||
|
// Sadly, this is O(N^2).
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
if (nodeIsInList(nodes[i]))
|
||
|
++nodesInList;
|
||
|
}
|
||
|
return (nodesInList != nodes.length);
|
||
|
}
|
||
|
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;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates commands for edit operations (cut, copy, paste, delete)
|
||
|
* @param inSysArea
|
||
|
* true if the selection intersects the read only "system" area.
|
||
|
* @param canInsert
|
||
|
* true if the item is a writable container that can be inserted
|
||
|
* into
|
||
|
*/
|
||
|
_updateEditCommands: function PC__updateEditCommands(inSysArea, canInsert) {
|
||
|
// Cut: only if there's a removable selection. cannot remove system items.
|
||
|
var removableSelection = this._hasRemovableSelection();
|
||
|
this._setEnabled("placesCmd_edit:cut", !inSysArea && removableSelection);
|
||
|
this._setEnabled("placesCmd_edit:delete",
|
||
|
!inSysArea && removableSelection);
|
||
|
// Copy: only if there's a selection. cannot copy system items.
|
||
|
this._setEnabled("placesCmd_edit:copy",
|
||
|
!inSysArea && this.activeView.hasSelection);
|
||
|
// Paste: cannot paste adjacent to system items. only if the containing
|
||
|
// folder is not read only. only if there is clipboard data. only
|
||
|
// if that data is a valid place format.
|
||
|
this._setEnabled("placesCmd_edit:paste", !inSysArea && canInsert &&
|
||
|
this._hasClipboardData() && this._canPaste());
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates commands for creating new bookmarks, folders and separators.
|
||
|
* @param inSysArea
|
||
|
* true if the selection intersects the read only "system" area.
|
||
|
* @param canInsert
|
||
|
* true if the item is a writable container that can be inserted
|
||
|
* into
|
||
|
*/
|
||
|
_updateCreateCommands:
|
||
|
function PC__updateCreateCommands(inSysArea, canInsert) {
|
||
|
var canInsertFolders = canInsertSeparators = canInsertURLs = false;
|
||
|
var peerTypes = this.activeView.peerDropTypes;
|
||
|
for (var i = 0; i < peerTypes.length; ++i) {
|
||
|
switch(peerTypes[i]) {
|
||
|
case TYPE_X_MOZ_PLACE_CONTAINER:
|
||
|
canInsertFolders = true;
|
||
|
break;
|
||
|
case TYPE_X_MOZ_PLACE_SEPARATOR:
|
||
|
canInsertSeparators = true;
|
||
|
break;
|
||
|
case TYPE_X_MOZ_URL:
|
||
|
canInsertURLs = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// New Folder - don't check inSysArea since we should be able to create
|
||
|
// folders in the left list even when elements at the top are selected.
|
||
|
this._setEnabled("placesCmd_new:folder", canInsertFolders && canInsert);
|
||
|
|
||
|
// New Bookmark
|
||
|
this._setEnabled("placesCmd_new:bookmark", !inSysArea && canInsertURLs && canInsert);
|
||
|
// New Separator
|
||
|
this._setEnabled("placesCmd_new:separator",
|
||
|
!inSysArea && canInsertSeparators && canInsert);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates Livemark Commands: Reload
|
||
|
* @param hasSingleSelection
|
||
|
* true if only one item is selected in the view
|
||
|
* @param selectedNode
|
||
|
* The selected nsINavHistoryResultNode
|
||
|
*/
|
||
|
_updateLivemarkCommands:
|
||
|
function PC__updateLivemarkCommands(hasSingleSelection, selectedNode) {
|
||
|
var isLivemarkItem = false;
|
||
|
var strings = document.getElementById("placeBundle");
|
||
|
var command = document.getElementById("placesCmd_reload");
|
||
|
if (hasSingleSelection) {
|
||
|
if (selectedNode.uri.indexOf("livemark%2F") != -1) {
|
||
|
isLivemarkItem = true;
|
||
|
command.setAttribute("label", strings.getString("livemarkReloadAll"));
|
||
|
}
|
||
|
else {
|
||
|
var name;
|
||
|
if (this.nodeIsLivemarkContainer(selectedNode)) {
|
||
|
isLivemarkItem = true;
|
||
|
name = selectedNode.title;
|
||
|
}
|
||
|
else if (this.nodeIsURI(selectedNode)) {
|
||
|
var uri = this._uri(selectedNode.uri);
|
||
|
isLivemarkItem = this.annotations.hasAnnotation(uri, "livemark/bookmarkFeedURI");
|
||
|
if (isLivemarkItem)
|
||
|
name = selectedNode.parent.title;
|
||
|
}
|
||
|
|
||
|
if (isLivemarkItem)
|
||
|
command.setAttribute("label", strings.getFormattedString("livemarkReloadOne", [name]));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!isLivemarkItem)
|
||
|
command.setAttribute("label", strings.getString("livemarkReload"));
|
||
|
|
||
|
this._setEnabled("placesCmd_reload", isLivemarkItem);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gather information about the selection according to the following
|
||
|
* rules:
|
||
|
* "link" single selection is URI
|
||
|
* "links" all selected items are links, and there are at least 2
|
||
|
* "folder" selection is a folder
|
||
|
* "query" selection is a query
|
||
|
* "remotecontainer" selection is a remote container
|
||
|
* "separator" selection is a separator line
|
||
|
* "host" selection is a host
|
||
|
* "mutable" selection can have items inserted or reordered
|
||
|
* "mixed" selection contains more than one type
|
||
|
* "allLivemarks" selection is a query containing every livemark
|
||
|
* "multiselect" seleciton contains more than one item
|
||
|
* In addition, a property is set corresponding to each of the selected
|
||
|
* items' annotation names.
|
||
|
*
|
||
|
* @returns an object with each of the properties above set if the selection
|
||
|
* matches that rule.
|
||
|
* Note: This can be slow, so don't call it anywhere performance critical!
|
||
|
*/
|
||
|
_buildSelectionMetadata: function PC__buildSelectionMetadata() {
|
||
|
var metadata = { };
|
||
|
var v = this.activeView;
|
||
|
var hasSingleSelection = v.hasSingleSelection;
|
||
|
if (v.selectedURINode && hasSingleSelection)
|
||
|
metadata["link"] = true;
|
||
|
if (hasSingleSelection) {
|
||
|
var selectedNode = v.selectedNode;
|
||
|
if (this.nodeIsFolder(selectedNode))
|
||
|
metadata["folder"] = true;
|
||
|
if (this.nodeIsQuery(selectedNode))
|
||
|
metadata["query"] = true;
|
||
|
if (this.nodeIsRemoteContainer(selectedNode))
|
||
|
metadata["remotecontainer"] = true;
|
||
|
if (this.nodeIsSeparator(selectedNode))
|
||
|
metadata["separator"] = true;
|
||
|
if (this.nodeIsHost(selectedNode))
|
||
|
metadata["host"] = 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.
|
||
|
//
|
||
|
// The mutability property starts out set to true, and is removed if
|
||
|
// any component of the selection is found to be part of a readonly
|
||
|
// container.
|
||
|
metadata["mutable"] = true;
|
||
|
|
||
|
var self = this;
|
||
|
/**
|
||
|
* Determines whether or not a node is a readonly folder.
|
||
|
* @param node
|
||
|
* The node to test.
|
||
|
* @returns true if the node is a readonly folder.
|
||
|
*/
|
||
|
function folderIsReadOnly(node) {
|
||
|
return self.nodeIsFolder(node) &&
|
||
|
self.bookmarks.getFolderReadonly(asFolder(node).folderId);
|
||
|
}
|
||
|
|
||
|
var foundNonURI = false;
|
||
|
var nodes = v.getSelectionNodes();
|
||
|
var root = v.getResult().root;
|
||
|
if (v.hasSelection)
|
||
|
var lastParent = nodes[0].parent, lastType = nodes[0].type;
|
||
|
else {
|
||
|
// If there is no selection, mutability is determined by the readonly-ness
|
||
|
// of the result root. See note above on mutability.
|
||
|
if (folderIsReadOnly(root))
|
||
|
delete metadata["mutable"];
|
||
|
}
|
||
|
// Walk the selection, gathering metadata about the selected items.
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
var node = nodes[i];
|
||
|
if (!this.nodeIsURI(node))
|
||
|
foundNonURI = true;
|
||
|
|
||
|
// If there is a selection, mutability is determined by the readonly-ness
|
||
|
// of the selected item, or the parent of the selection. See note above
|
||
|
// on mutability.
|
||
|
if (this.nodeIsReadOnly(node) || folderIsReadOnly(node.parent || root))
|
||
|
delete metadata["mutable"];
|
||
|
|
||
|
var uri = null;
|
||
|
if (this.nodeIsURI(node))
|
||
|
uri = this._uri(node.uri);
|
||
|
if (this.nodeIsFolder(node))
|
||
|
uri = this.bookmarks.getFolderURI(asFolder(node).folderId);
|
||
|
if (uri) {
|
||
|
var names = this.annotations.getPageAnnotationNames(uri, { });
|
||
|
for (var j = 0; j < names.length; ++j)
|
||
|
metadata[names[i]] = true;
|
||
|
}
|
||
|
else if (this.nodeIsQuery(node)) {
|
||
|
// 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 = this._uri(node.uri);
|
||
|
if (uri.spec == ORGANIZER_SUBSCRIPTIONS_QUERY)
|
||
|
metadata["allLivemarks"] = true;
|
||
|
}
|
||
|
|
||
|
if (nodes[i].parent != lastParent || nodes[i].type != lastType)
|
||
|
metadata["mixed"] = true;
|
||
|
}
|
||
|
|
||
|
if (v.selType != "single")
|
||
|
metadata["multiselect"] = true;
|
||
|
if (!foundNonURI && nodes.length > 1)
|
||
|
metadata["links"] = true;
|
||
|
|
||
|
return metadata;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Determines if a menuitem should be shown or not by comparing the rules
|
||
|
* that govern the item's display with the state of the selection.
|
||
|
* @param metadata
|
||
|
* metadata about the selection.
|
||
|
* @param rules
|
||
|
* rules that govern the item's display
|
||
|
* @returns true if the conditions are satisfied and the item can be
|
||
|
* displayed, false otherwise.
|
||
|
*/
|
||
|
_shouldShowMenuItem: function(metadata, rules) {
|
||
|
for (var i = 0; i < rules.length; ++i) {
|
||
|
if (rules[i] in metadata)
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Build a context menu for the selection, ensuring that the content of the
|
||
|
* selection is correct and enabling/disabling items according to the state
|
||
|
* of the commands.
|
||
|
* @param popup
|
||
|
* The menupopup to build children into.
|
||
|
*/
|
||
|
buildContextMenu: function PC_buildContextMenu(popup) {
|
||
|
if (document.popupNode.hasAttribute("view")) {
|
||
|
var view = document.popupNode.getAttribute("view");
|
||
|
this.activeView = document.getElementById(view);
|
||
|
}
|
||
|
|
||
|
// Determine availability/enabled state of commands
|
||
|
var metadata = this._buildSelectionMetadata();
|
||
|
|
||
|
var lastVisible = null;
|
||
|
for (var i = 0; i < popup.childNodes.length; ++i) {
|
||
|
var item = popup.childNodes[i];
|
||
|
var rules = item.getAttribute("selection");
|
||
|
item.hidden = !this._shouldShowMenuItem(metadata, rules.split("|"));
|
||
|
if (!item.hidden)
|
||
|
lastVisible = item;
|
||
|
if (item.hasAttribute("command")) {
|
||
|
var disabled = !this.isCommandEnabled(item.getAttribute("command"));
|
||
|
item.setAttribute("disabled", disabled);
|
||
|
}
|
||
|
}
|
||
|
if (lastVisible.localName == "menuseparator")
|
||
|
lastVisible.hidden = true;
|
||
|
|
||
|
return true;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Select all links in the current view.
|
||
|
*/
|
||
|
selectAll: function() {
|
||
|
this._activeView.selectAll();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Loads a URL in the appropriate tab or window, given the user's preference
|
||
|
* specified by modifier keys tracked by a DOM event
|
||
|
* @param event
|
||
|
* The DOM Mouse event with modifier keys set that track the user's
|
||
|
* preferred destination window or tab.
|
||
|
*/
|
||
|
mouseLoadURI: function PC_mouseLoadURI(event) {
|
||
|
var node = this._activeView.selectedURINode;
|
||
|
if (node) {
|
||
|
var browser = this._getBrowserWindow();
|
||
|
if (browser)
|
||
|
browser.openUILink(node.uri, event, false, false);
|
||
|
else
|
||
|
this._openBrowserWith(node.uri);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Opens the bookmark properties for the selected URI Node.
|
||
|
*/
|
||
|
showBookmarkPropertiesForSelection:
|
||
|
function PC_showBookmarkPropertiesForSelection() {
|
||
|
var node = this._activeView.selectedNode;
|
||
|
if (!node)
|
||
|
return;
|
||
|
|
||
|
if (this.nodeIsFolder(node))
|
||
|
this.showFolderProperties(asFolder(node).folderId);
|
||
|
else if (node.uri)
|
||
|
this.showBookmarkProperties(this._uri(node.uri));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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.");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Show an "Add Bookmark" dialog for the specified URI.
|
||
|
*
|
||
|
* @param uri an nsIURI object for which the "add bookmark" dialog is
|
||
|
* to be shown.
|
||
|
*/
|
||
|
showAddBookmarkUI: function PC_showAddBookmarkUI(uri) {
|
||
|
this._showBookmarkDialog(uri, "add");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Show an "Add Bookmarks" dialog to allow the adding of a folder full
|
||
|
* of bookmarks corresponding to the objects in the uriList. This will
|
||
|
* be called most often as the result of a "Bookmark All Tabs..." command.
|
||
|
*
|
||
|
* @param uriList List of nsIURI objects representing the locations
|
||
|
* to be bookmarked.
|
||
|
*/
|
||
|
showAddMultiBookmarkUI: function PC_showAddMultiBookmarkUI(uriList) {
|
||
|
NS_ASSERT(uriList.length, "showAddMultiBookmarkUI expects a list of nsIURI objects");
|
||
|
this._showBookmarkDialog(uriList, "addmulti");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Opens the bookmark properties panel for a given URI.
|
||
|
*
|
||
|
* @param uri an nsIURI object for which the properties are to be shown
|
||
|
*/
|
||
|
showBookmarkProperties: function PC_showBookmarkProperties(uri) {
|
||
|
this._showBookmarkDialog(uri, "edit");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Opens the folder properties panel for a given folder ID.
|
||
|
*
|
||
|
* @param folderid an integer representing the ID of the folder to edit
|
||
|
* @returns none
|
||
|
*/
|
||
|
showFolderProperties: function PC_showFolderProperties(folderId) {
|
||
|
NS_ASSERT(typeof(folderId)=="number",
|
||
|
"showFolderProperties received a non-numerical value for its folderId parameter");
|
||
|
this._showBookmarkDialog(folderId, "edit");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Shows the bookmark dialog corresponding to the specified user action.
|
||
|
* This is an implementation function, and shouldn't be called directly;
|
||
|
* rather, use the specific variant above that corresponds to your situation.
|
||
|
*
|
||
|
* @param identifier the URI or folder ID or URI list to show
|
||
|
* properties for
|
||
|
* @param action "add" or "edit", see _determineVariant in
|
||
|
* bookmarkProperties.js
|
||
|
*/
|
||
|
_showBookmarkDialog: function PC__showBookmarkDialog(identifier, action) {
|
||
|
window.openDialog("chrome://browser/content/places/bookmarkProperties.xul",
|
||
|
"", "width=600,height=400,chrome,dependent,modal,resizable",
|
||
|
identifier, this, action);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* This method changes the URI of a bookmark. Because of the URI-based
|
||
|
* identity model, it accomplishes this by replacing instances of the old
|
||
|
* URI with the new URI in each applicable folder, then copies the
|
||
|
* metadata from the old URI to the new URI.
|
||
|
*/
|
||
|
changeBookmarkURI: function PC_changeBookmarkProperties(oldURI, newURI) {
|
||
|
this.bookmarks.changeBookmarkURI(oldURI, newURI);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
*
|
||
|
* 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._activeView.selectedNode;
|
||
|
if (this._activeView.hasSingleSelection) {
|
||
|
if (selectedNode.uri.indexOf("livemark%2F") != -1) {
|
||
|
this.livemarks.reloadAllLivemarks();
|
||
|
}
|
||
|
else {
|
||
|
var folder = null;
|
||
|
if (this.nodeIsLivemarkContainer(selectedNode)) {
|
||
|
folder = asFolder(selectedNode);
|
||
|
}
|
||
|
else if (this.nodeIsURI(selectedNode)) {
|
||
|
var uri = this._uri(selectedNode.uri);
|
||
|
var isLivemarkItem = this.annotations.hasAnnotation(uri, "livemark/bookmarkFeedURI");
|
||
|
if (isLivemarkItem)
|
||
|
folder = asFolder(selectedNode.parent);
|
||
|
}
|
||
|
if (folder)
|
||
|
this.livemarks.reloadLivemarkFolder(folder.folderId);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the current active browser window.
|
||
|
*/
|
||
|
_getBrowserWindow: function PC__getBrowserWindow() {
|
||
|
var wm =
|
||
|
Cc["@mozilla.org/appshell/window-mediator;1"].
|
||
|
getService(Ci.nsIWindowMediator);
|
||
|
return wm.getMostRecentWindow("navigator:browser");
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Opens a new browser window, showing the specified url.
|
||
|
*/
|
||
|
_openBrowserWith: function PC__openBrowserWith(url) {
|
||
|
openDialog("chrome://browser/content/browser.xul", "_blank",
|
||
|
"chrome,all,dialog=no", url, null, null);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Loads the selected URL in a new tab.
|
||
|
*/
|
||
|
openLinkInNewTab: function PC_openLinkInNewTab() {
|
||
|
var node = this._activeView.selectedURINode;
|
||
|
if (node) {
|
||
|
var browser = this._getBrowserWindow();
|
||
|
if (browser)
|
||
|
browser.openNewTabWith(node.uri, null, null);
|
||
|
else
|
||
|
this._openBrowserWith(node.uri);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Loads the selected URL in a new window.
|
||
|
*/
|
||
|
openLinkInNewWindow: function PC_openLinkInNewWindow() {
|
||
|
var node = this._activeView.selectedURINode;
|
||
|
if (node) {
|
||
|
var browser = this._getBrowserWindow();
|
||
|
if (browser)
|
||
|
browser.openNewWindowWith(node.uri, null, null, false);
|
||
|
else
|
||
|
this._openBrowserWith(node.uri);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Loads the selected URL in the current window, replacing the Places page.
|
||
|
*/
|
||
|
openLinkInCurrentWindow: function PC_openLinkInCurrentWindow() {
|
||
|
var node = this._activeView.selectedURINode;
|
||
|
if (node) {
|
||
|
var browser = this._getBrowserWindow();
|
||
|
if (browser)
|
||
|
browser.loadURI(node.uri, null, null, false);
|
||
|
else
|
||
|
this._openBrowserWith(node.uri);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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._activeView.selectedNode;
|
||
|
if (this._activeView.hasSingleSelection && this.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
|
||
|
var browser = this._getBrowserWindow().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)
|
||
|
if (browser.browsers[firstIndex].currentURI.spec != "about:blank")
|
||
|
break;
|
||
|
++firstIndex;
|
||
|
}
|
||
|
|
||
|
// Open each uri in the folder in a tab.
|
||
|
var index = firstIndex;
|
||
|
asFolder(node);
|
||
|
var wasOpen = node.containerOpen;
|
||
|
node.containerOpen = true;
|
||
|
var cc = node.childCount;
|
||
|
for (var i = 0; i < cc; ++i) {
|
||
|
var childNode = node.getChild(i);
|
||
|
if (this.nodeIsURI(childNode)) {
|
||
|
// If there are tabs to load over, load the uri into the next tab.
|
||
|
if (index < tabCount)
|
||
|
tabPanels[index].loadURI(childNode.uri);
|
||
|
// Otherwise, create a new tab to load the uri into.
|
||
|
else
|
||
|
browser.addTab(childNode.uri);
|
||
|
++index;
|
||
|
}
|
||
|
}
|
||
|
node.containerOpen = wasOpen;
|
||
|
|
||
|
// 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
|
||
|
this.browserWindow.content.focus();
|
||
|
|
||
|
}
|
||
|
else {
|
||
|
var nodes = this._activeView.getSelectionNodes();
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
if (this.nodeIsURI(nodes[i]))
|
||
|
this._getBrowserWindow().openNewTabWith(nodes[i].uri,
|
||
|
null, null);
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Create a new Bookmark folder somewhere. Prompts the user for the name
|
||
|
* of the folder.
|
||
|
*/
|
||
|
newFolder: function PC_newFolder() {
|
||
|
var view = this._activeView;
|
||
|
|
||
|
view.saveSelection(view.SAVE_SELECTION_INSERT);
|
||
|
var ps =
|
||
|
Cc["@mozilla.org/embedcomp/prompt-service;1"].
|
||
|
getService(Ci.nsIPromptService);
|
||
|
var bundle = document.getElementById("placeBundle");
|
||
|
var title = bundle.getString("newFolderTitle");
|
||
|
var text = bundle.getString("newFolderMessage");
|
||
|
var value = { value: bundle.getString("newFolderDefault") };
|
||
|
if (ps.prompt(window, title, text, value, null, { })) {
|
||
|
var ip = view.insertionPoint;
|
||
|
if (!ip)
|
||
|
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||
|
var txn = new PlacesCreateFolderTransaction(value.value, ip.folderId,
|
||
|
ip.index);
|
||
|
this.tm.doTransaction(txn);
|
||
|
this._activeView.focus();
|
||
|
view.restoreSelection();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Create a new Bookmark separator somewhere.
|
||
|
*/
|
||
|
newSeparator: function PC_newSeparator() {
|
||
|
var ip = this._activeView.insertionPoint;
|
||
|
if (!ip)
|
||
|
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||
|
var txn = new PlacesCreateSeparatorTransaction(ip.folderId, ip.index);
|
||
|
this.tm.doTransaction(txn);
|
||
|
this._activeView.focus();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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 = this.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 (this.nodeIsFolder(node)) {
|
||
|
// TODO -- node.parent might be a query and not a folder. See bug 324948
|
||
|
var folder = asFolder(node);
|
||
|
removedFolders.push(folder);
|
||
|
transactions.push(new PlacesRemoveFolderTransaction(folder.folderId));
|
||
|
}
|
||
|
else if (this.nodeIsSeparator(node)) {
|
||
|
// A Bookmark separator.
|
||
|
transactions.push(new PlacesRemoveSeparatorTransaction(
|
||
|
asFolder(node.parent).folderId, index));
|
||
|
}
|
||
|
else if (this.nodeIsFolder(node.parent)) {
|
||
|
// A Bookmark in a Bookmark Folder.
|
||
|
transactions.push(new PlacesRemoveItemTransaction(
|
||
|
this._uri(node.uri), asFolder(node.parent).folderId, index));
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Removes the set of selected ranges from bookmarks.
|
||
|
* @param txnName
|
||
|
* See |remove|.
|
||
|
*/
|
||
|
_removeRowsFromBookmarks: function PC__removeRowsFromBookmarks(txnName) {
|
||
|
var ranges = this._activeView.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);
|
||
|
this.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._activeView.getSelectionNodes();
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
var node = nodes[i];
|
||
|
var bhist = this.history.QueryInterface(Ci.nsIBrowserHistory);
|
||
|
if (this.nodeIsHost(node))
|
||
|
bhist.removePagesFromHost(node.title, true);
|
||
|
else if (this.nodeIsURI(node))
|
||
|
bhist.removePage(this._uri(node.uri));
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Removes the selection
|
||
|
* @param txnName
|
||
|
* A name for the transaction if this is being performed
|
||
|
* as part of another operation.
|
||
|
*/
|
||
|
remove: function PC_remove(txnName) {
|
||
|
NS_ASSERT(txnName !== undefined, "Must supply Transaction Name");
|
||
|
this._activeView.saveSelection(this._activeView.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._activeView.getResult().root.type;
|
||
|
if (this.nodeIsFolder(this._activeView.getResult().root))
|
||
|
this._removeRowsFromBookmarks(txnName);
|
||
|
else
|
||
|
this._removeRowsFromHistory();
|
||
|
|
||
|
this._activeView.restoreSelection();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets the index of a node within its parent container
|
||
|
* @param node
|
||
|
* The node to look up
|
||
|
* @returns The index of the node within its parent container, or -1 if the
|
||
|
* node was not found or the node specified has no parent.
|
||
|
*/
|
||
|
getIndexOfNode: function PC_getIndexOfNode(node) {
|
||
|
var parent = node.parent;
|
||
|
if (!parent || !this.nodeIsContainer(parent))
|
||
|
return -1;
|
||
|
var wasOpen = parent.containerOpen;
|
||
|
parent.containerOpen = true;
|
||
|
var cc = parent.childCount;
|
||
|
for (var i = 0; i < cc && asContainer(parent).getChild(i) != node; ++i);
|
||
|
parent.containerOpen = wasOpen;
|
||
|
return i < cc ? i : -1;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* String-wraps a NavHistoryResultNode according to the rules of the specified
|
||
|
* content type.
|
||
|
* @param node
|
||
|
* The Result node to wrap (serialize)
|
||
|
* @param type
|
||
|
* The content type to serialize as
|
||
|
* @returns A string serialization of the node
|
||
|
*/
|
||
|
wrapNode: function PC_wrapNode(node, type) {
|
||
|
switch (type) {
|
||
|
case TYPE_X_MOZ_PLACE_CONTAINER:
|
||
|
case TYPE_X_MOZ_PLACE:
|
||
|
case TYPE_X_MOZ_PLACE_SEPARATOR:
|
||
|
// Data is encoded like this:
|
||
|
// bookmarks folder: <folderId>\n<>\n<parentId>\n<indexInParent>
|
||
|
// uri: 0\n<uri>\n<parentId>\n<indexInParent>
|
||
|
// separator: 0\n<>\n<parentId>\n<indexInParent>
|
||
|
var wrapped = "";
|
||
|
if (this.nodeIsFolder(node))
|
||
|
wrapped += asFolder(node).folderId + NEWLINE;
|
||
|
else
|
||
|
wrapped += "0" + NEWLINE;
|
||
|
|
||
|
if (this.nodeIsURI(node) || this.nodeIsQuery(node))
|
||
|
wrapped += node.uri + NEWLINE;
|
||
|
else
|
||
|
wrapped += NEWLINE;
|
||
|
|
||
|
if (this.nodeIsFolder(node.parent))
|
||
|
wrapped += asFolder(node.parent).folderId + NEWLINE;
|
||
|
else
|
||
|
wrapped += "0" + NEWLINE;
|
||
|
|
||
|
wrapped += this.getIndexOfNode(node);
|
||
|
return wrapped;
|
||
|
case TYPE_X_MOZ_URL:
|
||
|
return node.uri + NEWLINE + node.title;
|
||
|
case TYPE_HTML:
|
||
|
return "<A HREF=\"" + node.uri + "\">" + node.title + "</A>";
|
||
|
}
|
||
|
// case TYPE_UNICODE:
|
||
|
return node.uri;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Unwraps data from the Clipboard or the current Drag Session.
|
||
|
* @param blob
|
||
|
* A blob (string) of data, in some format we potentially know how
|
||
|
* to parse.
|
||
|
* @param type
|
||
|
* The content type of the blob.
|
||
|
* @returns An array of objects representing each item contained by the source.
|
||
|
*/
|
||
|
unwrapNodes: function PC_unwrapNodes(blob, type) {
|
||
|
// We use \n here because the transferable system converts \r\n to \n
|
||
|
var parts = blob.split("\n");
|
||
|
var nodes = [];
|
||
|
for (var i = 0; i < parts.length; ++i) {
|
||
|
var data = { };
|
||
|
switch (type) {
|
||
|
case TYPE_X_MOZ_PLACE_CONTAINER:
|
||
|
case TYPE_X_MOZ_PLACE:
|
||
|
case TYPE_X_MOZ_PLACE_SEPARATOR:
|
||
|
// Data in these types has 4 parts, so if there are less than 4 parts
|
||
|
// remaining, the data blob is malformed and we should stop.
|
||
|
if (i > (parts.length - 4))
|
||
|
break;
|
||
|
nodes.push({ folderId: parseInt(parts[i++]),
|
||
|
uri: parts[i] ? this._uri(parts[i]) : null,
|
||
|
parent: parseInt(parts[++i]),
|
||
|
index: parseInt(parts[++i]) });
|
||
|
break;
|
||
|
case TYPE_X_MOZ_URL:
|
||
|
// See above.
|
||
|
if (i > (parts.length - 2))
|
||
|
break;
|
||
|
nodes.push({ uri: this._uri(parts[i++]),
|
||
|
title: parts[i] });
|
||
|
break;
|
||
|
case TYPE_UNICODE:
|
||
|
// See above.
|
||
|
if (i > (parts.length - 1))
|
||
|
break;
|
||
|
nodes.push({ uri: this._uri(parts[i]) });
|
||
|
break;
|
||
|
default:
|
||
|
LOG("Cannot unwrap data of type " + type);
|
||
|
throw Cr.NS_ERROR_INVALID_ARG;
|
||
|
}
|
||
|
}
|
||
|
return nodes;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Get a transaction for copying a leaf item from one container to another.
|
||
|
* @param uri
|
||
|
* The URI of the item being copied
|
||
|
* @param container
|
||
|
* The container being copied into
|
||
|
* @param index
|
||
|
* The index within the container the item is copied to
|
||
|
* @returns A nsITransaction object that performs the copy.
|
||
|
*/
|
||
|
_getItemCopyTransaction: function (uri, container, index) {
|
||
|
var itemTitle = this.bookmarks.getItemTitle(uri);
|
||
|
var createTxn = new PlacesCreateItemTransaction(uri, container, index);
|
||
|
var editTxn = new PlacesEditItemTitleTransaction(uri, itemTitle);
|
||
|
return new PlacesAggregateTransaction("ItemCopy", [createTxn, editTxn]);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Gets a transaction for copying (recursively nesting to include children)
|
||
|
* a folder and its contents from one folder to another.
|
||
|
* @param data
|
||
|
* Unwrapped dropped folder data
|
||
|
* @param container
|
||
|
* The container we are copying into
|
||
|
* @param index
|
||
|
* The index in the destination container to insert the new items
|
||
|
* @returns A nsITransaction object that will perform the copy.
|
||
|
*/
|
||
|
_getFolderCopyTransaction:
|
||
|
function PC__getFolderCopyTransaction(data, container, index) {
|
||
|
var self = this;
|
||
|
function getChildTransactions(folderId) {
|
||
|
var childTransactions = [];
|
||
|
var children = self.getFolderContents(folderId, false, false);
|
||
|
var cc = children.childCount;
|
||
|
var txn = null;
|
||
|
for (var i = 0; i < cc; ++i) {
|
||
|
var node = children.getChild(i);
|
||
|
if (self.nodeIsFolder(node)) {
|
||
|
var nodeFolderId = asFolder(node).folderId;
|
||
|
var title = self.bookmarks.getFolderTitle(nodeFolderId);
|
||
|
txn = new PlacesCreateFolderTransaction(title, -1, index);
|
||
|
txn.childTransactions = getChildTransactions(nodeFolderId);
|
||
|
}
|
||
|
else if (self.nodeIsURI(node) || self.nodeIsQuery(node)) {
|
||
|
txn = self._getItemCopyTransaction(self._uri(node.uri), -1,
|
||
|
index);
|
||
|
}
|
||
|
else if (self.nodeIsSeparator(node)) {
|
||
|
txn = new PlacesCreateSeparatorTransaction(-1, index);
|
||
|
}
|
||
|
childTransactions.push(txn);
|
||
|
}
|
||
|
return childTransactions;
|
||
|
}
|
||
|
|
||
|
var title = this.bookmarks.getFolderTitle(data.folderId);
|
||
|
var createTxn =
|
||
|
new PlacesCreateFolderTransaction(title, container, index);
|
||
|
createTxn.childTransactions =
|
||
|
getChildTransactions(data.folderId, createTxn);
|
||
|
return createTxn;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Constructs a Transaction for the drop or paste of a blob of data into
|
||
|
* a container.
|
||
|
* @param data
|
||
|
* The unwrapped data blob of dropped or pasted data.
|
||
|
* @param type
|
||
|
* The content type of the data
|
||
|
* @param container
|
||
|
* The container the data was dropped or pasted into
|
||
|
* @param index
|
||
|
* The index within the container the item was dropped or pasted at
|
||
|
* @param copy
|
||
|
* The drag action was copy, so don't move folders or links.
|
||
|
* @returns An object implementing nsITransaction that can perform
|
||
|
* the move/insert.
|
||
|
*/
|
||
|
makeTransaction: function PC_makeTransaction(data, type, container,
|
||
|
index, copy) {
|
||
|
switch (type) {
|
||
|
case TYPE_X_MOZ_PLACE_CONTAINER:
|
||
|
case TYPE_X_MOZ_PLACE:
|
||
|
if (data.folderId > 0) {
|
||
|
// Place is a folder.
|
||
|
if (copy)
|
||
|
return this._getFolderCopyTransaction(data, container, index);
|
||
|
return new PlacesMoveFolderTransaction(data.folderId, data.parent,
|
||
|
data.index, container,
|
||
|
index);
|
||
|
}
|
||
|
if (copy)
|
||
|
return this._getItemCopyTransaction(data.uri, container, index);
|
||
|
return new PlacesMoveItemTransaction(data.uri, data.parent,
|
||
|
data.index, container,
|
||
|
index);
|
||
|
case TYPE_X_MOZ_PLACE_SEPARATOR:
|
||
|
if (copy) {
|
||
|
// There is no data in a separator, so copying it just amounts to
|
||
|
// inserting a new separator.
|
||
|
return new PlacesCreateSeparatorTransaction(container, index);
|
||
|
}
|
||
|
// Similarly, moving a separator is just removing the old one and
|
||
|
// then creating a new one.
|
||
|
var removeTxn =
|
||
|
new PlacesRemoveSeparatorTransaction(data.parent, data.index);
|
||
|
var createTxn =
|
||
|
new PlacesCreateSeparatorTransaction(container, index);
|
||
|
return new PlacesAggregateTransaction("SeparatorMove", [removeTxn, createTxn]);
|
||
|
case TYPE_X_MOZ_URL:
|
||
|
// Creating and Setting the title is a two step process, so create
|
||
|
// a transaction for each then aggregate them.
|
||
|
var createTxn =
|
||
|
new PlacesCreateItemTransaction(data.uri, container, index);
|
||
|
var editTxn =
|
||
|
new PlacesEditItemTitleTransaction(data.uri, data.title);
|
||
|
return new PlacesAggregateTransaction("DropMozURLItem", [createTxn, editTxn]);
|
||
|
case TYPE_UNICODE:
|
||
|
// Creating and Setting the title is a two step process, so create
|
||
|
// a transaction for each then aggregate them.
|
||
|
var createTxn =
|
||
|
new PlacesCreateItemTransaction(data.uri, container, index);
|
||
|
var editTxn =
|
||
|
new PlacesEditItemTitleTransaction(data.uri, data.uri);
|
||
|
return new PlacesAggregateTransaction("DropItem", [createTxn, editTxn]);
|
||
|
}
|
||
|
return null;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Wraps a string in a nsISupportsString wrapper
|
||
|
* @param str
|
||
|
* The string to wrap
|
||
|
* @returns A nsISupportsString object containing a string.
|
||
|
*/
|
||
|
_wrapString: function PC__wrapString(str) {
|
||
|
var s =
|
||
|
Cc["@mozilla.org/supports-string;1"].
|
||
|
createInstance(Ci.nsISupportsString);
|
||
|
s.data = str;
|
||
|
return s;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* 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._activeView.getCopyableSelection();
|
||
|
else
|
||
|
nodes = this._activeView.getDragableSelection();
|
||
|
var dataSet = new TransferDataSet();
|
||
|
for (var i = 0; i < nodes.length; ++i) {
|
||
|
var node = nodes[i];
|
||
|
|
||
|
var data = new TransferData();
|
||
|
var self = this;
|
||
|
function addData(type) {
|
||
|
data.addDataForFlavour(type, self._wrapString(self.wrapNode(node, type)));
|
||
|
}
|
||
|
|
||
|
if (this.nodeIsFolder(node) || this.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(TYPE_X_MOZ_PLACE_CONTAINER);
|
||
|
}
|
||
|
else if (this.nodeIsSeparator(node)) {
|
||
|
addData(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(TYPE_X_MOZ_PLACE);
|
||
|
addData(TYPE_UNICODE);
|
||
|
addData(TYPE_HTML);
|
||
|
addData(TYPE_X_MOZ_URL);
|
||
|
}
|
||
|
dataSet.push(data);
|
||
|
}
|
||
|
return dataSet;
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Copy Bookmarks and Folders to the clipboard
|
||
|
*/
|
||
|
copy: function() {
|
||
|
var nodes = this._activeView.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];
|
||
|
var self = this;
|
||
|
function generateChunk(type) {
|
||
|
var suffix = i < (nodes.length - 1) ? NEWLINE : "";
|
||
|
return self.wrapNode(node, type) + suffix;
|
||
|
}
|
||
|
if (this.nodeIsFolder(node) || this.nodeIsQuery(node)) {
|
||
|
pcString += generateChunk(TYPE_X_MOZ_PLACE_CONTAINER);
|
||
|
}
|
||
|
else if (this.nodeIsSeparator(node)) {
|
||
|
psString += generateChunk(TYPE_X_MOZ_PLACE_SEPARATOR);
|
||
|
}
|
||
|
else {
|
||
|
placeString += generateChunk(TYPE_X_MOZ_PLACE);
|
||
|
mozURLString += generateChunk(TYPE_X_MOZ_URL);
|
||
|
htmlString += generateChunk(TYPE_HTML);
|
||
|
unicodeString += generateChunk(TYPE_UNICODE);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var self = this;
|
||
|
function addData(type, data) {
|
||
|
xferable.addDataFlavor(type);
|
||
|
xferable.setTransferData(type, self._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(TYPE_X_MOZ_PLACE_CONTAINER, pcString);
|
||
|
if (psString)
|
||
|
addData(TYPE_X_MOZ_PLACE_SEPARATOR, psString);
|
||
|
if (placeString)
|
||
|
addData(TYPE_X_MOZ_PLACE, placeString);
|
||
|
if (mozURLString)
|
||
|
addData(TYPE_X_MOZ_URL, mozURLString);
|
||
|
if (unicodeString)
|
||
|
addData(TYPE_UNICODE, unicodeString);
|
||
|
if (htmlString)
|
||
|
addData(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() {
|
||
|
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.activeView.insertionPoint;
|
||
|
if (!ip)
|
||
|
throw Cr.NS_ERROR_NOT_AVAILABLE;
|
||
|
|
||
|
var self = this;
|
||
|
/**
|
||
|
* 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 = self.unwrapNodes(data, type.value);
|
||
|
var transactions = [];
|
||
|
for (var i = 0; i < items.length; ++i) {
|
||
|
transactions.push(self.makeTransaction(items[i], type.value,
|
||
|
ip.folderId, 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([TYPE_X_MOZ_PLACE_CONTAINER]),
|
||
|
getTransactions([TYPE_X_MOZ_PLACE_SEPARATOR]),
|
||
|
getTransactions([TYPE_X_MOZ_PLACE, TYPE_X_MOZ_URL,
|
||
|
TYPE_UNICODE]));
|
||
|
var txn = new PlacesAggregateTransaction("Paste", transactions);
|
||
|
this.tm.doTransaction(txn);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Handles drag and drop operations for views. Note that this is view agnostic!
|
||
|
* You should not use PlacesController.activeView 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 (PlacesController.nodeIsReadOnly(parent) ||
|
||
|
!PlacesController.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 = PlacesController.unwrapNodes(data.value.data,
|
||
|
flavor.value)[0];
|
||
|
transactions.push(PlacesController.makeTransaction(unwrapped,
|
||
|
flavor.value, insertionPoint.folderId,
|
||
|
insertionPoint.index, copy));
|
||
|
}
|
||
|
|
||
|
var txn = new PlacesAggregateTransaction("DropItems", transactions);
|
||
|
PlacesController.tm.doTransaction(txn);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Method and utility stubs for Place Edit Transactions
|
||
|
*/
|
||
|
function PlacesBaseTransaction() {
|
||
|
}
|
||
|
PlacesBaseTransaction.prototype = {
|
||
|
bookmarks: Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
|
||
|
getService(Ci.nsINavBookmarksService),
|
||
|
livemarks: Cc["@mozilla.org/browser/livemark-service;1"].
|
||
|
getService(Ci.nsILivemarkService),
|
||
|
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) ==============");
|
||
|
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();
|
||
|
}
|
||
|
this.bookmarks.endUpdateBatch();
|
||
|
this.LOG("== " + this._name + " (Aggregate Ends) =========");
|
||
|
},
|
||
|
|
||
|
undoTransaction: function() {
|
||
|
this.LOG("== UN" + this._name + " (UNAggregate) ============");
|
||
|
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();
|
||
|
}
|
||
|
this.bookmarks.endUpdateBatch();
|
||
|
this.LOG("== UN" + this._name + " (UNAggregate Ends) =======");
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Create a new Folder
|
||
|
*/
|
||
|
function PlacesCreateFolderTransaction(name, container, index) {
|
||
|
NS_ASSERT(index >= -1, "invalid insertion index");
|
||
|
this._name = name;
|
||
|
this.container = container;
|
||
|
this._index = index;
|
||
|
this._id = null;
|
||
|
this.childTransactions = [];
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesCreateFolderTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PCFT_doTransaction() {
|
||
|
this.LOG("Create Folder: " + this._name + " in: " + this.container + "," + this._index);
|
||
|
this._id = this.bookmarks.createFolder(this.container, this._name, this._index);
|
||
|
for (var i = 0; i < this.childTransactions.length; ++i) {
|
||
|
var txn = this.childTransactions[i];
|
||
|
txn.container = this._id;
|
||
|
txn.doTransaction();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PCFT_undoTransaction() {
|
||
|
this.LOG("UNCreate Folder: " + this._name + " from: " + this.container + "," + this._index);
|
||
|
this.bookmarks.removeFolder(this._id);
|
||
|
for (var i = 0; i < this.childTransactions.length; ++i) {
|
||
|
var txn = this.childTransactions[i];
|
||
|
txn.container = this._id;
|
||
|
txn.undoTransaction();
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Create a new Item
|
||
|
*/
|
||
|
function PlacesCreateItemTransaction(uri, container, index) {
|
||
|
NS_ASSERT(index >= -1, "invalid insertion index");
|
||
|
this._uri = uri;
|
||
|
this.container = container;
|
||
|
this._index = index;
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesCreateItemTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PCIT_doTransaction() {
|
||
|
this.LOG("Create Item: " + this._uri.spec + " in: " + this.container + "," + this._index);
|
||
|
this.bookmarks.insertItem(this.container, this._uri, this._index);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PCIT_undoTransaction() {
|
||
|
this.LOG("UNCreate Item: " + this._uri.spec + " from: " + this.container + "," + this._index);
|
||
|
this.bookmarks.removeItem(this.container, this._uri);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Create a new Separator
|
||
|
*/
|
||
|
function PlacesCreateSeparatorTransaction(container, index) {
|
||
|
NS_ASSERT(index >= -1, "invalid insertion index");
|
||
|
this.container = container;
|
||
|
this._index = index;
|
||
|
this._id = null;
|
||
|
}
|
||
|
PlacesCreateSeparatorTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PIST_doTransaction() {
|
||
|
this.LOG("Create separator in: " + this.container + "," + this._index);
|
||
|
this._id = this.bookmarks.insertSeparator(this.container, this._index);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PIST_undoTransaction() {
|
||
|
this.LOG("UNCreate separator from: " + this.container + "," + this._index);
|
||
|
this.bookmarks.removeChildAt(this.container, this._index);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Move a Folder
|
||
|
*/
|
||
|
function PlacesMoveFolderTransaction(id, oldContainer, oldIndex, newContainer, newIndex) {
|
||
|
NS_ASSERT(!isNaN(id + oldContainer + oldIndex + newContainer + newIndex), "Parameter is NaN!");
|
||
|
NS_ASSERT(newIndex >= -1, "invalid insertion index");
|
||
|
this._id = id;
|
||
|
this._oldContainer = oldContainer;
|
||
|
this._oldIndex = oldIndex;
|
||
|
this._newContainer = newContainer;
|
||
|
this._newIndex = newIndex;
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesMoveFolderTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PMFT_doTransaction() {
|
||
|
this.LOG("Move Folder: " + this._id + " from: " + this._oldContainer + "," + this._oldIndex + " to: " + this._newContainer + "," + this._newIndex);
|
||
|
this.bookmarks.moveFolder(this._id, this._newContainer, this._newIndex);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PMFT_undoTransaction() {
|
||
|
this.LOG("UNMove Folder: " + this._id + " from: " + this._oldContainer + "," + this._oldIndex + " to: " + this._newContainer + "," + this._newIndex);
|
||
|
this.bookmarks.moveFolder(this._id, this._oldContainer, this._oldIndex);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Move an Item
|
||
|
*/
|
||
|
function PlacesMoveItemTransaction(uri, oldContainer, oldIndex, newContainer, newIndex) {
|
||
|
this._uri = uri;
|
||
|
this._oldContainer = oldContainer;
|
||
|
this._oldIndex = oldIndex;
|
||
|
this._newContainer = newContainer;
|
||
|
this._newIndex = newIndex;
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesMoveItemTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PMIT_doTransaction() {
|
||
|
this.LOG("Move Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex + " to: " + this._newContainer + "," + this._newIndex);
|
||
|
this.bookmarks.removeItem(this._oldContainer, this._uri);
|
||
|
this.bookmarks.insertItem(this._newContainer, this._uri, this._newIndex);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PMIT_undoTransaction() {
|
||
|
this.LOG("UNMove Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex + " to: " + this._newContainer + "," + this._newIndex);
|
||
|
this.bookmarks.removeItem(this._newContainer, this._uri);
|
||
|
this.bookmarks.insertItem(this._oldContainer, this._uri, 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.
|
||
|
* @param id
|
||
|
* The id of the folder to save the contents of
|
||
|
*/
|
||
|
_saveFolderContents: function PRFT__saveFolderContents() {
|
||
|
this._transactions = [];
|
||
|
var contents = PlacesController.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) {
|
||
|
var folder = asFolder(child);
|
||
|
txn = new PlacesRemoveFolderTransaction(folder.folderId);
|
||
|
}
|
||
|
else if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
|
||
|
txn = new PlacesRemoveSeparatorTransaction(this._id, i);
|
||
|
}
|
||
|
else {
|
||
|
txn = new PlacesRemoveItemTransaction(ios.newURI(child.uri, null, null),
|
||
|
this._id, i);
|
||
|
}
|
||
|
this._transactions.push(txn);
|
||
|
}
|
||
|
},
|
||
|
|
||
|
doTransaction: function PRFT_doTransaction() {
|
||
|
var title = this.bookmarks.getFolderTitle(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.getFolderTitle(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(uri, oldContainer, oldIndex) {
|
||
|
this._uri = uri;
|
||
|
this._oldContainer = oldContainer;
|
||
|
this._oldIndex = oldIndex;
|
||
|
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.bookmarks.removeItem(this._oldContainer, this._uri);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PRIT_undoTransaction() {
|
||
|
this.LOG("UNRemove Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex);
|
||
|
this.bookmarks.insertItem(this._oldContainer, this._uri, this._oldIndex);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove a separator
|
||
|
*/
|
||
|
function PlacesRemoveSeparatorTransaction(oldContainer, oldIndex) {
|
||
|
this._oldContainer = oldContainer;
|
||
|
this._oldIndex = oldIndex;
|
||
|
}
|
||
|
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(uri, newTitle) {
|
||
|
this._uri = uri;
|
||
|
this._newTitle = newTitle;
|
||
|
this._oldTitle = "";
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesEditItemTitleTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PEITT_doTransaction() {
|
||
|
this._oldTitle = this.bookmarks.getItemTitle(this._uri);
|
||
|
this.bookmarks.setItemTitle(this._uri, this._newTitle);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PEITT_undoTransaction() {
|
||
|
this.bookmarks.setItemTitle(this._uri, this._oldTitle);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Edit a folder's title.
|
||
|
*/
|
||
|
function PlacesEditFolderTitleTransaction(id, newTitle) {
|
||
|
this._id = id;
|
||
|
this._newTitle = newTitle;
|
||
|
this._oldTitle = "";
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesEditFolderTitleTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PEFTT_doTransaction() {
|
||
|
this._oldTitle = this.bookmarks.getFolderTitle(this._id);
|
||
|
this.bookmarks.setFolderTitle(this._id, this._newTitle);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PEFTT_undoTransaction() {
|
||
|
this.bookmarks.setFolderTitle(this._id, this._oldTitle);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Edit a bookmark's keyword.
|
||
|
*/
|
||
|
function PlacesEditBookmarkKeywordTransaction(uri, newKeyword) {
|
||
|
this._uri = uri;
|
||
|
this._newKeyword = newKeyword;
|
||
|
this._oldKeyword = "";
|
||
|
this.redoTransaction = this.doTransaction;
|
||
|
}
|
||
|
PlacesEditBookmarkKeywordTransaction.prototype = {
|
||
|
__proto__: PlacesBaseTransaction.prototype,
|
||
|
|
||
|
doTransaction: function PEBKT_doTransaction() {
|
||
|
this._oldKeyword = this.bookmarks.getKeywordForURI(this._uri);
|
||
|
this.bookmarks.setKeywordForURI(this._uri, this._newKeyword);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PEBKT_undoTransaction() {
|
||
|
this.bookmarks.setKeywordForURI(this._uri, this._oldKeyword);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* 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(uri, newMicrosummary) {
|
||
|
this._uri = uri;
|
||
|
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._uri);
|
||
|
if (this._newMicrosummary)
|
||
|
this.mss.setMicrosummary(this._uri, this._newMicrosummary);
|
||
|
else
|
||
|
this.mss.removeMicrosummary(this._uri);
|
||
|
},
|
||
|
|
||
|
undoTransaction: function PEBMT_undoTransaction() {
|
||
|
if (this._oldMicrosummary)
|
||
|
this.mss.setMicrosummary(this._uri, this._oldMicrosummary);
|
||
|
else
|
||
|
this.mss.removeMicrosummary(this._uri);
|
||
|
}
|
||
|
};
|
||
|
|