/* -*- 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 * Myk Melez * * 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: \n<>\n\n // uri: 0\n\n\n // separator: 0\n<>\n\n 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 "" + node.title + ""; } // 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); } };