diff --git a/browser/components/places/content-shim/browserShim.css b/browser/components/places/content-shim/browserShim.css index a28a4e668750..3d82fb87bd81 100755 --- a/browser/components/places/content-shim/browserShim.css +++ b/browser/components/places/content-shim/browserShim.css @@ -6,3 +6,8 @@ menupopup[type="places"] { -moz-binding: url("chrome://browser/content/places/menu.xml#places-menupopup"); } + +#bookmarksBarShowPlaces { + list-style-image: url("chrome://browser/skin/places/places-icon.png"); +} + diff --git a/browser/components/places/content-shim/browserShim.js b/browser/components/places/content-shim/browserShim.js index 62cee3266608..af5346b41b17 100755 --- a/browser/components/places/content-shim/browserShim.js +++ b/browser/components/places/content-shim/browserShim.js @@ -69,6 +69,11 @@ PlacesBrowserShim.init = function PBS_init() { var result = this._hist.executeQuery(query, options); newMenuPopup._result = result; newMenuPopup._resultNode = result.root; + + window.controllers.appendController(PlacesController); + + PlacesController.topWindow = window; + PlacesController.tm = PlacesTransactionManager; }; PlacesBrowserShim.addBookmark = function PBS_addBookmark() { @@ -104,4 +109,169 @@ PlacesBrowserShim.addLivemark = function PBS_addLivemark(aURL, aFeedURL, aTitle, -1); }; +/** + * This is a custom implementation of nsITransactionManager. We do not chain + * or aggregate the default implementation because the order in which + * transactions are performed and undone is important to the user experience. + * There are two classes of transactions - those done by the browser window + * that contains this transaction manager, and those done by the embedded + * Places page. All transactions done in either part of the UI are recorded + * here, but ones performed by actions taken in the Places page affect the + * Undo/Redo menu items and keybindings in the browser window only when the + * Places page is the active tab. This is to prevent the user from accidentally + * undoing/redoing their changes while the Places page is not selected, and the + * user not noticing. + * + * When the Places page is navigated away from, the undo items registered for + * it are destroyed and the ability to undo those actions ceases. + */ +var PlacesTransactionManager = { + _undoItems: [], + _redoItems: [], + + hidePageTransactions: true, + + _getNextVisibleIndex: function PTM__getNextVisibleItem(list) { + if (!this.hidePageTransactions) + return list.length - 1; + + for (var i = list.length - 1; i >= 0; --i) { + if (!list[i].pageTransaction) + return i; + } + return -1; + }, + + updateCommands: function PTM__updateCommands() { + CommandUpdater.updateCommand("cmd_undo"); + CommandUpdater.updateCommand("cmd_redo"); + }, + + doTransaction: function PTM_doTransaction(transaction) { + transaction.doTransaction(); + this._undoItems.push(transaction); + this._redoItems = []; + this.updateCommands(); + }, + + undoTransaction: function PTM_undoTransaction() { + var index = this._getNextVisibleIndex(this._undoItems); + ASSERT(index >= 0, "Invalid Transaction index"); + var transaction = this._undoItems.splice(index, 1)[0]; + transaction.undoTransaction(); + this._redoItems.push(transaction); + this.updateCommands(); + }, + + redoTransaction: function PTM_redoTransaction() { + var index = this._getNextVisibleIndex(this._redoItems); + ASSERT(index >= 0, "Invalid Transaction index"); + var transaction = this._redoItems.splice(index, 1)[0]; + transaction.redoTransaction(); + this._undoItems.push(transaction); + this.updateCommands(); + }, + + clear: function PTM_clear() { + this._undoItems = []; + this._redoItems = []; + this.updateCommands(); + }, + + beginBatch: function PTM_beginBatch() { + }, + + endBatch: function PTM_endBatch() { + }, + + get numberOfUndoItems() { + return this.getUndoList().numItems; + }, + get numberOfRedoItems() { + return this.getRedoList().numItems; + }, + + maxTransactionCount: -1, + + peekUndoStack: function PTM_peekUndoStack() { + var index = this._getNextVisibleIndex(this._undoItems); + ASSERT(index >= 0, "Invalid Transaction index"); + return this._undoItems[index]; + }, + peekRedoStack: function PTM_peekRedoStack() { + var index = this._getNextVisibleIndex(this._redoItems); + ASSERT(index >= 0, "Invalid Transaction index"); + return this._redoItems[index]; + }, + + _filterList: function PTM__filterList(list) { + if (!this.hidePageTransactions) + return list; + + var transactions = []; + for (var i = 0; i < list.length; ++i) { + if (!list[i].pageTransaction) + transactions.push(list[i]); + } + return transactions; + }, + + getUndoList: function PTM_getUndoList() { + return new TransactionList(this._filterList(this._undoItems)); + }, + getRedoList: function PTM_getRedoList() { + return new TransactionList(this._filterList(this._redoItems)); + }, + + _listeners: [], + AddListener: function PTM_AddListener(listener) { + this._listeners.push(listener); + }, + RemoveListener: function PTM_RemoveListener(listener) { + for (var i = 0; i < this._listeners.length; ++i) { + if (this._listeners[i] == listener) + this._listeners.splice(i, 1); + } + }, + + QueryInterface: function PTM_QueryInterface(iid) { + if (iid.equals(Ci.nsITransactionManager) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NOINTERFACE; + } +}; + +function TransactionList(transactions) { + this._transactions = transactions; +} +TransactionList.prototype = { + get numItems() { + return this._transactions.length; + }, + + itemIsBatch: function TL_itemIsBatch(index) { + return false; + }, + + getItem: function TL_getItem(index) { + return this._transactions[i]; + }, + + getNumChildrenForItem: function TL_getNumChildrenForItem(index) { + return 0; + }, + + getChildListForItem: function TL_getChildListForItem(index) { + return null; + }, + + QueryInterface: function TL_QueryInterface(iid) { + if (iid.equals(Ci.nsITransactionList) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_ERROR_NOINTERFACE; + } +}; + addEventListener("load", function () { PlacesBrowserShim.init(); }, false); diff --git a/browser/components/places/content-shim/browserShim.xul b/browser/components/places/content-shim/browserShim.xul index 4be394dc4975..a2cf64e8e11f 100755 --- a/browser/components/places/content-shim/browserShim.xul +++ b/browser/components/places/content-shim/browserShim.xul @@ -28,7 +28,6 @@ diff --git a/browser/components/places/content/commands.inc b/browser/components/places/content/commands.inc index c4126c0247ed..02304c09f034 100755 --- a/browser/components/places/content/commands.inc +++ b/browser/components/places/content/commands.inc @@ -1,7 +1,8 @@ - + @@ -30,7 +31,7 @@ oncommand="PlacesController.paste();"/> + oncommand="PlacesController.remove('Remove Selection');"/> diff --git a/browser/components/places/content/controller.js b/browser/components/places/content/controller.js index a29407c4c6fd..0d4e039b5d02 100755 --- a/browser/components/places/content/controller.js +++ b/browser/components/places/content/controller.js @@ -39,6 +39,39 @@ function LOG(str) { dump("*** " + str + "\n"); } +var gTraceOnAssert = true; + +// XXXben - develop this further into a multi-purpose assertion system. +function ASSERT(condition, message) { + if (!condition) { + var caller = arguments.callee.caller; + var str = "ASSERT: "; + str += message; + LOG(str); + var assertionText = str + "\n"; + + var stackText = "Stack Trace: \n"; + if (gTraceOnAssert) { + var count = 0; + while (caller) { + stackText += count++ + ":" + caller.name + "("; + for (var i = 0; i < caller.arguments.length; ++i) { + var arg = caller.arguments[i]; + stackText += arg; + if (i < caller.arguments.length - 1) + stackText += ","; + } + stackText += ")\n"; + caller = caller.arguments.callee.caller; + } + } + var ps = + Cc["@mozilla.org/embedcomp/prompt-service;1"]. + getService(Ci.nsIPromptService); + ps.alert(window, "Assertion Failed", assertionText + stackText); + } +} + const Ci = Components.interfaces; const Cc = Components.classes; const Cr = Components.results; @@ -104,12 +137,13 @@ function InsertionPoint(folderId, index, orientation) { * Initialization Configuration for a View * @constructor */ -function ViewConfig(dropTypes, dropOnTypes, excludeItems, expandQueries, firstDropIndex) { +function ViewConfig(dropTypes, dropOnTypes, excludeItems, expandQueries, firstDropIndex, filterTransactions) { this.dropTypes = dropTypes; this.dropOnTypes = dropOnTypes; this.excludeItems = excludeItems; this.expandQueries = expandQueries; this.firstDropIndex = firstDropIndex; + this.filterTransactions = filterTransactions; } ViewConfig.GENERIC_DROP_TYPES = [TYPE_X_MOZ_PLACE_CONTAINER, TYPE_X_MOZ_PLACE, TYPE_X_MOZ_URL]; @@ -189,6 +223,22 @@ PrefHandler.prototype = { }, }; +function QI_node(node, iid) { + var result = null; + try { + result = node.QueryInterface(iid); + } + catch (e) { + } + ASSERT(result, "Node QI Failed"); + return result; +} +function asURI(node) { return QI_node(node, Ci.nsINavHistoryURIResultNode); } +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); } /** * The Master Places Controller @@ -255,7 +305,7 @@ var PlacesController = { options.expandQueries = expandQueries; var result = this._hist.executeQuery(query, options); result.root.containerOpen = true; - return result; + return asContainer(result.root); }, /** @@ -284,6 +334,16 @@ var PlacesController = { return this._activeView; }, + /** + * The top window + */ + topWindow: null, + + /** + * The Transaction Manager for this window. + */ + tm: null, + /** * The current groupable Places view. */ @@ -298,20 +358,30 @@ var PlacesController = { isCommandEnabled: function PC_isCommandEnabled(command) { //LOG("isCommandEnabled: " + command); + if (command == "cmd_undo") + return this.tm.numberOfUndoItems > 0; + if (command == "cmd_redo") + return this.tm.numberOfRedoItems > 0; return document.getElementById(command).getAttribute("disabled") == "true"; }, supportsCommand: function PC_supportsCommand(command) { //LOG("supportsCommand: " + command); + if (command == "cmd_undo" || command == "cmd_redo") + return true; return document.getElementById(command) != null; }, 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); + //LOG("onEvent: " + eventName); }, /** @@ -643,8 +713,7 @@ var PlacesController = { openLinkInNewTab: function PC_openLinkInNewTab() { var node = this._activeView.selectedURINode; if (node) - this._activeView.browserWindow.openNewTabWith( - node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri, null, null); + this._activeView.browserWindow.openNewTabWith(asURI(node).uri, null, null); }, /** @@ -653,8 +722,7 @@ var PlacesController = { openLinkInNewWindow: function PC_openLinkInNewWindow() { var node = this._activeView.selectedURINode; if (node) - this._activeView.browserWindow.openNewWindowWith( - node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri, null, null); + this._activeView.browserWindow.openNewWindowWith(asURI(node).uri, null, null); }, /** @@ -663,8 +731,7 @@ var PlacesController = { openLinkInCurrentWindow: function PC_openLinkInCurrentWindow() { var node = this._activeView.selectedURINode; if (node) - this._activeView.browserWindow.loadURI( - node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri, null, null); + this._activeView.browserWindow.loadURI(asURI(node).uri, null, null); }, /** @@ -673,13 +740,12 @@ var PlacesController = { openLinksInTabs: function PC_openLinksInTabs() { var node = this._activeView.selectedNode; if (this._activeView.hasSingleSelection && this.nodeIsFolder(node)) { - node.QueryInterface(Ci.nsINavHistoryFolderResultNode); + asFolder(node); var cc = node.childCount; for (var i = 0; i < cc; ++i) { var childNode = node.getChild(i); if (this.nodeIsURI(childNode)) - this._activeView.browserWindow.openNewTabWith( - childNode.QueryInterface(Ci.nsINavHistoryURIResultNode).uri, + this._activeView.browserWindow.openNewTabWith(asURI(childNode).uri, null, null); } } @@ -687,8 +753,7 @@ var PlacesController = { var nodes = this._activeView.getSelectionNodes(); for (var i = 0; i < nodes.length; ++i) { if (this.nodeIsURI(nodes[i])) - this._activeView.browserWindow.openNewTabWith( - nodes[i].QueryInterface(Ci.nsINavHistoryURIResultNode).uri, + this._activeView.browserWindow.openNewTabWith(asURI(nodes[i]).uri, null, null); } } @@ -711,7 +776,7 @@ var PlacesController = { if (!this._groupableView) return; var result = this._groupableView.getResult(); - var root = result.root.QueryInterface(Ci.nsINavHistoryQueryResultNode); + var root = asQuery(result.root); var queries = root.getQueries({ }); var newOptions = root.queryOptions.clone(); @@ -757,6 +822,7 @@ var PlacesController = { newFolder: function PC_newFolder() { var view = this._activeView; + view.saveSelection(view.SAVE_SELECTION_REMOVE); var ps = Cc["@mozilla.org/embedcomp/prompt-service;1"]. getService(Ci.nsIPromptService); @@ -768,58 +834,92 @@ var PlacesController = { var ip = view.insertionPoint; var txn = new PlacesCreateFolderTransaction(value.value, ip.folderId, ip.index); - this._hist.transactionManager.doTransaction(txn); + this.tm.doTransaction(txn); this._activeView.focus(); + view.restoreSelection(); } }, + /** + * 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) { + ASSERT(transactions instanceof Array, "Must pass a transactions array"); + var index = this.getIndexOfNode(range[0]); + + // Walk backwards to preserve insertion order on undo + for (var i = range.length - 1; i >= 0; --i) { + var node = range[i]; + if (this.nodeIsFolder(node)) { + // TODO -- node.parent might be a query and not a folder. See bug 324948 + transactions.push(new PlacesRemoveFolderTransaction( + asFolder(node).folderId, asFolder(node.parent).folderId, index)); + } + else if (this.nodeIsFolder(node.parent)) { + // A Bookmark in a Bookmark Folder. + transactions.push(new PlacesRemoveItemTransaction( + this._uri(asURI(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 (i = 0; i < nodes.length; ++i) { + var node = nodes[i]; + var bhist = this._hist.QueryInterface(Ci.nsIBrowserHistory); + if (this.nodeIsHost(node)) + bhist.removePagesFromHost(node.title, true); + else if (this.nodeIsURI(node)) + bhist.removePage(this._uri(asURI(node).uri)); + } + }, + /** * Removes the selection * @param txnName - * An optional name for the transaction if this is being performed + * A name for the transaction if this is being performed * as part of another operation. */ remove: function PC_remove(txnName) { - var nodes = this._activeView.getSelectionNodes(); - this._activeView.saveSelection(); + ASSERT(txnName !== undefined, "Must supply Transaction Name"); + this._activeView.saveSelection(this._activeView.SAVE_SELECTION_REMOVE); - // delete bookmarks - var txns = []; - for (var i = 0; i < nodes.length; ++i) { - var node = nodes[i]; - var index = this.getIndexOfNode(node); - if (this.nodeIsFolder(node)) { - // TODO -- node.parent might be a query and not a folder. See bug 324948 - txns.push(new PlacesRemoveFolderTransaction(node.QueryInterface(Ci.nsINavHistoryFolderResultNode).folderId, - node.parent.QueryInterface(Ci.nsINavHistoryFolderResultNode).folderId, - index)); - } - else if (this.nodeIsFolder(node.parent)) { - // this item is in a bookmark folder - txns.push(new PlacesRemoveItemTransaction(this._uri(node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri), - node.parent.QueryInterface(Ci.nsINavHistoryFolderResultNode).folderId, - index)); - } - else { - // other containers are history queries, just delete from history - // history deletes are not undoable. - var hist = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsIBrowserHistory); - for (var i = 0; i < nodes.length; ++i) { - var node = nodes[i]; - if (this.nodeIsHost(node)) { - hist.removePagesFromHost(node.title, true); - } else if (this.nodeIsURI(node)) { - hist.removePage(this._uri(node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri)); - } - } - } + // 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; + LOG("TYPE: " + type); + if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER) + this._removeRowsFromBookmarks(txnName); + else + this._removeRowsFromHistory(); - if (txns.length > 0) { - var txn = new PlacesAggregateTransaction(txnName || "RemoveItems", txns); - this._hist.transactionManager.doTransaction(txn); - } - } this._activeView.restoreSelection(); }, @@ -834,8 +934,8 @@ var PlacesController = { var parent = node.parent; if (!parent || !this.nodeIsContainer(parent)) return -1; - var cc = parent.QueryInterface(Ci.nsINavHistoryContainerResultNode).childCount; - for (var i = 0; i < cc && parent.QueryInterface(Ci.nsINavHistoryContainerResultNode).getChild(i) != node; ++i); + var cc = asContainer(parent).childCount; + for (var i = 0; i < cc && asContainer(parent).getChild(i) != node; ++i); return i < cc ? i : -1; }, @@ -854,29 +954,29 @@ var PlacesController = { case TYPE_X_MOZ_PLACE: var wrapped = ""; if (this.nodeIsFolder(node)) - wrapped += node.QueryInterface(Ci.nsINavHistoryFolderResultNode).folderId + "\n"; + wrapped += asFolder(node).folderId + "\n"; else wrapped += "0\n"; if (this.nodeIsURI(node)) - wrapped += node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri + "\n"; + wrapped += asURI(node).uri + "\n"; else wrapped += "\n"; if (this.nodeIsFolder(node.parent)) - wrapped += node.parent.QueryInterface(Ci.nsINavHistoryFolderResultNode).folderId + "\n"; + wrapped += asFolder(node.parent).folderId + "\n"; else wrapped += "0\n"; wrapped += this.getIndexOfNode(node); return wrapped; case TYPE_X_MOZ_URL: - return node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri + "\n" + node.title; + return asURI(node).uri + "\n" + node.title; case TYPE_HTML: - return "" + node.title + ""; + return "" + node.title + ""; } // case TYPE_UNICODE: - return node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri; + return asURI(node).uri; }, /** @@ -963,7 +1063,7 @@ var PlacesController = { if (self.nodeIsFolder(node)) createTransactions(node.folderId, folderId, i); else if (this.nodeIsURI(node)) { - var uri = self._uri(node.QueryInterface(Ci.nsINavHistoryURIResultNode).uri); + var uri = self._uri(asURI(node).uri); transactions.push(self._getItemCopyTransaction(uri, container, index)); } @@ -1052,14 +1152,20 @@ var PlacesController = { 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)) + + 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 { // This order is _important_! It controls how this and other // applications select data to be inserted based on type. @@ -1132,7 +1238,7 @@ var PlacesController = { */ cut: function() { this.copy(); - this.remove("Cut"); + this.remove("Cut Selection"); }, /** @@ -1163,7 +1269,7 @@ var PlacesController = { ip.folderId, ip.index, true)); var txn = new PlacesAggregateTransaction("Paste", transactions); - this._hist.transactionManager.doTransaction(txn); + this.tm.doTransaction(txn); }, }; @@ -1274,7 +1380,7 @@ var PlacesControllerDragHelper = { } var txn = new PlacesAggregateTransaction("DropItems", transactions); - PlacesController._hist.transactionManager.doTransaction(txn); + PlacesController.tm.doTransaction(txn); } }; @@ -1291,6 +1397,8 @@ PlacesBaseTransaction.prototype = { throw Cr.NS_ERROR_NOT_IMPLEMENTED; }, + pageTransaction: false, + get isTransient() { return false; }, @@ -1306,6 +1414,9 @@ PlacesBaseTransaction.prototype = { function PlacesAggregateTransaction(name, transactions) { this._transactions = transactions; this._name = name; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesAggregateTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, @@ -1338,6 +1449,9 @@ function PlacesCreateFolderTransaction(name, container, index) { this._container = container; this._index = index; this._id = null; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesCreateFolderTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, @@ -1360,6 +1474,9 @@ function PlacesCreateItemTransaction(uri, container, index) { this._uri = uri; this._container = container; this._index = index; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesCreateItemTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, @@ -1384,6 +1501,9 @@ function PlacesMoveFolderTransaction(id, oldContainer, oldIndex, newContainer, n this._oldIndex = oldIndex; this._newContainer = newContainer; this._newIndex = newIndex; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesMoveFolderTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, @@ -1408,6 +1528,9 @@ function PlacesMoveItemTransaction(uri, oldContainer, oldIndex, newContainer, ne this._oldIndex = oldIndex; this._newContainer = newContainer; this._newIndex = newIndex; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesMoveItemTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, @@ -1426,26 +1549,109 @@ PlacesMoveItemTransaction.prototype = { }; /** - * Remove a Folder + * 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(id, oldContainer, oldIndex) { this._id = id; this._oldContainer = oldContainer; this._oldIndex = oldIndex; this._oldFolderTitle = null; + this._contents = null; // The encoded contents of this folder + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesRemoveFolderTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, + + /** + * Save the contents of a folder (items and containers) for restoration + * purposes later. + * @param id + * The id of the folder + * @param parent + * The parent PlacesRemoveFolderSaveChildFolder object + */ + _saveFolderContents: function PRFT__saveFolderContents(id, parent) { + var contents = PlacesController.getFolderContents(id, false, false); + for (var i = contents.childCount - 1; i >= 0; --i) { + var child = contents.getChild(i); + var obj = null; + if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER) { + obj = new PlacesRemoveFolderSaveChildFolder(child.title); + parent.children.push(obj); + this._saveFolderContents(asFolder(child).folderId, obj); + } + else { + obj = new PlacesRemoveFolderSaveChildItem(child.title, asURI(child).uri); + parent.children.push(obj); + } + } + }, + + /** + * Recreate a folder hierarchy from a saved data set. + * @param parent + * The id of the folder to create the items beneath + * @param item + * The object that contains the saved data. + */ + _restore: function PRFT__restore(parent, item) { + if (item instanceof PlacesRemoveFolderSaveChildFolder) { + var id = this._bms.createFolder(parent, item.name, 0); + this._restore(id, item); + } + else { + this._bms.insertItem(parent, item.uri, 0); + this._bms.setItemTitle(item.uri, item.name); + } + }, doTransaction: function PRFT_doTransaction() { - LOG("Remove Folder: " + this._id + " from: " + this._oldContainer + "," + this._oldIndex); this._oldFolderTitle = this._bms.getFolderTitle(this._id); + LOG("Remove Folder: " + this._oldFolderTitle + " from: " + this._oldContainer + "," + this._oldIndex); + + this._contents = new PlacesRemoveFolderSaveChildFolder(this._oldFolderTitle); + this._saveFolderContents(this._id, this._contents); + this._bms.removeFolder(this._id); }, undoTransaction: function PRFT_undoTransaction() { - LOG("UNRemove Folder: " + this._id + " from: " + this._oldContainer + "," + this._oldIndex); + LOG("UNRemove Folder: " + this._oldFolderTitle + " from: " + this._oldContainer + "," + this._oldIndex); this._id = this._bms.createFolder(this._oldContainer, this._oldFolderTitle, this._oldIndex); + + for (var i = 0; i < this._contents.children.length; ++i) + this._restore(this._id, this._contents.children[i]); }, }; @@ -1456,18 +1662,23 @@ function PlacesRemoveItemTransaction(uri, oldContainer, oldIndex) { this._uri = uri; this._oldContainer = oldContainer; this._oldIndex = oldIndex; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesRemoveItemTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, - + doTransaction: function PRIT_doTransaction() { LOG("Remove Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex); this._bms.removeItem(this._oldContainer, this._uri); + LOG("DO: PAGETXN: " + this.pageTransaction); }, undoTransaction: function PRIT_undoTransaction() { LOG("UNRemove Item: " + this._uri.spec + " from: " + this._oldContainer + "," + this._oldIndex); this._bms.insertItem(this._oldContainer, this._uri, this._oldIndex); + LOG("UNDO: PAGETXN: " + this.pageTransaction); }, }; @@ -1478,6 +1689,9 @@ function PlacesEditFolderTransaction(id, oldAttributes, newAttributes) { this._id = id; this._oldAttributes = oldAttributes; this._newAttributes = newAttributes; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesEditFolderTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, @@ -1500,6 +1714,9 @@ function PlacesEditItemTransaction(uri, newAttributes) { this._uri = uri; this._newAttributes = newAttributes; this._oldAttributes = { }; + this.redoTransaction = this.doTransaction; + this.pageTransaction = PlacesController.activeView.filterTransactions; + ASSERT(this.pageTransaction !== undefined, "Don't know if this transaction must be filtered"); } PlacesEditItemTransaction.prototype = { __proto__: PlacesBaseTransaction.prototype, diff --git a/browser/components/places/content/menu.xml b/browser/components/places/content/menu.xml index dcbdf29bbf73..f05bdc3ec2bd 100755 --- a/browser/components/places/content/menu.xml +++ b/browser/components/places/content/menu.xml @@ -152,6 +152,12 @@ ]]> + + + + + false + [TYPE_X_MOZ_PLACE_CONTAINER, TYPE_X_MOZ_PLACE, TYPE_X_MOZ_URL] @@ -211,6 +219,7 @@ + diff --git a/browser/components/places/content/places.js b/browser/components/places/content/places.js index be7801dea939..2b332467406e 100755 --- a/browser/components/places/content/places.js +++ b/browser/components/places/content/places.js @@ -49,8 +49,12 @@ var PlacesUIHook = { _bundle: null, init: function PUIH_init(placesList) { + this._bundle = document.getElementById("placeBundle"); + try { this._topWindow = placesList.browserWindow; + PlacesController.topWindow = this._topWindow; + PlacesController.tm = PlacesController.topWindow.PlacesTransactionManager; this._tabbrowser = this._topWindow.getBrowser(); // Hook into the tab strip to get notifications about when the Places Page is @@ -64,10 +68,9 @@ var PlacesUIHook = { this._showPlacesUI(); } catch (e) { + LOG("Something bad happened initializing the UI Hook: " + e); } - this._bundle = document.getElementById("placeBundle"); - // Stop the browser from handling certain types of events. function onDragEvent(event) { event.stopPropagation(); @@ -111,23 +114,45 @@ var PlacesUIHook = { }, onTabSelect: function PP_onTabSelect(event) { - var tabURI = this._tabbrowser.selectedBrowser.currentURI.spec; - var isPlaces = tabURI.substr(0, this._placesURI.length) == this._placesURI; + var tabURI = this._tabbrowser.selectedBrowser.currentURI; + if (!tabURI) + var isPlaces = false; + else + isPlaces = + tabURI.spec.substr(0, this._placesURI.length) == this._placesURI; isPlaces ? this._showPlacesUI() : this._hidePlacesUI(); }, _topElement: function PUIH__topElement(id) { return this._topWindow.document.getElementById(id); }, + + onFindActivated: function PUIH_onFindActivated(event) { + PlacesSearchBox.focus(); + }, + _findWasHidden: false, + _showPlacesUI: function PP__showPlacesUI() { this._tabbrowser.setAttribute("places", "true"); var statusbar = this._topElement("status-bar"); statusbar.hidden = true; + + var findbar = this._topWindow.document.getElementById("FindToolbar"); + this._findWasHidden = findbar.hidden; + findbar.hidden = true; + this._disableCommands(); var findItem = this._topWindow.document.getElementById("menu_find"); findItem.setAttribute("label", this._bundle.getString("findPlaceLabel")); + + PlacesController.tm.hidePageTransactions = false; + PlacesController.tm.updateCommands(); + + // Disable the find bar so that we can capture key presses. + this._topWindow.gFindEnabled = false; + this._topWindow.addEventListener("find-activated", this.onFindActivated, false); }, _hidePlacesUI: function PP__hidePlacesUI() { @@ -142,10 +167,23 @@ var PlacesUIHook = { var statusbarMenu = this._topWindow.document.getElementById("toggle_taskbar"); var statusbar = this._topElement("status-bar"); statusbar.hidden = statusbarMenu.getAttribute("checked") != "true"; + + if (!this._findWasHidden) { + var findbar = this._topWindow.document.getElementById("FindToolbar"); + findbar.hidden = false; + } + this._enableCommands(); var findItem = this._topWindow.document.getElementById("menu_find"); findItem.setAttribute("label", this._bundle.getString("findPageLabel")); + + PlacesController.tm.hidePageTransactions = true; + PlacesController.tm.updateCommands(); + + // Enable the find bar again + this._topWindow.gFindEnabled = true; + this._topWindow.removeEventListener("find-activated", this.onFindActivated, false); }, }; @@ -172,10 +210,10 @@ var PlacesPage = { this._places.init(new ViewConfig([TYPE_X_MOZ_PLACE_CONTAINER], ViewConfig.GENERIC_DROP_TYPES, - true, false, 3)); + true, false, 3, true)); this._content.init(new ViewConfig(ViewConfig.GENERIC_DROP_TYPES, ViewConfig.GENERIC_DROP_TYPES, - false, false, 0)); + false, false, 0, true)); PlacesController.groupableView = this._content; @@ -465,6 +503,14 @@ var PlacesSearchBox = { return collectionName; }, + /** + * Focus the search box + */ + focus: function PS_focus() { + var searchFilter = document.getElementById("searchFilter"); + searchFilter.focus(); + }, + /** * When the field is activated, if the contents are the gray text, clear * the field, otherwise select the contents. diff --git a/browser/components/places/content/places.xul b/browser/components/places/content/places.xul index c0585c2c30d6..37ef75243cad 100755 --- a/browser/components/places/content/places.xul +++ b/browser/components/places/content/places.xul @@ -27,7 +27,7 @@ #include commands.inc - + diff --git a/browser/components/places/content/toolbar.xml b/browser/components/places/content/toolbar.xml index 7e3162023ff9..fb3d1050c996 100755 --- a/browser/components/places/content/toolbar.xml +++ b/browser/components/places/content/toolbar.xml @@ -196,6 +196,12 @@ ]]> + + + + + false + [TYPE_X_MOZ_PLACE_CONTAINER, TYPE_X_MOZ_PLACE, TYPE_X_MOZ_URL] @@ -357,6 +365,7 @@ })]]> + diff --git a/browser/components/places/content/tree.xml b/browser/components/places/content/tree.xml index 91ffab2de5af..5190834ee43d 100644 --- a/browser/components/places/content/tree.xml +++ b/browser/components/places/content/tree.xml @@ -32,6 +32,7 @@ this.supportedDropOnTypes = viewConfig.dropOnTypes; this.excludeItems = viewConfig.excludeItems; this.firstDropIndex = viewConfig.firstDropIndex; + this.filterTransactions = viewConfig.filterTransactions; ]]> @@ -215,6 +216,57 @@ ]]> + + + + + + true + 0 @@ -386,6 +441,7 @@ index = 0; else if (insertionPoint.index == -1 || insertionPoint.index >= result.root.childCount) index = result.root.childCount - 1; + ASSERT(index < result.childCount, "index out of range: " + index + " > " + result); return index > -1 ? result.root.getChild(index) : null; ]]> @@ -498,28 +554,54 @@ onPerformActionOnCell: function VO_onPerformActionOnCell(action, row, column) { }, })]]> - -1 + [] + 0 + 1 + 2 + -1 && range.max > -1) + this.view.selection.rangedSelect(range.min, range.max, true); + } ]]> diff --git a/browser/components/places/jar.mn b/browser/components/places/jar.mn index db1e1965717f..2e11b97ca551 100755 --- a/browser/components/places/jar.mn +++ b/browser/components/places/jar.mn @@ -7,7 +7,7 @@ browser.jar: content/browser/places/toolbar.xml (content/toolbar.xml) content/browser/places/menu.xml (content/menu.xml) content/browser/places/tree.xml (content/tree.xml) - content/browser/places/controller.js (content/controller.js) +* content/browser/places/controller.js (content/controller.js) content/browser/places/browserShim.js (content-shim/browserShim.js) * content/browser/places/browserShim.xul (content-shim/browserShim.xul) content/browser/places/browserShim.css (content-shim/browserShim.css) diff --git a/browser/components/places/locale/places.dtd b/browser/components/places/locale/places.dtd index 6f3a32116703..a8805b1d53de 100755 --- a/browser/components/places/locale/places.dtd +++ b/browser/components/places/locale/places.dtd @@ -196,3 +196,5 @@ "descending"> + diff --git a/browser/components/places/public/nsINavHistoryService.idl b/browser/components/places/public/nsINavHistoryService.idl index 836aaeeae8fe..fb1c1e7dbcfa 100644 --- a/browser/components/places/public/nsINavHistoryService.idl +++ b/browser/components/places/public/nsINavHistoryService.idl @@ -46,7 +46,6 @@ interface nsINavHistoryQueryResultNode; interface nsINavHistoryFolderResultNode; interface nsINavHistoryQuery; interface nsINavHistoryQueryOptions; -interface nsITransactionManager; interface nsITreeColumn; interface nsIWritablePropertyBag; @@ -1060,12 +1059,6 @@ interface nsINavHistoryService : nsISupports * done changing. Should match beginUpdateBatch or bad things will happen. */ void endUpdateBatch(); - - /** - * The Transaction Manager for edit operations performed on History and - * Bookmark items. - */ - readonly attribute nsITransactionManager transactionManager; }; /** diff --git a/browser/components/places/src/nsNavHistory.cpp b/browser/components/places/src/nsNavHistory.cpp index 1ded373f42f3..cf6fa751546a 100644 --- a/browser/components/places/src/nsNavHistory.cpp +++ b/browser/components/places/src/nsNavHistory.cpp @@ -1956,21 +1956,6 @@ nsNavHistory::EndUpdateBatch() return NS_OK; } -// nsNavHistory::GetTransactionManager - -NS_IMETHODIMP -nsNavHistory::GetTransactionManager(nsITransactionManager** result) -{ - if (!mTransactionManager) { - nsresult rv; - mTransactionManager = - do_CreateInstance(NS_TRANSACTIONMANAGER_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); - } - NS_ADDREF(*result = mTransactionManager); - return NS_OK; -} - // Browser history ************************************************************* diff --git a/browser/components/places/src/nsNavHistory.h b/browser/components/places/src/nsNavHistory.h index edaac30e63d4..526ee74e6ac3 100644 --- a/browser/components/places/src/nsNavHistory.h +++ b/browser/components/places/src/nsNavHistory.h @@ -63,7 +63,6 @@ #include "nsServiceManagerUtils.h" #include "nsIStringBundle.h" #include "nsITimer.h" -#include "nsITransactionManager.h" #include "nsITreeSelection.h" #include "nsITreeView.h" #include "nsString.h" @@ -423,9 +422,6 @@ protected: nsresult TokensToQueries(const nsTArray& aTokens, nsCOMArray* aQueries, nsNavHistoryQueryOptions* aOptions); - - // Transaction Manager - nsCOMPtr mTransactionManager; }; /** diff --git a/toolkit/components/typeaheadfind/content/findBar.js b/toolkit/components/typeaheadfind/content/findBar.js index 1442c885cd00..1aaf50304e4b 100755 --- a/toolkit/components/typeaheadfind/content/findBar.js +++ b/toolkit/components/typeaheadfind/content/findBar.js @@ -58,6 +58,7 @@ const CHAR_CODE_APOSTROPHE = "'".charCodeAt(0); */ var gFindBar = { + mFindEnabled: true, mFindMode: FIND_NORMAL, mFoundLink: null, mCurrentWindow: null, @@ -401,6 +402,13 @@ var gFindBar = { openFindBar: function () { + // Notify anyone else that might want to handle this event + var findActivatedEvent = document.createEvent("Events"); + findActivatedEvent.initEvent("find-activated", false, true); + window.dispatchEvent(findActivatedEvent); + if (!this.mFindEnabled) + throw Components.results.NS_OK; + if (!this.mNotFoundStr || !this.mWrappedToTopStr || !this.mWrappedToBottomStr) { var bundle = document.getElementById("bundle_findBar"); @@ -658,7 +666,13 @@ var gFindBar = { (this.mTypeAheadLinksOnly && evt.charCode != CHAR_CODE_SLASH)) ? FIND_LINKS : FIND_TYPEAHEAD; this.setFindMode(findMode); - if (this.openFindBar()) { + try { + var opened = this.openFindBar(); + } + catch (e) { + return; + } + if (opened) { this.setFindCloseTimeout(); this.selectFindBar(); this.focusFindBar(); @@ -804,8 +818,12 @@ var gFindBar = { onFindCmd: function () { + try { + this.openFindBar(); + } + catch (e) { + } this.setFindMode(FIND_NORMAL); - this.openFindBar(); if (this.mFlashFindBar) { this.mFlashFindBarTimeout = setInterval(this.flashFindBar, 500); var prefService = @@ -829,7 +847,12 @@ var gFindBar = { var res = this.findNext(); if (res == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND) { - if (this.openFindBar()) { + try { + var opened = this.openFindBar(); + } + catch(e) { + } + if (opened) { this.focusFindBar(); this.selectFindBar(); if (this.mFindMode != FIND_NORMAL) @@ -850,7 +873,12 @@ var gFindBar = { var res = this.findPrevious(); if (res == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND) { - if (this.openFindBar()) { + try { + var opened = this.openFindBar(); + } + catch (e) { + } + if (opened) { this.focusFindBar(); this.selectFindBar(); if (this.mFindMode != FIND_NORMAL) @@ -977,9 +1005,6 @@ var gFindBar = { setFindMode: function (mode) { - if (mode == this.mFindMode) - return; - this.mFindMode = mode; } }; diff --git a/toolkit/content/globalOverlay.js b/toolkit/content/globalOverlay.js index 76a0489abf04..348ab7067517 100644 --- a/toolkit/content/globalOverlay.js +++ b/toolkit/content/globalOverlay.js @@ -66,89 +66,100 @@ function goQuitApplication() return true; } -// -// Command Updater functions -// -function goUpdateCommand(command) -{ - try { - var controller = top.document.commandDispatcher.getControllerForCommand(command); - - var enabled = false; - - if ( controller ) - enabled = controller.isCommandEnabled(command); - - goSetCommandEnabled(command, enabled); - } - catch (e) { - dump("An error occurred updating the "+command+" command\n"); - } -} - -function goDoCommand(command) -{ - try { - var controller = top.document.commandDispatcher.getControllerForCommand(command); - if ( controller && controller.isCommandEnabled(command)) - controller.doCommand(command); - } - catch (e) { - dump("An error occurred executing the " + command + " command\n" + e + "\n"); - } -} - - -function goSetCommandEnabled(id, enabled) -{ - var node = document.getElementById(id); - - if ( node ) - { - if ( enabled ) - node.removeAttribute("disabled"); +/** + * Command Updater + */ +var CommandUpdater = { + /** + * Gets a controller that can handle a particular command. + * @param command + * A command to locate a controller for, preferring controllers that + * show the command as enabled. + * @returns In this order of precedence: + * - the first controller supporting the specified command + * associated with the focused element that advertises the + * command as ENABLED + * - the first controller supporting the specified command + * associated with the global window that advertises the + * command as ENABLED + * - the first controller supporting the specified command + * associated with the focused element + * - the first controller supporting the specified command + * associated with the global window + */ + _getControllerForCommand: function(command) { + try { + var controller = + top.document.commandDispatcher.getControllerForCommand(command); + if (controller && controller.isCommandEnabled(command)) + return controller; + } + catch(e) { + } + var controllerCount = window.controllers.getControllerCount(); + for (var i = 0; i < controllerCount; ++i) { + var current = window.controllers.getControllerAt(i); + try { + if (current.supportsCommand(command) && current.isCommandEnabled(command)) + return current; + } + catch (e) { + } + } + return controller || window.controllers.getControllerForCommand(command); + }, + + /** + * Updates the state of a XUL element for the specified command + * depending on its state. + * @param command + * The name of the command to update the XUL element for + */ + updateCommand: function(command) { + var controller = this._getControllerForCommand(command); + if (!controller) + return; + try { + this.enableCommand(command, controller.isCommandEnabled(command)); + } + catch (e) { + } + }, + + /** + * Enables or disables a XUL element. + * @param command + * The name of the command to enable or disable + * @param enabled + * true if the command should be enabled, false otherwise. + */ + enableCommand: function(command, enabled) { + var element = document.getElementById(command); + if (!element) + return; + if (enabled) + element.removeAttribute("disabled"); else - node.setAttribute('disabled', 'true'); - } -} - -function goSetMenuValue(command, labelAttribute) -{ - var commandNode = top.document.getElementById(command); - if ( commandNode ) - { - var label = commandNode.getAttribute(labelAttribute); - if ( label ) - commandNode.setAttribute('label', label); - } -} - -function goSetAccessKey(command, valueAttribute) -{ - var commandNode = top.document.getElementById(command); - if ( commandNode ) - { - var value = commandNode.getAttribute(valueAttribute); - if ( value ) - commandNode.setAttribute('accesskey', value); - } -} - -// this function is used to inform all the controllers attached to a node that an event has occurred -// (e.g. the tree controllers need to be informed of blur events so that they can change some of the -// menu items back to their default values) -function goOnEvent(node, event) -{ - var numControllers = node.controllers.getControllerCount(); - var controller; - - for ( var controllerIndex = 0; controllerIndex < numControllers; controllerIndex++ ) - { - controller = node.controllers.getControllerAt(controllerIndex); - if ( controller ) - controller.onEvent(event); - } -} + element.setAttribute("disabled", "true"); + }, + + /** + * Performs the action associated with a specified command using the most + * relevant controller. + * @param command + * The command to perform. + */ + doCommand: function(command) { + var controller = this._getControllerForCommand(command); + if (!controller) + return; + controller.doCommand(command); + } +}; +// Shim for compatibility with existing code. +function goDoCommand(command) { CommandUpdater.doCommand(command); } +function goUpdateCommand(command) { CommandUpdater.updateCommand(command); } +function goSetCommandEnabled(command, enabled) { CommandUpdater.enableCommand(command, enabled); } function visitLink(aEvent) { var node = aEvent.target;