diff --git a/mozilla/browser/components/places/content/context.inc b/mozilla/browser/components/places/content/context.inc index fa3113d1b87..d0abc751064 100755 --- a/mozilla/browser/components/places/content/context.inc +++ b/mozilla/browser/components/places/content/context.inc @@ -27,11 +27,11 @@ + selection="mutable"/> + selection="link|links|folder|mixed|mutable"/> = v.peerDropIndex) @@ -648,11 +651,10 @@ var PlacesController = { var selectedNode = v.selectedNode; var canInsert = this._canInsert(); - // Select All - this._setEnabled("placesCmd_select:all", v.selType != "single"); // Show Info var hasSingleSelection = v.hasSingleSelection; this._setEnabled("placesCmd_show:info", !inSysArea && hasSingleSelection); + this._updateSelectCommands(); this._updateEditCommands(inSysArea, canInsert); this._updateOpenCommands(inSysArea, hasSingleSelection, selectedNode); this._updateSortCommands(inSysArea, hasSingleSelection, selectedNode, canInsert); @@ -660,6 +662,19 @@ var PlacesController = { 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 @@ -686,17 +701,32 @@ var PlacesController = { // selected folder (if a single folder is selected). var sortingChildren = false; var name = result.root.title; - if (selectedNode && selectedNode.parent) + 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", - (!inSysArea || sortingChildren) && canInsert && viewIsFolder && - !("mixed" in metadata)); + (sortingChildren || !inSysArea) && canInsert && viewIsFolder && + !("mixed" in metadata) && enoughChildrenToSort); var strings = document.getElementById("placeBundle"); var command = document.getElementById("placesCmd_sortby:name"); @@ -729,6 +759,10 @@ var PlacesController = { // 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", @@ -750,6 +784,10 @@ var PlacesController = { /** * 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 */ @@ -772,7 +810,39 @@ var PlacesController = { if (!this._viewSupportsInsertingType(type.value)) return false; try { - this.unwrapNodes(data, type.value); + var nodes = this.unwrapNodes(data, type.value); + + var ip = this.activeView.insertionPoint; + var contents = this.getFolderContents(ip.folderId); + var cc = contents.childCount; + + /** + * 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) { + 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 @@ -780,7 +850,7 @@ var PlacesController = { // parse-able as a URI. return false; } - return true; + return false; }, /** @@ -833,8 +903,9 @@ var PlacesController = { } } - // New Folder - this._setEnabled("placesCmd_new:folder", !inSysArea && canInsertFolders && canInsert); + // 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); @@ -915,17 +986,38 @@ var PlacesController = { metadata["remotecontainer"] = 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 foundNonLeaf = false; var nodes = this._activeView.getSelectionNodes(); - if (nodes.length) + if (this._activeView.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 (this.nodeIsReadOnly(this._activeView.getResult().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)) foundNonLeaf = true; - if (!this.nodeIsReadOnly(node) && - (node.parent && !this.nodeIsReadOnly(node.parent))) - metadata["mutable"] = 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) || + (node.parent && this.nodeIsReadOnly(node.parent))) + delete metadata["mutable"]; var uri = null; if (this.nodeIsURI(node)) @@ -1017,8 +1109,13 @@ var PlacesController = { */ mouseLoadURI: function PC_mouseLoadURI(event) { var node = this._activeView.selectedURINode; - if (node) - this.browserWindow.openUILink(node.uri, event, false, false); + if (node) { + var browser = this._getBrowserWindow(); + if (browser) + browser.openUILink(node.uri, event, false, false); + else + this._openBrowserWith(node.uri); + } }, /** @@ -1107,20 +1204,36 @@ var PlacesController = { this.bookmarks.changeBookmarkURI(oldURI, newURI); }, - get browserWindow() { + /** + * 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) - this.browserWindow.openNewTabWith(node.uri, null, null); + if (node) { + var browser = this._getBrowserWindow(); + if (browser) + browser.openNewTabWith(node.uri, null, null); + else + this._openBrowserWith(node.uri); + } }, /** @@ -1128,22 +1241,36 @@ var PlacesController = { */ openLinkInNewWindow: function PC_openLinkInNewWindow() { var node = this._activeView.selectedURINode; - if (node) - this.browserWindow.openNewWindowWith(node.uri, null, null); + if (node) { + var browser = this._getBrowserWindow(); + if (browser) + browser.openNewWindowWith(node.uri, null, null); + else + this._openBrowserWith(node.uri); + } }, /** * Loads the selected URL in the current window, replacing the Places page. */ openLinkInCurrentWindow: function PC_openLinkInCurrentWindow() { - LOG("openLinkInCurrentWindow"); var node = this._activeView.selectedURINode; - if (node) - this.browserWindow.loadURI(node.uri, null, null); + if (node) { + var browser = this._getBrowserWindow(); + if (browser) + browser.loadURI(node.uri, null, null); + else + this._openBrowserWith(node.uri); + } }, /** * Opens the links in the selected folder, or the selected links in new tabs. + * XXXben this needs to handle the case when there are no open browser windows + * XXXben this function is really long, should be split apart. The codepaths + * seem different between load folder in tabs and load selection in + * tabs, too. + * See: https://bugzilla.mozilla.org/show_bug.cgi?id=331908 */ openLinksInTabs: function PC_openLinksInTabs() { var node = this._activeView.selectedNode; @@ -1152,7 +1279,7 @@ var PlacesController = { var doReplace = getBoolPref("browser.tabs.loadFolderAndReplace"); var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground"); // Get the start index to open tabs at - var browser = this.browserWindow.getBrowser(); + var browser = this._getBrowserWindow().getBrowser(); var tabPanels = browser.browsers; var tabCount = tabPanels.length; var firstIndex; @@ -1217,7 +1344,7 @@ var PlacesController = { var nodes = this._activeView.getSelectionNodes(); for (var i = 0; i < nodes.length; ++i) { if (this.nodeIsURI(nodes[i])) - this.browserWindow.openNewTabWith(nodes[i].uri, + this._getBrowserWindow().openNewTabWith(nodes[i].uri, null, null); } } @@ -1472,16 +1599,26 @@ var PlacesController = { 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: @@ -1750,31 +1887,75 @@ var PlacesController = { /** * Paste Bookmarks and Folders from the clipboard */ - paste: function() { - 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); - xferable.addDataFlavor(TYPE_UNICODE); - + 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); - clipboard.getData(xferable, Ci.nsIClipboard.kGlobalClipboard); - - var data = { }, type = { }; - xferable.getAnyTransferData(type, data, { }); - data = data.value.QueryInterface(Ci.nsISupportsString).data; - data = this.unwrapNodes(data, type.value); - - var ip = this._activeView.insertionPoint; - var transactions = []; - for (var i = 0; i < data.length; ++i) - transactions.push(this.makeTransaction(data[i], type.value, - ip.folderId, ip.index, true)); + + var ip = this.activeView.insertionPoint; + + 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); } @@ -2124,9 +2305,6 @@ PlacesRemoveFolderTransaction.prototype = { _saveFolderContents: function PRFT__saveFolderContents() { this._transactions = []; var contents = PlacesController.getFolderContents(this._id, false, false); - // Container open status doesn't need to be reset to what it was before - // because it's being deleted. - contents.containerOpen = true; var ios = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); @@ -2134,19 +2312,19 @@ PlacesRemoveFolderTransaction.prototype = { var child = contents.getChild(i); var txn; if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER) { - txn = this.bookmarks.getRemoveFolderTransaction(this._id); - var removeTxn = new PlacesRemoveFolderTransaction(txn, this._id); - this._transactions.push(txn); + var folder = asFolder(child); + var removeTxn = + this.bookmarks.getRemoveFolderTransaction(folder.folderId); + txn = new PlacesRemoveFolderTransaction(removeTxn, folder.folderId); } else if (child.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { txn = new PlacesRemoveSeparatorTransaction(this._id, i); - this._transactions.push(txn); } else { txn = new PlacesRemoveItemTransaction(ios.newURI(child.uri, null, null), this._id, i); - this._transactions.push(txn); } + this._transactions.push(txn); } }, diff --git a/mozilla/browser/components/places/content/places.js b/mozilla/browser/components/places/content/places.js index b1cf8d00b2f..eb8c98b0e06 100755 --- a/mozilla/browser/components/places/content/places.js +++ b/mozilla/browser/components/places/content/places.js @@ -65,6 +65,11 @@ var PlacesOrganizer = { if ("arguments" in window) placeURI = window.arguments[0]; selectPlaceURI(placeURI); + + // Initialize the active view so that all commands work properly without + // the user needing to explicitly click in a view (since the search box is + // focused by default). + PlacesController.activeView = this._places; // Set up the search UI. PlacesSearchBox.init(); diff --git a/mozilla/browser/components/places/content/tree.xml b/mozilla/browser/components/places/content/tree.xml index b9bef412fff..9c646e53d07 100644 --- a/mozilla/browser/components/places/content/tree.xml +++ b/mozilla/browser/components/places/content/tree.xml @@ -368,31 +368,66 @@ @@ -409,8 +444,7 @@ // the view is populated from (i.e. the result's folderId). if (index != -1) { var lastSelected = resultview.nodeForTreeIndex(index); - if (resultview.isContainer(index) && - (resultview.isContainerOpen(index) || orientation == NHRVO.DROP_ON)) { + if (resultview.isContainer(index) && orientation == NHRVO.DROP_ON) { // If the last selected item is an open container, append _into_ // it, rather than insert adjacent to it. container = lastSelected; diff --git a/mozilla/browser/components/places/src/nsNavHistoryResult.cpp b/mozilla/browser/components/places/src/nsNavHistoryResult.cpp index 3fb5d3f171b..e02a26a28f1 100755 --- a/mozilla/browser/components/places/src/nsNavHistoryResult.cpp +++ b/mozilla/browser/components/places/src/nsNavHistoryResult.cpp @@ -3414,15 +3414,19 @@ nsNavHistoryResult::GetRoot(nsINavHistoryQueryResultNode** aRoot) FolderObserverList* _fol = BookmarkObserversForId(_folderId, PR_FALSE); \ if (_fol) { \ FolderObserverList _listCopy(*_fol); \ - for (PRUint32 _fol_i = 0; _fol_i < _listCopy.Length(); _fol_i ++) \ - _listCopy[_fol_i]->_functionCall; \ + for (PRUint32 _fol_i = 0; _fol_i < _listCopy.Length(); _fol_i ++) { \ + if (_listCopy[_fol_i]) \ + _listCopy[_fol_i]->_functionCall; \ + } \ } \ } #define ENUMERATE_HISTORY_OBSERVERS(_functionCall) \ { \ nsTArray observerCopy(mEverythingObservers); \ - for (PRUint32 _obs_i = 0; _obs_i < observerCopy.Length(); _obs_i ++) \ + for (PRUint32 _obs_i = 0; _obs_i < observerCopy.Length(); _obs_i ++) { \ + if (observerCopy[_obs_i]) \ observerCopy[_obs_i]->_functionCall; \ + } \ } // nsNavHistoryResult::OnBeginUpdateBatch (nsINavBookmark/HistoryObserver) diff --git a/mozilla/browser/locales/en-US/chrome/browser/places/places.dtd b/mozilla/browser/locales/en-US/chrome/browser/places/places.dtd index 432ec27ba57..88dac8c1e2b 100644 --- a/mozilla/browser/locales/en-US/chrome/browser/places/places.dtd +++ b/mozilla/browser/locales/en-US/chrome/browser/places/places.dtd @@ -106,15 +106,15 @@ + "N"> + "w"> + "O">