Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 7 | XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils", |
michael@0 | 8 | "resource:///modules/MigrationUtils.jsm"); |
michael@0 | 9 | XPCOMUtils.defineLazyModuleGetter(this, "Task", |
michael@0 | 10 | "resource://gre/modules/Task.jsm"); |
michael@0 | 11 | XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils", |
michael@0 | 12 | "resource://gre/modules/BookmarkJSONUtils.jsm"); |
michael@0 | 13 | XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups", |
michael@0 | 14 | "resource://gre/modules/PlacesBackups.jsm"); |
michael@0 | 15 | XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", |
michael@0 | 16 | "resource://gre/modules/DownloadUtils.jsm"); |
michael@0 | 17 | |
michael@0 | 18 | var PlacesOrganizer = { |
michael@0 | 19 | _places: null, |
michael@0 | 20 | |
michael@0 | 21 | // IDs of fields from editBookmarkOverlay that should be hidden when infoBox |
michael@0 | 22 | // is minimal. IDs should be kept in sync with the IDs of the elements |
michael@0 | 23 | // observing additionalInfoBroadcaster. |
michael@0 | 24 | _additionalInfoFields: [ |
michael@0 | 25 | "editBMPanel_descriptionRow", |
michael@0 | 26 | "editBMPanel_loadInSidebarCheckbox", |
michael@0 | 27 | "editBMPanel_keywordRow", |
michael@0 | 28 | ], |
michael@0 | 29 | |
michael@0 | 30 | _initFolderTree: function() { |
michael@0 | 31 | var leftPaneRoot = PlacesUIUtils.leftPaneFolderId; |
michael@0 | 32 | this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot; |
michael@0 | 33 | }, |
michael@0 | 34 | |
michael@0 | 35 | selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) { |
michael@0 | 36 | var itemId = PlacesUIUtils.leftPaneQueries[aQueryName]; |
michael@0 | 37 | this._places.selectItems([itemId]); |
michael@0 | 38 | // Forcefully expand all-bookmarks |
michael@0 | 39 | if (aQueryName == "AllBookmarks" || aQueryName == "History") |
michael@0 | 40 | PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; |
michael@0 | 41 | }, |
michael@0 | 42 | |
michael@0 | 43 | /** |
michael@0 | 44 | * Opens a given hierarchy in the left pane, stopping at the last reachable |
michael@0 | 45 | * container. |
michael@0 | 46 | * |
michael@0 | 47 | * @param aHierarchy A single container or an array of containers, sorted from |
michael@0 | 48 | * the outmost to the innermost in the hierarchy. Each |
michael@0 | 49 | * container may be either an item id, a Places URI string, |
michael@0 | 50 | * or a named query. |
michael@0 | 51 | * @see PlacesUIUtils.leftPaneQueries for supported named queries. |
michael@0 | 52 | */ |
michael@0 | 53 | selectLeftPaneContainerByHierarchy: |
michael@0 | 54 | function PO_selectLeftPaneContainerByHierarchy(aHierarchy) { |
michael@0 | 55 | if (!aHierarchy) |
michael@0 | 56 | throw new Error("Invalid containers hierarchy"); |
michael@0 | 57 | let hierarchy = [].concat(aHierarchy); |
michael@0 | 58 | let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed; |
michael@0 | 59 | if (!selectWasSuppressed) |
michael@0 | 60 | this._places.view.selection.selectEventsSuppressed = true; |
michael@0 | 61 | try { |
michael@0 | 62 | for (let container of hierarchy) { |
michael@0 | 63 | switch (typeof container) { |
michael@0 | 64 | case "number": |
michael@0 | 65 | this._places.selectItems([container], false); |
michael@0 | 66 | break; |
michael@0 | 67 | case "string": |
michael@0 | 68 | if (container.substr(0, 6) == "place:") |
michael@0 | 69 | this._places.selectPlaceURI(container); |
michael@0 | 70 | else if (container in PlacesUIUtils.leftPaneQueries) |
michael@0 | 71 | this.selectLeftPaneQuery(container); |
michael@0 | 72 | else |
michael@0 | 73 | throw new Error("Invalid container found: " + container); |
michael@0 | 74 | break; |
michael@0 | 75 | default: |
michael@0 | 76 | throw new Error("Invalid container type found: " + container); |
michael@0 | 77 | break; |
michael@0 | 78 | } |
michael@0 | 79 | PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; |
michael@0 | 80 | } |
michael@0 | 81 | } finally { |
michael@0 | 82 | if (!selectWasSuppressed) |
michael@0 | 83 | this._places.view.selection.selectEventsSuppressed = false; |
michael@0 | 84 | } |
michael@0 | 85 | }, |
michael@0 | 86 | |
michael@0 | 87 | init: function PO_init() { |
michael@0 | 88 | ContentArea.init(); |
michael@0 | 89 | |
michael@0 | 90 | this._places = document.getElementById("placesList"); |
michael@0 | 91 | this._initFolderTree(); |
michael@0 | 92 | |
michael@0 | 93 | var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks |
michael@0 | 94 | if (window.arguments && window.arguments[0]) |
michael@0 | 95 | leftPaneSelection = window.arguments[0]; |
michael@0 | 96 | |
michael@0 | 97 | this.selectLeftPaneContainerByHierarchy(leftPaneSelection); |
michael@0 | 98 | if (leftPaneSelection === "History") { |
michael@0 | 99 | let historyNode = this._places.selectedNode; |
michael@0 | 100 | if (historyNode.childCount > 0) |
michael@0 | 101 | this._places.selectNode(historyNode.getChild(0)); |
michael@0 | 102 | } |
michael@0 | 103 | |
michael@0 | 104 | // clear the back-stack |
michael@0 | 105 | this._backHistory.splice(0, this._backHistory.length); |
michael@0 | 106 | document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); |
michael@0 | 107 | |
michael@0 | 108 | // Set up the search UI. |
michael@0 | 109 | PlacesSearchBox.init(); |
michael@0 | 110 | |
michael@0 | 111 | window.addEventListener("AppCommand", this, true); |
michael@0 | 112 | #ifdef XP_MACOSX |
michael@0 | 113 | // 1. Map Edit->Find command to OrganizerCommand_find:all. Need to map |
michael@0 | 114 | // both the menuitem and the Find key. |
michael@0 | 115 | var findMenuItem = document.getElementById("menu_find"); |
michael@0 | 116 | findMenuItem.setAttribute("command", "OrganizerCommand_find:all"); |
michael@0 | 117 | var findKey = document.getElementById("key_find"); |
michael@0 | 118 | findKey.setAttribute("command", "OrganizerCommand_find:all"); |
michael@0 | 119 | |
michael@0 | 120 | // 2. Disable some keybindings from browser.xul |
michael@0 | 121 | var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"]; |
michael@0 | 122 | for (var i=0; i < elements.length; i++) { |
michael@0 | 123 | document.getElementById(elements[i]).setAttribute("disabled", "true"); |
michael@0 | 124 | } |
michael@0 | 125 | #endif |
michael@0 | 126 | |
michael@0 | 127 | // remove the "Properties" context-menu item, we've our own details pane |
michael@0 | 128 | document.getElementById("placesContext") |
michael@0 | 129 | .removeChild(document.getElementById("placesContext_show:info")); |
michael@0 | 130 | |
michael@0 | 131 | ContentArea.focus(); |
michael@0 | 132 | }, |
michael@0 | 133 | |
michael@0 | 134 | QueryInterface: function PO_QueryInterface(aIID) { |
michael@0 | 135 | if (aIID.equals(Components.interfaces.nsIDOMEventListener) || |
michael@0 | 136 | aIID.equals(Components.interfaces.nsISupports)) |
michael@0 | 137 | return this; |
michael@0 | 138 | |
michael@0 | 139 | throw Components.results.NS_NOINTERFACE; |
michael@0 | 140 | }, |
michael@0 | 141 | |
michael@0 | 142 | handleEvent: function PO_handleEvent(aEvent) { |
michael@0 | 143 | if (aEvent.type != "AppCommand") |
michael@0 | 144 | return; |
michael@0 | 145 | |
michael@0 | 146 | aEvent.stopPropagation(); |
michael@0 | 147 | switch (aEvent.command) { |
michael@0 | 148 | case "Back": |
michael@0 | 149 | if (this._backHistory.length > 0) |
michael@0 | 150 | this.back(); |
michael@0 | 151 | break; |
michael@0 | 152 | case "Forward": |
michael@0 | 153 | if (this._forwardHistory.length > 0) |
michael@0 | 154 | this.forward(); |
michael@0 | 155 | break; |
michael@0 | 156 | case "Search": |
michael@0 | 157 | PlacesSearchBox.findAll(); |
michael@0 | 158 | break; |
michael@0 | 159 | } |
michael@0 | 160 | }, |
michael@0 | 161 | |
michael@0 | 162 | destroy: function PO_destroy() { |
michael@0 | 163 | }, |
michael@0 | 164 | |
michael@0 | 165 | _location: null, |
michael@0 | 166 | get location() { |
michael@0 | 167 | return this._location; |
michael@0 | 168 | }, |
michael@0 | 169 | |
michael@0 | 170 | set location(aLocation) { |
michael@0 | 171 | if (!aLocation || this._location == aLocation) |
michael@0 | 172 | return aLocation; |
michael@0 | 173 | |
michael@0 | 174 | if (this.location) { |
michael@0 | 175 | this._backHistory.unshift(this.location); |
michael@0 | 176 | this._forwardHistory.splice(0, this._forwardHistory.length); |
michael@0 | 177 | } |
michael@0 | 178 | |
michael@0 | 179 | this._location = aLocation; |
michael@0 | 180 | this._places.selectPlaceURI(aLocation); |
michael@0 | 181 | |
michael@0 | 182 | if (!this._places.hasSelection) { |
michael@0 | 183 | // If no node was found for the given place: uri, just load it directly |
michael@0 | 184 | ContentArea.currentPlace = aLocation; |
michael@0 | 185 | } |
michael@0 | 186 | this.updateDetailsPane(); |
michael@0 | 187 | |
michael@0 | 188 | // update navigation commands |
michael@0 | 189 | if (this._backHistory.length == 0) |
michael@0 | 190 | document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true); |
michael@0 | 191 | else |
michael@0 | 192 | document.getElementById("OrganizerCommand:Back").removeAttribute("disabled"); |
michael@0 | 193 | if (this._forwardHistory.length == 0) |
michael@0 | 194 | document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true); |
michael@0 | 195 | else |
michael@0 | 196 | document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled"); |
michael@0 | 197 | |
michael@0 | 198 | return aLocation; |
michael@0 | 199 | }, |
michael@0 | 200 | |
michael@0 | 201 | _backHistory: [], |
michael@0 | 202 | _forwardHistory: [], |
michael@0 | 203 | |
michael@0 | 204 | back: function PO_back() { |
michael@0 | 205 | this._forwardHistory.unshift(this.location); |
michael@0 | 206 | var historyEntry = this._backHistory.shift(); |
michael@0 | 207 | this._location = null; |
michael@0 | 208 | this.location = historyEntry; |
michael@0 | 209 | }, |
michael@0 | 210 | forward: function PO_forward() { |
michael@0 | 211 | this._backHistory.unshift(this.location); |
michael@0 | 212 | var historyEntry = this._forwardHistory.shift(); |
michael@0 | 213 | this._location = null; |
michael@0 | 214 | this.location = historyEntry; |
michael@0 | 215 | }, |
michael@0 | 216 | |
michael@0 | 217 | /** |
michael@0 | 218 | * Called when a place folder is selected in the left pane. |
michael@0 | 219 | * @param resetSearchBox |
michael@0 | 220 | * true if the search box should also be reset, false otherwise. |
michael@0 | 221 | * The search box should be reset when a new folder in the left |
michael@0 | 222 | * pane is selected; the search scope and text need to be cleared in |
michael@0 | 223 | * preparation for the new folder. Note that if the user manually |
michael@0 | 224 | * resets the search box, either by clicking its reset button or by |
michael@0 | 225 | * deleting its text, this will be false. |
michael@0 | 226 | */ |
michael@0 | 227 | _cachedLeftPaneSelectedURI: null, |
michael@0 | 228 | onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) { |
michael@0 | 229 | // Don't change the right-hand pane contents when there's no selection. |
michael@0 | 230 | if (!this._places.hasSelection) |
michael@0 | 231 | return; |
michael@0 | 232 | |
michael@0 | 233 | var node = this._places.selectedNode; |
michael@0 | 234 | var queries = PlacesUtils.asQuery(node).getQueries(); |
michael@0 | 235 | |
michael@0 | 236 | // Items are only excluded on the left pane. |
michael@0 | 237 | var options = node.queryOptions.clone(); |
michael@0 | 238 | options.excludeItems = false; |
michael@0 | 239 | var placeURI = PlacesUtils.history.queriesToQueryString(queries, |
michael@0 | 240 | queries.length, |
michael@0 | 241 | options); |
michael@0 | 242 | |
michael@0 | 243 | // If either the place of the content tree in the right pane has changed or |
michael@0 | 244 | // the user cleared the search box, update the place, hide the search UI, |
michael@0 | 245 | // and update the back/forward buttons by setting location. |
michael@0 | 246 | if (ContentArea.currentPlace != placeURI || !resetSearchBox) { |
michael@0 | 247 | ContentArea.currentPlace = placeURI; |
michael@0 | 248 | this.location = node.uri; |
michael@0 | 249 | } |
michael@0 | 250 | |
michael@0 | 251 | // When we invalidate a container we use suppressSelectionEvent, when it is |
michael@0 | 252 | // unset a select event is fired, in many cases the selection did not really |
michael@0 | 253 | // change, so we should check for it, and return early in such a case. Note |
michael@0 | 254 | // that we cannot return any earlier than this point, because when |
michael@0 | 255 | // !resetSearchBox, we need to update location and hide the UI as above, |
michael@0 | 256 | // even though the selection has not changed. |
michael@0 | 257 | if (node.uri == this._cachedLeftPaneSelectedURI) |
michael@0 | 258 | return; |
michael@0 | 259 | this._cachedLeftPaneSelectedURI = node.uri; |
michael@0 | 260 | |
michael@0 | 261 | // At this point, resetSearchBox is true, because the left pane selection |
michael@0 | 262 | // has changed; otherwise we would have returned earlier. |
michael@0 | 263 | |
michael@0 | 264 | PlacesSearchBox.searchFilter.reset(); |
michael@0 | 265 | this._setSearchScopeForNode(node); |
michael@0 | 266 | this.updateDetailsPane(); |
michael@0 | 267 | }, |
michael@0 | 268 | |
michael@0 | 269 | /** |
michael@0 | 270 | * Sets the search scope based on aNode's properties. |
michael@0 | 271 | * @param aNode |
michael@0 | 272 | * the node to set up scope from |
michael@0 | 273 | */ |
michael@0 | 274 | _setSearchScopeForNode: function PO__setScopeForNode(aNode) { |
michael@0 | 275 | let itemId = aNode.itemId; |
michael@0 | 276 | |
michael@0 | 277 | if (PlacesUtils.nodeIsHistoryContainer(aNode) || |
michael@0 | 278 | itemId == PlacesUIUtils.leftPaneQueries["History"]) { |
michael@0 | 279 | PlacesQueryBuilder.setScope("history"); |
michael@0 | 280 | } |
michael@0 | 281 | else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) { |
michael@0 | 282 | PlacesQueryBuilder.setScope("downloads"); |
michael@0 | 283 | } |
michael@0 | 284 | else { |
michael@0 | 285 | // Default to All Bookmarks for all other nodes, per bug 469437. |
michael@0 | 286 | PlacesQueryBuilder.setScope("bookmarks"); |
michael@0 | 287 | } |
michael@0 | 288 | }, |
michael@0 | 289 | |
michael@0 | 290 | /** |
michael@0 | 291 | * Handle clicks on the places list. |
michael@0 | 292 | * Single Left click, right click or modified click do not result in any |
michael@0 | 293 | * special action, since they're related to selection. |
michael@0 | 294 | * @param aEvent |
michael@0 | 295 | * The mouse event. |
michael@0 | 296 | */ |
michael@0 | 297 | onPlacesListClick: function PO_onPlacesListClick(aEvent) { |
michael@0 | 298 | // Only handle clicks on tree children. |
michael@0 | 299 | if (aEvent.target.localName != "treechildren") |
michael@0 | 300 | return; |
michael@0 | 301 | |
michael@0 | 302 | let node = this._places.selectedNode; |
michael@0 | 303 | if (node) { |
michael@0 | 304 | let middleClick = aEvent.button == 1 && aEvent.detail == 1; |
michael@0 | 305 | if (middleClick && PlacesUtils.nodeIsContainer(node)) { |
michael@0 | 306 | // The command execution function will take care of seeing if the |
michael@0 | 307 | // selection is a folder or a different container type, and will |
michael@0 | 308 | // load its contents in tabs. |
michael@0 | 309 | PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places); |
michael@0 | 310 | } |
michael@0 | 311 | } |
michael@0 | 312 | }, |
michael@0 | 313 | |
michael@0 | 314 | /** |
michael@0 | 315 | * Handle focus changes on the places list and the current content view. |
michael@0 | 316 | */ |
michael@0 | 317 | updateDetailsPane: function PO_updateDetailsPane() { |
michael@0 | 318 | if (!ContentArea.currentViewOptions.showDetailsPane) |
michael@0 | 319 | return; |
michael@0 | 320 | let view = PlacesUIUtils.getViewForNode(document.activeElement); |
michael@0 | 321 | if (view) { |
michael@0 | 322 | let selectedNodes = view.selectedNode ? |
michael@0 | 323 | [view.selectedNode] : view.selectedNodes; |
michael@0 | 324 | this._fillDetailsPane(selectedNodes); |
michael@0 | 325 | } |
michael@0 | 326 | }, |
michael@0 | 327 | |
michael@0 | 328 | openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) { |
michael@0 | 329 | if (aContainer.itemId != -1) { |
michael@0 | 330 | PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true; |
michael@0 | 331 | this._places.selectItems([aContainer.itemId], false); |
michael@0 | 332 | } |
michael@0 | 333 | else if (PlacesUtils.nodeIsQuery(aContainer)) { |
michael@0 | 334 | this._places.selectPlaceURI(aContainer.uri); |
michael@0 | 335 | } |
michael@0 | 336 | }, |
michael@0 | 337 | |
michael@0 | 338 | /** |
michael@0 | 339 | * Returns the options associated with the query currently loaded in the |
michael@0 | 340 | * main places pane. |
michael@0 | 341 | */ |
michael@0 | 342 | getCurrentOptions: function PO_getCurrentOptions() { |
michael@0 | 343 | return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions; |
michael@0 | 344 | }, |
michael@0 | 345 | |
michael@0 | 346 | /** |
michael@0 | 347 | * Returns the queries associated with the query currently loaded in the |
michael@0 | 348 | * main places pane. |
michael@0 | 349 | */ |
michael@0 | 350 | getCurrentQueries: function PO_getCurrentQueries() { |
michael@0 | 351 | return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries(); |
michael@0 | 352 | }, |
michael@0 | 353 | |
michael@0 | 354 | /** |
michael@0 | 355 | * Show the migration wizard for importing passwords, |
michael@0 | 356 | * cookies, history, preferences, and bookmarks. |
michael@0 | 357 | */ |
michael@0 | 358 | importFromBrowser: function PO_importFromBrowser() { |
michael@0 | 359 | MigrationUtils.showMigrationWizard(window); |
michael@0 | 360 | }, |
michael@0 | 361 | |
michael@0 | 362 | /** |
michael@0 | 363 | * Open a file-picker and import the selected file into the bookmarks store |
michael@0 | 364 | */ |
michael@0 | 365 | importFromFile: function PO_importFromFile() { |
michael@0 | 366 | let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
michael@0 | 367 | let fpCallback = function fpCallback_done(aResult) { |
michael@0 | 368 | if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) { |
michael@0 | 369 | Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); |
michael@0 | 370 | BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false) |
michael@0 | 371 | .then(null, Components.utils.reportError); |
michael@0 | 372 | } |
michael@0 | 373 | }; |
michael@0 | 374 | |
michael@0 | 375 | fp.init(window, PlacesUIUtils.getString("SelectImport"), |
michael@0 | 376 | Ci.nsIFilePicker.modeOpen); |
michael@0 | 377 | fp.appendFilters(Ci.nsIFilePicker.filterHTML); |
michael@0 | 378 | fp.open(fpCallback); |
michael@0 | 379 | }, |
michael@0 | 380 | |
michael@0 | 381 | /** |
michael@0 | 382 | * Allows simple exporting of bookmarks. |
michael@0 | 383 | */ |
michael@0 | 384 | exportBookmarks: function PO_exportBookmarks() { |
michael@0 | 385 | let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
michael@0 | 386 | let fpCallback = function fpCallback_done(aResult) { |
michael@0 | 387 | if (aResult != Ci.nsIFilePicker.returnCancel) { |
michael@0 | 388 | Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); |
michael@0 | 389 | BookmarkHTMLUtils.exportToFile(fp.file.path) |
michael@0 | 390 | .then(null, Components.utils.reportError); |
michael@0 | 391 | } |
michael@0 | 392 | }; |
michael@0 | 393 | |
michael@0 | 394 | fp.init(window, PlacesUIUtils.getString("EnterExport"), |
michael@0 | 395 | Ci.nsIFilePicker.modeSave); |
michael@0 | 396 | fp.appendFilters(Ci.nsIFilePicker.filterHTML); |
michael@0 | 397 | fp.defaultString = "bookmarks.html"; |
michael@0 | 398 | fp.open(fpCallback); |
michael@0 | 399 | }, |
michael@0 | 400 | |
michael@0 | 401 | /** |
michael@0 | 402 | * Populates the restore menu with the dates of the backups available. |
michael@0 | 403 | */ |
michael@0 | 404 | populateRestoreMenu: function PO_populateRestoreMenu() { |
michael@0 | 405 | let restorePopup = document.getElementById("fileRestorePopup"); |
michael@0 | 406 | |
michael@0 | 407 | let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]. |
michael@0 | 408 | getService(Ci.nsIScriptableDateFormat); |
michael@0 | 409 | |
michael@0 | 410 | // Remove existing menu items. Last item is the restoreFromFile item. |
michael@0 | 411 | while (restorePopup.childNodes.length > 1) |
michael@0 | 412 | restorePopup.removeChild(restorePopup.firstChild); |
michael@0 | 413 | |
michael@0 | 414 | Task.spawn(function() { |
michael@0 | 415 | let backupFiles = yield PlacesBackups.getBackupFiles(); |
michael@0 | 416 | if (backupFiles.length == 0) |
michael@0 | 417 | return; |
michael@0 | 418 | |
michael@0 | 419 | // Populate menu with backups. |
michael@0 | 420 | for (let i = 0; i < backupFiles.length; i++) { |
michael@0 | 421 | let fileSize = (yield OS.File.stat(backupFiles[i])).size; |
michael@0 | 422 | let [size, unit] = DownloadUtils.convertByteUnits(fileSize); |
michael@0 | 423 | let sizeString = PlacesUtils.getFormattedString("backupFileSizeText", |
michael@0 | 424 | [size, unit]); |
michael@0 | 425 | let sizeInfo; |
michael@0 | 426 | let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]); |
michael@0 | 427 | if (bookmarkCount != null) { |
michael@0 | 428 | sizeInfo = " (" + sizeString + " - " + |
michael@0 | 429 | PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", |
michael@0 | 430 | bookmarkCount, |
michael@0 | 431 | [bookmarkCount]) + |
michael@0 | 432 | ")"; |
michael@0 | 433 | } else { |
michael@0 | 434 | sizeInfo = " (" + sizeString + ")"; |
michael@0 | 435 | } |
michael@0 | 436 | |
michael@0 | 437 | let backupDate = PlacesBackups.getDateForFile(backupFiles[i]); |
michael@0 | 438 | let m = restorePopup.insertBefore(document.createElement("menuitem"), |
michael@0 | 439 | document.getElementById("restoreFromFile")); |
michael@0 | 440 | m.setAttribute("label", |
michael@0 | 441 | dateSvc.FormatDate("", |
michael@0 | 442 | Ci.nsIScriptableDateFormat.dateFormatLong, |
michael@0 | 443 | backupDate.getFullYear(), |
michael@0 | 444 | backupDate.getMonth() + 1, |
michael@0 | 445 | backupDate.getDate()) + |
michael@0 | 446 | sizeInfo); |
michael@0 | 447 | m.setAttribute("value", OS.Path.basename(backupFiles[i])); |
michael@0 | 448 | m.setAttribute("oncommand", |
michael@0 | 449 | "PlacesOrganizer.onRestoreMenuItemClick(this);"); |
michael@0 | 450 | } |
michael@0 | 451 | |
michael@0 | 452 | // Add the restoreFromFile item. |
michael@0 | 453 | restorePopup.insertBefore(document.createElement("menuseparator"), |
michael@0 | 454 | document.getElementById("restoreFromFile")); |
michael@0 | 455 | }); |
michael@0 | 456 | }, |
michael@0 | 457 | |
michael@0 | 458 | /** |
michael@0 | 459 | * Called when a menuitem is selected from the restore menu. |
michael@0 | 460 | */ |
michael@0 | 461 | onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) { |
michael@0 | 462 | Task.spawn(function() { |
michael@0 | 463 | let backupName = aMenuItem.getAttribute("value"); |
michael@0 | 464 | let backupFilePaths = yield PlacesBackups.getBackupFiles(); |
michael@0 | 465 | for (let backupFilePath of backupFilePaths) { |
michael@0 | 466 | if (OS.Path.basename(backupFilePath) == backupName) { |
michael@0 | 467 | PlacesOrganizer.restoreBookmarksFromFile(backupFilePath); |
michael@0 | 468 | break; |
michael@0 | 469 | } |
michael@0 | 470 | } |
michael@0 | 471 | }); |
michael@0 | 472 | }, |
michael@0 | 473 | |
michael@0 | 474 | /** |
michael@0 | 475 | * Called when 'Choose File...' is selected from the restore menu. |
michael@0 | 476 | * Prompts for a file and restores bookmarks to those in the file. |
michael@0 | 477 | */ |
michael@0 | 478 | onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() { |
michael@0 | 479 | let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. |
michael@0 | 480 | getService(Ci.nsIProperties); |
michael@0 | 481 | let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); |
michael@0 | 482 | let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
michael@0 | 483 | let fpCallback = function fpCallback_done(aResult) { |
michael@0 | 484 | if (aResult != Ci.nsIFilePicker.returnCancel) { |
michael@0 | 485 | this.restoreBookmarksFromFile(fp.file.path); |
michael@0 | 486 | } |
michael@0 | 487 | }.bind(this); |
michael@0 | 488 | |
michael@0 | 489 | fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"), |
michael@0 | 490 | Ci.nsIFilePicker.modeOpen); |
michael@0 | 491 | fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), |
michael@0 | 492 | PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); |
michael@0 | 493 | fp.appendFilters(Ci.nsIFilePicker.filterAll); |
michael@0 | 494 | fp.displayDirectory = backupsDir; |
michael@0 | 495 | fp.open(fpCallback); |
michael@0 | 496 | }, |
michael@0 | 497 | |
michael@0 | 498 | /** |
michael@0 | 499 | * Restores bookmarks from a JSON file. |
michael@0 | 500 | */ |
michael@0 | 501 | restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) { |
michael@0 | 502 | // check file extension |
michael@0 | 503 | if (!aFilePath.endsWith("json")) { |
michael@0 | 504 | this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError")); |
michael@0 | 505 | return; |
michael@0 | 506 | } |
michael@0 | 507 | |
michael@0 | 508 | // confirm ok to delete existing bookmarks |
michael@0 | 509 | var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]. |
michael@0 | 510 | getService(Ci.nsIPromptService); |
michael@0 | 511 | if (!prompts.confirm(null, |
michael@0 | 512 | PlacesUIUtils.getString("bookmarksRestoreAlertTitle"), |
michael@0 | 513 | PlacesUIUtils.getString("bookmarksRestoreAlert"))) |
michael@0 | 514 | return; |
michael@0 | 515 | |
michael@0 | 516 | Task.spawn(function() { |
michael@0 | 517 | try { |
michael@0 | 518 | yield BookmarkJSONUtils.importFromFile(aFilePath, true); |
michael@0 | 519 | } catch(ex) { |
michael@0 | 520 | PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError")); |
michael@0 | 521 | } |
michael@0 | 522 | }); |
michael@0 | 523 | }, |
michael@0 | 524 | |
michael@0 | 525 | _showErrorAlert: function PO__showErrorAlert(aMsg) { |
michael@0 | 526 | var brandShortName = document.getElementById("brandStrings"). |
michael@0 | 527 | getString("brandShortName"); |
michael@0 | 528 | |
michael@0 | 529 | Cc["@mozilla.org/embedcomp/prompt-service;1"]. |
michael@0 | 530 | getService(Ci.nsIPromptService). |
michael@0 | 531 | alert(window, brandShortName, aMsg); |
michael@0 | 532 | }, |
michael@0 | 533 | |
michael@0 | 534 | /** |
michael@0 | 535 | * Backup bookmarks to desktop, auto-generate a filename with a date. |
michael@0 | 536 | * The file is a JSON serialization of bookmarks, tags and any annotations |
michael@0 | 537 | * of those items. |
michael@0 | 538 | */ |
michael@0 | 539 | backupBookmarks: function PO_backupBookmarks() { |
michael@0 | 540 | let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. |
michael@0 | 541 | getService(Ci.nsIProperties); |
michael@0 | 542 | let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile); |
michael@0 | 543 | let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); |
michael@0 | 544 | let fpCallback = function fpCallback_done(aResult) { |
michael@0 | 545 | if (aResult != Ci.nsIFilePicker.returnCancel) { |
michael@0 | 546 | // There is no OS.File version of the filepicker yet (Bug 937812). |
michael@0 | 547 | PlacesBackups.saveBookmarksToJSONFile(fp.file.path); |
michael@0 | 548 | } |
michael@0 | 549 | }; |
michael@0 | 550 | |
michael@0 | 551 | fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"), |
michael@0 | 552 | Ci.nsIFilePicker.modeSave); |
michael@0 | 553 | fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"), |
michael@0 | 554 | PlacesUIUtils.getString("bookmarksRestoreFilterExtension")); |
michael@0 | 555 | fp.defaultString = PlacesBackups.getFilenameForDate(); |
michael@0 | 556 | fp.displayDirectory = backupsDir; |
michael@0 | 557 | fp.open(fpCallback); |
michael@0 | 558 | }, |
michael@0 | 559 | |
michael@0 | 560 | _paneDisabled: false, |
michael@0 | 561 | _setDetailsFieldsDisabledState: |
michael@0 | 562 | function PO__setDetailsFieldsDisabledState(aDisabled) { |
michael@0 | 563 | if (aDisabled) { |
michael@0 | 564 | document.getElementById("paneElementsBroadcaster") |
michael@0 | 565 | .setAttribute("disabled", "true"); |
michael@0 | 566 | } |
michael@0 | 567 | else { |
michael@0 | 568 | document.getElementById("paneElementsBroadcaster") |
michael@0 | 569 | .removeAttribute("disabled"); |
michael@0 | 570 | } |
michael@0 | 571 | }, |
michael@0 | 572 | |
michael@0 | 573 | _detectAndSetDetailsPaneMinimalState: |
michael@0 | 574 | function PO__detectAndSetDetailsPaneMinimalState(aNode) { |
michael@0 | 575 | /** |
michael@0 | 576 | * The details of simple folder-items (as opposed to livemarks) or the |
michael@0 | 577 | * of livemark-children are not likely to fill the infoBox anyway, |
michael@0 | 578 | * thus we remove the "More/Less" button and show all details. |
michael@0 | 579 | * |
michael@0 | 580 | * the wasminimal attribute here is used to persist the "more/less" |
michael@0 | 581 | * state in a bookmark->folder->bookmark scenario. |
michael@0 | 582 | */ |
michael@0 | 583 | var infoBox = document.getElementById("infoBox"); |
michael@0 | 584 | var infoBoxExpander = document.getElementById("infoBoxExpander"); |
michael@0 | 585 | var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper"); |
michael@0 | 586 | var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); |
michael@0 | 587 | |
michael@0 | 588 | if (!aNode) { |
michael@0 | 589 | infoBoxExpanderWrapper.hidden = true; |
michael@0 | 590 | return; |
michael@0 | 591 | } |
michael@0 | 592 | if (aNode.itemId != -1 && |
michael@0 | 593 | PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) { |
michael@0 | 594 | if (infoBox.getAttribute("minimal") == "true") |
michael@0 | 595 | infoBox.setAttribute("wasminimal", "true"); |
michael@0 | 596 | infoBox.removeAttribute("minimal"); |
michael@0 | 597 | infoBoxExpanderWrapper.hidden = true; |
michael@0 | 598 | } |
michael@0 | 599 | else { |
michael@0 | 600 | if (infoBox.getAttribute("wasminimal") == "true") |
michael@0 | 601 | infoBox.setAttribute("minimal", "true"); |
michael@0 | 602 | infoBox.removeAttribute("wasminimal"); |
michael@0 | 603 | infoBoxExpanderWrapper.hidden = |
michael@0 | 604 | this._additionalInfoFields.every(function (id) |
michael@0 | 605 | document.getElementById(id).collapsed); |
michael@0 | 606 | } |
michael@0 | 607 | additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true"; |
michael@0 | 608 | }, |
michael@0 | 609 | |
michael@0 | 610 | // NOT YET USED |
michael@0 | 611 | updateThumbnailProportions: function PO_updateThumbnailProportions() { |
michael@0 | 612 | var previewBox = document.getElementById("previewBox"); |
michael@0 | 613 | var canvas = document.getElementById("itemThumbnail"); |
michael@0 | 614 | var height = previewBox.boxObject.height; |
michael@0 | 615 | var width = height * (screen.width / screen.height); |
michael@0 | 616 | canvas.width = width; |
michael@0 | 617 | canvas.height = height; |
michael@0 | 618 | }, |
michael@0 | 619 | |
michael@0 | 620 | _fillDetailsPane: function PO__fillDetailsPane(aNodeList) { |
michael@0 | 621 | var infoBox = document.getElementById("infoBox"); |
michael@0 | 622 | var detailsDeck = document.getElementById("detailsDeck"); |
michael@0 | 623 | |
michael@0 | 624 | // Make sure the infoBox UI is visible if we need to use it, we hide it |
michael@0 | 625 | // below when we don't. |
michael@0 | 626 | infoBox.hidden = false; |
michael@0 | 627 | var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null; |
michael@0 | 628 | // If a textbox within a panel is focused, force-blur it so its contents |
michael@0 | 629 | // are saved |
michael@0 | 630 | if (gEditItemOverlay.itemId != -1) { |
michael@0 | 631 | var focusedElement = document.commandDispatcher.focusedElement; |
michael@0 | 632 | if ((focusedElement instanceof HTMLInputElement || |
michael@0 | 633 | focusedElement instanceof HTMLTextAreaElement) && |
michael@0 | 634 | /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id)) |
michael@0 | 635 | focusedElement.blur(); |
michael@0 | 636 | |
michael@0 | 637 | // don't update the panel if we are already editing this node unless we're |
michael@0 | 638 | // in multi-edit mode |
michael@0 | 639 | if (aSelectedNode) { |
michael@0 | 640 | var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); |
michael@0 | 641 | var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId || |
michael@0 | 642 | gEditItemOverlay.itemId == concreteId || |
michael@0 | 643 | (aSelectedNode.itemId == -1 && gEditItemOverlay.uri && |
michael@0 | 644 | gEditItemOverlay.uri == aSelectedNode.uri); |
michael@0 | 645 | if (nodeIsSame && detailsDeck.selectedIndex == 1 && |
michael@0 | 646 | !gEditItemOverlay.multiEdit) |
michael@0 | 647 | return; |
michael@0 | 648 | } |
michael@0 | 649 | } |
michael@0 | 650 | |
michael@0 | 651 | // Clean up the panel before initing it again. |
michael@0 | 652 | gEditItemOverlay.uninitPanel(false); |
michael@0 | 653 | |
michael@0 | 654 | if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) { |
michael@0 | 655 | detailsDeck.selectedIndex = 1; |
michael@0 | 656 | // Using the concrete itemId is arguably wrong. The bookmarks API |
michael@0 | 657 | // does allow setting properties for folder shortcuts as well, but since |
michael@0 | 658 | // the UI does not distinct between the couple, we better just show |
michael@0 | 659 | // the concrete item properties for shortcuts to root nodes. |
michael@0 | 660 | var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode); |
michael@0 | 661 | var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId); |
michael@0 | 662 | var readOnly = isRootItem || |
michael@0 | 663 | aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId; |
michael@0 | 664 | var useConcreteId = isRootItem || |
michael@0 | 665 | PlacesUtils.nodeIsTagQuery(aSelectedNode); |
michael@0 | 666 | var itemId = -1; |
michael@0 | 667 | if (concreteId != -1 && useConcreteId) |
michael@0 | 668 | itemId = concreteId; |
michael@0 | 669 | else if (aSelectedNode.itemId != -1) |
michael@0 | 670 | itemId = aSelectedNode.itemId; |
michael@0 | 671 | else |
michael@0 | 672 | itemId = PlacesUtils._uri(aSelectedNode.uri); |
michael@0 | 673 | |
michael@0 | 674 | gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"] |
michael@0 | 675 | , forceReadOnly: readOnly |
michael@0 | 676 | , titleOverride: aSelectedNode.title |
michael@0 | 677 | }); |
michael@0 | 678 | |
michael@0 | 679 | // Dynamically generated queries, like history date containers, have |
michael@0 | 680 | // itemId !=0 and do not exist in history. For them the panel is |
michael@0 | 681 | // read-only, but empty, since it can't get a valid title for the object. |
michael@0 | 682 | // In such a case we force the title using the selectedNode one, for UI |
michael@0 | 683 | // polishness. |
michael@0 | 684 | if (aSelectedNode.itemId == -1 && |
michael@0 | 685 | (PlacesUtils.nodeIsDay(aSelectedNode) || |
michael@0 | 686 | PlacesUtils.nodeIsHost(aSelectedNode))) |
michael@0 | 687 | gEditItemOverlay._element("namePicker").value = aSelectedNode.title; |
michael@0 | 688 | |
michael@0 | 689 | this._detectAndSetDetailsPaneMinimalState(aSelectedNode); |
michael@0 | 690 | } |
michael@0 | 691 | else if (!aSelectedNode && aNodeList[0]) { |
michael@0 | 692 | var itemIds = []; |
michael@0 | 693 | for (var i = 0; i < aNodeList.length; i++) { |
michael@0 | 694 | if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) && |
michael@0 | 695 | !PlacesUtils.nodeIsURI(aNodeList[i])) { |
michael@0 | 696 | detailsDeck.selectedIndex = 0; |
michael@0 | 697 | var selectItemDesc = document.getElementById("selectItemDescription"); |
michael@0 | 698 | var itemsCountLabel = document.getElementById("itemsCountText"); |
michael@0 | 699 | selectItemDesc.hidden = false; |
michael@0 | 700 | itemsCountLabel.value = |
michael@0 | 701 | PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", |
michael@0 | 702 | aNodeList.length, [aNodeList.length]); |
michael@0 | 703 | infoBox.hidden = true; |
michael@0 | 704 | return; |
michael@0 | 705 | } |
michael@0 | 706 | itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId : |
michael@0 | 707 | PlacesUtils._uri(aNodeList[i].uri); |
michael@0 | 708 | } |
michael@0 | 709 | detailsDeck.selectedIndex = 1; |
michael@0 | 710 | gEditItemOverlay.initPanel(itemIds, |
michael@0 | 711 | { hiddenRows: ["folderPicker", |
michael@0 | 712 | "loadInSidebar", |
michael@0 | 713 | "location", |
michael@0 | 714 | "keyword", |
michael@0 | 715 | "description", |
michael@0 | 716 | "name"]}); |
michael@0 | 717 | this._detectAndSetDetailsPaneMinimalState(aSelectedNode); |
michael@0 | 718 | } |
michael@0 | 719 | else { |
michael@0 | 720 | detailsDeck.selectedIndex = 0; |
michael@0 | 721 | infoBox.hidden = true; |
michael@0 | 722 | let selectItemDesc = document.getElementById("selectItemDescription"); |
michael@0 | 723 | let itemsCountLabel = document.getElementById("itemsCountText"); |
michael@0 | 724 | let itemsCount = 0; |
michael@0 | 725 | if (ContentArea.currentView.result) { |
michael@0 | 726 | let rootNode = ContentArea.currentView.result.root; |
michael@0 | 727 | if (rootNode.containerOpen) |
michael@0 | 728 | itemsCount = rootNode.childCount; |
michael@0 | 729 | } |
michael@0 | 730 | if (itemsCount == 0) { |
michael@0 | 731 | selectItemDesc.hidden = true; |
michael@0 | 732 | itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems"); |
michael@0 | 733 | } |
michael@0 | 734 | else { |
michael@0 | 735 | selectItemDesc.hidden = false; |
michael@0 | 736 | itemsCountLabel.value = |
michael@0 | 737 | PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel", |
michael@0 | 738 | itemsCount, [itemsCount]); |
michael@0 | 739 | } |
michael@0 | 740 | } |
michael@0 | 741 | }, |
michael@0 | 742 | |
michael@0 | 743 | // NOT YET USED |
michael@0 | 744 | _updateThumbnail: function PO__updateThumbnail() { |
michael@0 | 745 | var bo = document.getElementById("previewBox").boxObject; |
michael@0 | 746 | var width = bo.width; |
michael@0 | 747 | var height = bo.height; |
michael@0 | 748 | |
michael@0 | 749 | var canvas = document.getElementById("itemThumbnail"); |
michael@0 | 750 | var ctx = canvas.getContext('2d'); |
michael@0 | 751 | var notAvailableText = canvas.getAttribute("notavailabletext"); |
michael@0 | 752 | ctx.save(); |
michael@0 | 753 | ctx.fillStyle = "-moz-Dialog"; |
michael@0 | 754 | ctx.fillRect(0, 0, width, height); |
michael@0 | 755 | ctx.translate(width/2, height/2); |
michael@0 | 756 | |
michael@0 | 757 | ctx.fillStyle = "GrayText"; |
michael@0 | 758 | ctx.mozTextStyle = "12pt sans serif"; |
michael@0 | 759 | var len = ctx.mozMeasureText(notAvailableText); |
michael@0 | 760 | ctx.translate(-len/2,0); |
michael@0 | 761 | ctx.mozDrawText(notAvailableText); |
michael@0 | 762 | ctx.restore(); |
michael@0 | 763 | }, |
michael@0 | 764 | |
michael@0 | 765 | toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() { |
michael@0 | 766 | var infoBox = document.getElementById("infoBox"); |
michael@0 | 767 | var infoBoxExpander = document.getElementById("infoBoxExpander"); |
michael@0 | 768 | var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel"); |
michael@0 | 769 | var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster"); |
michael@0 | 770 | |
michael@0 | 771 | if (infoBox.getAttribute("minimal") == "true") { |
michael@0 | 772 | infoBox.removeAttribute("minimal"); |
michael@0 | 773 | infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel"); |
michael@0 | 774 | infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey"); |
michael@0 | 775 | infoBoxExpander.className = "expander-up"; |
michael@0 | 776 | additionalInfoBroadcaster.removeAttribute("hidden"); |
michael@0 | 777 | } |
michael@0 | 778 | else { |
michael@0 | 779 | infoBox.setAttribute("minimal", "true"); |
michael@0 | 780 | infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel"); |
michael@0 | 781 | infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey"); |
michael@0 | 782 | infoBoxExpander.className = "expander-down"; |
michael@0 | 783 | additionalInfoBroadcaster.setAttribute("hidden", "true"); |
michael@0 | 784 | } |
michael@0 | 785 | }, |
michael@0 | 786 | }; |
michael@0 | 787 | |
michael@0 | 788 | /** |
michael@0 | 789 | * A set of utilities relating to search within Bookmarks and History. |
michael@0 | 790 | */ |
michael@0 | 791 | var PlacesSearchBox = { |
michael@0 | 792 | |
michael@0 | 793 | /** |
michael@0 | 794 | * The Search text field |
michael@0 | 795 | */ |
michael@0 | 796 | get searchFilter() { |
michael@0 | 797 | return document.getElementById("searchFilter"); |
michael@0 | 798 | }, |
michael@0 | 799 | |
michael@0 | 800 | /** |
michael@0 | 801 | * Folders to include when searching. |
michael@0 | 802 | */ |
michael@0 | 803 | _folders: [], |
michael@0 | 804 | get folders() { |
michael@0 | 805 | if (this._folders.length == 0) { |
michael@0 | 806 | this._folders.push(PlacesUtils.bookmarksMenuFolderId, |
michael@0 | 807 | PlacesUtils.unfiledBookmarksFolderId, |
michael@0 | 808 | PlacesUtils.toolbarFolderId); |
michael@0 | 809 | } |
michael@0 | 810 | return this._folders; |
michael@0 | 811 | }, |
michael@0 | 812 | set folders(aFolders) { |
michael@0 | 813 | this._folders = aFolders; |
michael@0 | 814 | return aFolders; |
michael@0 | 815 | }, |
michael@0 | 816 | |
michael@0 | 817 | /** |
michael@0 | 818 | * Run a search for the specified text, over the collection specified by |
michael@0 | 819 | * the dropdown arrow. The default is all bookmarks, but can be |
michael@0 | 820 | * localized to the active collection. |
michael@0 | 821 | * @param filterString |
michael@0 | 822 | * The text to search for. |
michael@0 | 823 | */ |
michael@0 | 824 | search: function PSB_search(filterString) { |
michael@0 | 825 | var PO = PlacesOrganizer; |
michael@0 | 826 | // If the user empties the search box manually, reset it and load all |
michael@0 | 827 | // contents of the current scope. |
michael@0 | 828 | // XXX this might be to jumpy, maybe should search for "", so results |
michael@0 | 829 | // are ungrouped, and search box not reset |
michael@0 | 830 | if (filterString == "") { |
michael@0 | 831 | PO.onPlaceSelected(false); |
michael@0 | 832 | return; |
michael@0 | 833 | } |
michael@0 | 834 | |
michael@0 | 835 | let currentView = ContentArea.currentView; |
michael@0 | 836 | let currentOptions = PO.getCurrentOptions(); |
michael@0 | 837 | |
michael@0 | 838 | // Search according to the current scope, which was set by |
michael@0 | 839 | // PQB_setScope() |
michael@0 | 840 | switch (PlacesSearchBox.filterCollection) { |
michael@0 | 841 | case "bookmarks": |
michael@0 | 842 | currentView.applyFilter(filterString, this.folders); |
michael@0 | 843 | break; |
michael@0 | 844 | case "history": |
michael@0 | 845 | if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) { |
michael@0 | 846 | var query = PlacesUtils.history.getNewQuery(); |
michael@0 | 847 | query.searchTerms = filterString; |
michael@0 | 848 | var options = currentOptions.clone(); |
michael@0 | 849 | // Make sure we're getting uri results. |
michael@0 | 850 | options.resultType = currentOptions.RESULTS_AS_URI; |
michael@0 | 851 | options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; |
michael@0 | 852 | options.includeHidden = true; |
michael@0 | 853 | currentView.load([query], options); |
michael@0 | 854 | } |
michael@0 | 855 | else { |
michael@0 | 856 | currentView.applyFilter(filterString, null, true); |
michael@0 | 857 | } |
michael@0 | 858 | break; |
michael@0 | 859 | case "downloads": |
michael@0 | 860 | if (currentView == ContentTree.view) { |
michael@0 | 861 | let query = PlacesUtils.history.getNewQuery(); |
michael@0 | 862 | query.searchTerms = filterString; |
michael@0 | 863 | query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1); |
michael@0 | 864 | let options = currentOptions.clone(); |
michael@0 | 865 | // Make sure we're getting uri results. |
michael@0 | 866 | options.resultType = currentOptions.RESULTS_AS_URI; |
michael@0 | 867 | options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; |
michael@0 | 868 | options.includeHidden = true; |
michael@0 | 869 | currentView.load([query], options); |
michael@0 | 870 | } |
michael@0 | 871 | else { |
michael@0 | 872 | // The new downloads view doesn't use places for searching downloads. |
michael@0 | 873 | currentView.searchTerm = filterString; |
michael@0 | 874 | } |
michael@0 | 875 | break; |
michael@0 | 876 | default: |
michael@0 | 877 | throw "Invalid filterCollection on search"; |
michael@0 | 878 | } |
michael@0 | 879 | |
michael@0 | 880 | // Update the details panel |
michael@0 | 881 | PlacesOrganizer.updateDetailsPane(); |
michael@0 | 882 | }, |
michael@0 | 883 | |
michael@0 | 884 | /** |
michael@0 | 885 | * Finds across all history, downloads or all bookmarks. |
michael@0 | 886 | */ |
michael@0 | 887 | findAll: function PSB_findAll() { |
michael@0 | 888 | switch (this.filterCollection) { |
michael@0 | 889 | case "history": |
michael@0 | 890 | PlacesQueryBuilder.setScope("history"); |
michael@0 | 891 | break; |
michael@0 | 892 | case "downloads": |
michael@0 | 893 | PlacesQueryBuilder.setScope("downloads"); |
michael@0 | 894 | break; |
michael@0 | 895 | default: |
michael@0 | 896 | PlacesQueryBuilder.setScope("bookmarks"); |
michael@0 | 897 | break; |
michael@0 | 898 | } |
michael@0 | 899 | this.focus(); |
michael@0 | 900 | }, |
michael@0 | 901 | |
michael@0 | 902 | /** |
michael@0 | 903 | * Updates the display with the title of the current collection. |
michael@0 | 904 | * @param aTitle |
michael@0 | 905 | * The title of the current collection. |
michael@0 | 906 | */ |
michael@0 | 907 | updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) { |
michael@0 | 908 | let title = ""; |
michael@0 | 909 | switch (this.filterCollection) { |
michael@0 | 910 | case "history": |
michael@0 | 911 | title = PlacesUIUtils.getString("searchHistory"); |
michael@0 | 912 | break; |
michael@0 | 913 | case "downloads": |
michael@0 | 914 | title = PlacesUIUtils.getString("searchDownloads"); |
michael@0 | 915 | break; |
michael@0 | 916 | default: |
michael@0 | 917 | title = PlacesUIUtils.getString("searchBookmarks"); |
michael@0 | 918 | } |
michael@0 | 919 | this.searchFilter.placeholder = title; |
michael@0 | 920 | }, |
michael@0 | 921 | |
michael@0 | 922 | /** |
michael@0 | 923 | * Gets/sets the active collection from the dropdown menu. |
michael@0 | 924 | */ |
michael@0 | 925 | get filterCollection() { |
michael@0 | 926 | return this.searchFilter.getAttribute("collection"); |
michael@0 | 927 | }, |
michael@0 | 928 | set filterCollection(collectionName) { |
michael@0 | 929 | if (collectionName == this.filterCollection) |
michael@0 | 930 | return collectionName; |
michael@0 | 931 | |
michael@0 | 932 | this.searchFilter.setAttribute("collection", collectionName); |
michael@0 | 933 | this.updateCollectionTitle(); |
michael@0 | 934 | |
michael@0 | 935 | return collectionName; |
michael@0 | 936 | }, |
michael@0 | 937 | |
michael@0 | 938 | /** |
michael@0 | 939 | * Focus the search box |
michael@0 | 940 | */ |
michael@0 | 941 | focus: function PSB_focus() { |
michael@0 | 942 | this.searchFilter.focus(); |
michael@0 | 943 | }, |
michael@0 | 944 | |
michael@0 | 945 | /** |
michael@0 | 946 | * Set up the gray text in the search bar as the Places View loads. |
michael@0 | 947 | */ |
michael@0 | 948 | init: function PSB_init() { |
michael@0 | 949 | this.updateCollectionTitle(); |
michael@0 | 950 | }, |
michael@0 | 951 | |
michael@0 | 952 | /** |
michael@0 | 953 | * Gets or sets the text shown in the Places Search Box |
michael@0 | 954 | */ |
michael@0 | 955 | get value() { |
michael@0 | 956 | return this.searchFilter.value; |
michael@0 | 957 | }, |
michael@0 | 958 | set value(value) { |
michael@0 | 959 | return this.searchFilter.value = value; |
michael@0 | 960 | }, |
michael@0 | 961 | }; |
michael@0 | 962 | |
michael@0 | 963 | /** |
michael@0 | 964 | * Functions and data for advanced query builder |
michael@0 | 965 | */ |
michael@0 | 966 | var PlacesQueryBuilder = { |
michael@0 | 967 | |
michael@0 | 968 | queries: [], |
michael@0 | 969 | queryOptions: null, |
michael@0 | 970 | |
michael@0 | 971 | /** |
michael@0 | 972 | * Sets the search scope. This can be called when no search is active, and |
michael@0 | 973 | * in that case, when the user does begin a search aScope will be used (see |
michael@0 | 974 | * PSB_search()). If there is an active search, it's performed again to |
michael@0 | 975 | * update the content tree. |
michael@0 | 976 | * @param aScope |
michael@0 | 977 | * The search scope: "bookmarks", "collection", "downloads" or |
michael@0 | 978 | * "history". |
michael@0 | 979 | */ |
michael@0 | 980 | setScope: function PQB_setScope(aScope) { |
michael@0 | 981 | // Determine filterCollection, folders, and scopeButtonId based on aScope. |
michael@0 | 982 | var filterCollection; |
michael@0 | 983 | var folders = []; |
michael@0 | 984 | switch (aScope) { |
michael@0 | 985 | case "history": |
michael@0 | 986 | filterCollection = "history"; |
michael@0 | 987 | break; |
michael@0 | 988 | case "bookmarks": |
michael@0 | 989 | filterCollection = "bookmarks"; |
michael@0 | 990 | folders.push(PlacesUtils.bookmarksMenuFolderId, |
michael@0 | 991 | PlacesUtils.toolbarFolderId, |
michael@0 | 992 | PlacesUtils.unfiledBookmarksFolderId); |
michael@0 | 993 | break; |
michael@0 | 994 | case "downloads": |
michael@0 | 995 | filterCollection = "downloads"; |
michael@0 | 996 | break; |
michael@0 | 997 | default: |
michael@0 | 998 | throw "Invalid search scope"; |
michael@0 | 999 | break; |
michael@0 | 1000 | } |
michael@0 | 1001 | |
michael@0 | 1002 | // Update the search box. Re-search if there's an active search. |
michael@0 | 1003 | PlacesSearchBox.filterCollection = filterCollection; |
michael@0 | 1004 | PlacesSearchBox.folders = folders; |
michael@0 | 1005 | var searchStr = PlacesSearchBox.searchFilter.value; |
michael@0 | 1006 | if (searchStr) |
michael@0 | 1007 | PlacesSearchBox.search(searchStr); |
michael@0 | 1008 | } |
michael@0 | 1009 | }; |
michael@0 | 1010 | |
michael@0 | 1011 | /** |
michael@0 | 1012 | * Population and commands for the View Menu. |
michael@0 | 1013 | */ |
michael@0 | 1014 | var ViewMenu = { |
michael@0 | 1015 | /** |
michael@0 | 1016 | * Removes content generated previously from a menupopup. |
michael@0 | 1017 | * @param popup |
michael@0 | 1018 | * The popup that contains the previously generated content. |
michael@0 | 1019 | * @param startID |
michael@0 | 1020 | * The id attribute of an element that is the start of the |
michael@0 | 1021 | * dynamically generated region - remove elements after this |
michael@0 | 1022 | * item only. |
michael@0 | 1023 | * Must be contained by popup. Can be null (in which case the |
michael@0 | 1024 | * contents of popup are removed). |
michael@0 | 1025 | * @param endID |
michael@0 | 1026 | * The id attribute of an element that is the end of the |
michael@0 | 1027 | * dynamically generated region - remove elements up to this |
michael@0 | 1028 | * item only. |
michael@0 | 1029 | * Must be contained by popup. Can be null (in which case all |
michael@0 | 1030 | * items until the end of the popup will be removed). Ignored |
michael@0 | 1031 | * if startID is null. |
michael@0 | 1032 | * @returns The element for the caller to insert new items before, |
michael@0 | 1033 | * null if the caller should just append to the popup. |
michael@0 | 1034 | */ |
michael@0 | 1035 | _clean: function VM__clean(popup, startID, endID) { |
michael@0 | 1036 | if (endID) |
michael@0 | 1037 | NS_ASSERT(startID, "meaningless to have valid endID and null startID"); |
michael@0 | 1038 | if (startID) { |
michael@0 | 1039 | var startElement = document.getElementById(startID); |
michael@0 | 1040 | NS_ASSERT(startElement.parentNode == |
michael@0 | 1041 | popup, "startElement is not in popup"); |
michael@0 | 1042 | NS_ASSERT(startElement, |
michael@0 | 1043 | "startID does not correspond to an existing element"); |
michael@0 | 1044 | var endElement = null; |
michael@0 | 1045 | if (endID) { |
michael@0 | 1046 | endElement = document.getElementById(endID); |
michael@0 | 1047 | NS_ASSERT(endElement.parentNode == popup, |
michael@0 | 1048 | "endElement is not in popup"); |
michael@0 | 1049 | NS_ASSERT(endElement, |
michael@0 | 1050 | "endID does not correspond to an existing element"); |
michael@0 | 1051 | } |
michael@0 | 1052 | while (startElement.nextSibling != endElement) |
michael@0 | 1053 | popup.removeChild(startElement.nextSibling); |
michael@0 | 1054 | return endElement; |
michael@0 | 1055 | } |
michael@0 | 1056 | else { |
michael@0 | 1057 | while(popup.hasChildNodes()) |
michael@0 | 1058 | popup.removeChild(popup.firstChild); |
michael@0 | 1059 | } |
michael@0 | 1060 | return null; |
michael@0 | 1061 | }, |
michael@0 | 1062 | |
michael@0 | 1063 | /** |
michael@0 | 1064 | * Fills a menupopup with a list of columns |
michael@0 | 1065 | * @param event |
michael@0 | 1066 | * The popupshowing event that invoked this function. |
michael@0 | 1067 | * @param startID |
michael@0 | 1068 | * see _clean |
michael@0 | 1069 | * @param endID |
michael@0 | 1070 | * see _clean |
michael@0 | 1071 | * @param type |
michael@0 | 1072 | * the type of the menuitem, e.g. "radio" or "checkbox". |
michael@0 | 1073 | * Can be null (no-type). |
michael@0 | 1074 | * Checkboxes are checked if the column is visible. |
michael@0 | 1075 | * @param propertyPrefix |
michael@0 | 1076 | * If propertyPrefix is non-null: |
michael@0 | 1077 | * propertyPrefix + column ID + ".label" will be used to get the |
michael@0 | 1078 | * localized label string. |
michael@0 | 1079 | * propertyPrefix + column ID + ".accesskey" will be used to get the |
michael@0 | 1080 | * localized accesskey. |
michael@0 | 1081 | * If propertyPrefix is null, the column label is used as label and |
michael@0 | 1082 | * no accesskey is assigned. |
michael@0 | 1083 | */ |
michael@0 | 1084 | fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) { |
michael@0 | 1085 | var popup = event.target; |
michael@0 | 1086 | var pivot = this._clean(popup, startID, endID); |
michael@0 | 1087 | |
michael@0 | 1088 | // If no column is "sort-active", the "Unsorted" item needs to be checked, |
michael@0 | 1089 | // so track whether or not we find a column that is sort-active. |
michael@0 | 1090 | var isSorted = false; |
michael@0 | 1091 | var content = document.getElementById("placeContent"); |
michael@0 | 1092 | var columns = content.columns; |
michael@0 | 1093 | for (var i = 0; i < columns.count; ++i) { |
michael@0 | 1094 | var column = columns.getColumnAt(i).element; |
michael@0 | 1095 | var menuitem = document.createElement("menuitem"); |
michael@0 | 1096 | menuitem.id = "menucol_" + column.id; |
michael@0 | 1097 | menuitem.column = column; |
michael@0 | 1098 | var label = column.getAttribute("label"); |
michael@0 | 1099 | if (propertyPrefix) { |
michael@0 | 1100 | var menuitemPrefix = propertyPrefix; |
michael@0 | 1101 | // for string properties, use "name" as the id, instead of "title" |
michael@0 | 1102 | // see bug #386287 for details |
michael@0 | 1103 | var columnId = column.getAttribute("anonid"); |
michael@0 | 1104 | menuitemPrefix += columnId == "title" ? "name" : columnId; |
michael@0 | 1105 | label = PlacesUIUtils.getString(menuitemPrefix + ".label"); |
michael@0 | 1106 | var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey"); |
michael@0 | 1107 | menuitem.setAttribute("accesskey", accesskey); |
michael@0 | 1108 | } |
michael@0 | 1109 | menuitem.setAttribute("label", label); |
michael@0 | 1110 | if (type == "radio") { |
michael@0 | 1111 | menuitem.setAttribute("type", "radio"); |
michael@0 | 1112 | menuitem.setAttribute("name", "columns"); |
michael@0 | 1113 | // This column is the sort key. Its item is checked. |
michael@0 | 1114 | if (column.getAttribute("sortDirection") != "") { |
michael@0 | 1115 | menuitem.setAttribute("checked", "true"); |
michael@0 | 1116 | isSorted = true; |
michael@0 | 1117 | } |
michael@0 | 1118 | } |
michael@0 | 1119 | else if (type == "checkbox") { |
michael@0 | 1120 | menuitem.setAttribute("type", "checkbox"); |
michael@0 | 1121 | // Cannot uncheck the primary column. |
michael@0 | 1122 | if (column.getAttribute("primary") == "true") |
michael@0 | 1123 | menuitem.setAttribute("disabled", "true"); |
michael@0 | 1124 | // Items for visible columns are checked. |
michael@0 | 1125 | if (!column.hidden) |
michael@0 | 1126 | menuitem.setAttribute("checked", "true"); |
michael@0 | 1127 | } |
michael@0 | 1128 | if (pivot) |
michael@0 | 1129 | popup.insertBefore(menuitem, pivot); |
michael@0 | 1130 | else |
michael@0 | 1131 | popup.appendChild(menuitem); |
michael@0 | 1132 | } |
michael@0 | 1133 | event.stopPropagation(); |
michael@0 | 1134 | }, |
michael@0 | 1135 | |
michael@0 | 1136 | /** |
michael@0 | 1137 | * Set up the content of the view menu. |
michael@0 | 1138 | */ |
michael@0 | 1139 | populateSortMenu: function VM_populateSortMenu(event) { |
michael@0 | 1140 | this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1."); |
michael@0 | 1141 | |
michael@0 | 1142 | var sortColumn = this._getSortColumn(); |
michael@0 | 1143 | var viewSortAscending = document.getElementById("viewSortAscending"); |
michael@0 | 1144 | var viewSortDescending = document.getElementById("viewSortDescending"); |
michael@0 | 1145 | // We need to remove an existing checked attribute because the unsorted |
michael@0 | 1146 | // menu item is not rebuilt every time we open the menu like the others. |
michael@0 | 1147 | var viewUnsorted = document.getElementById("viewUnsorted"); |
michael@0 | 1148 | if (!sortColumn) { |
michael@0 | 1149 | viewSortAscending.removeAttribute("checked"); |
michael@0 | 1150 | viewSortDescending.removeAttribute("checked"); |
michael@0 | 1151 | viewUnsorted.setAttribute("checked", "true"); |
michael@0 | 1152 | } |
michael@0 | 1153 | else if (sortColumn.getAttribute("sortDirection") == "ascending") { |
michael@0 | 1154 | viewSortAscending.setAttribute("checked", "true"); |
michael@0 | 1155 | viewSortDescending.removeAttribute("checked"); |
michael@0 | 1156 | viewUnsorted.removeAttribute("checked"); |
michael@0 | 1157 | } |
michael@0 | 1158 | else if (sortColumn.getAttribute("sortDirection") == "descending") { |
michael@0 | 1159 | viewSortDescending.setAttribute("checked", "true"); |
michael@0 | 1160 | viewSortAscending.removeAttribute("checked"); |
michael@0 | 1161 | viewUnsorted.removeAttribute("checked"); |
michael@0 | 1162 | } |
michael@0 | 1163 | }, |
michael@0 | 1164 | |
michael@0 | 1165 | /** |
michael@0 | 1166 | * Shows/Hides a tree column. |
michael@0 | 1167 | * @param element |
michael@0 | 1168 | * The menuitem element for the column |
michael@0 | 1169 | */ |
michael@0 | 1170 | showHideColumn: function VM_showHideColumn(element) { |
michael@0 | 1171 | var column = element.column; |
michael@0 | 1172 | |
michael@0 | 1173 | var splitter = column.nextSibling; |
michael@0 | 1174 | if (splitter && splitter.localName != "splitter") |
michael@0 | 1175 | splitter = null; |
michael@0 | 1176 | |
michael@0 | 1177 | if (element.getAttribute("checked") == "true") { |
michael@0 | 1178 | column.setAttribute("hidden", "false"); |
michael@0 | 1179 | if (splitter) |
michael@0 | 1180 | splitter.removeAttribute("hidden"); |
michael@0 | 1181 | } |
michael@0 | 1182 | else { |
michael@0 | 1183 | column.setAttribute("hidden", "true"); |
michael@0 | 1184 | if (splitter) |
michael@0 | 1185 | splitter.setAttribute("hidden", "true"); |
michael@0 | 1186 | } |
michael@0 | 1187 | }, |
michael@0 | 1188 | |
michael@0 | 1189 | /** |
michael@0 | 1190 | * Gets the last column that was sorted. |
michael@0 | 1191 | * @returns the currently sorted column, null if there is no sorted column. |
michael@0 | 1192 | */ |
michael@0 | 1193 | _getSortColumn: function VM__getSortColumn() { |
michael@0 | 1194 | var content = document.getElementById("placeContent"); |
michael@0 | 1195 | var cols = content.columns; |
michael@0 | 1196 | for (var i = 0; i < cols.count; ++i) { |
michael@0 | 1197 | var column = cols.getColumnAt(i).element; |
michael@0 | 1198 | var sortDirection = column.getAttribute("sortDirection"); |
michael@0 | 1199 | if (sortDirection == "ascending" || sortDirection == "descending") |
michael@0 | 1200 | return column; |
michael@0 | 1201 | } |
michael@0 | 1202 | return null; |
michael@0 | 1203 | }, |
michael@0 | 1204 | |
michael@0 | 1205 | /** |
michael@0 | 1206 | * Sorts the view by the specified column. |
michael@0 | 1207 | * @param aColumn |
michael@0 | 1208 | * The colum that is the sort key. Can be null - the |
michael@0 | 1209 | * current sort column or the title column will be used. |
michael@0 | 1210 | * @param aDirection |
michael@0 | 1211 | * The direction to sort - "ascending" or "descending". |
michael@0 | 1212 | * Can be null - the last direction or descending will be used. |
michael@0 | 1213 | * |
michael@0 | 1214 | * If both aColumnID and aDirection are null, the view will be unsorted. |
michael@0 | 1215 | */ |
michael@0 | 1216 | setSortColumn: function VM_setSortColumn(aColumn, aDirection) { |
michael@0 | 1217 | var result = document.getElementById("placeContent").result; |
michael@0 | 1218 | if (!aColumn && !aDirection) { |
michael@0 | 1219 | result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; |
michael@0 | 1220 | return; |
michael@0 | 1221 | } |
michael@0 | 1222 | |
michael@0 | 1223 | var columnId; |
michael@0 | 1224 | if (aColumn) { |
michael@0 | 1225 | columnId = aColumn.getAttribute("anonid"); |
michael@0 | 1226 | if (!aDirection) { |
michael@0 | 1227 | var sortColumn = this._getSortColumn(); |
michael@0 | 1228 | if (sortColumn) |
michael@0 | 1229 | aDirection = sortColumn.getAttribute("sortDirection"); |
michael@0 | 1230 | } |
michael@0 | 1231 | } |
michael@0 | 1232 | else { |
michael@0 | 1233 | var sortColumn = this._getSortColumn(); |
michael@0 | 1234 | columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title"; |
michael@0 | 1235 | } |
michael@0 | 1236 | |
michael@0 | 1237 | // This maps the possible values of columnId (i.e., anonid's of treecols in |
michael@0 | 1238 | // placeContent) to the default sortingMode and sortingAnnotation values for |
michael@0 | 1239 | // each column. |
michael@0 | 1240 | // key: Sort key in the name of one of the |
michael@0 | 1241 | // nsINavHistoryQueryOptions.SORT_BY_* constants |
michael@0 | 1242 | // dir: Default sort direction to use if none has been specified |
michael@0 | 1243 | // anno: The annotation to sort by, if key is "ANNOTATION" |
michael@0 | 1244 | var colLookupTable = { |
michael@0 | 1245 | title: { key: "TITLE", dir: "ascending" }, |
michael@0 | 1246 | tags: { key: "TAGS", dir: "ascending" }, |
michael@0 | 1247 | url: { key: "URI", dir: "ascending" }, |
michael@0 | 1248 | date: { key: "DATE", dir: "descending" }, |
michael@0 | 1249 | visitCount: { key: "VISITCOUNT", dir: "descending" }, |
michael@0 | 1250 | keyword: { key: "KEYWORD", dir: "ascending" }, |
michael@0 | 1251 | dateAdded: { key: "DATEADDED", dir: "descending" }, |
michael@0 | 1252 | lastModified: { key: "LASTMODIFIED", dir: "descending" }, |
michael@0 | 1253 | description: { key: "ANNOTATION", |
michael@0 | 1254 | dir: "ascending", |
michael@0 | 1255 | anno: PlacesUIUtils.DESCRIPTION_ANNO } |
michael@0 | 1256 | }; |
michael@0 | 1257 | |
michael@0 | 1258 | // Make sure we have a valid column. |
michael@0 | 1259 | if (!colLookupTable.hasOwnProperty(columnId)) |
michael@0 | 1260 | throw("Invalid column"); |
michael@0 | 1261 | |
michael@0 | 1262 | // Use a default sort direction if none has been specified. If aDirection |
michael@0 | 1263 | // is invalid, result.sortingMode will be undefined, which has the effect |
michael@0 | 1264 | // of unsorting the tree. |
michael@0 | 1265 | aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase(); |
michael@0 | 1266 | |
michael@0 | 1267 | var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection; |
michael@0 | 1268 | result.sortingAnnotation = colLookupTable[columnId].anno || ""; |
michael@0 | 1269 | result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst]; |
michael@0 | 1270 | } |
michael@0 | 1271 | } |
michael@0 | 1272 | |
michael@0 | 1273 | let ContentArea = { |
michael@0 | 1274 | _specialViews: new Map(), |
michael@0 | 1275 | |
michael@0 | 1276 | init: function CA_init() { |
michael@0 | 1277 | this._deck = document.getElementById("placesViewsDeck"); |
michael@0 | 1278 | this._toolbar = document.getElementById("placesToolbar"); |
michael@0 | 1279 | ContentTree.init(); |
michael@0 | 1280 | this._setupView(); |
michael@0 | 1281 | }, |
michael@0 | 1282 | |
michael@0 | 1283 | /** |
michael@0 | 1284 | * Gets the content view to be used for loading the given query. |
michael@0 | 1285 | * If a custom view was set by setContentViewForQueryString, that |
michael@0 | 1286 | * view would be returned, else the default tree view is returned |
michael@0 | 1287 | * |
michael@0 | 1288 | * @param aQueryString |
michael@0 | 1289 | * a query string |
michael@0 | 1290 | * @return the view to be used for loading aQueryString. |
michael@0 | 1291 | */ |
michael@0 | 1292 | getContentViewForQueryString: |
michael@0 | 1293 | function CA_getContentViewForQueryString(aQueryString) { |
michael@0 | 1294 | try { |
michael@0 | 1295 | if (this._specialViews.has(aQueryString)) { |
michael@0 | 1296 | let { view, options } = this._specialViews.get(aQueryString); |
michael@0 | 1297 | if (typeof view == "function") { |
michael@0 | 1298 | view = view(); |
michael@0 | 1299 | this._specialViews.set(aQueryString, { view: view, options: options }); |
michael@0 | 1300 | } |
michael@0 | 1301 | return view; |
michael@0 | 1302 | } |
michael@0 | 1303 | } |
michael@0 | 1304 | catch(ex) { |
michael@0 | 1305 | Components.utils.reportError(ex); |
michael@0 | 1306 | } |
michael@0 | 1307 | return ContentTree.view; |
michael@0 | 1308 | }, |
michael@0 | 1309 | |
michael@0 | 1310 | /** |
michael@0 | 1311 | * Sets a custom view to be used rather than the default places tree |
michael@0 | 1312 | * whenever the given query is selected in the left pane. |
michael@0 | 1313 | * @param aQueryString |
michael@0 | 1314 | * a query string |
michael@0 | 1315 | * @param aView |
michael@0 | 1316 | * Either the custom view or a function that will return the view |
michael@0 | 1317 | * the first (and only) time it's called. |
michael@0 | 1318 | * @param [optional] aOptions |
michael@0 | 1319 | * Object defining special options for the view. |
michael@0 | 1320 | * @see ContentTree.viewOptions for supported options and default values. |
michael@0 | 1321 | */ |
michael@0 | 1322 | setContentViewForQueryString: |
michael@0 | 1323 | function CA_setContentViewForQueryString(aQueryString, aView, aOptions) { |
michael@0 | 1324 | if (!aQueryString || |
michael@0 | 1325 | typeof aView != "object" && typeof aView != "function") |
michael@0 | 1326 | throw new Error("Invalid arguments"); |
michael@0 | 1327 | |
michael@0 | 1328 | this._specialViews.set(aQueryString, { view: aView, |
michael@0 | 1329 | options: aOptions || new Object() }); |
michael@0 | 1330 | }, |
michael@0 | 1331 | |
michael@0 | 1332 | get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel), |
michael@0 | 1333 | set currentView(aNewView) { |
michael@0 | 1334 | let oldView = this.currentView; |
michael@0 | 1335 | if (oldView != aNewView) { |
michael@0 | 1336 | this._deck.selectedPanel = aNewView.associatedElement; |
michael@0 | 1337 | |
michael@0 | 1338 | // If the content area inactivated view was focused, move focus |
michael@0 | 1339 | // to the new view. |
michael@0 | 1340 | if (document.activeElement == oldView.associatedElement) |
michael@0 | 1341 | aNewView.associatedElement.focus(); |
michael@0 | 1342 | } |
michael@0 | 1343 | return aNewView; |
michael@0 | 1344 | }, |
michael@0 | 1345 | |
michael@0 | 1346 | get currentPlace() this.currentView.place, |
michael@0 | 1347 | set currentPlace(aQueryString) { |
michael@0 | 1348 | let oldView = this.currentView; |
michael@0 | 1349 | let newView = this.getContentViewForQueryString(aQueryString); |
michael@0 | 1350 | newView.place = aQueryString; |
michael@0 | 1351 | if (oldView != newView) { |
michael@0 | 1352 | oldView.active = false; |
michael@0 | 1353 | this.currentView = newView; |
michael@0 | 1354 | this._setupView(); |
michael@0 | 1355 | newView.active = true; |
michael@0 | 1356 | } |
michael@0 | 1357 | return aQueryString; |
michael@0 | 1358 | }, |
michael@0 | 1359 | |
michael@0 | 1360 | /** |
michael@0 | 1361 | * Applies view options. |
michael@0 | 1362 | */ |
michael@0 | 1363 | _setupView: function CA__setupView() { |
michael@0 | 1364 | let options = this.currentViewOptions; |
michael@0 | 1365 | |
michael@0 | 1366 | // showDetailsPane. |
michael@0 | 1367 | let detailsDeck = document.getElementById("detailsDeck"); |
michael@0 | 1368 | detailsDeck.hidden = !options.showDetailsPane; |
michael@0 | 1369 | |
michael@0 | 1370 | // toolbarSet. |
michael@0 | 1371 | for (let elt of this._toolbar.childNodes) { |
michael@0 | 1372 | // On Windows and Linux the menu buttons are menus wrapped in a menubar. |
michael@0 | 1373 | if (elt.id == "placesMenu") { |
michael@0 | 1374 | for (let menuElt of elt.childNodes) { |
michael@0 | 1375 | menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1; |
michael@0 | 1376 | } |
michael@0 | 1377 | } |
michael@0 | 1378 | else { |
michael@0 | 1379 | elt.hidden = options.toolbarSet.indexOf(elt.id) == -1; |
michael@0 | 1380 | } |
michael@0 | 1381 | } |
michael@0 | 1382 | }, |
michael@0 | 1383 | |
michael@0 | 1384 | /** |
michael@0 | 1385 | * Options for the current view. |
michael@0 | 1386 | * |
michael@0 | 1387 | * @see ContentTree.viewOptions for supported options and default values. |
michael@0 | 1388 | */ |
michael@0 | 1389 | get currentViewOptions() { |
michael@0 | 1390 | // Use ContentTree options as default. |
michael@0 | 1391 | let viewOptions = ContentTree.viewOptions; |
michael@0 | 1392 | if (this._specialViews.has(this.currentPlace)) { |
michael@0 | 1393 | let { view, options } = this._specialViews.get(this.currentPlace); |
michael@0 | 1394 | for (let option in options) { |
michael@0 | 1395 | viewOptions[option] = options[option]; |
michael@0 | 1396 | } |
michael@0 | 1397 | } |
michael@0 | 1398 | return viewOptions; |
michael@0 | 1399 | }, |
michael@0 | 1400 | |
michael@0 | 1401 | focus: function() { |
michael@0 | 1402 | this._deck.selectedPanel.focus(); |
michael@0 | 1403 | } |
michael@0 | 1404 | }; |
michael@0 | 1405 | |
michael@0 | 1406 | let ContentTree = { |
michael@0 | 1407 | init: function CT_init() { |
michael@0 | 1408 | this._view = document.getElementById("placeContent"); |
michael@0 | 1409 | }, |
michael@0 | 1410 | |
michael@0 | 1411 | get view() this._view, |
michael@0 | 1412 | |
michael@0 | 1413 | get viewOptions() Object.seal({ |
michael@0 | 1414 | showDetailsPane: true, |
michael@0 | 1415 | toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter" |
michael@0 | 1416 | }), |
michael@0 | 1417 | |
michael@0 | 1418 | openSelectedNode: function CT_openSelectedNode(aEvent) { |
michael@0 | 1419 | let view = this.view; |
michael@0 | 1420 | PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view); |
michael@0 | 1421 | }, |
michael@0 | 1422 | |
michael@0 | 1423 | onClick: function CT_onClick(aEvent) { |
michael@0 | 1424 | let node = this.view.selectedNode; |
michael@0 | 1425 | if (node) { |
michael@0 | 1426 | let doubleClick = aEvent.button == 0 && aEvent.detail == 2; |
michael@0 | 1427 | let middleClick = aEvent.button == 1 && aEvent.detail == 1; |
michael@0 | 1428 | if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) { |
michael@0 | 1429 | // Open associated uri in the browser. |
michael@0 | 1430 | this.openSelectedNode(aEvent); |
michael@0 | 1431 | } |
michael@0 | 1432 | else if (middleClick && PlacesUtils.nodeIsContainer(node)) { |
michael@0 | 1433 | // The command execution function will take care of seeing if the |
michael@0 | 1434 | // selection is a folder or a different container type, and will |
michael@0 | 1435 | // load its contents in tabs. |
michael@0 | 1436 | PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view); |
michael@0 | 1437 | } |
michael@0 | 1438 | } |
michael@0 | 1439 | }, |
michael@0 | 1440 | |
michael@0 | 1441 | onKeyPress: function CT_onKeyPress(aEvent) { |
michael@0 | 1442 | if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) |
michael@0 | 1443 | this.openSelectedNode(aEvent); |
michael@0 | 1444 | } |
michael@0 | 1445 | }; |