browser/components/places/content/places.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/places/content/places.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1445 @@
     1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
    1.10 +XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils",
    1.11 +                                  "resource:///modules/MigrationUtils.jsm");
    1.12 +XPCOMUtils.defineLazyModuleGetter(this, "Task",
    1.13 +                                  "resource://gre/modules/Task.jsm");
    1.14 +XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
    1.15 +                                  "resource://gre/modules/BookmarkJSONUtils.jsm");
    1.16 +XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
    1.17 +                                  "resource://gre/modules/PlacesBackups.jsm");
    1.18 +XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
    1.19 +                                  "resource://gre/modules/DownloadUtils.jsm");
    1.20 +
    1.21 +var PlacesOrganizer = {
    1.22 +  _places: null,
    1.23 +
    1.24 +  // IDs of fields from editBookmarkOverlay that should be hidden when infoBox
    1.25 +  // is minimal. IDs should be kept in sync with the IDs of the elements
    1.26 +  // observing additionalInfoBroadcaster.
    1.27 +  _additionalInfoFields: [
    1.28 +    "editBMPanel_descriptionRow",
    1.29 +    "editBMPanel_loadInSidebarCheckbox",
    1.30 +    "editBMPanel_keywordRow",
    1.31 +  ],
    1.32 +
    1.33 +  _initFolderTree: function() {
    1.34 +    var leftPaneRoot = PlacesUIUtils.leftPaneFolderId;
    1.35 +    this._places.place = "place:excludeItems=1&expandQueries=0&folder=" + leftPaneRoot;
    1.36 +  },
    1.37 +
    1.38 +  selectLeftPaneQuery: function PO_selectLeftPaneQuery(aQueryName) {
    1.39 +    var itemId = PlacesUIUtils.leftPaneQueries[aQueryName];
    1.40 +    this._places.selectItems([itemId]);
    1.41 +    // Forcefully expand all-bookmarks
    1.42 +    if (aQueryName == "AllBookmarks" || aQueryName == "History")
    1.43 +      PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
    1.44 +  },
    1.45 +
    1.46 +  /**
    1.47 +   * Opens a given hierarchy in the left pane, stopping at the last reachable
    1.48 +   * container.
    1.49 +   *
    1.50 +   * @param aHierarchy A single container or an array of containers, sorted from
    1.51 +   *                   the outmost to the innermost in the hierarchy. Each
    1.52 +   *                   container may be either an item id, a Places URI string,
    1.53 +   *                   or a named query.
    1.54 +   * @see PlacesUIUtils.leftPaneQueries for supported named queries.
    1.55 +   */
    1.56 +  selectLeftPaneContainerByHierarchy:
    1.57 +  function PO_selectLeftPaneContainerByHierarchy(aHierarchy) {
    1.58 +    if (!aHierarchy)
    1.59 +      throw new Error("Invalid containers hierarchy");
    1.60 +    let hierarchy = [].concat(aHierarchy);
    1.61 +    let selectWasSuppressed = this._places.view.selection.selectEventsSuppressed;
    1.62 +    if (!selectWasSuppressed)
    1.63 +      this._places.view.selection.selectEventsSuppressed = true;
    1.64 +    try {
    1.65 +      for (let container of hierarchy) {
    1.66 +        switch (typeof container) {
    1.67 +          case "number":
    1.68 +            this._places.selectItems([container], false);
    1.69 +            break;
    1.70 +          case "string":
    1.71 +            if (container.substr(0, 6) == "place:")
    1.72 +              this._places.selectPlaceURI(container);
    1.73 +            else if (container in PlacesUIUtils.leftPaneQueries)
    1.74 +              this.selectLeftPaneQuery(container);
    1.75 +            else
    1.76 +              throw new Error("Invalid container found: " + container);
    1.77 +            break;
    1.78 +          default:
    1.79 +            throw new Error("Invalid container type found: " + container);
    1.80 +            break;
    1.81 +        }
    1.82 +        PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
    1.83 +      }
    1.84 +    } finally {
    1.85 +      if (!selectWasSuppressed)
    1.86 +        this._places.view.selection.selectEventsSuppressed = false;
    1.87 +    }
    1.88 +  },
    1.89 +
    1.90 +  init: function PO_init() {
    1.91 +    ContentArea.init();
    1.92 +
    1.93 +    this._places = document.getElementById("placesList");
    1.94 +    this._initFolderTree();
    1.95 +
    1.96 +    var leftPaneSelection = "AllBookmarks"; // default to all-bookmarks
    1.97 +    if (window.arguments && window.arguments[0])
    1.98 +      leftPaneSelection = window.arguments[0];
    1.99 +
   1.100 +    this.selectLeftPaneContainerByHierarchy(leftPaneSelection);
   1.101 +    if (leftPaneSelection === "History") {
   1.102 +      let historyNode = this._places.selectedNode;
   1.103 +      if (historyNode.childCount > 0)
   1.104 +        this._places.selectNode(historyNode.getChild(0));
   1.105 +    }
   1.106 +
   1.107 +    // clear the back-stack
   1.108 +    this._backHistory.splice(0, this._backHistory.length);
   1.109 +    document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
   1.110 +
   1.111 +    // Set up the search UI.
   1.112 +    PlacesSearchBox.init();
   1.113 +
   1.114 +    window.addEventListener("AppCommand", this, true);
   1.115 +#ifdef XP_MACOSX
   1.116 +    // 1. Map Edit->Find command to OrganizerCommand_find:all.  Need to map
   1.117 +    // both the menuitem and the Find key.
   1.118 +    var findMenuItem = document.getElementById("menu_find");
   1.119 +    findMenuItem.setAttribute("command", "OrganizerCommand_find:all");
   1.120 +    var findKey = document.getElementById("key_find");
   1.121 +    findKey.setAttribute("command", "OrganizerCommand_find:all");
   1.122 +
   1.123 +    // 2. Disable some keybindings from browser.xul
   1.124 +    var elements = ["cmd_handleBackspace", "cmd_handleShiftBackspace"];
   1.125 +    for (var i=0; i < elements.length; i++) {
   1.126 +      document.getElementById(elements[i]).setAttribute("disabled", "true");
   1.127 +    }
   1.128 +#endif
   1.129 +
   1.130 +    // remove the "Properties" context-menu item, we've our own details pane
   1.131 +    document.getElementById("placesContext")
   1.132 +            .removeChild(document.getElementById("placesContext_show:info"));
   1.133 +
   1.134 +    ContentArea.focus();
   1.135 +  },
   1.136 +
   1.137 +  QueryInterface: function PO_QueryInterface(aIID) {
   1.138 +    if (aIID.equals(Components.interfaces.nsIDOMEventListener) ||
   1.139 +        aIID.equals(Components.interfaces.nsISupports))
   1.140 +      return this;
   1.141 +
   1.142 +    throw Components.results.NS_NOINTERFACE;
   1.143 +  },
   1.144 +
   1.145 +  handleEvent: function PO_handleEvent(aEvent) {
   1.146 +    if (aEvent.type != "AppCommand")
   1.147 +      return;
   1.148 +
   1.149 +    aEvent.stopPropagation();
   1.150 +    switch (aEvent.command) {
   1.151 +      case "Back":
   1.152 +        if (this._backHistory.length > 0)
   1.153 +          this.back();
   1.154 +        break;
   1.155 +      case "Forward":
   1.156 +        if (this._forwardHistory.length > 0)
   1.157 +          this.forward();
   1.158 +        break;
   1.159 +      case "Search":
   1.160 +        PlacesSearchBox.findAll();
   1.161 +        break;
   1.162 +    }
   1.163 +  },
   1.164 +
   1.165 +  destroy: function PO_destroy() {
   1.166 +  },
   1.167 +
   1.168 +  _location: null,
   1.169 +  get location() {
   1.170 +    return this._location;
   1.171 +  },
   1.172 +
   1.173 +  set location(aLocation) {
   1.174 +    if (!aLocation || this._location == aLocation)
   1.175 +      return aLocation;
   1.176 +
   1.177 +    if (this.location) {
   1.178 +      this._backHistory.unshift(this.location);
   1.179 +      this._forwardHistory.splice(0, this._forwardHistory.length);
   1.180 +    }
   1.181 +
   1.182 +    this._location = aLocation;
   1.183 +    this._places.selectPlaceURI(aLocation);
   1.184 +
   1.185 +    if (!this._places.hasSelection) {
   1.186 +      // If no node was found for the given place: uri, just load it directly
   1.187 +      ContentArea.currentPlace = aLocation;
   1.188 +    }
   1.189 +    this.updateDetailsPane();
   1.190 +
   1.191 +    // update navigation commands
   1.192 +    if (this._backHistory.length == 0)
   1.193 +      document.getElementById("OrganizerCommand:Back").setAttribute("disabled", true);
   1.194 +    else
   1.195 +      document.getElementById("OrganizerCommand:Back").removeAttribute("disabled");
   1.196 +    if (this._forwardHistory.length == 0)
   1.197 +      document.getElementById("OrganizerCommand:Forward").setAttribute("disabled", true);
   1.198 +    else
   1.199 +      document.getElementById("OrganizerCommand:Forward").removeAttribute("disabled");
   1.200 +
   1.201 +    return aLocation;
   1.202 +  },
   1.203 +
   1.204 +  _backHistory: [],
   1.205 +  _forwardHistory: [],
   1.206 +
   1.207 +  back: function PO_back() {
   1.208 +    this._forwardHistory.unshift(this.location);
   1.209 +    var historyEntry = this._backHistory.shift();
   1.210 +    this._location = null;
   1.211 +    this.location = historyEntry;
   1.212 +  },
   1.213 +  forward: function PO_forward() {
   1.214 +    this._backHistory.unshift(this.location);
   1.215 +    var historyEntry = this._forwardHistory.shift();
   1.216 +    this._location = null;
   1.217 +    this.location = historyEntry;
   1.218 +  },
   1.219 +
   1.220 +  /**
   1.221 +   * Called when a place folder is selected in the left pane.
   1.222 +   * @param   resetSearchBox
   1.223 +   *          true if the search box should also be reset, false otherwise.
   1.224 +   *          The search box should be reset when a new folder in the left
   1.225 +   *          pane is selected; the search scope and text need to be cleared in
   1.226 +   *          preparation for the new folder.  Note that if the user manually
   1.227 +   *          resets the search box, either by clicking its reset button or by
   1.228 +   *          deleting its text, this will be false.
   1.229 +   */
   1.230 +  _cachedLeftPaneSelectedURI: null,
   1.231 +  onPlaceSelected: function PO_onPlaceSelected(resetSearchBox) {
   1.232 +    // Don't change the right-hand pane contents when there's no selection.
   1.233 +    if (!this._places.hasSelection)
   1.234 +      return;
   1.235 +
   1.236 +    var node = this._places.selectedNode;
   1.237 +    var queries = PlacesUtils.asQuery(node).getQueries();
   1.238 +
   1.239 +    // Items are only excluded on the left pane.
   1.240 +    var options = node.queryOptions.clone();
   1.241 +    options.excludeItems = false;
   1.242 +    var placeURI = PlacesUtils.history.queriesToQueryString(queries,
   1.243 +                                                            queries.length,
   1.244 +                                                            options);
   1.245 +
   1.246 +    // If either the place of the content tree in the right pane has changed or
   1.247 +    // the user cleared the search box, update the place, hide the search UI,
   1.248 +    // and update the back/forward buttons by setting location.
   1.249 +    if (ContentArea.currentPlace != placeURI || !resetSearchBox) {
   1.250 +      ContentArea.currentPlace = placeURI;
   1.251 +      this.location = node.uri;
   1.252 +    }
   1.253 +
   1.254 +    // When we invalidate a container we use suppressSelectionEvent, when it is
   1.255 +    // unset a select event is fired, in many cases the selection did not really
   1.256 +    // change, so we should check for it, and return early in such a case. Note
   1.257 +    // that we cannot return any earlier than this point, because when
   1.258 +    // !resetSearchBox, we need to update location and hide the UI as above,
   1.259 +    // even though the selection has not changed.
   1.260 +    if (node.uri == this._cachedLeftPaneSelectedURI)
   1.261 +      return;
   1.262 +    this._cachedLeftPaneSelectedURI = node.uri;
   1.263 +
   1.264 +    // At this point, resetSearchBox is true, because the left pane selection
   1.265 +    // has changed; otherwise we would have returned earlier.
   1.266 +
   1.267 +    PlacesSearchBox.searchFilter.reset();
   1.268 +    this._setSearchScopeForNode(node);
   1.269 +    this.updateDetailsPane();
   1.270 +  },
   1.271 +
   1.272 +  /**
   1.273 +   * Sets the search scope based on aNode's properties.
   1.274 +   * @param   aNode
   1.275 +   *          the node to set up scope from
   1.276 +   */
   1.277 +  _setSearchScopeForNode: function PO__setScopeForNode(aNode) {
   1.278 +    let itemId = aNode.itemId;
   1.279 +
   1.280 +    if (PlacesUtils.nodeIsHistoryContainer(aNode) ||
   1.281 +        itemId == PlacesUIUtils.leftPaneQueries["History"]) {
   1.282 +      PlacesQueryBuilder.setScope("history");
   1.283 +    }
   1.284 +    else if (itemId == PlacesUIUtils.leftPaneQueries["Downloads"]) {
   1.285 +      PlacesQueryBuilder.setScope("downloads");
   1.286 +    }
   1.287 +    else {
   1.288 +      // Default to All Bookmarks for all other nodes, per bug 469437.
   1.289 +      PlacesQueryBuilder.setScope("bookmarks");
   1.290 +    }
   1.291 +  },
   1.292 +
   1.293 +  /**
   1.294 +   * Handle clicks on the places list.
   1.295 +   * Single Left click, right click or modified click do not result in any
   1.296 +   * special action, since they're related to selection.
   1.297 +   * @param   aEvent
   1.298 +   *          The mouse event.
   1.299 +   */
   1.300 +  onPlacesListClick: function PO_onPlacesListClick(aEvent) {
   1.301 +    // Only handle clicks on tree children.
   1.302 +    if (aEvent.target.localName != "treechildren")
   1.303 +      return;
   1.304 +
   1.305 +    let node = this._places.selectedNode;
   1.306 +    if (node) {
   1.307 +      let middleClick = aEvent.button == 1 && aEvent.detail == 1;
   1.308 +      if (middleClick && PlacesUtils.nodeIsContainer(node)) {
   1.309 +        // The command execution function will take care of seeing if the
   1.310 +        // selection is a folder or a different container type, and will
   1.311 +        // load its contents in tabs.
   1.312 +        PlacesUIUtils.openContainerNodeInTabs(selectedNode, aEvent, this._places);
   1.313 +      }
   1.314 +    }
   1.315 +  },
   1.316 +
   1.317 +  /**
   1.318 +   * Handle focus changes on the places list and the current content view.
   1.319 +   */
   1.320 +  updateDetailsPane: function PO_updateDetailsPane() {
   1.321 +    if (!ContentArea.currentViewOptions.showDetailsPane)
   1.322 +      return;
   1.323 +    let view = PlacesUIUtils.getViewForNode(document.activeElement);
   1.324 +    if (view) {
   1.325 +      let selectedNodes = view.selectedNode ?
   1.326 +                          [view.selectedNode] : view.selectedNodes;
   1.327 +      this._fillDetailsPane(selectedNodes);
   1.328 +    }
   1.329 +  },
   1.330 +
   1.331 +  openFlatContainer: function PO_openFlatContainerFlatContainer(aContainer) {
   1.332 +    if (aContainer.itemId != -1) {
   1.333 +      PlacesUtils.asContainer(this._places.selectedNode).containerOpen = true;
   1.334 +      this._places.selectItems([aContainer.itemId], false);
   1.335 +    }
   1.336 +    else if (PlacesUtils.nodeIsQuery(aContainer)) {
   1.337 +      this._places.selectPlaceURI(aContainer.uri);
   1.338 +    }
   1.339 +  },
   1.340 +
   1.341 +  /**
   1.342 +   * Returns the options associated with the query currently loaded in the
   1.343 +   * main places pane.
   1.344 +   */
   1.345 +  getCurrentOptions: function PO_getCurrentOptions() {
   1.346 +    return PlacesUtils.asQuery(ContentArea.currentView.result.root).queryOptions;
   1.347 +  },
   1.348 +
   1.349 +  /**
   1.350 +   * Returns the queries associated with the query currently loaded in the
   1.351 +   * main places pane.
   1.352 +   */
   1.353 +  getCurrentQueries: function PO_getCurrentQueries() {
   1.354 +    return PlacesUtils.asQuery(ContentArea.currentView.result.root).getQueries();
   1.355 +  },
   1.356 +
   1.357 +  /**
   1.358 +   * Show the migration wizard for importing passwords,
   1.359 +   * cookies, history, preferences, and bookmarks.
   1.360 +   */
   1.361 +  importFromBrowser: function PO_importFromBrowser() {
   1.362 +    MigrationUtils.showMigrationWizard(window);
   1.363 +  },
   1.364 +
   1.365 +  /**
   1.366 +   * Open a file-picker and import the selected file into the bookmarks store
   1.367 +   */
   1.368 +  importFromFile: function PO_importFromFile() {
   1.369 +    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   1.370 +    let fpCallback = function fpCallback_done(aResult) {
   1.371 +      if (aResult != Ci.nsIFilePicker.returnCancel && fp.fileURL) {
   1.372 +        Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
   1.373 +        BookmarkHTMLUtils.importFromURL(fp.fileURL.spec, false)
   1.374 +                         .then(null, Components.utils.reportError);
   1.375 +      }
   1.376 +    };
   1.377 +
   1.378 +    fp.init(window, PlacesUIUtils.getString("SelectImport"),
   1.379 +            Ci.nsIFilePicker.modeOpen);
   1.380 +    fp.appendFilters(Ci.nsIFilePicker.filterHTML);
   1.381 +    fp.open(fpCallback);
   1.382 +  },
   1.383 +
   1.384 +  /**
   1.385 +   * Allows simple exporting of bookmarks.
   1.386 +   */
   1.387 +  exportBookmarks: function PO_exportBookmarks() {
   1.388 +    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   1.389 +    let fpCallback = function fpCallback_done(aResult) {
   1.390 +      if (aResult != Ci.nsIFilePicker.returnCancel) {
   1.391 +        Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
   1.392 +        BookmarkHTMLUtils.exportToFile(fp.file.path)
   1.393 +                         .then(null, Components.utils.reportError);
   1.394 +      }
   1.395 +    };
   1.396 +
   1.397 +    fp.init(window, PlacesUIUtils.getString("EnterExport"),
   1.398 +            Ci.nsIFilePicker.modeSave);
   1.399 +    fp.appendFilters(Ci.nsIFilePicker.filterHTML);
   1.400 +    fp.defaultString = "bookmarks.html";
   1.401 +    fp.open(fpCallback);
   1.402 +  },
   1.403 +
   1.404 +  /**
   1.405 +   * Populates the restore menu with the dates of the backups available.
   1.406 +   */
   1.407 +  populateRestoreMenu: function PO_populateRestoreMenu() {
   1.408 +    let restorePopup = document.getElementById("fileRestorePopup");
   1.409 +
   1.410 +    let dateSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"].
   1.411 +                  getService(Ci.nsIScriptableDateFormat);
   1.412 +
   1.413 +    // Remove existing menu items.  Last item is the restoreFromFile item.
   1.414 +    while (restorePopup.childNodes.length > 1)
   1.415 +      restorePopup.removeChild(restorePopup.firstChild);
   1.416 +
   1.417 +    Task.spawn(function() {
   1.418 +      let backupFiles = yield PlacesBackups.getBackupFiles();
   1.419 +      if (backupFiles.length == 0)
   1.420 +        return;
   1.421 +
   1.422 +      // Populate menu with backups.
   1.423 +      for (let i = 0; i < backupFiles.length; i++) {
   1.424 +        let fileSize = (yield OS.File.stat(backupFiles[i])).size;
   1.425 +        let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
   1.426 +        let sizeString = PlacesUtils.getFormattedString("backupFileSizeText",
   1.427 +                                                        [size, unit]);
   1.428 +        let sizeInfo;
   1.429 +        let bookmarkCount = PlacesBackups.getBookmarkCountForFile(backupFiles[i]);
   1.430 +        if (bookmarkCount != null) {
   1.431 +          sizeInfo = " (" + sizeString + " - " +
   1.432 +                     PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   1.433 +                                                   bookmarkCount,
   1.434 +                                                   [bookmarkCount]) +
   1.435 +                     ")";
   1.436 +        } else {
   1.437 +          sizeInfo = " (" + sizeString + ")";
   1.438 +        }
   1.439 +
   1.440 +        let backupDate = PlacesBackups.getDateForFile(backupFiles[i]);
   1.441 +        let m = restorePopup.insertBefore(document.createElement("menuitem"),
   1.442 +                                          document.getElementById("restoreFromFile"));
   1.443 +        m.setAttribute("label",
   1.444 +                       dateSvc.FormatDate("",
   1.445 +                                          Ci.nsIScriptableDateFormat.dateFormatLong,
   1.446 +                                          backupDate.getFullYear(),
   1.447 +                                          backupDate.getMonth() + 1,
   1.448 +                                          backupDate.getDate()) +
   1.449 +                                          sizeInfo);
   1.450 +        m.setAttribute("value", OS.Path.basename(backupFiles[i]));
   1.451 +        m.setAttribute("oncommand",
   1.452 +                       "PlacesOrganizer.onRestoreMenuItemClick(this);");
   1.453 +      }
   1.454 +
   1.455 +      // Add the restoreFromFile item.
   1.456 +      restorePopup.insertBefore(document.createElement("menuseparator"),
   1.457 +                                document.getElementById("restoreFromFile"));
   1.458 +    });
   1.459 +  },
   1.460 +
   1.461 +  /**
   1.462 +   * Called when a menuitem is selected from the restore menu.
   1.463 +   */
   1.464 +  onRestoreMenuItemClick: function PO_onRestoreMenuItemClick(aMenuItem) {
   1.465 +    Task.spawn(function() {
   1.466 +      let backupName = aMenuItem.getAttribute("value");
   1.467 +      let backupFilePaths = yield PlacesBackups.getBackupFiles();
   1.468 +      for (let backupFilePath of backupFilePaths) {
   1.469 +        if (OS.Path.basename(backupFilePath) == backupName) {
   1.470 +          PlacesOrganizer.restoreBookmarksFromFile(backupFilePath);
   1.471 +          break;
   1.472 +        }
   1.473 +      }
   1.474 +    });
   1.475 +  },
   1.476 +
   1.477 +  /**
   1.478 +   * Called when 'Choose File...' is selected from the restore menu.
   1.479 +   * Prompts for a file and restores bookmarks to those in the file.
   1.480 +   */
   1.481 +  onRestoreBookmarksFromFile: function PO_onRestoreBookmarksFromFile() {
   1.482 +    let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
   1.483 +                 getService(Ci.nsIProperties);
   1.484 +    let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
   1.485 +    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   1.486 +    let fpCallback = function fpCallback_done(aResult) {
   1.487 +      if (aResult != Ci.nsIFilePicker.returnCancel) {
   1.488 +        this.restoreBookmarksFromFile(fp.file.path);
   1.489 +      }
   1.490 +    }.bind(this);
   1.491 +
   1.492 +    fp.init(window, PlacesUIUtils.getString("bookmarksRestoreTitle"),
   1.493 +            Ci.nsIFilePicker.modeOpen);
   1.494 +    fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
   1.495 +                    PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
   1.496 +    fp.appendFilters(Ci.nsIFilePicker.filterAll);
   1.497 +    fp.displayDirectory = backupsDir;
   1.498 +    fp.open(fpCallback);
   1.499 +  },
   1.500 +
   1.501 +  /**
   1.502 +   * Restores bookmarks from a JSON file.
   1.503 +   */
   1.504 +  restoreBookmarksFromFile: function PO_restoreBookmarksFromFile(aFilePath) {
   1.505 +    // check file extension
   1.506 +    if (!aFilePath.endsWith("json")) {
   1.507 +      this._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreFormatError"));
   1.508 +      return;
   1.509 +    }
   1.510 +
   1.511 +    // confirm ok to delete existing bookmarks
   1.512 +    var prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"].
   1.513 +                  getService(Ci.nsIPromptService);
   1.514 +    if (!prompts.confirm(null,
   1.515 +                         PlacesUIUtils.getString("bookmarksRestoreAlertTitle"),
   1.516 +                         PlacesUIUtils.getString("bookmarksRestoreAlert")))
   1.517 +      return;
   1.518 +
   1.519 +    Task.spawn(function() {
   1.520 +      try {
   1.521 +        yield BookmarkJSONUtils.importFromFile(aFilePath, true);
   1.522 +      } catch(ex) {
   1.523 +        PlacesOrganizer._showErrorAlert(PlacesUIUtils.getString("bookmarksRestoreParseError"));
   1.524 +      }
   1.525 +    });
   1.526 +  },
   1.527 +
   1.528 +  _showErrorAlert: function PO__showErrorAlert(aMsg) {
   1.529 +    var brandShortName = document.getElementById("brandStrings").
   1.530 +                                  getString("brandShortName");
   1.531 +
   1.532 +    Cc["@mozilla.org/embedcomp/prompt-service;1"].
   1.533 +      getService(Ci.nsIPromptService).
   1.534 +      alert(window, brandShortName, aMsg);
   1.535 +  },
   1.536 +
   1.537 +  /**
   1.538 +   * Backup bookmarks to desktop, auto-generate a filename with a date.
   1.539 +   * The file is a JSON serialization of bookmarks, tags and any annotations
   1.540 +   * of those items.
   1.541 +   */
   1.542 +  backupBookmarks: function PO_backupBookmarks() {
   1.543 +    let dirSvc = Cc["@mozilla.org/file/directory_service;1"].
   1.544 +                 getService(Ci.nsIProperties);
   1.545 +    let backupsDir = dirSvc.get("Desk", Ci.nsILocalFile);
   1.546 +    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
   1.547 +    let fpCallback = function fpCallback_done(aResult) {
   1.548 +      if (aResult != Ci.nsIFilePicker.returnCancel) {
   1.549 +        // There is no OS.File version of the filepicker yet (Bug 937812).
   1.550 +        PlacesBackups.saveBookmarksToJSONFile(fp.file.path);
   1.551 +      }
   1.552 +    };
   1.553 +
   1.554 +    fp.init(window, PlacesUIUtils.getString("bookmarksBackupTitle"),
   1.555 +            Ci.nsIFilePicker.modeSave);
   1.556 +    fp.appendFilter(PlacesUIUtils.getString("bookmarksRestoreFilterName"),
   1.557 +                    PlacesUIUtils.getString("bookmarksRestoreFilterExtension"));
   1.558 +    fp.defaultString = PlacesBackups.getFilenameForDate();
   1.559 +    fp.displayDirectory = backupsDir;
   1.560 +    fp.open(fpCallback);
   1.561 +  },
   1.562 +
   1.563 +  _paneDisabled: false,
   1.564 +  _setDetailsFieldsDisabledState:
   1.565 +  function PO__setDetailsFieldsDisabledState(aDisabled) {
   1.566 +    if (aDisabled) {
   1.567 +      document.getElementById("paneElementsBroadcaster")
   1.568 +              .setAttribute("disabled", "true");
   1.569 +    }
   1.570 +    else {
   1.571 +      document.getElementById("paneElementsBroadcaster")
   1.572 +              .removeAttribute("disabled");
   1.573 +    }
   1.574 +  },
   1.575 +
   1.576 +  _detectAndSetDetailsPaneMinimalState:
   1.577 +  function PO__detectAndSetDetailsPaneMinimalState(aNode) {
   1.578 +    /**
   1.579 +     * The details of simple folder-items (as opposed to livemarks) or the
   1.580 +     * of livemark-children are not likely to fill the infoBox anyway,
   1.581 +     * thus we remove the "More/Less" button and show all details.
   1.582 +     *
   1.583 +     * the wasminimal attribute here is used to persist the "more/less"
   1.584 +     * state in a bookmark->folder->bookmark scenario.
   1.585 +     */
   1.586 +    var infoBox = document.getElementById("infoBox");
   1.587 +    var infoBoxExpander = document.getElementById("infoBoxExpander");
   1.588 +    var infoBoxExpanderWrapper = document.getElementById("infoBoxExpanderWrapper");
   1.589 +    var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
   1.590 +
   1.591 +    if (!aNode) {
   1.592 +      infoBoxExpanderWrapper.hidden = true;
   1.593 +      return;
   1.594 +    }
   1.595 +    if (aNode.itemId != -1 &&
   1.596 +        PlacesUtils.nodeIsFolder(aNode) && !aNode._feedURI) {
   1.597 +      if (infoBox.getAttribute("minimal") == "true")
   1.598 +        infoBox.setAttribute("wasminimal", "true");
   1.599 +      infoBox.removeAttribute("minimal");
   1.600 +      infoBoxExpanderWrapper.hidden = true;
   1.601 +    }
   1.602 +    else {
   1.603 +      if (infoBox.getAttribute("wasminimal") == "true")
   1.604 +        infoBox.setAttribute("minimal", "true");
   1.605 +      infoBox.removeAttribute("wasminimal");
   1.606 +      infoBoxExpanderWrapper.hidden =
   1.607 +        this._additionalInfoFields.every(function (id)
   1.608 +          document.getElementById(id).collapsed);
   1.609 +    }
   1.610 +    additionalInfoBroadcaster.hidden = infoBox.getAttribute("minimal") == "true";
   1.611 +  },
   1.612 +
   1.613 +  // NOT YET USED
   1.614 +  updateThumbnailProportions: function PO_updateThumbnailProportions() {
   1.615 +    var previewBox = document.getElementById("previewBox");
   1.616 +    var canvas = document.getElementById("itemThumbnail");
   1.617 +    var height = previewBox.boxObject.height;
   1.618 +    var width = height * (screen.width / screen.height);
   1.619 +    canvas.width = width;
   1.620 +    canvas.height = height;
   1.621 +  },
   1.622 +
   1.623 +  _fillDetailsPane: function PO__fillDetailsPane(aNodeList) {
   1.624 +    var infoBox = document.getElementById("infoBox");
   1.625 +    var detailsDeck = document.getElementById("detailsDeck");
   1.626 +
   1.627 +    // Make sure the infoBox UI is visible if we need to use it, we hide it
   1.628 +    // below when we don't.
   1.629 +    infoBox.hidden = false;
   1.630 +    var aSelectedNode = aNodeList.length == 1 ? aNodeList[0] : null;
   1.631 +    // If a textbox within a panel is focused, force-blur it so its contents
   1.632 +    // are saved
   1.633 +    if (gEditItemOverlay.itemId != -1) {
   1.634 +      var focusedElement = document.commandDispatcher.focusedElement;
   1.635 +      if ((focusedElement instanceof HTMLInputElement ||
   1.636 +           focusedElement instanceof HTMLTextAreaElement) &&
   1.637 +          /^editBMPanel.*/.test(focusedElement.parentNode.parentNode.id))
   1.638 +        focusedElement.blur();
   1.639 +
   1.640 +      // don't update the panel if we are already editing this node unless we're
   1.641 +      // in multi-edit mode
   1.642 +      if (aSelectedNode) {
   1.643 +        var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
   1.644 +        var nodeIsSame = gEditItemOverlay.itemId == aSelectedNode.itemId ||
   1.645 +                         gEditItemOverlay.itemId == concreteId ||
   1.646 +                         (aSelectedNode.itemId == -1 && gEditItemOverlay.uri &&
   1.647 +                          gEditItemOverlay.uri == aSelectedNode.uri);
   1.648 +        if (nodeIsSame && detailsDeck.selectedIndex == 1 &&
   1.649 +            !gEditItemOverlay.multiEdit)
   1.650 +          return;
   1.651 +      }
   1.652 +    }
   1.653 +
   1.654 +    // Clean up the panel before initing it again.
   1.655 +    gEditItemOverlay.uninitPanel(false);
   1.656 +
   1.657 +    if (aSelectedNode && !PlacesUtils.nodeIsSeparator(aSelectedNode)) {
   1.658 +      detailsDeck.selectedIndex = 1;
   1.659 +      // Using the concrete itemId is arguably wrong.  The bookmarks API
   1.660 +      // does allow setting properties for folder shortcuts as well, but since
   1.661 +      // the UI does not distinct between the couple, we better just show
   1.662 +      // the concrete item properties for shortcuts to root nodes.
   1.663 +      var concreteId = PlacesUtils.getConcreteItemId(aSelectedNode);
   1.664 +      var isRootItem = concreteId != -1 && PlacesUtils.isRootItem(concreteId);
   1.665 +      var readOnly = isRootItem ||
   1.666 +                     aSelectedNode.parent.itemId == PlacesUIUtils.leftPaneFolderId;
   1.667 +      var useConcreteId = isRootItem ||
   1.668 +                          PlacesUtils.nodeIsTagQuery(aSelectedNode);
   1.669 +      var itemId = -1;
   1.670 +      if (concreteId != -1 && useConcreteId)
   1.671 +        itemId = concreteId;
   1.672 +      else if (aSelectedNode.itemId != -1)
   1.673 +        itemId = aSelectedNode.itemId;
   1.674 +      else
   1.675 +        itemId = PlacesUtils._uri(aSelectedNode.uri);
   1.676 +
   1.677 +      gEditItemOverlay.initPanel(itemId, { hiddenRows: ["folderPicker"]
   1.678 +                                         , forceReadOnly: readOnly
   1.679 +                                         , titleOverride: aSelectedNode.title
   1.680 +                                         });
   1.681 +
   1.682 +      // Dynamically generated queries, like history date containers, have
   1.683 +      // itemId !=0 and do not exist in history.  For them the panel is
   1.684 +      // read-only, but empty, since it can't get a valid title for the object.
   1.685 +      // In such a case we force the title using the selectedNode one, for UI
   1.686 +      // polishness.
   1.687 +      if (aSelectedNode.itemId == -1 &&
   1.688 +          (PlacesUtils.nodeIsDay(aSelectedNode) ||
   1.689 +           PlacesUtils.nodeIsHost(aSelectedNode)))
   1.690 +        gEditItemOverlay._element("namePicker").value = aSelectedNode.title;
   1.691 +
   1.692 +      this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
   1.693 +    }
   1.694 +    else if (!aSelectedNode && aNodeList[0]) {
   1.695 +      var itemIds = [];
   1.696 +      for (var i = 0; i < aNodeList.length; i++) {
   1.697 +        if (!PlacesUtils.nodeIsBookmark(aNodeList[i]) &&
   1.698 +            !PlacesUtils.nodeIsURI(aNodeList[i])) {
   1.699 +          detailsDeck.selectedIndex = 0;
   1.700 +          var selectItemDesc = document.getElementById("selectItemDescription");
   1.701 +          var itemsCountLabel = document.getElementById("itemsCountText");
   1.702 +          selectItemDesc.hidden = false;
   1.703 +          itemsCountLabel.value =
   1.704 +            PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   1.705 +                                          aNodeList.length, [aNodeList.length]);
   1.706 +          infoBox.hidden = true;
   1.707 +          return;
   1.708 +        }
   1.709 +        itemIds[i] = aNodeList[i].itemId != -1 ? aNodeList[i].itemId :
   1.710 +                     PlacesUtils._uri(aNodeList[i].uri);
   1.711 +      }
   1.712 +      detailsDeck.selectedIndex = 1;
   1.713 +      gEditItemOverlay.initPanel(itemIds,
   1.714 +                                 { hiddenRows: ["folderPicker",
   1.715 +                                                "loadInSidebar",
   1.716 +                                                "location",
   1.717 +                                                "keyword",
   1.718 +                                                "description",
   1.719 +                                                "name"]});
   1.720 +      this._detectAndSetDetailsPaneMinimalState(aSelectedNode);
   1.721 +    }
   1.722 +    else {
   1.723 +      detailsDeck.selectedIndex = 0;
   1.724 +      infoBox.hidden = true;
   1.725 +      let selectItemDesc = document.getElementById("selectItemDescription");
   1.726 +      let itemsCountLabel = document.getElementById("itemsCountText");
   1.727 +      let itemsCount = 0;
   1.728 +      if (ContentArea.currentView.result) {
   1.729 +        let rootNode = ContentArea.currentView.result.root;
   1.730 +        if (rootNode.containerOpen)
   1.731 +          itemsCount = rootNode.childCount;
   1.732 +      }
   1.733 +      if (itemsCount == 0) {
   1.734 +        selectItemDesc.hidden = true;
   1.735 +        itemsCountLabel.value = PlacesUIUtils.getString("detailsPane.noItems");
   1.736 +      }
   1.737 +      else {
   1.738 +        selectItemDesc.hidden = false;
   1.739 +        itemsCountLabel.value =
   1.740 +          PlacesUIUtils.getPluralString("detailsPane.itemsCountLabel",
   1.741 +                                        itemsCount, [itemsCount]);
   1.742 +      }
   1.743 +    }
   1.744 +  },
   1.745 +
   1.746 +  // NOT YET USED
   1.747 +  _updateThumbnail: function PO__updateThumbnail() {
   1.748 +    var bo = document.getElementById("previewBox").boxObject;
   1.749 +    var width  = bo.width;
   1.750 +    var height = bo.height;
   1.751 +
   1.752 +    var canvas = document.getElementById("itemThumbnail");
   1.753 +    var ctx = canvas.getContext('2d');
   1.754 +    var notAvailableText = canvas.getAttribute("notavailabletext");
   1.755 +    ctx.save();
   1.756 +    ctx.fillStyle = "-moz-Dialog";
   1.757 +    ctx.fillRect(0, 0, width, height);
   1.758 +    ctx.translate(width/2, height/2);
   1.759 +
   1.760 +    ctx.fillStyle = "GrayText";
   1.761 +    ctx.mozTextStyle = "12pt sans serif";
   1.762 +    var len = ctx.mozMeasureText(notAvailableText);
   1.763 +    ctx.translate(-len/2,0);
   1.764 +    ctx.mozDrawText(notAvailableText);
   1.765 +    ctx.restore();
   1.766 +  },
   1.767 +
   1.768 +  toggleAdditionalInfoFields: function PO_toggleAdditionalInfoFields() {
   1.769 +    var infoBox = document.getElementById("infoBox");
   1.770 +    var infoBoxExpander = document.getElementById("infoBoxExpander");
   1.771 +    var infoBoxExpanderLabel = document.getElementById("infoBoxExpanderLabel");
   1.772 +    var additionalInfoBroadcaster = document.getElementById("additionalInfoBroadcaster");
   1.773 +
   1.774 +    if (infoBox.getAttribute("minimal") == "true") {
   1.775 +      infoBox.removeAttribute("minimal");
   1.776 +      infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("lesslabel");
   1.777 +      infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("lessaccesskey");
   1.778 +      infoBoxExpander.className = "expander-up";
   1.779 +      additionalInfoBroadcaster.removeAttribute("hidden");
   1.780 +    }
   1.781 +    else {
   1.782 +      infoBox.setAttribute("minimal", "true");
   1.783 +      infoBoxExpanderLabel.value = infoBoxExpanderLabel.getAttribute("morelabel");
   1.784 +      infoBoxExpanderLabel.accessKey = infoBoxExpanderLabel.getAttribute("moreaccesskey");
   1.785 +      infoBoxExpander.className = "expander-down";
   1.786 +      additionalInfoBroadcaster.setAttribute("hidden", "true");
   1.787 +    }
   1.788 +  },
   1.789 +};
   1.790 +
   1.791 +/**
   1.792 + * A set of utilities relating to search within Bookmarks and History.
   1.793 + */
   1.794 +var PlacesSearchBox = {
   1.795 +
   1.796 +  /**
   1.797 +   * The Search text field
   1.798 +   */
   1.799 +  get searchFilter() {
   1.800 +    return document.getElementById("searchFilter");
   1.801 +  },
   1.802 +   
   1.803 +  /**
   1.804 +   * Folders to include when searching.
   1.805 +   */
   1.806 +  _folders: [],
   1.807 +  get folders() {
   1.808 +    if (this._folders.length == 0) {
   1.809 +      this._folders.push(PlacesUtils.bookmarksMenuFolderId,
   1.810 +                         PlacesUtils.unfiledBookmarksFolderId,
   1.811 +                         PlacesUtils.toolbarFolderId);
   1.812 +    }
   1.813 +    return this._folders;
   1.814 +  },
   1.815 +  set folders(aFolders) {
   1.816 +    this._folders = aFolders;
   1.817 +    return aFolders;
   1.818 +  },
   1.819 +
   1.820 +  /**
   1.821 +   * Run a search for the specified text, over the collection specified by
   1.822 +   * the dropdown arrow. The default is all bookmarks, but can be
   1.823 +   * localized to the active collection.
   1.824 +   * @param   filterString
   1.825 +   *          The text to search for.
   1.826 +   */
   1.827 +  search: function PSB_search(filterString) {
   1.828 +    var PO = PlacesOrganizer;
   1.829 +    // If the user empties the search box manually, reset it and load all
   1.830 +    // contents of the current scope.
   1.831 +    // XXX this might be to jumpy, maybe should search for "", so results
   1.832 +    // are ungrouped, and search box not reset
   1.833 +    if (filterString == "") {
   1.834 +      PO.onPlaceSelected(false);
   1.835 +      return;
   1.836 +    }
   1.837 +
   1.838 +    let currentView = ContentArea.currentView;
   1.839 +    let currentOptions = PO.getCurrentOptions();
   1.840 +
   1.841 +    // Search according to the current scope, which was set by
   1.842 +    // PQB_setScope()
   1.843 +    switch (PlacesSearchBox.filterCollection) {
   1.844 +      case "bookmarks":
   1.845 +        currentView.applyFilter(filterString, this.folders);
   1.846 +        break;
   1.847 +      case "history":
   1.848 +        if (currentOptions.queryType != Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY) {
   1.849 +          var query = PlacesUtils.history.getNewQuery();
   1.850 +          query.searchTerms = filterString;
   1.851 +          var options = currentOptions.clone();
   1.852 +          // Make sure we're getting uri results.
   1.853 +          options.resultType = currentOptions.RESULTS_AS_URI;
   1.854 +          options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
   1.855 +          options.includeHidden = true;
   1.856 +          currentView.load([query], options);
   1.857 +        }
   1.858 +        else {
   1.859 +          currentView.applyFilter(filterString, null, true);
   1.860 +        }
   1.861 +        break;
   1.862 +      case "downloads":
   1.863 +        if (currentView == ContentTree.view) {
   1.864 +          let query = PlacesUtils.history.getNewQuery();
   1.865 +          query.searchTerms = filterString;
   1.866 +          query.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], 1);
   1.867 +          let options = currentOptions.clone();
   1.868 +          // Make sure we're getting uri results.
   1.869 +          options.resultType = currentOptions.RESULTS_AS_URI;
   1.870 +          options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
   1.871 +          options.includeHidden = true;
   1.872 +          currentView.load([query], options);
   1.873 +        }
   1.874 +        else {
   1.875 +          // The new downloads view doesn't use places for searching downloads.
   1.876 +          currentView.searchTerm = filterString;
   1.877 +        }
   1.878 +        break;
   1.879 +      default:
   1.880 +        throw "Invalid filterCollection on search";
   1.881 +    }
   1.882 +
   1.883 +    // Update the details panel
   1.884 +    PlacesOrganizer.updateDetailsPane();
   1.885 +  },
   1.886 +
   1.887 +  /**
   1.888 +   * Finds across all history, downloads or all bookmarks.
   1.889 +   */
   1.890 +  findAll: function PSB_findAll() {
   1.891 +    switch (this.filterCollection) {
   1.892 +      case "history":
   1.893 +        PlacesQueryBuilder.setScope("history");
   1.894 +        break;
   1.895 +      case "downloads":
   1.896 +        PlacesQueryBuilder.setScope("downloads");
   1.897 +        break;
   1.898 +      default:
   1.899 +        PlacesQueryBuilder.setScope("bookmarks");
   1.900 +        break;
   1.901 +    }
   1.902 +    this.focus();
   1.903 +  },
   1.904 +
   1.905 +  /**
   1.906 +   * Updates the display with the title of the current collection.
   1.907 +   * @param   aTitle
   1.908 +   *          The title of the current collection.
   1.909 +   */
   1.910 +  updateCollectionTitle: function PSB_updateCollectionTitle(aTitle) {
   1.911 +    let title = "";
   1.912 +    switch (this.filterCollection) {
   1.913 +      case "history":
   1.914 +        title = PlacesUIUtils.getString("searchHistory");
   1.915 +        break;
   1.916 +      case "downloads":
   1.917 +        title = PlacesUIUtils.getString("searchDownloads");
   1.918 +        break;
   1.919 +      default:
   1.920 +        title = PlacesUIUtils.getString("searchBookmarks");                                    
   1.921 +    }
   1.922 +    this.searchFilter.placeholder = title;
   1.923 +  },
   1.924 +
   1.925 +  /**
   1.926 +   * Gets/sets the active collection from the dropdown menu.
   1.927 +   */
   1.928 +  get filterCollection() {
   1.929 +    return this.searchFilter.getAttribute("collection");
   1.930 +  },
   1.931 +  set filterCollection(collectionName) {
   1.932 +    if (collectionName == this.filterCollection)
   1.933 +      return collectionName;
   1.934 +
   1.935 +    this.searchFilter.setAttribute("collection", collectionName);
   1.936 +    this.updateCollectionTitle();
   1.937 +
   1.938 +    return collectionName;
   1.939 +  },
   1.940 +
   1.941 +  /**
   1.942 +   * Focus the search box
   1.943 +   */
   1.944 +  focus: function PSB_focus() {
   1.945 +    this.searchFilter.focus();
   1.946 +  },
   1.947 +
   1.948 +  /**
   1.949 +   * Set up the gray text in the search bar as the Places View loads.
   1.950 +   */
   1.951 +  init: function PSB_init() {
   1.952 +    this.updateCollectionTitle();
   1.953 +  },
   1.954 +
   1.955 +  /**
   1.956 +   * Gets or sets the text shown in the Places Search Box
   1.957 +   */
   1.958 +  get value() {
   1.959 +    return this.searchFilter.value;
   1.960 +  },
   1.961 +  set value(value) {
   1.962 +    return this.searchFilter.value = value;
   1.963 +  },
   1.964 +};
   1.965 +
   1.966 +/**
   1.967 + * Functions and data for advanced query builder
   1.968 + */
   1.969 +var PlacesQueryBuilder = {
   1.970 +
   1.971 +  queries: [],
   1.972 +  queryOptions: null,
   1.973 +
   1.974 +  /**
   1.975 +   * Sets the search scope.  This can be called when no search is active, and
   1.976 +   * in that case, when the user does begin a search aScope will be used (see
   1.977 +   * PSB_search()).  If there is an active search, it's performed again to
   1.978 +   * update the content tree.
   1.979 +   * @param   aScope
   1.980 +   *          The search scope: "bookmarks", "collection", "downloads" or
   1.981 +   *          "history".
   1.982 +   */
   1.983 +  setScope: function PQB_setScope(aScope) {
   1.984 +    // Determine filterCollection, folders, and scopeButtonId based on aScope.
   1.985 +    var filterCollection;
   1.986 +    var folders = [];
   1.987 +    switch (aScope) {
   1.988 +      case "history":
   1.989 +        filterCollection = "history";
   1.990 +        break;
   1.991 +      case "bookmarks":
   1.992 +        filterCollection = "bookmarks";
   1.993 +        folders.push(PlacesUtils.bookmarksMenuFolderId,
   1.994 +                     PlacesUtils.toolbarFolderId,
   1.995 +                     PlacesUtils.unfiledBookmarksFolderId);
   1.996 +        break;
   1.997 +      case "downloads":
   1.998 +        filterCollection = "downloads";
   1.999 +        break;
  1.1000 +      default:
  1.1001 +        throw "Invalid search scope";
  1.1002 +        break;
  1.1003 +    }
  1.1004 +
  1.1005 +    // Update the search box.  Re-search if there's an active search.
  1.1006 +    PlacesSearchBox.filterCollection = filterCollection;
  1.1007 +    PlacesSearchBox.folders = folders;
  1.1008 +    var searchStr = PlacesSearchBox.searchFilter.value;
  1.1009 +    if (searchStr)
  1.1010 +      PlacesSearchBox.search(searchStr);
  1.1011 +  }
  1.1012 +};
  1.1013 +
  1.1014 +/**
  1.1015 + * Population and commands for the View Menu.
  1.1016 + */
  1.1017 +var ViewMenu = {
  1.1018 +  /**
  1.1019 +   * Removes content generated previously from a menupopup.
  1.1020 +   * @param   popup
  1.1021 +   *          The popup that contains the previously generated content.
  1.1022 +   * @param   startID
  1.1023 +   *          The id attribute of an element that is the start of the
  1.1024 +   *          dynamically generated region - remove elements after this
  1.1025 +   *          item only.
  1.1026 +   *          Must be contained by popup. Can be null (in which case the
  1.1027 +   *          contents of popup are removed).
  1.1028 +   * @param   endID
  1.1029 +   *          The id attribute of an element that is the end of the
  1.1030 +   *          dynamically generated region - remove elements up to this
  1.1031 +   *          item only.
  1.1032 +   *          Must be contained by popup. Can be null (in which case all
  1.1033 +   *          items until the end of the popup will be removed). Ignored
  1.1034 +   *          if startID is null.
  1.1035 +   * @returns The element for the caller to insert new items before,
  1.1036 +   *          null if the caller should just append to the popup.
  1.1037 +   */
  1.1038 +  _clean: function VM__clean(popup, startID, endID) {
  1.1039 +    if (endID)
  1.1040 +      NS_ASSERT(startID, "meaningless to have valid endID and null startID");
  1.1041 +    if (startID) {
  1.1042 +      var startElement = document.getElementById(startID);
  1.1043 +      NS_ASSERT(startElement.parentNode ==
  1.1044 +                popup, "startElement is not in popup");
  1.1045 +      NS_ASSERT(startElement,
  1.1046 +                "startID does not correspond to an existing element");
  1.1047 +      var endElement = null;
  1.1048 +      if (endID) {
  1.1049 +        endElement = document.getElementById(endID);
  1.1050 +        NS_ASSERT(endElement.parentNode == popup,
  1.1051 +                  "endElement is not in popup");
  1.1052 +        NS_ASSERT(endElement,
  1.1053 +                  "endID does not correspond to an existing element");
  1.1054 +      }
  1.1055 +      while (startElement.nextSibling != endElement)
  1.1056 +        popup.removeChild(startElement.nextSibling);
  1.1057 +      return endElement;
  1.1058 +    }
  1.1059 +    else {
  1.1060 +      while(popup.hasChildNodes())
  1.1061 +        popup.removeChild(popup.firstChild);
  1.1062 +    }
  1.1063 +    return null;
  1.1064 +  },
  1.1065 +
  1.1066 +  /**
  1.1067 +   * Fills a menupopup with a list of columns
  1.1068 +   * @param   event
  1.1069 +   *          The popupshowing event that invoked this function.
  1.1070 +   * @param   startID
  1.1071 +   *          see _clean
  1.1072 +   * @param   endID
  1.1073 +   *          see _clean
  1.1074 +   * @param   type
  1.1075 +   *          the type of the menuitem, e.g. "radio" or "checkbox".
  1.1076 +   *          Can be null (no-type).
  1.1077 +   *          Checkboxes are checked if the column is visible.
  1.1078 +   * @param   propertyPrefix
  1.1079 +   *          If propertyPrefix is non-null:
  1.1080 +   *          propertyPrefix + column ID + ".label" will be used to get the
  1.1081 +   *          localized label string.
  1.1082 +   *          propertyPrefix + column ID + ".accesskey" will be used to get the
  1.1083 +   *          localized accesskey.
  1.1084 +   *          If propertyPrefix is null, the column label is used as label and
  1.1085 +   *          no accesskey is assigned.
  1.1086 +   */
  1.1087 +  fillWithColumns: function VM_fillWithColumns(event, startID, endID, type, propertyPrefix) {
  1.1088 +    var popup = event.target;
  1.1089 +    var pivot = this._clean(popup, startID, endID);
  1.1090 +
  1.1091 +    // If no column is "sort-active", the "Unsorted" item needs to be checked,
  1.1092 +    // so track whether or not we find a column that is sort-active.
  1.1093 +    var isSorted = false;
  1.1094 +    var content = document.getElementById("placeContent");
  1.1095 +    var columns = content.columns;
  1.1096 +    for (var i = 0; i < columns.count; ++i) {
  1.1097 +      var column = columns.getColumnAt(i).element;
  1.1098 +      var menuitem = document.createElement("menuitem");
  1.1099 +      menuitem.id = "menucol_" + column.id;
  1.1100 +      menuitem.column = column;
  1.1101 +      var label = column.getAttribute("label");
  1.1102 +      if (propertyPrefix) {
  1.1103 +        var menuitemPrefix = propertyPrefix;
  1.1104 +        // for string properties, use "name" as the id, instead of "title"
  1.1105 +        // see bug #386287 for details
  1.1106 +        var columnId = column.getAttribute("anonid");
  1.1107 +        menuitemPrefix += columnId == "title" ? "name" : columnId;
  1.1108 +        label = PlacesUIUtils.getString(menuitemPrefix + ".label");
  1.1109 +        var accesskey = PlacesUIUtils.getString(menuitemPrefix + ".accesskey");
  1.1110 +        menuitem.setAttribute("accesskey", accesskey);
  1.1111 +      }
  1.1112 +      menuitem.setAttribute("label", label);
  1.1113 +      if (type == "radio") {
  1.1114 +        menuitem.setAttribute("type", "radio");
  1.1115 +        menuitem.setAttribute("name", "columns");
  1.1116 +        // This column is the sort key. Its item is checked.
  1.1117 +        if (column.getAttribute("sortDirection") != "") {
  1.1118 +          menuitem.setAttribute("checked", "true");
  1.1119 +          isSorted = true;
  1.1120 +        }
  1.1121 +      }
  1.1122 +      else if (type == "checkbox") {
  1.1123 +        menuitem.setAttribute("type", "checkbox");
  1.1124 +        // Cannot uncheck the primary column.
  1.1125 +        if (column.getAttribute("primary") == "true")
  1.1126 +          menuitem.setAttribute("disabled", "true");
  1.1127 +        // Items for visible columns are checked.
  1.1128 +        if (!column.hidden)
  1.1129 +          menuitem.setAttribute("checked", "true");
  1.1130 +      }
  1.1131 +      if (pivot)
  1.1132 +        popup.insertBefore(menuitem, pivot);
  1.1133 +      else
  1.1134 +        popup.appendChild(menuitem);
  1.1135 +    }
  1.1136 +    event.stopPropagation();
  1.1137 +  },
  1.1138 +
  1.1139 +  /**
  1.1140 +   * Set up the content of the view menu.
  1.1141 +   */
  1.1142 +  populateSortMenu: function VM_populateSortMenu(event) {
  1.1143 +    this.fillWithColumns(event, "viewUnsorted", "directionSeparator", "radio", "view.sortBy.1.");
  1.1144 +
  1.1145 +    var sortColumn = this._getSortColumn();
  1.1146 +    var viewSortAscending = document.getElementById("viewSortAscending");
  1.1147 +    var viewSortDescending = document.getElementById("viewSortDescending");
  1.1148 +    // We need to remove an existing checked attribute because the unsorted
  1.1149 +    // menu item is not rebuilt every time we open the menu like the others.
  1.1150 +    var viewUnsorted = document.getElementById("viewUnsorted");
  1.1151 +    if (!sortColumn) {
  1.1152 +      viewSortAscending.removeAttribute("checked");
  1.1153 +      viewSortDescending.removeAttribute("checked");
  1.1154 +      viewUnsorted.setAttribute("checked", "true");
  1.1155 +    }
  1.1156 +    else if (sortColumn.getAttribute("sortDirection") == "ascending") {
  1.1157 +      viewSortAscending.setAttribute("checked", "true");
  1.1158 +      viewSortDescending.removeAttribute("checked");
  1.1159 +      viewUnsorted.removeAttribute("checked");
  1.1160 +    }
  1.1161 +    else if (sortColumn.getAttribute("sortDirection") == "descending") {
  1.1162 +      viewSortDescending.setAttribute("checked", "true");
  1.1163 +      viewSortAscending.removeAttribute("checked");
  1.1164 +      viewUnsorted.removeAttribute("checked");
  1.1165 +    }
  1.1166 +  },
  1.1167 +
  1.1168 +  /**
  1.1169 +   * Shows/Hides a tree column.
  1.1170 +   * @param   element
  1.1171 +   *          The menuitem element for the column
  1.1172 +   */
  1.1173 +  showHideColumn: function VM_showHideColumn(element) {
  1.1174 +    var column = element.column;
  1.1175 +
  1.1176 +    var splitter = column.nextSibling;
  1.1177 +    if (splitter && splitter.localName != "splitter")
  1.1178 +      splitter = null;
  1.1179 +
  1.1180 +    if (element.getAttribute("checked") == "true") {
  1.1181 +      column.setAttribute("hidden", "false");
  1.1182 +      if (splitter)
  1.1183 +        splitter.removeAttribute("hidden");
  1.1184 +    }
  1.1185 +    else {
  1.1186 +      column.setAttribute("hidden", "true");
  1.1187 +      if (splitter)
  1.1188 +        splitter.setAttribute("hidden", "true");
  1.1189 +    }
  1.1190 +  },
  1.1191 +
  1.1192 +  /**
  1.1193 +   * Gets the last column that was sorted.
  1.1194 +   * @returns  the currently sorted column, null if there is no sorted column.
  1.1195 +   */
  1.1196 +  _getSortColumn: function VM__getSortColumn() {
  1.1197 +    var content = document.getElementById("placeContent");
  1.1198 +    var cols = content.columns;
  1.1199 +    for (var i = 0; i < cols.count; ++i) {
  1.1200 +      var column = cols.getColumnAt(i).element;
  1.1201 +      var sortDirection = column.getAttribute("sortDirection");
  1.1202 +      if (sortDirection == "ascending" || sortDirection == "descending")
  1.1203 +        return column;
  1.1204 +    }
  1.1205 +    return null;
  1.1206 +  },
  1.1207 +
  1.1208 +  /**
  1.1209 +   * Sorts the view by the specified column.
  1.1210 +   * @param   aColumn
  1.1211 +   *          The colum that is the sort key. Can be null - the
  1.1212 +   *          current sort column or the title column will be used.
  1.1213 +   * @param   aDirection
  1.1214 +   *          The direction to sort - "ascending" or "descending".
  1.1215 +   *          Can be null - the last direction or descending will be used.
  1.1216 +   *
  1.1217 +   * If both aColumnID and aDirection are null, the view will be unsorted.
  1.1218 +   */
  1.1219 +  setSortColumn: function VM_setSortColumn(aColumn, aDirection) {
  1.1220 +    var result = document.getElementById("placeContent").result;
  1.1221 +    if (!aColumn && !aDirection) {
  1.1222 +      result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
  1.1223 +      return;
  1.1224 +    }
  1.1225 +
  1.1226 +    var columnId;
  1.1227 +    if (aColumn) {
  1.1228 +      columnId = aColumn.getAttribute("anonid");
  1.1229 +      if (!aDirection) {
  1.1230 +        var sortColumn = this._getSortColumn();
  1.1231 +        if (sortColumn)
  1.1232 +          aDirection = sortColumn.getAttribute("sortDirection");
  1.1233 +      }
  1.1234 +    }
  1.1235 +    else {
  1.1236 +      var sortColumn = this._getSortColumn();
  1.1237 +      columnId = sortColumn ? sortColumn.getAttribute("anonid") : "title";
  1.1238 +    }
  1.1239 +
  1.1240 +    // This maps the possible values of columnId (i.e., anonid's of treecols in
  1.1241 +    // placeContent) to the default sortingMode and sortingAnnotation values for
  1.1242 +    // each column.
  1.1243 +    //   key:  Sort key in the name of one of the
  1.1244 +    //         nsINavHistoryQueryOptions.SORT_BY_* constants
  1.1245 +    //   dir:  Default sort direction to use if none has been specified
  1.1246 +    //   anno: The annotation to sort by, if key is "ANNOTATION"
  1.1247 +    var colLookupTable = {
  1.1248 +      title:        { key: "TITLE",        dir: "ascending"  },
  1.1249 +      tags:         { key: "TAGS",         dir: "ascending"  },
  1.1250 +      url:          { key: "URI",          dir: "ascending"  },
  1.1251 +      date:         { key: "DATE",         dir: "descending" },
  1.1252 +      visitCount:   { key: "VISITCOUNT",   dir: "descending" },
  1.1253 +      keyword:      { key: "KEYWORD",      dir: "ascending"  },
  1.1254 +      dateAdded:    { key: "DATEADDED",    dir: "descending" },
  1.1255 +      lastModified: { key: "LASTMODIFIED", dir: "descending" },
  1.1256 +      description:  { key: "ANNOTATION",
  1.1257 +                      dir: "ascending",
  1.1258 +                      anno: PlacesUIUtils.DESCRIPTION_ANNO }
  1.1259 +    };
  1.1260 +
  1.1261 +    // Make sure we have a valid column.
  1.1262 +    if (!colLookupTable.hasOwnProperty(columnId))
  1.1263 +      throw("Invalid column");
  1.1264 +
  1.1265 +    // Use a default sort direction if none has been specified.  If aDirection
  1.1266 +    // is invalid, result.sortingMode will be undefined, which has the effect
  1.1267 +    // of unsorting the tree.
  1.1268 +    aDirection = (aDirection || colLookupTable[columnId].dir).toUpperCase();
  1.1269 +
  1.1270 +    var sortConst = "SORT_BY_" + colLookupTable[columnId].key + "_" + aDirection;
  1.1271 +    result.sortingAnnotation = colLookupTable[columnId].anno || "";
  1.1272 +    result.sortingMode = Ci.nsINavHistoryQueryOptions[sortConst];
  1.1273 +  }
  1.1274 +}
  1.1275 +
  1.1276 +let ContentArea = {
  1.1277 +  _specialViews: new Map(),
  1.1278 +
  1.1279 +  init: function CA_init() {
  1.1280 +    this._deck = document.getElementById("placesViewsDeck");
  1.1281 +    this._toolbar = document.getElementById("placesToolbar");
  1.1282 +    ContentTree.init();
  1.1283 +    this._setupView();
  1.1284 +  },
  1.1285 +
  1.1286 +  /**
  1.1287 +   * Gets the content view to be used for loading the given query.
  1.1288 +   * If a custom view was set by setContentViewForQueryString, that
  1.1289 +   * view would be returned, else the default tree view is returned
  1.1290 +   *
  1.1291 +   * @param aQueryString
  1.1292 +   *        a query string
  1.1293 +   * @return the view to be used for loading aQueryString.
  1.1294 +   */
  1.1295 +  getContentViewForQueryString:
  1.1296 +  function CA_getContentViewForQueryString(aQueryString) {
  1.1297 +    try {
  1.1298 +      if (this._specialViews.has(aQueryString)) {
  1.1299 +        let { view, options } = this._specialViews.get(aQueryString);
  1.1300 +        if (typeof view == "function") {
  1.1301 +          view = view();
  1.1302 +          this._specialViews.set(aQueryString, { view: view, options: options });
  1.1303 +        }
  1.1304 +        return view;
  1.1305 +      }
  1.1306 +    }
  1.1307 +    catch(ex) {
  1.1308 +      Components.utils.reportError(ex);
  1.1309 +    }
  1.1310 +    return ContentTree.view;
  1.1311 +  },
  1.1312 +
  1.1313 +  /**
  1.1314 +   * Sets a custom view to be used rather than the default places tree
  1.1315 +   * whenever the given query is selected in the left pane.
  1.1316 +   * @param aQueryString
  1.1317 +   *        a query string
  1.1318 +   * @param aView
  1.1319 +   *        Either the custom view or a function that will return the view
  1.1320 +   *        the first (and only) time it's called.
  1.1321 +   * @param [optional] aOptions
  1.1322 +   *        Object defining special options for the view.
  1.1323 +   * @see ContentTree.viewOptions for supported options and default values.
  1.1324 +   */
  1.1325 +  setContentViewForQueryString:
  1.1326 +  function CA_setContentViewForQueryString(aQueryString, aView, aOptions) {
  1.1327 +    if (!aQueryString ||
  1.1328 +        typeof aView != "object" && typeof aView != "function")
  1.1329 +      throw new Error("Invalid arguments");
  1.1330 +
  1.1331 +    this._specialViews.set(aQueryString, { view: aView,
  1.1332 +                                           options: aOptions || new Object() });
  1.1333 +  },
  1.1334 +
  1.1335 +  get currentView() PlacesUIUtils.getViewForNode(this._deck.selectedPanel),
  1.1336 +  set currentView(aNewView) {
  1.1337 +    let oldView = this.currentView;
  1.1338 +    if (oldView != aNewView) {
  1.1339 +      this._deck.selectedPanel = aNewView.associatedElement;
  1.1340 +
  1.1341 +      // If the content area inactivated view was focused, move focus
  1.1342 +      // to the new view.
  1.1343 +      if (document.activeElement == oldView.associatedElement)
  1.1344 +        aNewView.associatedElement.focus();
  1.1345 +    }
  1.1346 +    return aNewView;
  1.1347 +  },
  1.1348 +
  1.1349 +  get currentPlace() this.currentView.place,
  1.1350 +  set currentPlace(aQueryString) {
  1.1351 +    let oldView = this.currentView;
  1.1352 +    let newView = this.getContentViewForQueryString(aQueryString);
  1.1353 +    newView.place = aQueryString;
  1.1354 +    if (oldView != newView) {
  1.1355 +      oldView.active = false;
  1.1356 +      this.currentView = newView;
  1.1357 +      this._setupView();
  1.1358 +      newView.active = true;
  1.1359 +    }
  1.1360 +    return aQueryString;
  1.1361 +  },
  1.1362 +
  1.1363 +  /**
  1.1364 +   * Applies view options.
  1.1365 +   */
  1.1366 +  _setupView: function CA__setupView() {
  1.1367 +    let options = this.currentViewOptions;
  1.1368 +
  1.1369 +    // showDetailsPane.
  1.1370 +    let detailsDeck = document.getElementById("detailsDeck");
  1.1371 +    detailsDeck.hidden = !options.showDetailsPane;
  1.1372 +
  1.1373 +    // toolbarSet.
  1.1374 +    for (let elt of this._toolbar.childNodes) {
  1.1375 +      // On Windows and Linux the menu buttons are menus wrapped in a menubar.
  1.1376 +      if (elt.id == "placesMenu") {
  1.1377 +        for (let menuElt of elt.childNodes) {
  1.1378 +          menuElt.hidden = options.toolbarSet.indexOf(menuElt.id) == -1;
  1.1379 +        }
  1.1380 +      }
  1.1381 +      else {
  1.1382 +        elt.hidden = options.toolbarSet.indexOf(elt.id) == -1;
  1.1383 +      }
  1.1384 +    }
  1.1385 +  },
  1.1386 +
  1.1387 +  /**
  1.1388 +   * Options for the current view.
  1.1389 +   *
  1.1390 +   * @see ContentTree.viewOptions for supported options and default values.
  1.1391 +   */
  1.1392 +  get currentViewOptions() {
  1.1393 +    // Use ContentTree options as default.
  1.1394 +    let viewOptions = ContentTree.viewOptions;
  1.1395 +    if (this._specialViews.has(this.currentPlace)) {
  1.1396 +      let { view, options } = this._specialViews.get(this.currentPlace);
  1.1397 +      for (let option in options) {
  1.1398 +        viewOptions[option] = options[option];
  1.1399 +      }
  1.1400 +    }
  1.1401 +    return viewOptions;
  1.1402 +  },
  1.1403 +
  1.1404 +  focus: function() {
  1.1405 +    this._deck.selectedPanel.focus();
  1.1406 +  }
  1.1407 +};
  1.1408 +
  1.1409 +let ContentTree = {
  1.1410 +  init: function CT_init() {
  1.1411 +    this._view = document.getElementById("placeContent");
  1.1412 +  },
  1.1413 +
  1.1414 +  get view() this._view,
  1.1415 +
  1.1416 +  get viewOptions() Object.seal({
  1.1417 +    showDetailsPane: true,
  1.1418 +    toolbarSet: "back-button, forward-button, organizeButton, viewMenu, maintenanceButton, libraryToolbarSpacer, searchFilter"
  1.1419 +  }),
  1.1420 +
  1.1421 +  openSelectedNode: function CT_openSelectedNode(aEvent) {
  1.1422 +    let view = this.view;
  1.1423 +    PlacesUIUtils.openNodeWithEvent(view.selectedNode, aEvent, view);
  1.1424 +  },
  1.1425 +
  1.1426 +  onClick: function CT_onClick(aEvent) {
  1.1427 +    let node = this.view.selectedNode;
  1.1428 +    if (node) {
  1.1429 +      let doubleClick = aEvent.button == 0 && aEvent.detail == 2;
  1.1430 +      let middleClick = aEvent.button == 1 && aEvent.detail == 1;
  1.1431 +      if (PlacesUtils.nodeIsURI(node) && (doubleClick || middleClick)) {
  1.1432 +        // Open associated uri in the browser.
  1.1433 +        this.openSelectedNode(aEvent);
  1.1434 +      }
  1.1435 +      else if (middleClick && PlacesUtils.nodeIsContainer(node)) {
  1.1436 +        // The command execution function will take care of seeing if the
  1.1437 +        // selection is a folder or a different container type, and will
  1.1438 +        // load its contents in tabs.
  1.1439 +        PlacesUIUtils.openContainerNodeInTabs(node, aEvent, this.view);
  1.1440 +      }
  1.1441 +    }
  1.1442 +  },
  1.1443 +
  1.1444 +  onKeyPress: function CT_onKeyPress(aEvent) {
  1.1445 +    if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN)
  1.1446 +      this.openSelectedNode(aEvent);
  1.1447 +  }
  1.1448 +};

mercurial