michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils", michael@0: "resource:///modules/MigrationUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", michael@0: "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", michael@0: "resource://gre/modules/BookmarkJSONUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", michael@0: "resource://gre/modules/PlacesBackups.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", michael@0: "resource://gre/modules/DownloadUtils.jsm"); michael@0: michael@0: var PlacesOrganizer = { michael@0: _places: null, michael@0: michael@0: // IDs of fields from editBookmarkOverlay that should be hidden when infoBox michael@0: // is minimal. IDs should be kept in sync with the IDs of the elements michael@0: // observing additionalInfoBroadcaster. michael@0: _additionalInfoFields: [ michael@0: "editBMPanel_descriptionRow", michael@0: "editBMPanel_loadInSidebarCheckbox", michael@0: "editBMPanel_keywordRow", michael@0: ], michael@0: michael@0: _initFolderTree: function() { michael@0: var leftPaneRoot = PlacesUIUtils.leftPaneFolderId; michael@0: this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot; michael@0: }, michael@0: michael@0: selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) { michael@0: var itemId = PlacesUIUtils.leftPaneQueries[aQueryName]; michael@0: this._places.selectItems([itemId]); michael@0: // Forcefully expand all-bookmarks michael@0: if (aQueryName == "AllBookmarks" || aQueryName == "History") michael@0: PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; michael@0: }, michael@0: michael@0: /** michael@0: * Opens a given hierarchy in the left pane, stopping at the last reachable michael@0: * container. michael@0: * michael@0: * @param aHierarchy A single container or an array of containers, sorted from michael@0: * the outmost to the innermost in the hierarchy. Each michael@0: * container may be either an item id, a Places URI string, michael@0: * or a named query. michael@0: * @see PlacesUIUtils.leftPaneQueries for supported named queries. michael@0: */ michael@0: selectLeftPaneContainerByHierarchy: michael@0: function PO_selectLeftPaneContainerByHierarchy(aHierarchy) { michael@0: if (!aHierarchy) michael@0: throw new Error("Invalid containers hierarchy"); michael@0: let hierarchy = [].concat(aHierarchy); michael@0: let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed; michael@0: if (!selectWasSuppressed) michael@0: this._places.view.selection.selectEventsSuppressed = true; michael@0: try { michael@0: for (let container of hierarchy) { michael@0: switch (typeof container) { michael@0: case "number": michael@0: this._places.selectItems([container], false); michael@0: break; michael@0: case "string": michael@0: if (container.substr(0, 6) == "place:") michael@0: this._places.selectPlaceURI(container); michael@0: else if (container in PlacesUIUtils.leftPaneQueries) michael@0: this.selectLeftPaneQuery(container); michael@0: else michael@0: throw new Error("Invalid container found: " + container); michael@0: break; michael@0: default: michael@0: throw new Error("Invalid container type found: " + container); michael@0: break; michael@0: } michael@0: PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; michael@0: } michael@0: } finally { michael@0: if (!selectWasSuppressed) michael@0: this._places.view.selection.selectEventsSuppressed = false; michael@0: } michael@0: }, michael@0: michael@0: init: function PO_init() { michael@0: ContentArea.init(); michael@0: michael@0: this._places = document.getElementById("placesList"); michael@0: this._initFolderTree(); michael@0: michael@0: var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks michael@0: if (window.arguments && window.arguments[0]) michael@0: leftPaneSelection = window.arguments[0]; michael@0: michael@0: this.selectLeftPaneContainerByHierarchy(leftPaneSelection); michael@0: if (leftPaneSelection === "History") { michael@0: let historyNode = this._places.selectedNode; michael@0: if (historyNode.childCount > 0) michael@0: this._places.selectNode(historyNode.getChild(0)); michael@0: } michael@0: michael@0: // clear the back-stack michael@0: this._backHistory.splice(0, this._backHistory.length); michael@0: document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); michael@0: michael@0: // Set up the search UI. michael@0: PlacesSearchBox.init(); michael@0: michael@0: window.addEventListener("AppCommand", this, true); michael@0: #ifdef XP_MACOSX michael@0: // 1. Map Edit->Find command to OrganizerCommand_find:all. Need to map michael@0: // both the menuitem and the Find key. michael@0: var findMenuItem = document.getElementById("menu_find"); michael@0: findMenuItem.setAttribute("command", "OrganizerCommand_find:all"); michael@0: var findKey = document.getElementById("key_find"); michael@0: findKey.setAttribute("command", "OrganizerCommand_find:all"); michael@0: michael@0: // 2. Disable some keybindings from browser.xul michael@0: var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"]; michael@0: for (var i=0; i < elements.length; i++) { michael@0: document.getElementById(elements[i]).setAttribute("disabled", "true"); michael@0: } michael@0: #endif michael@0: michael@0: // remove the "Properties" context-menu item, we've our own details pane michael@0: document.getElementById("placesContext") michael@0: .removeChild(document.getElementById("placesContext_show:info")); michael@0: michael@0: ContentArea.focus(); michael@0: }, michael@0: michael@0: QueryInterface: function PO_QueryInterface(aIID) { michael@0: if (aIID.equals(Components.interfaces.nsIDOMEventListener) || michael@0: aIID.equals(Components.interfaces.nsISupports)) michael@0: return this; michael@0: michael@0: throw Components.results.NS_NOINTERFACE; michael@0: }, michael@0: michael@0: handleEvent: function PO_handleEvent(aEvent) { michael@0: if (aEvent.type != "AppCommand") michael@0: return; michael@0: michael@0: aEvent.stopPropagation(); michael@0: switch (aEvent.command) { michael@0: case "Back": michael@0: if (this._backHistory.length > 0) michael@0: this.back(); michael@0: break; michael@0: case "Forward": michael@0: if (this._forwardHistory.length > 0) michael@0: this.forward(); michael@0: break; michael@0: case "Search": michael@0: PlacesSearchBox.findAll(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: destroy: function PO_destroy() { michael@0: }, michael@0: michael@0: _location: null, michael@0: get location() { michael@0: return this._location; michael@0: }, michael@0: michael@0: set location(aLocation) { michael@0: if (!aLocation || this._location == aLocation) michael@0: return aLocation; michael@0: michael@0: if (this.location) { michael@0: this._backHistory.unshift(this.location); michael@0: this._forwardHistory.splice(0, this._forwardHistory.length); michael@0: } michael@0: michael@0: this._location = aLocation; michael@0: this._places.selectPlaceURI(aLocation); michael@0: michael@0: if (!this._places.hasSelection) { michael@0: // If no node was found for the given place: uri, just load it directly michael@0: ContentArea.currentPlace = aLocation; michael@0: } michael@0: this.updateDetailsPane(); michael@0: michael@0: // update navigation commands michael@0: if (this._backHistory.length == 0) michael@0: document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); michael@0: else michael@0: document.getElementById("OrganizerCommand:Back").removeAttribute("disabled"); michael@0: if (this._forwardHistory.length == 0) michael@0: document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true); michael@0: else michael@0: document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled"); michael@0: michael@0: return aLocation; michael@0: }, michael@0: michael@0: _backHistory: [], michael@0: _forwardHistory: [], michael@0: michael@0: back: function PO_back() { michael@0: this._forwardHistory.unshift(this.location); michael@0: var historyEntry = this._backHistory.shift(); michael@0: this._location = null; michael@0: this.location = historyEntry; michael@0: }, michael@0: forward: function PO_forward() { michael@0: this._backHistory.unshift(this.location); michael@0: var historyEntry = this._forwardHistory.shift(); michael@0: this._location = null; michael@0: this.location = historyEntry; michael@0: }, michael@0: michael@0: /** michael@0: * Called when a place folder is selected in the left pane. michael@0: * @param resetSearchBox michael@0: * true if the search box should also be reset, false otherwise. michael@0: * The search box should be reset when a new folder in the left michael@0: * pane is selected; the search scope and text need to be cleared in michael@0: * preparation for the new folder. Note that if the user manually michael@0: * resets the search box, either by clicking its reset button or by michael@0: * deleting its text, this will be false. michael@0: */ michael@0: _cachedLeftPaneSelectedURI: null, michael@0: onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) { michael@0: // Don't change the right-hand pane contents when there's no selection. michael@0: if (!this._places.hasSelection) michael@0: return; michael@0: michael@0: var node = this._places.selectedNode; michael@0: var queries = PlacesUtils.asQuery(node).getQueries(); michael@0: michael@0: // Items are only excluded on the left pane. michael@0: var options = node.queryOptions.clone(); michael@0: options.excludeItems = false; michael@0: var placeURI = PlacesUtils.history.queriesToQueryString(queries, michael@0: queries.length, michael@0: options); michael@0: michael@0: // If either the place of the content tree in the right pane has changed or michael@0: // the user cleared the search box, update the place, hide the search UI, michael@0: // and update the back/forward buttons by setting location. michael@0: if (ContentArea.currentPlace != placeURI || !resetSearchBox) { michael@0: ContentArea.currentPlace = placeURI; michael@0: this.location = node.uri; michael@0: } michael@0: michael@0: // When we invalidate a container we use suppressSelectionEvent, when it is michael@0: // unset a select event is fired, in many cases the selection did not really michael@0: // change, so we should check for it, and return early in such a case. Note michael@0: // that we cannot return any earlier than this point, because when michael@0: // !resetSearchBox, we need to update location and hide the UI as above, michael@0: // even though the selection has not changed. michael@0: if (node.uri == this._cachedLeftPaneSelectedURI) michael@0: return; michael@0: this._cachedLeftPaneSelectedURI = node.uri; michael@0: michael@0: // At this point, resetSearchBox is true, because the left pane selection michael@0: // has changed; otherwise we would have returned earlier. michael@0: michael@0: PlacesSearchBox.searchFilter.reset(); michael@0: this._setSearchScopeForNode(node); michael@0: this.updateDetailsPane(); michael@0: }, michael@0: michael@0: /** michael@0: * Sets the search scope based on aNode's properties. michael@0: * @param aNode michael@0: * the node to set up scope from michael@0: */ michael@0: _setSearchScopeForNode: function PO__setScopeForNode(aNode) { michael@0: let itemId = aNode.itemId; michael@0: michael@0: if (PlacesUtils.nodeIsHistoryContainer(aNode) || michael@0: itemId == PlacesUIUtils.leftPaneQueries["History"]) { michael@0: PlacesQueryBuilder.setScope("history"); michael@0: } michael@0: else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) { michael@0: PlacesQueryBuilder.setScope("downloads"); michael@0: } michael@0: else { michael@0: // Default to All Bookmarks for all other nodes, per bug 469437. michael@0: PlacesQueryBuilder.setScope("bookmarks"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle clicks on the places list. michael@0: * Single Left click, right click or modified click do not result in any michael@0: * special action, since they're related to selection. michael@0: * @param aEvent michael@0: * The mouse event. michael@0: */ michael@0: onPlacesListClick: function PO_onPlacesListClick(aEvent) { michael@0: // Only handle clicks on tree children. michael@0: if (aEvent.target.localName != "treechildren") michael@0: return; michael@0: michael@0: let node = this._places.selectedNode; michael@0: if (node) { michael@0: let middleClick = aEvent.button == 1 && aEvent.detail == 1; michael@0: if (middleClick && PlacesUtils.nodeIsContainer(node)) { michael@0: // The command execution function will take care of seeing if the michael@0: // selection is a folder or a different container type, and will michael@0: // load its contents in tabs. michael@0: PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle focus changes on the places list and the current content view. michael@0: */ michael@0: updateDetailsPane: function PO_updateDetailsPane() { michael@0: if (!ContentArea.currentViewOptions.showDetailsPane) michael@0: return; michael@0: let view = PlacesUIUtils.getViewForNode(document.activeElement); michael@0: if (view) { michael@0: let selectedNodes = view.selectedNode ? michael@0: [view.selectedNode] : view.selectedNodes; michael@0: this._fillDetailsPane(selectedNodes); michael@0: } michael@0: }, michael@0: michael@0: openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) { michael@0: if (aContainer.itemId != -1) { michael@0: PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; michael@0: this._places.selectItems([aContainer.itemId], false); michael@0: } michael@0: else if (PlacesUtils.nodeIsQuery(aContainer)) { michael@0: this._places.selectPlaceURI(aContainer.uri); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Returns the options associated with the query currently loaded in the michael@0: * main places pane. michael@0: */ michael@0: getCurrentOptions: function PO_getCurrentOptions() { michael@0: return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the queries associated with the query currently loaded in the michael@0: * main places pane. michael@0: */ michael@0: getCurrentQueries: function PO_getCurrentQueries() { michael@0: return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries(); michael@0: }, michael@0: michael@0: /** michael@0: * Show the migration wizard for importing passwords, michael@0: * cookies, history, preferences, and bookmarks. michael@0: */ michael@0: importFromBrowser: function PO_importFromBrowser() { michael@0: MigrationUtils.showMigrationWizard(window); michael@0: }, michael@0: michael@0: /** michael@0: * Open a file-picker and import the selected file into the bookmarks store michael@0: */ michael@0: importFromFile: function PO_importFromFile() { michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: let fpCallback = function fpCallback_done(aResult) { michael@0: if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) { michael@0: Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); michael@0: BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false) michael@0: .then(null, Components.utils.reportError); michael@0: } michael@0: }; michael@0: michael@0: fp.init(window, PlacesUIUtils.getString("SelectImport"), michael@0: Ci.nsIFilePicker.modeOpen); michael@0: fp.appendFilters(Ci.nsIFilePicker.filterHTML); michael@0: fp.open(fpCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Allows simple exporting of bookmarks. michael@0: */ michael@0: exportBookmarks: function PO_exportBookmarks() { michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: let fpCallback = function fpCallback_done(aResult) { michael@0: if (aResult != Ci.nsIFilePicker.returnCancel) { michael@0: Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); michael@0: BookmarkHTMLUtils.exportToFile(fp.file.path) michael@0: .then(null, Components.utils.reportError); michael@0: } michael@0: }; michael@0: michael@0: fp.init(window, PlacesUIUtils.getString("EnterExport"), michael@0: Ci.nsIFilePicker.modeSave); michael@0: fp.appendFilters(Ci.nsIFilePicker.filterHTML); michael@0: fp.defaultString = "bookmarks.html"; michael@0: fp.open(fpCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Populates the restore menu with the dates of the backups available. michael@0: */ michael@0: populateRestoreMenu: function PO_populateRestoreMenu() { michael@0: let restorePopup = document.getElementById("fileRestorePopup"); michael@0: michael@0: let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]. michael@0: getService(Ci.nsIScriptableDateFormat); michael@0: michael@0: // Remove existing menu items. Last item is the restoreFromFile item. michael@0: while (restorePopup.childNodes.length > 1) michael@0: restorePopup.removeChild(restorePopup.firstChild); michael@0: michael@0: Task.spawn(function() { michael@0: let backupFiles = yield PlacesBackups.getBackupFiles(); michael@0: if (backupFiles.length == 0) michael@0: return; michael@0: michael@0: // Populate menu with backups. michael@0: for (let i = 0; i < backupFiles.length; i++) { michael@0: let fileSize = (yield OS.File.stat(backupFiles[i])).size; michael@0: let [size, unit] = DownloadUtils.convertByteUnits(fileSize); michael@0: let sizeString = PlacesUtils.getFormattedString("backupFileSizeText", michael@0: [size, unit]); michael@0: let sizeInfo; michael@0: let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]); michael@0: if (bookmarkCount != null) { michael@0: sizeInfo = " (" + sizeString + " - " + michael@0: PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", michael@0: bookmarkCount, michael@0: [bookmarkCount]) + michael@0: ")"; michael@0: } else { michael@0: sizeInfo = " (" + sizeString + ")"; michael@0: } michael@0: michael@0: let backupDate = PlacesBackups.getDateForFile(backupFiles[i]); michael@0: let m = restorePopup.insertBefore(document.createElement("menuitem"), michael@0: document.getElementById("restoreFromFile")); michael@0: m.setAttribute("label", michael@0: dateSvc.FormatDate("", michael@0: Ci.nsIScriptableDateFormat.dateFormatLong, michael@0: backupDate.getFullYear(), michael@0: backupDate.getMonth() + 1, michael@0: backupDate.getDate()) + michael@0: sizeInfo); michael@0: m.setAttribute("value", OS.Path.basename(backupFiles[i])); michael@0: m.setAttribute("oncommand", michael@0: "PlacesOrganizer.onRestoreMenuItemClick(this);"); michael@0: } michael@0: michael@0: // Add the restoreFromFile item. michael@0: restorePopup.insertBefore(document.createElement("menuseparator"), michael@0: document.getElementById("restoreFromFile")); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a menuitem is selected from the restore menu. michael@0: */ michael@0: onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) { michael@0: Task.spawn(function() { michael@0: let backupName = aMenuItem.getAttribute("value"); michael@0: let backupFilePaths = yield PlacesBackups.getBackupFiles(); michael@0: for (let backupFilePath of backupFilePaths) { michael@0: if (OS.Path.basename(backupFilePath) == backupName) { michael@0: PlacesOrganizer.restoreBookmarksFromFile(backupFilePath); michael@0: break; michael@0: } michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Called when 'Choose File...' is selected from the restore menu. michael@0: * Prompts for a file and restores bookmarks to those in the file. michael@0: */ michael@0: onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() { michael@0: let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties); michael@0: let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: let fpCallback = function fpCallback_done(aResult) { michael@0: if (aResult != Ci.nsIFilePicker.returnCancel) { michael@0: this.restoreBookmarksFromFile(fp.file.path); michael@0: } michael@0: }.bind(this); michael@0: michael@0: fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"), michael@0: Ci.nsIFilePicker.modeOpen); michael@0: fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), michael@0: PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); michael@0: fp.appendFilters(Ci.nsIFilePicker.filterAll); michael@0: fp.displayDirectory = backupsDir; michael@0: fp.open(fpCallback); michael@0: }, michael@0: michael@0: /** michael@0: * Restores bookmarks from a JSON file. michael@0: */ michael@0: restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) { michael@0: // check file extension michael@0: if (!aFilePath.endsWith("json")) { michael@0: this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError")); michael@0: return; michael@0: } michael@0: michael@0: // confirm ok to delete existing bookmarks michael@0: var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]. michael@0: getService(Ci.nsIPromptService); michael@0: if (!prompts.confirm(null, michael@0: PlacesUIUtils.getString("bookmarksRestoreAlertTitle"), michael@0: PlacesUIUtils.getString("bookmarksRestoreAlert"))) michael@0: return; michael@0: michael@0: Task.spawn(function() { michael@0: try { michael@0: yield BookmarkJSONUtils.importFromFile(aFilePath, true); michael@0: } catch(ex) { michael@0: PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError")); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: _showErrorAlert: function PO__showErrorAlert(aMsg) { michael@0: var brandShortName = document.getElementById("brandStrings"). michael@0: getString("brandShortName"); michael@0: michael@0: Cc["@mozilla.org/embedcomp/prompt-service;1"]. michael@0: getService(Ci.nsIPromptService). michael@0: alert(window, brandShortName, aMsg); michael@0: }, michael@0: michael@0: /** michael@0: * Backup bookmarks to desktop, auto-generate a filename with a date. michael@0: * The file is a JSON serialization of bookmarks, tags and any annotations michael@0: * of those items. michael@0: */ michael@0: backupBookmarks: function PO_backupBookmarks() { michael@0: let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. michael@0: getService(Ci.nsIProperties); michael@0: let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); michael@0: let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); michael@0: let fpCallback = function fpCallback_done(aResult) { michael@0: if (aResult != Ci.nsIFilePicker.returnCancel) { michael@0: // There is no OS.File version of the filepicker yet (Bug 937812). michael@0: PlacesBackups.saveBookmarksToJSONFile(fp.file.path); michael@0: } michael@0: }; michael@0: michael@0: fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"), michael@0: Ci.nsIFilePicker.modeSave); michael@0: fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), michael@0: PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); michael@0: fp.defaultString = PlacesBackups.getFilenameForDate(); michael@0: fp.displayDirectory = backupsDir; michael@0: fp.open(fpCallback); michael@0: }, michael@0: michael@0: _paneDisabled: false, michael@0: _setDetailsFieldsDisabledState: michael@0: function PO__setDetailsFieldsDisabledState(aDisabled) { michael@0: if (aDisabled) { michael@0: document.getElementById("paneElementsBroadcaster") michael@0: .setAttribute("disabled", "true"); michael@0: } michael@0: else { michael@0: document.getElementById("paneElementsBroadcaster") michael@0: .removeAttribute("disabled"); michael@0: } michael@0: }, michael@0: michael@0: _detectAndSetDetailsPaneMinimalState: michael@0: function PO__detectAndSetDetailsPaneMinimalState(aNode) { michael@0: /** michael@0: * The details of simple folder-items (as opposed to livemarks) or the michael@0: * of livemark-children are not likely to fill the infoBox anyway, michael@0: * thus we remove the "More/Less" button and show all details. michael@0: * michael@0: * the wasminimal attribute here is used to persist the "more/less" michael@0: * state in a bookmark->folder->bookmark scenario. michael@0: */ michael@0: var infoBox = document.getElementById("infoBox"); michael@0: var infoBoxExpander = document.getElementById("infoBoxExpander"); michael@0: var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper"); michael@0: var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); michael@0: michael@0: if (!aNode) { michael@0: infoBoxExpanderWrapper.hidden = true; michael@0: return; michael@0: } michael@0: if (aNode.itemId != -1 && michael@0: PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) { michael@0: if (infoBox.getAttribute("minimal") == "true") michael@0: infoBox.setAttribute("wasminimal", "true"); michael@0: infoBox.removeAttribute("minimal"); michael@0: infoBoxExpanderWrapper.hidden = true; michael@0: } michael@0: else { michael@0: if (infoBox.getAttribute("wasminimal") == "true") michael@0: infoBox.setAttribute("minimal", "true"); michael@0: infoBox.removeAttribute("wasminimal"); michael@0: infoBoxExpanderWrapper.hidden = michael@0: this._additionalInfoFields.every(function (id) michael@0: document.getElementById(id).collapsed); michael@0: } michael@0: additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true"; michael@0: }, michael@0: michael@0: // NOT YET USED michael@0: updateThumbnailProportions: function PO_updateThumbnailProportions() { michael@0: var previewBox = document.getElementById("previewBox"); michael@0: var canvas = document.getElementById("itemThumbnail"); michael@0: var height = previewBox.boxObject.height; michael@0: var width = height * (screen.width / screen.height); michael@0: canvas.width = width; michael@0: canvas.height = height; michael@0: }, michael@0: michael@0: _fillDetailsPane: function PO__fillDetailsPane(aNodeList) { michael@0: var infoBox = document.getElementById("infoBox"); michael@0: var detailsDeck = document.getElementById("detailsDeck"); michael@0: michael@0: // Make sure the infoBox UI is visible if we need to use it, we hide it michael@0: // below when we don't. michael@0: infoBox.hidden = false; michael@0: var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null; michael@0: // If a textbox within a panel is focused, force-blur it so its contents michael@0: // are saved michael@0: if (gEditItemOverlay.itemId != -1) { michael@0: var focusedElement = document.commandDispatcher.focusedElement; michael@0: if ((focusedElement instanceof HTMLInputElement || michael@0: focusedElement instanceof HTMLTextAreaElement) && michael@0: /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id)) michael@0: focusedElement.blur(); michael@0: michael@0: // don't update the panel if we are already editing this node unless we're michael@0: // in multi-edit mode michael@0: if (aSelectedNode) { michael@0: var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); michael@0: var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId || michael@0: gEditItemOverlay.itemId == concreteId || michael@0: (aSelectedNode.itemId == -1 && gEditItemOverlay.uri && michael@0: gEditItemOverlay.uri == aSelectedNode.uri); michael@0: if (nodeIsSame && detailsDeck.selectedIndex == 1 && michael@0: !gEditItemOverlay.multiEdit) michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Clean up the panel before initing it again. michael@0: gEditItemOverlay.uninitPanel(false); michael@0: michael@0: if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) { michael@0: detailsDeck.selectedIndex = 1; michael@0: // Using the concrete itemId is arguably wrong. The bookmarks API michael@0: // does allow setting properties for folder shortcuts as well, but since michael@0: // the UI does not distinct between the couple, we better just show michael@0: // the concrete item properties for shortcuts to root nodes. michael@0: var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); michael@0: var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId); michael@0: var readOnly = isRootItem || michael@0: aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId; michael@0: var useConcreteId = isRootItem || michael@0: PlacesUtils.nodeIsTagQuery(aSelectedNode); michael@0: var itemId = -1; michael@0: if (concreteId != -1 && useConcreteId) michael@0: itemId = concreteId; michael@0: else if (aSelectedNode.itemId != -1) michael@0: itemId = aSelectedNode.itemId; michael@0: else michael@0: itemId = PlacesUtils._uri(aSelectedNode.uri); michael@0: michael@0: gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"] michael@0: , forceReadOnly: readOnly michael@0: , titleOverride: aSelectedNode.title michael@0: }); michael@0: michael@0: // Dynamically generated queries, like history date containers, have michael@0: // itemId !=0 and do not exist in history. For them the panel is michael@0: // read-only, but empty, since it can't get a valid title for the object. michael@0: // In such a case we force the title using the selectedNode one, for UI michael@0: // polishness. michael@0: if (aSelectedNode.itemId == -1 && michael@0: (PlacesUtils.nodeIsDay(aSelectedNode) || michael@0: PlacesUtils.nodeIsHost(aSelectedNode))) michael@0: gEditItemOverlay._element("namePicker").value = aSelectedNode.title; michael@0: michael@0: this._detectAndSetDetailsPaneMinimalState(aSelectedNode); michael@0: } michael@0: else if (!aSelectedNode && aNodeList[0]) { michael@0: var itemIds = []; michael@0: for (var i = 0; i < aNodeList.length; i++) { michael@0: if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) && michael@0: !PlacesUtils.nodeIsURI(aNodeList[i])) { michael@0: detailsDeck.selectedIndex = 0; michael@0: var selectItemDesc = document.getElementById("selectItemDescription"); michael@0: var itemsCountLabel = document.getElementById("itemsCountText"); michael@0: selectItemDesc.hidden = false; michael@0: itemsCountLabel.value = michael@0: PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", michael@0: aNodeList.length, [aNodeList.length]); michael@0: infoBox.hidden = true; michael@0: return; michael@0: } michael@0: itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId : michael@0: PlacesUtils._uri(aNodeList[i].uri); michael@0: } michael@0: detailsDeck.selectedIndex = 1; michael@0: gEditItemOverlay.initPanel(itemIds, michael@0: { hiddenRows: ["folderPicker", michael@0: "loadInSidebar", michael@0: "location", michael@0: "keyword", michael@0: "description", michael@0: "name"]}); michael@0: this._detectAndSetDetailsPaneMinimalState(aSelectedNode); michael@0: } michael@0: else { michael@0: detailsDeck.selectedIndex = 0; michael@0: infoBox.hidden = true; michael@0: let selectItemDesc = document.getElementById("selectItemDescription"); michael@0: let itemsCountLabel = document.getElementById("itemsCountText"); michael@0: let itemsCount = 0; michael@0: if (ContentArea.currentView.result) { michael@0: let rootNode = ContentArea.currentView.result.root; michael@0: if (rootNode.containerOpen) michael@0: itemsCount = rootNode.childCount; michael@0: } michael@0: if (itemsCount == 0) { michael@0: selectItemDesc.hidden = true; michael@0: itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems"); michael@0: } michael@0: else { michael@0: selectItemDesc.hidden = false; michael@0: itemsCountLabel.value = michael@0: PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", michael@0: itemsCount, [itemsCount]); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // NOT YET USED michael@0: _updateThumbnail: function PO__updateThumbnail() { michael@0: var bo = document.getElementById("previewBox").boxObject; michael@0: var width = bo.width; michael@0: var height = bo.height; michael@0: michael@0: var canvas = document.getElementById("itemThumbnail"); michael@0: var ctx = canvas.getContext('2d'); michael@0: var notAvailableText = canvas.getAttribute("notavailabletext"); michael@0: ctx.save(); michael@0: ctx.fillStyle = "-moz-Dialog"; michael@0: ctx.fillRect(0, 0, width, height); michael@0: ctx.translate(width/2, height/2); michael@0: michael@0: ctx.fillStyle = "GrayText"; michael@0: ctx.mozTextStyle = "12pt sans serif"; michael@0: var len = ctx.mozMeasureText(notAvailableText); michael@0: ctx.translate(-len/2,0); michael@0: ctx.mozDrawText(notAvailableText); michael@0: ctx.restore(); michael@0: }, michael@0: michael@0: toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() { michael@0: var infoBox = document.getElementById("infoBox"); michael@0: var infoBoxExpander = document.getElementById("infoBoxExpander"); michael@0: var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel"); michael@0: var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); michael@0: michael@0: if (infoBox.getAttribute("minimal") == "true") { michael@0: infoBox.removeAttribute("minimal"); michael@0: infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel"); michael@0: infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey"); michael@0: infoBoxExpander.className = "expander-up"; michael@0: additionalInfoBroadcaster.removeAttribute("hidden"); michael@0: } michael@0: else { michael@0: infoBox.setAttribute("minimal", "true"); michael@0: infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel"); michael@0: infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey"); michael@0: infoBoxExpander.className = "expander-down"; michael@0: additionalInfoBroadcaster.setAttribute("hidden", "true"); michael@0: } michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * A set of utilities relating to search within Bookmarks and History. michael@0: */ michael@0: var PlacesSearchBox = { michael@0: michael@0: /** michael@0: * The Search text field michael@0: */ michael@0: get searchFilter() { michael@0: return document.getElementById("searchFilter"); michael@0: }, michael@0: michael@0: /** michael@0: * Folders to include when searching. michael@0: */ michael@0: _folders: [], michael@0: get folders() { michael@0: if (this._folders.length == 0) { michael@0: this._folders.push(PlacesUtils.bookmarksMenuFolderId, michael@0: PlacesUtils.unfiledBookmarksFolderId, michael@0: PlacesUtils.toolbarFolderId); michael@0: } michael@0: return this._folders; michael@0: }, michael@0: set folders(aFolders) { michael@0: this._folders = aFolders; michael@0: return aFolders; michael@0: }, michael@0: michael@0: /** michael@0: * Run a search for the specified text, over the collection specified by michael@0: * the dropdown arrow. The default is all bookmarks, but can be michael@0: * localized to the active collection. michael@0: * @param filterString michael@0: * The text to search for. michael@0: */ michael@0: search: function PSB_search(filterString) { michael@0: var PO = PlacesOrganizer; michael@0: // If the user empties the search box manually, reset it and load all michael@0: // contents of the current scope. michael@0: // XXX this might be to jumpy, maybe should search for "", so results michael@0: // are ungrouped, and search box not reset michael@0: if (filterString == "") { michael@0: PO.onPlaceSelected(false); michael@0: return; michael@0: } michael@0: michael@0: let currentView = ContentArea.currentView; michael@0: let currentOptions = PO.getCurrentOptions(); michael@0: michael@0: // Search according to the current scope, which was set by michael@0: // PQB_setScope() michael@0: switch (PlacesSearchBox.filterCollection) { michael@0: case "bookmarks": michael@0: currentView.applyFilter(filterString, this.folders); michael@0: break; michael@0: case "history": michael@0: if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { michael@0: var query = PlacesUtils.history.getNewQuery(); michael@0: query.searchTerms = filterString; michael@0: var options = currentOptions.clone(); michael@0: // Make sure we're getting uri results. michael@0: options.resultType = currentOptions.RESULTS_AS_URI; michael@0: options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; michael@0: options.includeHidden = true; michael@0: currentView.load([query], options); michael@0: } michael@0: else { michael@0: currentView.applyFilter(filterString, null, true); michael@0: } michael@0: break; michael@0: case "downloads": michael@0: if (currentView == ContentTree.view) { michael@0: let query = PlacesUtils.history.getNewQuery(); michael@0: query.searchTerms = filterString; michael@0: query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1); michael@0: let options = currentOptions.clone(); michael@0: // Make sure we're getting uri results. michael@0: options.resultType = currentOptions.RESULTS_AS_URI; michael@0: options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; michael@0: options.includeHidden = true; michael@0: currentView.load([query], options); michael@0: } michael@0: else { michael@0: // The new downloads view doesn't use places for searching downloads. michael@0: currentView.searchTerm = filterString; michael@0: } michael@0: break; michael@0: default: michael@0: throw "Invalid filterCollection on search"; michael@0: } michael@0: michael@0: // Update the details panel michael@0: PlacesOrganizer.updateDetailsPane(); michael@0: }, michael@0: michael@0: /** michael@0: * Finds across all history, downloads or all bookmarks. michael@0: */ michael@0: findAll: function PSB_findAll() { michael@0: switch (this.filterCollection) { michael@0: case "history": michael@0: PlacesQueryBuilder.setScope("history"); michael@0: break; michael@0: case "downloads": michael@0: PlacesQueryBuilder.setScope("downloads"); michael@0: break; michael@0: default: michael@0: PlacesQueryBuilder.setScope("bookmarks"); michael@0: break; michael@0: } michael@0: this.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Updates the display with the title of the current collection. michael@0: * @param aTitle michael@0: * The title of the current collection. michael@0: */ michael@0: updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) { michael@0: let title = ""; michael@0: switch (this.filterCollection) { michael@0: case "history": michael@0: title = PlacesUIUtils.getString("searchHistory"); michael@0: break; michael@0: case "downloads": michael@0: title = PlacesUIUtils.getString("searchDownloads"); michael@0: break; michael@0: default: michael@0: title = PlacesUIUtils.getString("searchBookmarks"); michael@0: } michael@0: this.searchFilter.placeholder = title; michael@0: }, michael@0: michael@0: /** michael@0: * Gets/sets the active collection from the dropdown menu. michael@0: */ michael@0: get filterCollection() { michael@0: return this.searchFilter.getAttribute("collection"); michael@0: }, michael@0: set filterCollection(collectionName) { michael@0: if (collectionName == this.filterCollection) michael@0: return collectionName; michael@0: michael@0: this.searchFilter.setAttribute("collection", collectionName); michael@0: this.updateCollectionTitle(); michael@0: michael@0: return collectionName; michael@0: }, michael@0: michael@0: /** michael@0: * Focus the search box michael@0: */ michael@0: focus: function PSB_focus() { michael@0: this.searchFilter.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Set up the gray text in the search bar as the Places View loads. michael@0: */ michael@0: init: function PSB_init() { michael@0: this.updateCollectionTitle(); michael@0: }, michael@0: michael@0: /** michael@0: * Gets or sets the text shown in the Places Search Box michael@0: */ michael@0: get value() { michael@0: return this.searchFilter.value; michael@0: }, michael@0: set value(value) { michael@0: return this.searchFilter.value = value; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Functions and data for advanced query builder michael@0: */ michael@0: var PlacesQueryBuilder = { michael@0: michael@0: queries: [], michael@0: queryOptions: null, michael@0: michael@0: /** michael@0: * Sets the search scope. This can be called when no search is active, and michael@0: * in that case, when the user does begin a search aScope will be used (see michael@0: * PSB_search()). If there is an active search, it's performed again to michael@0: * update the content tree. michael@0: * @param aScope michael@0: * The search scope: "bookmarks", "collection", "downloads" or michael@0: * "history". michael@0: */ michael@0: setScope: function PQB_setScope(aScope) { michael@0: // Determine filterCollection, folders, and scopeButtonId based on aScope. michael@0: var filterCollection; michael@0: var folders = []; michael@0: switch (aScope) { michael@0: case "history": michael@0: filterCollection = "history"; michael@0: break; michael@0: case "bookmarks": michael@0: filterCollection = "bookmarks"; michael@0: folders.push(PlacesUtils.bookmarksMenuFolderId, michael@0: PlacesUtils.toolbarFolderId, michael@0: PlacesUtils.unfiledBookmarksFolderId); michael@0: break; michael@0: case "downloads": michael@0: filterCollection = "downloads"; michael@0: break; michael@0: default: michael@0: throw "Invalid search scope"; michael@0: break; michael@0: } michael@0: michael@0: // Update the search box. Re-search if there's an active search. michael@0: PlacesSearchBox.filterCollection = filterCollection; michael@0: PlacesSearchBox.folders = folders; michael@0: var searchStr = PlacesSearchBox.searchFilter.value; michael@0: if (searchStr) michael@0: PlacesSearchBox.search(searchStr); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Population and commands for the View Menu. michael@0: */ michael@0: var ViewMenu = { michael@0: /** michael@0: * Removes content generated previously from a menupopup. michael@0: * @param popup michael@0: * The popup that contains the previously generated content. michael@0: * @param startID michael@0: * The id attribute of an element that is the start of the michael@0: * dynamically generated region - remove elements after this michael@0: * item only. michael@0: * Must be contained by popup. Can be null (in which case the michael@0: * contents of popup are removed). michael@0: * @param endID michael@0: * The id attribute of an element that is the end of the michael@0: * dynamically generated region - remove elements up to this michael@0: * item only. michael@0: * Must be contained by popup. Can be null (in which case all michael@0: * items until the end of the popup will be removed). Ignored michael@0: * if startID is null. michael@0: * @returns The element for the caller to insert new items before, michael@0: * null if the caller should just append to the popup. michael@0: */ michael@0: _clean: function VM__clean(popup, startID, endID) { michael@0: if (endID) michael@0: NS_ASSERT(startID, "meaningless to have valid endID and null startID"); michael@0: if (startID) { michael@0: var startElement = document.getElementById(startID); michael@0: NS_ASSERT(startElement.parentNode == michael@0: popup, "startElement is not in popup"); michael@0: NS_ASSERT(startElement, michael@0: "startID does not correspond to an existing element"); michael@0: var endElement = null; michael@0: if (endID) { michael@0: endElement = document.getElementById(endID); michael@0: NS_ASSERT(endElement.parentNode == popup, michael@0: "endElement is not in popup"); michael@0: NS_ASSERT(endElement, michael@0: "endID does not correspond to an existing element"); michael@0: } michael@0: while (startElement.nextSibling != endElement) michael@0: popup.removeChild(startElement.nextSibling); michael@0: return endElement; michael@0: } michael@0: else { michael@0: while(popup.hasChildNodes()) michael@0: popup.removeChild(popup.firstChild); michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Fills a menupopup with a list of columns michael@0: * @param event michael@0: * The popupshowing event that invoked this function. michael@0: * @param startID michael@0: * see _clean michael@0: * @param endID michael@0: * see _clean michael@0: * @param type michael@0: * the type of the menuitem, e.g. "radio" or "checkbox". michael@0: * Can be null (no-type). michael@0: * Checkboxes are checked if the column is visible. michael@0: * @param propertyPrefix michael@0: * If propertyPrefix is non-null: michael@0: * propertyPrefix + column ID + ".label" will be used to get the michael@0: * localized label string. michael@0: * propertyPrefix + column ID + ".accesskey" will be used to get the michael@0: * localized accesskey. michael@0: * If propertyPrefix is null, the column label is used as label and michael@0: * no accesskey is assigned. michael@0: */ michael@0: fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) { michael@0: var popup = event.target; michael@0: var pivot = this._clean(popup, startID, endID); michael@0: michael@0: // If no column is "sort-active", the "Unsorted" item needs to be checked, michael@0: // so track whether or not we find a column that is sort-active. michael@0: var isSorted = false; michael@0: var content = document.getElementById("placeContent"); michael@0: var columns = content.columns; michael@0: for (var i = 0; i < columns.count; ++i) { michael@0: var column = columns.getColumnAt(i).element; michael@0: var menuitem = document.createElement("menuitem"); michael@0: menuitem.id = "menucol_" + column.id; michael@0: menuitem.column = column; michael@0: var label = column.getAttribute("label"); michael@0: if (propertyPrefix) { michael@0: var menuitemPrefix = propertyPrefix; michael@0: // for string properties, use "name" as the id, instead of "title" michael@0: // see bug #386287 for details michael@0: var columnId = column.getAttribute("anonid"); michael@0: menuitemPrefix += columnId == "title" ? "name" : columnId; michael@0: label = PlacesUIUtils.getString(menuitemPrefix + ".label"); michael@0: var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey"); michael@0: menuitem.setAttribute("accesskey", accesskey); michael@0: } michael@0: menuitem.setAttribute("label", label); michael@0: if (type == "radio") { michael@0: menuitem.setAttribute("type", "radio"); michael@0: menuitem.setAttribute("name", "columns"); michael@0: // This column is the sort key. Its item is checked. michael@0: if (column.getAttribute("sortDirection") != "") { michael@0: menuitem.setAttribute("checked", "true"); michael@0: isSorted = true; michael@0: } michael@0: } michael@0: else if (type == "checkbox") { michael@0: menuitem.setAttribute("type", "checkbox"); michael@0: // Cannot uncheck the primary column. michael@0: if (column.getAttribute("primary") == "true") michael@0: menuitem.setAttribute("disabled", "true"); michael@0: // Items for visible columns are checked. michael@0: if (!column.hidden) michael@0: menuitem.setAttribute("checked", "true"); michael@0: } michael@0: if (pivot) michael@0: popup.insertBefore(menuitem, pivot); michael@0: else michael@0: popup.appendChild(menuitem); michael@0: } michael@0: event.stopPropagation(); michael@0: }, michael@0: michael@0: /** michael@0: * Set up the content of the view menu. michael@0: */ michael@0: populateSortMenu: function VM_populateSortMenu(event) { michael@0: this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1."); michael@0: michael@0: var sortColumn = this._getSortColumn(); michael@0: var viewSortAscending = document.getElementById("viewSortAscending"); michael@0: var viewSortDescending = document.getElementById("viewSortDescending"); michael@0: // We need to remove an existing checked attribute because the unsorted michael@0: // menu item is not rebuilt every time we open the menu like the others. michael@0: var viewUnsorted = document.getElementById("viewUnsorted"); michael@0: if (!sortColumn) { michael@0: viewSortAscending.removeAttribute("checked"); michael@0: viewSortDescending.removeAttribute("checked"); michael@0: viewUnsorted.setAttribute("checked", "true"); michael@0: } michael@0: else if (sortColumn.getAttribute("sortDirection") == "ascending") { michael@0: viewSortAscending.setAttribute("checked", "true"); michael@0: viewSortDescending.removeAttribute("checked"); michael@0: viewUnsorted.removeAttribute("checked"); michael@0: } michael@0: else if (sortColumn.getAttribute("sortDirection") == "descending") { michael@0: viewSortDescending.setAttribute("checked", "true"); michael@0: viewSortAscending.removeAttribute("checked"); michael@0: viewUnsorted.removeAttribute("checked"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Shows/Hides a tree column. michael@0: * @param element michael@0: * The menuitem element for the column michael@0: */ michael@0: showHideColumn: function VM_showHideColumn(element) { michael@0: var column = element.column; michael@0: michael@0: var splitter = column.nextSibling; michael@0: if (splitter && splitter.localName != "splitter") michael@0: splitter = null; michael@0: michael@0: if (element.getAttribute("checked") == "true") { michael@0: column.setAttribute("hidden", "false"); michael@0: if (splitter) michael@0: splitter.removeAttribute("hidden"); michael@0: } michael@0: else { michael@0: column.setAttribute("hidden", "true"); michael@0: if (splitter) michael@0: splitter.setAttribute("hidden", "true"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Gets the last column that was sorted. michael@0: * @returns the currently sorted column, null if there is no sorted column. michael@0: */ michael@0: _getSortColumn: function VM__getSortColumn() { michael@0: var content = document.getElementById("placeContent"); michael@0: var cols = content.columns; michael@0: for (var i = 0; i < cols.count; ++i) { michael@0: var column = cols.getColumnAt(i).element; michael@0: var sortDirection = column.getAttribute("sortDirection"); michael@0: if (sortDirection == "ascending" || sortDirection == "descending") michael@0: return column; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Sorts the view by the specified column. michael@0: * @param aColumn michael@0: * The colum that is the sort key. Can be null - the michael@0: * current sort column or the title column will be used. michael@0: * @param aDirection michael@0: * The direction to sort - "ascending" or "descending". michael@0: * Can be null - the last direction or descending will be used. michael@0: * michael@0: * If both aColumnID and aDirection are null, the view will be unsorted. michael@0: */ michael@0: setSortColumn: function VM_setSortColumn(aColumn, aDirection) { michael@0: var result = document.getElementById("placeContent").result; michael@0: if (!aColumn && !aDirection) { michael@0: result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; michael@0: return; michael@0: } michael@0: michael@0: var columnId; michael@0: if (aColumn) { michael@0: columnId = aColumn.getAttribute("anonid"); michael@0: if (!aDirection) { michael@0: var sortColumn = this._getSortColumn(); michael@0: if (sortColumn) michael@0: aDirection = sortColumn.getAttribute("sortDirection"); michael@0: } michael@0: } michael@0: else { michael@0: var sortColumn = this._getSortColumn(); michael@0: columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title"; michael@0: } michael@0: michael@0: // This maps the possible values of columnId (i.e., anonid's of treecols in michael@0: // placeContent) to the default sortingMode and sortingAnnotation values for michael@0: // each column. michael@0: // key: Sort key in the name of one of the michael@0: // nsINavHistoryQueryOptions.SORT_BY_* constants michael@0: // dir: Default sort direction to use if none has been specified michael@0: // anno: The annotation to sort by, if key is "ANNOTATION" michael@0: var colLookupTable = { michael@0: title: { key: "TITLE", dir: "ascending" }, michael@0: tags: { key: "TAGS", dir: "ascending" }, michael@0: url: { key: "URI", dir: "ascending" }, michael@0: date: { key: "DATE", dir: "descending" }, michael@0: visitCount: { key: "VISITCOUNT", dir: "descending" }, michael@0: keyword: { key: "KEYWORD", dir: "ascending" }, michael@0: dateAdded: { key: "DATEADDED", dir: "descending" }, michael@0: lastModified: { key: "LASTMODIFIED", dir: "descending" }, michael@0: description: { key: "ANNOTATION", michael@0: dir: "ascending", michael@0: anno: PlacesUIUtils.DESCRIPTION_ANNO } michael@0: }; michael@0: michael@0: // Make sure we have a valid column. michael@0: if (!colLookupTable.hasOwnProperty(columnId)) michael@0: throw("Invalid column"); michael@0: michael@0: // Use a default sort direction if none has been specified. If aDirection michael@0: // is invalid, result.sortingMode will be undefined, which has the effect michael@0: // of unsorting the tree. michael@0: aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase(); michael@0: michael@0: var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection; michael@0: result.sortingAnnotation = colLookupTable[columnId].anno || ""; michael@0: result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst]; michael@0: } michael@0: } michael@0: michael@0: let ContentArea = { michael@0: _specialViews: new Map(), michael@0: michael@0: init: function CA_init() { michael@0: this._deck = document.getElementById("placesViewsDeck"); michael@0: this._toolbar = document.getElementById("placesToolbar"); michael@0: ContentTree.init(); michael@0: this._setupView(); michael@0: }, michael@0: michael@0: /** michael@0: * Gets the content view to be used for loading the given query. michael@0: * If a custom view was set by setContentViewForQueryString, that michael@0: * view would be returned, else the default tree view is returned michael@0: * michael@0: * @param aQueryString michael@0: * a query string michael@0: * @return the view to be used for loading aQueryString. michael@0: */ michael@0: getContentViewForQueryString: michael@0: function CA_getContentViewForQueryString(aQueryString) { michael@0: try { michael@0: if (this._specialViews.has(aQueryString)) { michael@0: let { view, options } = this._specialViews.get(aQueryString); michael@0: if (typeof view == "function") { michael@0: view = view(); michael@0: this._specialViews.set(aQueryString, { view: view, options: options }); michael@0: } michael@0: return view; michael@0: } michael@0: } michael@0: catch(ex) { michael@0: Components.utils.reportError(ex); michael@0: } michael@0: return ContentTree.view; michael@0: }, michael@0: michael@0: /** michael@0: * Sets a custom view to be used rather than the default places tree michael@0: * whenever the given query is selected in the left pane. michael@0: * @param aQueryString michael@0: * a query string michael@0: * @param aView michael@0: * Either the custom view or a function that will return the view michael@0: * the first (and only) time it's called. michael@0: * @param [optional] aOptions michael@0: * Object defining special options for the view. michael@0: * @see ContentTree.viewOptions for supported options and default values. michael@0: */ michael@0: setContentViewForQueryString: michael@0: function CA_setContentViewForQueryString(aQueryString, aView, aOptions) { michael@0: if (!aQueryString || michael@0: typeof aView != "object" && typeof aView != "function") michael@0: throw new Error("Invalid arguments"); michael@0: michael@0: this._specialViews.set(aQueryString, { view: aView, michael@0: options: aOptions || new Object() }); michael@0: }, michael@0: michael@0: get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel), michael@0: set currentView(aNewView) { michael@0: let oldView = this.currentView; michael@0: if (oldView != aNewView) { michael@0: this._deck.selectedPanel = aNewView.associatedElement; michael@0: michael@0: // If the content area inactivated view was focused, move focus michael@0: // to the new view. michael@0: if (document.activeElement == oldView.associatedElement) michael@0: aNewView.associatedElement.focus(); michael@0: } michael@0: return aNewView; michael@0: }, michael@0: michael@0: get currentPlace() this.currentView.place, michael@0: set currentPlace(aQueryString) { michael@0: let oldView = this.currentView; michael@0: let newView = this.getContentViewForQueryString(aQueryString); michael@0: newView.place = aQueryString; michael@0: if (oldView != newView) { michael@0: oldView.active = false; michael@0: this.currentView = newView; michael@0: this._setupView(); michael@0: newView.active = true; michael@0: } michael@0: return aQueryString; michael@0: }, michael@0: michael@0: /** michael@0: * Applies view options. michael@0: */ michael@0: _setupView: function CA__setupView() { michael@0: let options = this.currentViewOptions; michael@0: michael@0: // showDetailsPane. michael@0: let detailsDeck = document.getElementById("detailsDeck"); michael@0: detailsDeck.hidden = !options.showDetailsPane; michael@0: michael@0: // toolbarSet. michael@0: for (let elt of this._toolbar.childNodes) { michael@0: // On Windows and Linux the menu buttons are menus wrapped in a menubar. michael@0: if (elt.id == "placesMenu") { michael@0: for (let menuElt of elt.childNodes) { michael@0: menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1; michael@0: } michael@0: } michael@0: else { michael@0: elt.hidden = options.toolbarSet.indexOf(elt.id) == -1; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Options for the current view. michael@0: * michael@0: * @see ContentTree.viewOptions for supported options and default values. michael@0: */ michael@0: get currentViewOptions() { michael@0: // Use ContentTree options as default. michael@0: let viewOptions = ContentTree.viewOptions; michael@0: if (this._specialViews.has(this.currentPlace)) { michael@0: let { view, options } = this._specialViews.get(this.currentPlace); michael@0: for (let option in options) { michael@0: viewOptions[option] = options[option]; michael@0: } michael@0: } michael@0: return viewOptions; michael@0: }, michael@0: michael@0: focus: function() { michael@0: this._deck.selectedPanel.focus(); michael@0: } michael@0: }; michael@0: michael@0: let ContentTree = { michael@0: init: function CT_init() { michael@0: this._view = document.getElementById("placeContent"); michael@0: }, michael@0: michael@0: get view() this._view, michael@0: michael@0: get viewOptions() Object.seal({ michael@0: showDetailsPane: true, michael@0: toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter" michael@0: }), michael@0: michael@0: openSelectedNode: function CT_openSelectedNode(aEvent) { michael@0: let view = this.view; michael@0: PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view); michael@0: }, michael@0: michael@0: onClick: function CT_onClick(aEvent) { michael@0: let node = this.view.selectedNode; michael@0: if (node) { michael@0: let doubleClick = aEvent.button == 0 && aEvent.detail == 2; michael@0: let middleClick = aEvent.button == 1 && aEvent.detail == 1; michael@0: if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) { michael@0: // Open associated uri in the browser. michael@0: this.openSelectedNode(aEvent); michael@0: } michael@0: else if (middleClick && PlacesUtils.nodeIsContainer(node)) { michael@0: // The command execution function will take care of seeing if the michael@0: // selection is a folder or a different container type, and will michael@0: // load its contents in tabs. michael@0: PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onKeyPress: function CT_onKeyPress(aEvent) { michael@0: if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) michael@0: this.openSelectedNode(aEvent); michael@0: } michael@0: };