Files
tubestation/browser/components/places/content/controller.js
joe@retrovirus.com 444f9b2f4a Convert all the DB-modifying operations in the Bookmark Properties to use
transactions so that they can be undone.  (The "change URI" is the only one
that's not transactionized yet, because it needs deeper work, cf. bug 331655)

bug=326321
r=annie.sullivan@gmail.com
sr=ben@mozilla.org
2006-03-31 00:34:03 +00:00

2508 lines
87 KiB
JavaScript
Executable File

//* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Places Command Controller.
*
* The Initial Developer of the Original Code is Google Inc.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Ben Goodger <beng@google.com>
*
* 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";
// 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;
}
/**
* Manages grouping options for a particular view type.
* @param pref
* The preference that stores these grouping options.
* @param defaultGroupings
* The default groupings to be used for views of this type.
* @param serializable
* An object bearing a serialize and deserialize method that
* read and write the object's string representation from/to
* preferences.
* @constructor
*/
function PrefHandler(pref, defaultValue, serializable) {
this._pref = pref;
this._defaultValue = defaultValue;
this._serializable = serializable;
this._pb =
Cc["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch2);
this._pb.addObserver(this._pref, this, false);
}
PrefHandler.prototype = {
/**
* Clean up when the window is going away to avoid leaks.
*/
destroy: function PC_PH_destroy() {
this._pb.removeObserver(this._pref, this);
},
/**
* Observes changes to the preferences.
* @param subject
* @param topic
* The preference changed notification
* @param data
* The preference that changed
*/
observe: function PC_PH_observe(subject, topic, data) {
if (topic == "nsPref:changed" && data == this._pref)
this._value = null;
},
/**
* The cached value, null if it needs to be rebuilt from preferences.
*/
_value: null,
/**
* Get the preference value, reading from preferences if necessary.
*/
get value() {
if (!this._value) {
if (this._pb.prefHasUserValue(this._pref)) {
var valueString = this._pb.getCharPref(this._pref);
this._value = this._serializable.deserialize(valueString);
}
else
this._value = this._defaultValue;
}
return this._value;
},
/**
* Stores a value in preferences.
* @param value
* The data to be stored.
*/
set value(value) {
if (value != this._value)
this._pb.setCharPref(this._pref, this._serializable.serialize(value));
return value;
}
};
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, expandQueries,
peerDropIndex) {
this.peerDropTypes = peerDropTypes;
this.childDropTypes = childDropTypes;
this.excludeItems = excludeItems;
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, 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.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 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;
},
/**
* 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._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 if (this.nodeIsURI(selectedNode)) {
var uri = this._uri(selectedNode.uri);
isLivemarkItem =
this.annotations.hasAnnotation(uri, "livemark/bookmarkFeedURI");
if (isLivemarkItem && selectedNode.parent)
var name = selectedNode.parent.title;
if (!isLivemarkItem && this.nodeIsFolder(selectedNode)) {
var folderId = asFolder(selectedNode).folderId;
uri = this.bookmarks.getFolderURI(folderId);
isLivemarkItem = this.annotations.hasAnnotation(uri, "livemark/feedURI");
name = selectedNode.title;
}
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:
* Selection Grammar:
* is-link "link"
* is-links "links"
* is-folder "folder"
* is-mutable "mutable"
* is-mixed "mixed"
* is-removable "removable"
* is-multiselect"multiselect"
* is-container "remotecontainer"
* is-query "query"
* @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;
}
// 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;
}
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");
},
/**
* 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 to show the dialog for
* @param action "add" or "edit", see _determineVariant in
* bookmarkProperties.js
*/
_showBookmarkDialog: function PC__showBookmarkDialog(identifier, action) {
// The identifier parameter can be either an integer (for folders) or
// a nsIURI object (for bookmarks/history items); if it isn't a number
// here, we're going to make sure it's a URI object rather than a string.
if (typeof(identifier) != "number")
this._assertURINotString(identifier);
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);
},
/**
* 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);
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);
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 PlacesInsertSeparatorTransaction(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);
// Undo for bookmark folder removals is handled by the bookmarks service,
// for various reasons. See:
// http://groups.google.com/group/mozilla.dev.apps.firefox/msg/215e88fcdb91ff25
var txn = this.bookmarks.getRemoveFolderTransaction(folder.folderId);
var removeTxn = new PlacesRemoveFolderTransaction(txn, folder.folderId);
transactions.push(removeTxn);
}
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))
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 transactions = [];
var self = this;
function createTransactions(folderId, container, index) {
var folderTitle = self.bookmarks.getFolderTitle(folderId);
var createTxn =
new PlacesCreateFolderTransaction(folderTitle, container, index);
transactions.push(createTxn);
// Get the folder's children
var kids = self.getFolderContents(folderId, false, false);
var wasOpen = kids.containerOpen;
kids.containerOpen = true;
var cc = kids.childCount;
for (var i = 0; i < cc; ++i) {
var node = kids.getChild(i);
if (self.nodeIsFolder(node))
createTransactions(node.folderId, folderId, i);
else if (self.nodeIsURI(node)) {
var uri = self._uri(node.uri);
transactions.push(self._getItemCopyTransaction(uri, container,
index));
}
}
kids.containerOpen = wasOpen;
}
createTransactions(data.folderId, container, index);
return new PlacesAggregateTransaction("FolderCopy", transactions);
},
/**
* 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 PlacesInsertSeparatorTransaction(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 PlacesInsertSeparatorTransaction(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 (unicodeString)
addData(TYPE_UNICODE, unicodeString);
if (htmlString)
addData(TYPE_HTML, htmlString);
if (mozURLString)
addData(TYPE_X_MOZ_URL, mozURLString);
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 = {
/**
* 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 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(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 j = 0; j < types.length; ++j)
xferable.addDataFlavor(types[j]);
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
* @param visibleInsertCount
* The number of visible items to be inserted. This can be zero
* even when items are dropped because this is how many items will
* be _visible_ in the resulting tree.
* XXXben this parameter appears to be unused! check call sites.
* check d&d feedback.
*/
onDrop: function PCDH_onDrop(sourceView, targetView, insertionPoint,
visibleInsertCount) {
var session = this.getSession();
var copy = session.dragAction & Ci.nsIDragService.DRAGDROP_ACTION_COPY;
var transactions = [];
var xferable = this._initTransferable(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),
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.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)
this._transactions[i].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)
this._transactions[i].undoTransaction();
this.bookmarks.endUpdateBatch();
this.LOG("== UN" + this._name + " (UNAggregate Ends) =======");
}
};
/**
* Create a new Folder
*/
function PlacesCreateFolderTransaction(name, container, index) {
this._name = name;
this._container = container;
this._index = index;
this._id = null;
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);
},
undoTransaction: function PCFT_undoTransaction() {
this.LOG("UNCreate Folder: " + this._name + " from: " + this._container + "," + this._index);
this.bookmarks.removeFolder(this._id);
}
};
/**
* Create a new Item
*/
function PlacesCreateItemTransaction(uri, container, 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 PlacesInsertSeparatorTransaction(container, index) {
this._container = container;
this._index = index;
this._id = null;
}
PlacesInsertSeparatorTransaction.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!");
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);
}
};
/**
* A named leaf item.
* @param name
* The name of the item
* @param uri
* The URI fo the item
*/
function PlacesRemoveFolderSaveChildItem(name, uri) {
this.name = name;
var ios =
Cc["@mozilla.org/network/io-service;1"].
getService(Ci.nsIIOService);
this.uri = ios.newURI(uri, null, null);
}
/**
* A named folder, with children.
* @param name
* The name of the folder.
*/
function PlacesRemoveFolderSaveChildFolder(name) {
this.name = name;
this.children = [];
}
/**
* 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(removeTxn, id) {
this._removeTxn = removeTxn;
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);
var removeTxn =
this.bookmarks.getRemoveFolderTransaction(folder.folderId);
txn = new PlacesRemoveFolderTransaction(removeTxn, 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);
}
};