browser/components/places/content/treeView.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/components/places/content/treeView.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1689 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
     1.9 +
    1.10 +const PTV_interfaces = [Ci.nsITreeView,
    1.11 +                        Ci.nsINavHistoryResultObserver,
    1.12 +                        Ci.nsINavHistoryResultTreeViewer,
    1.13 +                        Ci.nsISupportsWeakReference];
    1.14 +
    1.15 +function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) {
    1.16 +  this._tree = null;
    1.17 +  this._result = null;
    1.18 +  this._selection = null;
    1.19 +  this._rootNode = null;
    1.20 +  this._rows = [];
    1.21 +  this._flatList = aFlatList;
    1.22 +  this._openContainerCallback = aOnOpenFlatContainer;
    1.23 +  this._controller = aController;
    1.24 +}
    1.25 +
    1.26 +PlacesTreeView.prototype = {
    1.27 +  get wrappedJSObject() this,
    1.28 +
    1.29 +  __dateService: null,
    1.30 +  get _dateService() {
    1.31 +    if (!this.__dateService) {
    1.32 +      this.__dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"].
    1.33 +                           getService(Ci.nsIScriptableDateFormat);
    1.34 +    }
    1.35 +    return this.__dateService;
    1.36 +  },
    1.37 +
    1.38 +  QueryInterface: XPCOMUtils.generateQI(PTV_interfaces),
    1.39 +
    1.40 +  // Bug 761494:
    1.41 +  // ----------
    1.42 +  // Some addons use methods from nsINavHistoryResultObserver and
    1.43 +  // nsINavHistoryResultTreeViewer, without QIing to these interfaces first.
    1.44 +  // That's not a problem when the view is retrieved through the
    1.45 +  // <tree>.view getter (which returns the wrappedJSObject of this object),
    1.46 +  // it raises an issue when the view retrieved through the treeBoxObject.view
    1.47 +  // getter.  Thus, to avoid breaking addons, the interfaces are prefetched.
    1.48 +  classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }),
    1.49 +
    1.50 +  /**
    1.51 +   * This is called once both the result and the tree are set.
    1.52 +   */
    1.53 +  _finishInit: function PTV__finishInit() {
    1.54 +    let selection = this.selection;
    1.55 +    if (selection)
    1.56 +      selection.selectEventsSuppressed = true;
    1.57 +
    1.58 +    if (!this._rootNode.containerOpen) {
    1.59 +      // This triggers containerStateChanged which then builds the visible
    1.60 +      // section.
    1.61 +      this._rootNode.containerOpen = true;
    1.62 +    }
    1.63 +    else
    1.64 +      this.invalidateContainer(this._rootNode);
    1.65 +
    1.66 +    // "Activate" the sorting column and update commands.
    1.67 +    this.sortingChanged(this._result.sortingMode);
    1.68 +
    1.69 +    if (selection)
    1.70 +      selection.selectEventsSuppressed = false;
    1.71 +  },
    1.72 +
    1.73 +  /**
    1.74 +   * Plain Container: container result nodes which may never include sub
    1.75 +   * hierarchies.
    1.76 +   *
    1.77 +   * When the rows array is constructed, we don't set the children of plain
    1.78 +   * containers.  Instead, we keep placeholders for these children.  We then
    1.79 +   * build these children lazily as the tree asks us for information about each
    1.80 +   * row.  Luckily, the tree doesn't ask about rows outside the visible area.
    1.81 +   *
    1.82 +   * @see _getNodeForRow and _getRowForNode for the actual magic.
    1.83 +   *
    1.84 +   * @note It's guaranteed that all containers are listed in the rows
    1.85 +   * elements array.  It's also guaranteed that separators (if they're not
    1.86 +   * filtered, see below) are listed in the visible elements array, because
    1.87 +   * bookmark folders are never built lazily, as described above.
    1.88 +   *
    1.89 +   * @param aContainer
    1.90 +   *        A container result node.
    1.91 +   *
    1.92 +   * @return true if aContainer is a plain container, false otherwise.
    1.93 +   */
    1.94 +  _isPlainContainer: function PTV__isPlainContainer(aContainer) {
    1.95 +    // Livemarks are always plain containers.
    1.96 +    if (this._controller.hasCachedLivemarkInfo(aContainer))
    1.97 +      return true;
    1.98 +
    1.99 +    // We don't know enough about non-query containers.
   1.100 +    if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode))
   1.101 +      return false;
   1.102 +
   1.103 +    switch (aContainer.queryOptions.resultType) {
   1.104 +      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
   1.105 +      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
   1.106 +      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
   1.107 +      case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY:
   1.108 +        return false;
   1.109 +    }
   1.110 +
   1.111 +    // If it's a folder, it's not a plain container.
   1.112 +    let nodeType = aContainer.type;
   1.113 +    return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER &&
   1.114 +           nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
   1.115 +  },
   1.116 +
   1.117 +  /**
   1.118 +   * Gets the row number for a given node.  Assumes that the given node is
   1.119 +   * visible (i.e. it's not an obsolete node).
   1.120 +   *
   1.121 +   * @param aNode
   1.122 +   *        A result node.  Do not pass an obsolete node, or any
   1.123 +   *        node which isn't supposed to be in the tree (e.g. separators in
   1.124 +   *        sorted trees).
   1.125 +   * @param [optional] aForceBuild
   1.126 +   *        @see _isPlainContainer.
   1.127 +   *        If true, the row will be computed even if the node still isn't set
   1.128 +   *        in our rows array.
   1.129 +   * @param [optional] aParentRow
   1.130 +   *        The row of aNode's parent. Ignored for the root node.
   1.131 +   * @param [optional] aNodeIndex
   1.132 +   *        The index of aNode in its parent.  Only used if aParentRow is
   1.133 +   *        set too.
   1.134 +   *
   1.135 +   * @throws if aNode is invisible.
   1.136 +   * @note If aParentRow and aNodeIndex are passed and parent is a plain
   1.137 +   * container, this method will just return a calculated row value, without
   1.138 +   * making assumptions on existence of the node at that position.
   1.139 +   * @return aNode's row if it's in the rows list or if aForceBuild is set, -1
   1.140 +   *         otherwise.
   1.141 +   */
   1.142 +  _getRowForNode:
   1.143 +  function PTV__getRowForNode(aNode, aForceBuild, aParentRow, aNodeIndex) {
   1.144 +    if (aNode == this._rootNode)
   1.145 +      throw new Error("The root node is never visible");
   1.146 +
   1.147 +    // A node is removed form the view either if it has no parent or if its
   1.148 +    // root-ancestor is not the root node (in which case that's the node
   1.149 +    // for which nodeRemoved was called).
   1.150 +    let ancestors = [x for each (x in PlacesUtils.nodeAncestors(aNode))];
   1.151 +    if (ancestors.length == 0 ||
   1.152 +        ancestors[ancestors.length - 1] != this._rootNode) {
   1.153 +      throw new Error("Removed node passed to _getRowForNode");
   1.154 +    }
   1.155 +
   1.156 +    // Ensure that the entire chain is open, otherwise that node is invisible.
   1.157 +    for (let ancestor of ancestors) {
   1.158 +      if (!ancestor.containerOpen)
   1.159 +        throw new Error("Invisible node passed to _getRowForNode");
   1.160 +    }
   1.161 +
   1.162 +    // Non-plain containers are initially built with their contents.
   1.163 +    let parent = aNode.parent;
   1.164 +    let parentIsPlain = this._isPlainContainer(parent);
   1.165 +    if (!parentIsPlain) {
   1.166 +      if (parent == this._rootNode)
   1.167 +        return this._rows.indexOf(aNode);
   1.168 +
   1.169 +      return this._rows.indexOf(aNode, aParentRow);
   1.170 +    }
   1.171 +
   1.172 +    let row = -1;
   1.173 +    let useNodeIndex = typeof(aNodeIndex) == "number";
   1.174 +    if (parent == this._rootNode)
   1.175 +      row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode);
   1.176 +    else if (useNodeIndex && typeof(aParentRow) == "number") {
   1.177 +      // If we have both the row of the parent node, and the node's index, we
   1.178 +      // can avoid searching the rows array if the parent is a plain container.
   1.179 +      row = aParentRow + aNodeIndex + 1;
   1.180 +    }
   1.181 +    else {
   1.182 +      // Look for the node in the nodes array.  Start the search at the parent
   1.183 +      // row.  If the parent row isn't passed, we'll pass undefined to indexOf,
   1.184 +      // which is fine.
   1.185 +      row = this._rows.indexOf(aNode, aParentRow);
   1.186 +      if (row == -1 && aForceBuild) {
   1.187 +        let parentRow = typeof(aParentRow) == "number" ? aParentRow
   1.188 +                                                       : this._getRowForNode(parent);
   1.189 +        row = parentRow + parent.getChildIndex(aNode) + 1;
   1.190 +      }
   1.191 +    }
   1.192 +
   1.193 +    if (row != -1)
   1.194 +      this._rows[row] = aNode;
   1.195 +
   1.196 +    return row;
   1.197 +  },
   1.198 +
   1.199 +  /**
   1.200 +   * Given a row, finds and returns the parent details of the associated node.
   1.201 +   *
   1.202 +   * @param aChildRow
   1.203 +   *        Row number.
   1.204 +   * @return [parentNode, parentRow]
   1.205 +   */
   1.206 +  _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) {
   1.207 +    let node = this._getNodeForRow(aChildRow);
   1.208 +    let parent = (node === null) ? this._rootNode : node.parent;
   1.209 +
   1.210 +    // The root node is never visible
   1.211 +    if (parent == this._rootNode)
   1.212 +      return [this._rootNode, -1];
   1.213 +
   1.214 +    let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1);
   1.215 +    return [parent, parentRow];
   1.216 +  },
   1.217 +
   1.218 +  /**
   1.219 +   * Gets the node at a given row.
   1.220 +   */
   1.221 +  _getNodeForRow: function PTV__getNodeForRow(aRow) {
   1.222 +    if (aRow < 0) {
   1.223 +      return null;
   1.224 +    }
   1.225 +  
   1.226 +    let node = this._rows[aRow];
   1.227 +    if (node !== undefined)
   1.228 +      return node;
   1.229 +
   1.230 +    // Find the nearest node.
   1.231 +    let rowNode, row;
   1.232 +    for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) {
   1.233 +      rowNode = this._rows[i];
   1.234 +      row = i;
   1.235 +    }
   1.236 +
   1.237 +    // If there's no container prior to the given row, it's a child of
   1.238 +    // the root node (remember: all containers are listed in the rows array).
   1.239 +    if (!rowNode)
   1.240 +      return this._rows[aRow] = this._rootNode.getChild(aRow);
   1.241 +
   1.242 +    // Unset elements may exist only in plain containers.  Thus, if the nearest
   1.243 +    // node is a container, it's the row's parent, otherwise, it's a sibling.
   1.244 +    if (rowNode instanceof Ci.nsINavHistoryContainerResultNode)
   1.245 +      return this._rows[aRow] = rowNode.getChild(aRow - row - 1);
   1.246 +
   1.247 +    let [parent, parentRow] = this._getParentByChildRow(row);
   1.248 +    return this._rows[aRow] = parent.getChild(aRow - parentRow - 1);
   1.249 +  },
   1.250 +
   1.251 +  /**
   1.252 +   * This takes a container and recursively appends our rows array per its
   1.253 +   * contents.  Assumes that the rows arrays has no rows for the given
   1.254 +   * container.
   1.255 +   *
   1.256 +   * @param [in] aContainer
   1.257 +   *        A container result node.
   1.258 +   * @param [in] aFirstChildRow
   1.259 +   *        The first row at which nodes may be inserted to the row array.
   1.260 +   *        In other words, that's aContainer's row + 1.
   1.261 +   * @param [out] aToOpen
   1.262 +   *        An array of containers to open once the build is done.
   1.263 +   *
   1.264 +   * @return the number of rows which were inserted.
   1.265 +   */
   1.266 +  _buildVisibleSection:
   1.267 +  function PTV__buildVisibleSection(aContainer, aFirstChildRow, aToOpen)
   1.268 +  {
   1.269 +    // There's nothing to do if the container is closed.
   1.270 +    if (!aContainer.containerOpen)
   1.271 +      return 0;
   1.272 +
   1.273 +    // Inserting the new elements into the rows array in one shot (by
   1.274 +    // Array.concat) is faster than resizing the array (by splice) on each loop
   1.275 +    // iteration.
   1.276 +    let cc = aContainer.childCount;
   1.277 +    let newElements = new Array(cc);
   1.278 +    this._rows = this._rows.splice(0, aFirstChildRow)
   1.279 +                     .concat(newElements, this._rows);
   1.280 +
   1.281 +    if (this._isPlainContainer(aContainer))
   1.282 +      return cc;
   1.283 +
   1.284 +    const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open");
   1.285 +    const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true");
   1.286 +    let sortingMode = this._result.sortingMode;
   1.287 +
   1.288 +    let rowsInserted = 0;
   1.289 +    for (let i = 0; i < cc; i++) {
   1.290 +      let curChild = aContainer.getChild(i);
   1.291 +      let curChildType = curChild.type;
   1.292 +
   1.293 +      let row = aFirstChildRow + rowsInserted;
   1.294 +
   1.295 +      // Don't display separators when sorted.
   1.296 +      if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
   1.297 +        if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
   1.298 +          // Remove the element for the filtered separator.
   1.299 +          // Notice that the rows array was initially resized to include all
   1.300 +          // children.
   1.301 +          this._rows.splice(row, 1);
   1.302 +          continue;
   1.303 +        }
   1.304 +      }
   1.305 +
   1.306 +      this._rows[row] = curChild;
   1.307 +      rowsInserted++;
   1.308 +
   1.309 +      // Recursively do containers.
   1.310 +      if (!this._flatList &&
   1.311 +          curChild instanceof Ci.nsINavHistoryContainerResultNode &&
   1.312 +          !this._controller.hasCachedLivemarkInfo(curChild)) {
   1.313 +        let resource = this._getResourceForNode(curChild);
   1.314 +        let isopen = resource != null &&
   1.315 +                     PlacesUIUtils.localStore.HasAssertion(resource,
   1.316 +                                                           openLiteral,
   1.317 +                                                           trueLiteral, true);
   1.318 +        if (isopen != curChild.containerOpen)
   1.319 +          aToOpen.push(curChild);
   1.320 +        else if (curChild.containerOpen && curChild.childCount > 0)
   1.321 +          rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen);
   1.322 +      }
   1.323 +    }
   1.324 +
   1.325 +    return rowsInserted;
   1.326 +  },
   1.327 +
   1.328 +  /**
   1.329 +   * This counts how many rows a node takes in the tree.  For containers it
   1.330 +   * will count the node itself plus any child node following it.
   1.331 +   */
   1.332 +  _countVisibleRowsForNodeAtRow:
   1.333 +  function PTV__countVisibleRowsForNodeAtRow(aNodeRow) {
   1.334 +    let node = this._rows[aNodeRow];
   1.335 +
   1.336 +    // If it's not listed yet, we know that it's a leaf node (instanceof also
   1.337 +    // null-checks).
   1.338 +    if (!(node instanceof Ci.nsINavHistoryContainerResultNode))
   1.339 +      return 1;
   1.340 +
   1.341 +    let outerLevel = node.indentLevel;
   1.342 +    for (let i = aNodeRow + 1; i < this._rows.length; i++) {
   1.343 +      let rowNode = this._rows[i];
   1.344 +      if (rowNode && rowNode.indentLevel <= outerLevel)
   1.345 +        return i - aNodeRow;
   1.346 +    }
   1.347 +
   1.348 +    // This node plus its children take up the bottom of the list.
   1.349 +    return this._rows.length - aNodeRow;
   1.350 +  },
   1.351 +
   1.352 +  _getSelectedNodesInRange:
   1.353 +  function PTV__getSelectedNodesInRange(aFirstRow, aLastRow) {
   1.354 +    let selection = this.selection;
   1.355 +    let rc = selection.getRangeCount();
   1.356 +    if (rc == 0)
   1.357 +      return [];
   1.358 +
   1.359 +    // The visible-area borders are needed for checking whether a
   1.360 +    // selected row is also visible.
   1.361 +    let firstVisibleRow = this._tree.getFirstVisibleRow();
   1.362 +    let lastVisibleRow = this._tree.getLastVisibleRow();
   1.363 +
   1.364 +    let nodesInfo = [];
   1.365 +    for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) {
   1.366 +      let min = { }, max = { };
   1.367 +      selection.getRangeAt(rangeIndex, min, max);
   1.368 +
   1.369 +      // If this range does not overlap the replaced chunk, we don't need to
   1.370 +      // persist the selection.
   1.371 +      if (max.value < aFirstRow || min.value > aLastRow)
   1.372 +        continue;
   1.373 +
   1.374 +      let firstRow = Math.max(min.value, aFirstRow);
   1.375 +      let lastRow = Math.min(max.value, aLastRow);
   1.376 +      for (let i = firstRow; i <= lastRow; i++) {
   1.377 +        nodesInfo.push({
   1.378 +          node: this._rows[i],
   1.379 +          oldRow: i,
   1.380 +          wasVisible: i >= firstVisibleRow && i <= lastVisibleRow
   1.381 +        });
   1.382 +      }
   1.383 +    }
   1.384 +
   1.385 +    return nodesInfo;
   1.386 +  },
   1.387 +
   1.388 +  /**
   1.389 +   * Tries to find an equivalent node for a node which was removed.  We first
   1.390 +   * look for the original node, in case it was just relocated.  Then, if we
   1.391 +   * that node was not found, we look for a node that has the same itemId, uri
   1.392 +   * and time values.
   1.393 +   *
   1.394 +   * @param aUpdatedContainer
   1.395 +   *        An ancestor of the node which was removed.  It does not have to be
   1.396 +   *        its direct parent.
   1.397 +   * @param aOldNode
   1.398 +   *        The node which was removed.
   1.399 +   *
   1.400 +   * @return the row number of an equivalent node for aOldOne, if one was
   1.401 +   *         found, -1 otherwise.
   1.402 +   */
   1.403 +  _getNewRowForRemovedNode:
   1.404 +  function PTV__getNewRowForRemovedNode(aUpdatedContainer, aOldNode) {
   1.405 +    let parent = aOldNode.parent;
   1.406 +    if (parent) {
   1.407 +      // If the node's parent is still set, the node is not obsolete
   1.408 +      // and we should just find out its new position.
   1.409 +      // However, if any of the node's ancestor is closed, the node is
   1.410 +      // invisible.
   1.411 +      let ancestors = PlacesUtils.nodeAncestors(aOldNode);
   1.412 +      for (let ancestor in ancestors) {
   1.413 +        if (!ancestor.containerOpen)
   1.414 +          return -1;
   1.415 +      }
   1.416 +
   1.417 +      return this._getRowForNode(aOldNode, true);
   1.418 +    }
   1.419 +
   1.420 +    // There's a broken edge case here.
   1.421 +    // If a visit appears in two queries, and the second one was
   1.422 +    // the old node, we'll select the first one after refresh.  There's
   1.423 +    // nothing we could do about that, because aOldNode.parent is
   1.424 +    // gone by the time invalidateContainer is called.
   1.425 +    let newNode = aUpdatedContainer.findNodeByDetails(aOldNode.uri,
   1.426 +                                                      aOldNode.time,
   1.427 +                                                      aOldNode.itemId,
   1.428 +                                                      true);
   1.429 +    if (!newNode)
   1.430 +      return -1;
   1.431 +
   1.432 +    return this._getRowForNode(newNode, true);
   1.433 +  },
   1.434 +
   1.435 +  /**
   1.436 +   * Restores a given selection state as near as possible to the original
   1.437 +   * selection state.
   1.438 +   *
   1.439 +   * @param aNodesInfo
   1.440 +   *        The persisted selection state as returned by
   1.441 +   *        _getSelectedNodesInRange.
   1.442 +   * @param aUpdatedContainer
   1.443 +   *        The container which was updated.
   1.444 +   */
   1.445 +  _restoreSelection:
   1.446 +  function PTV__restoreSelection(aNodesInfo, aUpdatedContainer) {
   1.447 +    if (aNodesInfo.length == 0)
   1.448 +      return;
   1.449 +
   1.450 +    let selection = this.selection;
   1.451 +
   1.452 +    // Attempt to ensure that previously-visible selection will be visible
   1.453 +    // if it's re-selected.  However, we can only ensure that for one row.
   1.454 +    let scrollToRow = -1;
   1.455 +    for (let i = 0; i < aNodesInfo.length; i++) {
   1.456 +      let nodeInfo = aNodesInfo[i];
   1.457 +      let row = this._getNewRowForRemovedNode(aUpdatedContainer,
   1.458 +                                              nodeInfo.node);
   1.459 +      // Select the found node, if any.
   1.460 +      if (row != -1) {
   1.461 +        selection.rangedSelect(row, row, true);
   1.462 +        if (nodeInfo.wasVisible && scrollToRow == -1)
   1.463 +          scrollToRow = row;
   1.464 +      }
   1.465 +    }
   1.466 +
   1.467 +    // If only one node was previously selected and there's no selection now,
   1.468 +    // select the node at its old row, if any.
   1.469 +    if (aNodesInfo.length == 1 && selection.count == 0) {
   1.470 +      let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1);
   1.471 +      if (row != -1) {
   1.472 +        selection.rangedSelect(row, row, true);
   1.473 +        if (aNodesInfo[0].wasVisible && scrollToRow == -1)
   1.474 +          scrollToRow = aNodesInfo[0].oldRow;
   1.475 +      }
   1.476 +    }
   1.477 +
   1.478 +    if (scrollToRow != -1)
   1.479 +      this._tree.ensureRowIsVisible(scrollToRow);
   1.480 +  },
   1.481 +
   1.482 +  _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) {
   1.483 +    const MS_PER_MINUTE = 60000;
   1.484 +    const MS_PER_DAY = 86400000;
   1.485 +    let timeMs = aTime / 1000; // PRTime is in microseconds
   1.486 +
   1.487 +    // Date is calculated starting from midnight, so the modulo with a day are
   1.488 +    // milliseconds from today's midnight.
   1.489 +    // getTimezoneOffset corrects that based on local time, notice midnight
   1.490 +    // can have a different offset during DST-change days.
   1.491 +    let dateObj = new Date();
   1.492 +    let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
   1.493 +    let midnight = now - (now % MS_PER_DAY);
   1.494 +    midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
   1.495 +
   1.496 +    let dateFormat = timeMs >= midnight ?
   1.497 +                      Ci.nsIScriptableDateFormat.dateFormatNone :
   1.498 +                      Ci.nsIScriptableDateFormat.dateFormatShort;
   1.499 +
   1.500 +    let timeObj = new Date(timeMs);
   1.501 +    return (this._dateService.FormatDateTime("", dateFormat,
   1.502 +      Ci.nsIScriptableDateFormat.timeFormatNoSeconds,
   1.503 +      timeObj.getFullYear(), timeObj.getMonth() + 1,
   1.504 +      timeObj.getDate(), timeObj.getHours(),
   1.505 +      timeObj.getMinutes(), timeObj.getSeconds()));
   1.506 +  },
   1.507 +
   1.508 +  COLUMN_TYPE_UNKNOWN: 0,
   1.509 +  COLUMN_TYPE_TITLE: 1,
   1.510 +  COLUMN_TYPE_URI: 2,
   1.511 +  COLUMN_TYPE_DATE: 3,
   1.512 +  COLUMN_TYPE_VISITCOUNT: 4,
   1.513 +  COLUMN_TYPE_KEYWORD: 5,
   1.514 +  COLUMN_TYPE_DESCRIPTION: 6,
   1.515 +  COLUMN_TYPE_DATEADDED: 7,
   1.516 +  COLUMN_TYPE_LASTMODIFIED: 8,
   1.517 +  COLUMN_TYPE_TAGS: 9,
   1.518 +
   1.519 +  _getColumnType: function PTV__getColumnType(aColumn) {
   1.520 +    let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
   1.521 +
   1.522 +    switch (columnType) {
   1.523 +      case "title":
   1.524 +        return this.COLUMN_TYPE_TITLE;
   1.525 +      case "url":
   1.526 +        return this.COLUMN_TYPE_URI;
   1.527 +      case "date":
   1.528 +        return this.COLUMN_TYPE_DATE;
   1.529 +      case "visitCount":
   1.530 +        return this.COLUMN_TYPE_VISITCOUNT;
   1.531 +      case "keyword":
   1.532 +        return this.COLUMN_TYPE_KEYWORD;
   1.533 +      case "description":
   1.534 +        return this.COLUMN_TYPE_DESCRIPTION;
   1.535 +      case "dateAdded":
   1.536 +        return this.COLUMN_TYPE_DATEADDED;
   1.537 +      case "lastModified":
   1.538 +        return this.COLUMN_TYPE_LASTMODIFIED;
   1.539 +      case "tags":
   1.540 +        return this.COLUMN_TYPE_TAGS;
   1.541 +    }
   1.542 +    return this.COLUMN_TYPE_UNKNOWN;
   1.543 +  },
   1.544 +
   1.545 +  _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) {
   1.546 +    switch (aSortType) {
   1.547 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
   1.548 +        return [this.COLUMN_TYPE_TITLE, false];
   1.549 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
   1.550 +        return [this.COLUMN_TYPE_TITLE, true];
   1.551 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
   1.552 +        return [this.COLUMN_TYPE_DATE, false];
   1.553 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
   1.554 +        return [this.COLUMN_TYPE_DATE, true];
   1.555 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
   1.556 +        return [this.COLUMN_TYPE_URI, false];
   1.557 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
   1.558 +        return [this.COLUMN_TYPE_URI, true];
   1.559 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
   1.560 +        return [this.COLUMN_TYPE_VISITCOUNT, false];
   1.561 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
   1.562 +        return [this.COLUMN_TYPE_VISITCOUNT, true];
   1.563 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING:
   1.564 +        return [this.COLUMN_TYPE_KEYWORD, false];
   1.565 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING:
   1.566 +        return [this.COLUMN_TYPE_KEYWORD, true];
   1.567 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING:
   1.568 +        if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
   1.569 +          return [this.COLUMN_TYPE_DESCRIPTION, false];
   1.570 +        break;
   1.571 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING:
   1.572 +        if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
   1.573 +          return [this.COLUMN_TYPE_DESCRIPTION, true];
   1.574 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
   1.575 +        return [this.COLUMN_TYPE_DATEADDED, false];
   1.576 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
   1.577 +        return [this.COLUMN_TYPE_DATEADDED, true];
   1.578 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING:
   1.579 +        return [this.COLUMN_TYPE_LASTMODIFIED, false];
   1.580 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING:
   1.581 +        return [this.COLUMN_TYPE_LASTMODIFIED, true];
   1.582 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING:
   1.583 +        return [this.COLUMN_TYPE_TAGS, false];
   1.584 +      case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING:
   1.585 +        return [this.COLUMN_TYPE_TAGS, true];
   1.586 +    }
   1.587 +    return [this.COLUMN_TYPE_UNKNOWN, false];
   1.588 +  },
   1.589 +
   1.590 +  // nsINavHistoryResultObserver
   1.591 +  nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) {
   1.592 +    NS_ASSERT(this._result, "Got a notification but have no result!");
   1.593 +    if (!this._tree || !this._result)
   1.594 +      return;
   1.595 +
   1.596 +    // Bail out for hidden separators.
   1.597 +    if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
   1.598 +      return;
   1.599 +
   1.600 +    let parentRow;
   1.601 +    if (aParentNode != this._rootNode) {
   1.602 +      parentRow = this._getRowForNode(aParentNode);
   1.603 +
   1.604 +      // Update parent when inserting the first item, since twisty has changed.
   1.605 +      if (aParentNode.childCount == 1)
   1.606 +        this._tree.invalidateRow(parentRow);
   1.607 +    }
   1.608 +
   1.609 +    // Compute the new row number of the node.
   1.610 +    let row = -1;
   1.611 +    let cc = aParentNode.childCount;
   1.612 +    if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) {
   1.613 +      // We don't need to worry about sub hierarchies of the parent node
   1.614 +      // if it's a plain container, or if the new node is its first child.
   1.615 +      if (aParentNode == this._rootNode)
   1.616 +        row = aNewIndex;
   1.617 +      else
   1.618 +        row = parentRow + aNewIndex + 1;
   1.619 +    }
   1.620 +    else {
   1.621 +      // Here, we try to find the next visible element in the child list so we
   1.622 +      // can set the new visible index to be right before that.  Note that we
   1.623 +      // have to search down instead of up, because some siblings could have
   1.624 +      // children themselves that would be in the way.
   1.625 +      let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) &&
   1.626 +                                this.isSorted();
   1.627 +      for (let i = aNewIndex + 1; i < cc; i++) {
   1.628 +        let node = aParentNode.getChild(i);
   1.629 +        if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) {
   1.630 +          // The children have not been shifted so the next item will have what
   1.631 +          // should be our index.
   1.632 +          row = this._getRowForNode(node, false, parentRow, i);
   1.633 +          break;
   1.634 +        }
   1.635 +      }
   1.636 +      if (row < 0) {
   1.637 +        // At the end of the child list without finding a visible sibling. This
   1.638 +        // is a little harder because we don't know how many rows the last item
   1.639 +        // in our list takes up (it could be a container with many children).
   1.640 +        let prevChild = aParentNode.getChild(aNewIndex - 1);
   1.641 +        let prevIndex = this._getRowForNode(prevChild, false, parentRow,
   1.642 +                                            aNewIndex - 1);
   1.643 +        row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex);
   1.644 +      }
   1.645 +    }
   1.646 +
   1.647 +    this._rows.splice(row, 0, aNode);
   1.648 +    this._tree.rowCountChanged(row, 1);
   1.649 +
   1.650 +    if (PlacesUtils.nodeIsContainer(aNode) &&
   1.651 +        PlacesUtils.asContainer(aNode).containerOpen) {
   1.652 +      this.invalidateContainer(aNode);
   1.653 +    }
   1.654 +  },
   1.655 +
   1.656 +  /**
   1.657 +   * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being
   1.658 +   * removed but the node it is collapsed with is not being removed (this then
   1.659 +   * just swap out the removee with its collapsing partner). The only time
   1.660 +   * when we really remove things is when deleting URIs, which will apply to
   1.661 +   * all collapsees. This function is called sometimes when resorting items.
   1.662 +   * However, we won't do this when sorted by date because dates will never
   1.663 +   * change for visits, and date sorting is the only time things are collapsed.
   1.664 +   */
   1.665 +  nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) {
   1.666 +    NS_ASSERT(this._result, "Got a notification but have no result!");
   1.667 +    if (!this._tree || !this._result)
   1.668 +      return;
   1.669 +
   1.670 +    // XXX bug 517701: We don't know what to do when the root node is removed.
   1.671 +    if (aNode == this._rootNode)
   1.672 +      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   1.673 +
   1.674 +    // Bail out for hidden separators.
   1.675 +    if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
   1.676 +      return;
   1.677 +
   1.678 +    let parentRow = aParentNode == this._rootNode ?
   1.679 +                    undefined : this._getRowForNode(aParentNode, true);
   1.680 +    let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex);
   1.681 +    if (oldRow < 0)
   1.682 +      throw Cr.NS_ERROR_UNEXPECTED;
   1.683 +
   1.684 +    // If the node was exclusively selected, the node next to it will be
   1.685 +    // selected.
   1.686 +    let selectNext = false;
   1.687 +    let selection = this.selection;
   1.688 +    if (selection.getRangeCount() == 1) {
   1.689 +      let min = { }, max = { };
   1.690 +      selection.getRangeAt(0, min, max);
   1.691 +      if (min.value == max.value &&
   1.692 +          this.nodeForTreeIndex(min.value) == aNode)
   1.693 +        selectNext = true;
   1.694 +    }
   1.695 +
   1.696 +    // Remove the node and its children, if any.
   1.697 +    let count = this._countVisibleRowsForNodeAtRow(oldRow);
   1.698 +    this._rows.splice(oldRow, count);
   1.699 +    this._tree.rowCountChanged(oldRow, -count);
   1.700 +
   1.701 +    // Redraw the parent if its twisty state has changed.
   1.702 +    if (aParentNode != this._rootNode && !aParentNode.hasChildren) {
   1.703 +      let parentRow = oldRow - 1;
   1.704 +      this._tree.invalidateRow(parentRow);
   1.705 +    }
   1.706 +
   1.707 +    // Restore selection if the node was exclusively selected.
   1.708 +    if (!selectNext)
   1.709 +      return;
   1.710 +
   1.711 +    // Restore selection.
   1.712 +    let rowToSelect = Math.min(oldRow, this._rows.length - 1);
   1.713 +    if (rowToSelect != -1)
   1.714 +      this.selection.rangedSelect(rowToSelect, rowToSelect, true);
   1.715 +  },
   1.716 +
   1.717 +  nodeMoved:
   1.718 +  function PTV_nodeMoved(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) {
   1.719 +    NS_ASSERT(this._result, "Got a notification but have no result!");
   1.720 +    if (!this._tree || !this._result)
   1.721 +      return;
   1.722 +
   1.723 +    // Bail out for hidden separators.
   1.724 +    if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
   1.725 +      return;
   1.726 +
   1.727 +    // Note that at this point the node has already been moved by the backend,
   1.728 +    // so we must give hints to _getRowForNode to get the old row position.
   1.729 +    let oldParentRow = aOldParent == this._rootNode ?
   1.730 +                         undefined : this._getRowForNode(aOldParent, true);
   1.731 +    let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex);
   1.732 +    if (oldRow < 0)
   1.733 +      throw Cr.NS_ERROR_UNEXPECTED;
   1.734 +
   1.735 +    // If this node is a container it could take up more than one row.
   1.736 +    let count = this._countVisibleRowsForNodeAtRow(oldRow);
   1.737 +
   1.738 +    // Persist selection state.
   1.739 +    let nodesToReselect =
   1.740 +      this._getSelectedNodesInRange(oldRow, oldRow + count);
   1.741 +    if (nodesToReselect.length > 0)
   1.742 +      this.selection.selectEventsSuppressed = true;
   1.743 +
   1.744 +    // Redraw the parent if its twisty state has changed.
   1.745 +    if (aOldParent != this._rootNode && !aOldParent.hasChildren) {
   1.746 +      let parentRow = oldRow - 1;
   1.747 +      this._tree.invalidateRow(parentRow);
   1.748 +    }
   1.749 +
   1.750 +    // Remove node and its children, if any, from the old position.
   1.751 +    this._rows.splice(oldRow, count);
   1.752 +    this._tree.rowCountChanged(oldRow, -count);
   1.753 +
   1.754 +    // Insert the node into the new position.
   1.755 +    this.nodeInserted(aNewParent, aNode, aNewIndex);
   1.756 +
   1.757 +    // Restore selection.
   1.758 +    if (nodesToReselect.length > 0) {
   1.759 +      this._restoreSelection(nodesToReselect, aNewParent);
   1.760 +      this.selection.selectEventsSuppressed = false;
   1.761 +    }
   1.762 +  },
   1.763 +
   1.764 +  _invalidateCellValue: function PTV__invalidateCellValue(aNode,
   1.765 +                                                          aColumnType) {
   1.766 +    NS_ASSERT(this._result, "Got a notification but have no result!");
   1.767 +    if (!this._tree || !this._result)
   1.768 +      return;
   1.769 +
   1.770 +    // Nothing to do for the root node.
   1.771 +    if (aNode == this._rootNode)
   1.772 +      return;
   1.773 +
   1.774 +    let row = this._getRowForNode(aNode);
   1.775 +    if (row == -1)
   1.776 +      return;
   1.777 +
   1.778 +    let column = this._findColumnByType(aColumnType);
   1.779 +    if (column && !column.element.hidden)
   1.780 +      this._tree.invalidateCell(row, column);
   1.781 +
   1.782 +    // Last modified time is altered for almost all node changes.
   1.783 +    if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) {
   1.784 +      let lastModifiedColumn =
   1.785 +        this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED);
   1.786 +      if (lastModifiedColumn && !lastModifiedColumn.hidden)
   1.787 +        this._tree.invalidateCell(row, lastModifiedColumn);
   1.788 +    }
   1.789 +  },
   1.790 +
   1.791 +  _populateLivemarkContainer: function PTV__populateLivemarkContainer(aNode) {
   1.792 +    PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
   1.793 +      .then(aLivemark => {
   1.794 +        let placesNode = aNode;
   1.795 +        // Need to check containerOpen since getLivemark is async.
   1.796 +        if (!placesNode.containerOpen)
   1.797 +          return;
   1.798 +
   1.799 +        let children = aLivemark.getNodesForContainer(placesNode);
   1.800 +        for (let i = 0; i < children.length; i++) {
   1.801 +          let child = children[i];
   1.802 +          this.nodeInserted(placesNode, child, i);
   1.803 +        }
   1.804 +      }, Components.utils.reportError);
   1.805 +  },
   1.806 +
   1.807 +  nodeTitleChanged: function PTV_nodeTitleChanged(aNode, aNewTitle) {
   1.808 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   1.809 +  },
   1.810 +
   1.811 +  nodeURIChanged: function PTV_nodeURIChanged(aNode, aNewURI) {
   1.812 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
   1.813 +  },
   1.814 +
   1.815 +  nodeIconChanged: function PTV_nodeIconChanged(aNode) {
   1.816 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   1.817 +  },
   1.818 +
   1.819 +  nodeHistoryDetailsChanged:
   1.820 +  function PTV_nodeHistoryDetailsChanged(aNode, aUpdatedVisitDate,
   1.821 +                                         aUpdatedVisitCount) {
   1.822 +    if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) {
   1.823 +      // Find the node in the parent.
   1.824 +      let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent);
   1.825 +      for (let i = parentRow; i < this._rows.length; i++) {
   1.826 +        let child = this.nodeForTreeIndex(i);
   1.827 +        if (child.uri == aNode.uri) {
   1.828 +          this._cellProperties.delete(child);
   1.829 +          this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE);
   1.830 +          break;
   1.831 +        }
   1.832 +      }
   1.833 +      return;
   1.834 +    }
   1.835 +
   1.836 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
   1.837 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
   1.838 +  },
   1.839 +
   1.840 +  nodeTagsChanged: function PTV_nodeTagsChanged(aNode) {
   1.841 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
   1.842 +  },
   1.843 +
   1.844 +  nodeKeywordChanged: function PTV_nodeKeywordChanged(aNode, aNewKeyword) {
   1.845 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD);
   1.846 +  },
   1.847 +
   1.848 +  nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) {
   1.849 +    if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
   1.850 +      this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
   1.851 +    }
   1.852 +    else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
   1.853 +      PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
   1.854 +        .then(aLivemark => {
   1.855 +          this._controller.cacheLivemarkInfo(aNode, aLivemark);
   1.856 +          let properties = this._cellProperties.get(aNode);
   1.857 +          this._cellProperties.set(aNode, properties += " livemark ");
   1.858 +
   1.859 +          // The livemark attribute is set as a cell property on the title cell.
   1.860 +          this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   1.861 +        }, Components.utils.reportError);
   1.862 +    }
   1.863 +  },
   1.864 +
   1.865 +  nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode, aNewValue) {
   1.866 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
   1.867 +  },
   1.868 +
   1.869 +  nodeLastModifiedChanged:
   1.870 +  function PTV_nodeLastModifiedChanged(aNode, aNewValue) {
   1.871 +    this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED);
   1.872 +  },
   1.873 +
   1.874 +  containerStateChanged:
   1.875 +  function PTV_containerStateChanged(aNode, aOldState, aNewState) {
   1.876 +    this.invalidateContainer(aNode);
   1.877 +
   1.878 +    if (PlacesUtils.nodeIsFolder(aNode) ||
   1.879 +        (this._flatList && aNode == this._rootNode)) {
   1.880 +      let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions;
   1.881 +      if (queryOptions.excludeItems) {
   1.882 +        return;
   1.883 +      }
   1.884 +
   1.885 +      PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
   1.886 +        .then(aLivemark => {
   1.887 +          let shouldInvalidate = 
   1.888 +            !this._controller.hasCachedLivemarkInfo(aNode);
   1.889 +          this._controller.cacheLivemarkInfo(aNode, aLivemark);
   1.890 +          if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) {
   1.891 +            aLivemark.registerForUpdates(aNode, this);
   1.892 +            // Prioritize the current livemark.
   1.893 +            aLivemark.reload();
   1.894 +            PlacesUtils.livemarks.reloadLivemarks();
   1.895 +            if (shouldInvalidate)
   1.896 +              this.invalidateContainer(aNode);
   1.897 +          }
   1.898 +          else {
   1.899 +            aLivemark.unregisterForUpdates(aNode);
   1.900 +          }
   1.901 +        }, () => undefined);
   1.902 +    }
   1.903 +  },
   1.904 +
   1.905 +  invalidateContainer: function PTV_invalidateContainer(aContainer) {
   1.906 +    NS_ASSERT(this._result, "Need to have a result to update");
   1.907 +    if (!this._tree)
   1.908 +      return;
   1.909 +
   1.910 +    let startReplacement, replaceCount;
   1.911 +    if (aContainer == this._rootNode) {
   1.912 +      startReplacement = 0;
   1.913 +      replaceCount = this._rows.length;
   1.914 +
   1.915 +      // If the root node is now closed, the tree is empty.
   1.916 +      if (!this._rootNode.containerOpen) {
   1.917 +        this._rows = [];
   1.918 +        if (replaceCount)
   1.919 +          this._tree.rowCountChanged(startReplacement, -replaceCount);
   1.920 +
   1.921 +        return;
   1.922 +      }
   1.923 +    }
   1.924 +    else {
   1.925 +      // Update the twisty state.
   1.926 +      let row = this._getRowForNode(aContainer);
   1.927 +      this._tree.invalidateRow(row);
   1.928 +
   1.929 +      // We don't replace the container node itself, so we should decrease the
   1.930 +      // replaceCount by 1.
   1.931 +      startReplacement = row + 1;
   1.932 +      replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1;
   1.933 +    }
   1.934 +
   1.935 +    // Persist selection state.
   1.936 +    let nodesToReselect =
   1.937 +      this._getSelectedNodesInRange(startReplacement,
   1.938 +                                    startReplacement + replaceCount);
   1.939 +
   1.940 +    // Now update the number of elements.
   1.941 +    this.selection.selectEventsSuppressed = true;
   1.942 +
   1.943 +    // First remove the old elements
   1.944 +    this._rows.splice(startReplacement, replaceCount);
   1.945 +
   1.946 +    // If the container is now closed, we're done.
   1.947 +    if (!aContainer.containerOpen) {
   1.948 +      let oldSelectionCount = this.selection.count;
   1.949 +      if (replaceCount)
   1.950 +        this._tree.rowCountChanged(startReplacement, -replaceCount);
   1.951 +
   1.952 +      // Select the row next to the closed container if any of its
   1.953 +      // children were selected, and nothing else is selected.
   1.954 +      if (nodesToReselect.length > 0 &&
   1.955 +          nodesToReselect.length == oldSelectionCount) {
   1.956 +        this.selection.rangedSelect(startReplacement, startReplacement, true);
   1.957 +        this._tree.ensureRowIsVisible(startReplacement);
   1.958 +      }
   1.959 +
   1.960 +      this.selection.selectEventsSuppressed = false;
   1.961 +      return;
   1.962 +    }
   1.963 +
   1.964 +    // Otherwise, start a batch first.
   1.965 +    this._tree.beginUpdateBatch();
   1.966 +    if (replaceCount)
   1.967 +      this._tree.rowCountChanged(startReplacement, -replaceCount);
   1.968 +
   1.969 +    let toOpenElements = [];
   1.970 +    let elementsAddedCount = this._buildVisibleSection(aContainer,
   1.971 +                                                       startReplacement,
   1.972 +                                                       toOpenElements);
   1.973 +    if (elementsAddedCount)
   1.974 +      this._tree.rowCountChanged(startReplacement, elementsAddedCount);
   1.975 +
   1.976 +    if (!this._flatList) {
   1.977 +      // Now, open any containers that were persisted.
   1.978 +      for (let i = 0; i < toOpenElements.length; i++) {
   1.979 +        let item = toOpenElements[i];
   1.980 +        let parent = item.parent;
   1.981 +
   1.982 +        // Avoid recursively opening containers.
   1.983 +        while (parent) {
   1.984 +          if (parent.uri == item.uri)
   1.985 +            break;
   1.986 +          parent = parent.parent;
   1.987 +        }
   1.988 +
   1.989 +        // If we don't have a parent, we made it all the way to the root
   1.990 +        // and didn't find a match, so we can open our item.
   1.991 +        if (!parent && !item.containerOpen)
   1.992 +          item.containerOpen = true;
   1.993 +      }
   1.994 +    }
   1.995 +
   1.996 +    if (this._controller.hasCachedLivemarkInfo(aContainer)) {
   1.997 +      let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
   1.998 +      if (!queryOptions.excludeItems) {
   1.999 +        this._populateLivemarkContainer(aContainer);
  1.1000 +      }
  1.1001 +    }
  1.1002 +
  1.1003 +    this._tree.endUpdateBatch();
  1.1004 +
  1.1005 +    // Restore selection.
  1.1006 +    this._restoreSelection(nodesToReselect, aContainer);
  1.1007 +    this.selection.selectEventsSuppressed = false;
  1.1008 +  },
  1.1009 +
  1.1010 +  _columns: [],
  1.1011 +  _findColumnByType: function PTV__findColumnByType(aColumnType) {
  1.1012 +    if (this._columns[aColumnType])
  1.1013 +      return this._columns[aColumnType];
  1.1014 +
  1.1015 +    let columns = this._tree.columns;
  1.1016 +    let colCount = columns.count;
  1.1017 +    for (let i = 0; i < colCount; i++) {
  1.1018 +      let column = columns.getColumnAt(i);
  1.1019 +      let columnType = this._getColumnType(column);
  1.1020 +      this._columns[columnType] = column;
  1.1021 +      if (columnType == aColumnType)
  1.1022 +        return column;
  1.1023 +    }
  1.1024 +
  1.1025 +    // That's completely valid.  Most of our trees actually include just the
  1.1026 +    // title column.
  1.1027 +    return null;
  1.1028 +  },
  1.1029 +
  1.1030 +  sortingChanged: function PTV__sortingChanged(aSortingMode) {
  1.1031 +    if (!this._tree || !this._result)
  1.1032 +      return;
  1.1033 +
  1.1034 +    // Depending on the sort mode, certain commands may be disabled.
  1.1035 +    window.updateCommands("sort");
  1.1036 +
  1.1037 +    let columns = this._tree.columns;
  1.1038 +
  1.1039 +    // Clear old sorting indicator.
  1.1040 +    let sortedColumn = columns.getSortedColumn();
  1.1041 +    if (sortedColumn)
  1.1042 +      sortedColumn.element.removeAttribute("sortDirection");
  1.1043 +
  1.1044 +    // Set new sorting indicator by looking through all columns for ours.
  1.1045 +    if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE)
  1.1046 +      return;
  1.1047 +
  1.1048 +    let [desiredColumn, desiredIsDescending] =
  1.1049 +      this._sortTypeToColumnType(aSortingMode);
  1.1050 +    let colCount = columns.count;
  1.1051 +    let column = this._findColumnByType(desiredColumn);
  1.1052 +    if (column) {
  1.1053 +      let sortDir = desiredIsDescending ? "descending" : "ascending";
  1.1054 +      column.element.setAttribute("sortDirection", sortDir);
  1.1055 +    }
  1.1056 +  },
  1.1057 +
  1.1058 +  _inBatchMode: false,
  1.1059 +  batching: function PTV__batching(aToggleMode) {
  1.1060 +    if (this._inBatchMode != aToggleMode) {
  1.1061 +      this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode;
  1.1062 +      if (this._inBatchMode) {
  1.1063 +        this._tree.beginUpdateBatch();
  1.1064 +      }
  1.1065 +      else {
  1.1066 +        this._tree.endUpdateBatch();
  1.1067 +      }
  1.1068 +    }
  1.1069 +  },
  1.1070 +
  1.1071 +  get result() this._result,
  1.1072 +  set result(val) {
  1.1073 +    if (this._result) {
  1.1074 +      this._result.removeObserver(this);
  1.1075 +      this._rootNode.containerOpen = false;
  1.1076 +    }
  1.1077 +
  1.1078 +    if (val) {
  1.1079 +      this._result = val;
  1.1080 +      this._rootNode = this._result.root;
  1.1081 +      this._cellProperties = new Map();
  1.1082 +      this._cuttingNodes = new Set();
  1.1083 +    }
  1.1084 +    else if (this._result) {
  1.1085 +      delete this._result;
  1.1086 +      delete this._rootNode;
  1.1087 +      delete this._cellProperties;
  1.1088 +      delete this._cuttingNodes;
  1.1089 +    }
  1.1090 +
  1.1091 +    // If the tree is not set yet, setTree will call finishInit.
  1.1092 +    if (this._tree && val)
  1.1093 +      this._finishInit();
  1.1094 +
  1.1095 +    return val;
  1.1096 +  },
  1.1097 +
  1.1098 +  nodeForTreeIndex: function PTV_nodeForTreeIndex(aIndex) {
  1.1099 +    if (aIndex > this._rows.length)
  1.1100 +      throw Cr.NS_ERROR_INVALID_ARG;
  1.1101 +
  1.1102 +    return this._getNodeForRow(aIndex);
  1.1103 +  },
  1.1104 +
  1.1105 +  treeIndexForNode: function PTV_treeNodeForIndex(aNode) {
  1.1106 +    // The API allows passing invisible nodes.
  1.1107 +    try {
  1.1108 +      return this._getRowForNode(aNode, true);
  1.1109 +    }
  1.1110 +    catch(ex) { }
  1.1111 +
  1.1112 +    return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE;
  1.1113 +  },
  1.1114 +
  1.1115 +  _getResourceForNode: function PTV_getResourceForNode(aNode)
  1.1116 +  {
  1.1117 +    let uri = aNode.uri;
  1.1118 +    NS_ASSERT(uri, "if there is no uri, we can't persist the open state");
  1.1119 +    return uri ? PlacesUIUtils.RDF.GetResource(uri) : null;
  1.1120 +  },
  1.1121 +
  1.1122 +  // nsITreeView
  1.1123 +  get rowCount() this._rows.length,
  1.1124 +  get selection() this._selection,
  1.1125 +  set selection(val) this._selection = val,
  1.1126 +
  1.1127 +  getRowProperties: function() { return ""; },
  1.1128 +
  1.1129 +  getCellProperties:
  1.1130 +  function PTV_getCellProperties(aRow, aColumn) {
  1.1131 +    // for anonid-trees, we need to add the column-type manually
  1.1132 +    var props = "";
  1.1133 +    let columnType = aColumn.element.getAttribute("anonid");
  1.1134 +    if (columnType)
  1.1135 +      props += columnType;
  1.1136 +    else
  1.1137 +      columnType = aColumn.id;
  1.1138 +
  1.1139 +    // Set the "ltr" property on url cells
  1.1140 +    if (columnType == "url")
  1.1141 +      props += " ltr";
  1.1142 +
  1.1143 +    if (columnType != "title")
  1.1144 +      return props;
  1.1145 +
  1.1146 +    let node = this._getNodeForRow(aRow);
  1.1147 +
  1.1148 +    if (this._cuttingNodes.has(node)) {
  1.1149 +      props += " cutting";
  1.1150 +    }
  1.1151 +
  1.1152 +    let properties = this._cellProperties.get(node);
  1.1153 +    if (properties === undefined) {
  1.1154 +      properties = "";
  1.1155 +      let itemId = node.itemId;
  1.1156 +      let nodeType = node.type;
  1.1157 +      if (PlacesUtils.containerTypes.indexOf(nodeType) != -1) {
  1.1158 +        if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
  1.1159 +          properties += " query";
  1.1160 +          if (PlacesUtils.nodeIsTagQuery(node))
  1.1161 +            properties += " tagContainer";
  1.1162 +          else if (PlacesUtils.nodeIsDay(node))
  1.1163 +            properties += " dayContainer";
  1.1164 +          else if (PlacesUtils.nodeIsHost(node))
  1.1165 +            properties += " hostContainer";
  1.1166 +        }
  1.1167 +        else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
  1.1168 +                 nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
  1.1169 +          if (this._controller.hasCachedLivemarkInfo(node)) {
  1.1170 +            properties += " livemark";
  1.1171 +          }
  1.1172 +          else {
  1.1173 +            PlacesUtils.livemarks.getLivemark({ id: node.itemId })
  1.1174 +              .then(aLivemark => {
  1.1175 +                this._controller.cacheLivemarkInfo(node, aLivemark);
  1.1176 +                properties += " livemark";
  1.1177 +                // The livemark attribute is set as a cell property on the title cell.
  1.1178 +                this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
  1.1179 +              }, () => undefined);
  1.1180 +          }
  1.1181 +        }
  1.1182 +
  1.1183 +        if (itemId != -1) {
  1.1184 +          let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId);
  1.1185 +          if (queryName)
  1.1186 +            properties += " OrganizerQuery_" + queryName;
  1.1187 +        }
  1.1188 +      }
  1.1189 +      else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR)
  1.1190 +        properties += " separator";
  1.1191 +      else if (PlacesUtils.nodeIsURI(node)) {
  1.1192 +        properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri);
  1.1193 +
  1.1194 +        if (this._controller.hasCachedLivemarkInfo(node.parent)) {
  1.1195 +          properties += " livemarkItem";
  1.1196 +          if (node.accessCount) {
  1.1197 +            properties += " visited";
  1.1198 +          }
  1.1199 +        }
  1.1200 +      }
  1.1201 +
  1.1202 +      this._cellProperties.set(node, properties);
  1.1203 +    }
  1.1204 +
  1.1205 +    return props + " " + properties;
  1.1206 +  },
  1.1207 +
  1.1208 +  getColumnProperties: function(aColumn) { return ""; },
  1.1209 +
  1.1210 +  isContainer: function PTV_isContainer(aRow) {
  1.1211 +    // Only leaf nodes aren't listed in the rows array.
  1.1212 +    let node = this._rows[aRow];
  1.1213 +    if (node === undefined)
  1.1214 +      return false;
  1.1215 +
  1.1216 +    if (PlacesUtils.nodeIsContainer(node)) {
  1.1217 +      // Flat-lists may ignore expandQueries and other query options when
  1.1218 +      // they are asked to open a container.
  1.1219 +      if (this._flatList)
  1.1220 +        return true;
  1.1221 +
  1.1222 +      // treat non-expandable childless queries as non-containers
  1.1223 +      if (PlacesUtils.nodeIsQuery(node)) {
  1.1224 +        let parent = node.parent;
  1.1225 +        if ((PlacesUtils.nodeIsQuery(parent) ||
  1.1226 +             PlacesUtils.nodeIsFolder(parent)) &&
  1.1227 +            !PlacesUtils.asQuery(node).hasChildren)
  1.1228 +          return PlacesUtils.asQuery(parent).queryOptions.expandQueries;
  1.1229 +      }
  1.1230 +      return true;
  1.1231 +    }
  1.1232 +    return false;
  1.1233 +  },
  1.1234 +
  1.1235 +  isContainerOpen: function PTV_isContainerOpen(aRow) {
  1.1236 +    if (this._flatList)
  1.1237 +      return false;
  1.1238 +
  1.1239 +    // All containers are listed in the rows array.
  1.1240 +    return this._rows[aRow].containerOpen;
  1.1241 +  },
  1.1242 +
  1.1243 +  isContainerEmpty: function PTV_isContainerEmpty(aRow) {
  1.1244 +    if (this._flatList)
  1.1245 +      return true;
  1.1246 +
  1.1247 +    let node = this._rows[aRow];
  1.1248 +    if (this._controller.hasCachedLivemarkInfo(node)) {
  1.1249 +      let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
  1.1250 +      return queryOptions.excludeItems;
  1.1251 +    }
  1.1252 +
  1.1253 +    // All containers are listed in the rows array.
  1.1254 +    return !node.hasChildren;
  1.1255 +  },
  1.1256 +
  1.1257 +  isSeparator: function PTV_isSeparator(aRow) {
  1.1258 +    // All separators are listed in the rows array.
  1.1259 +    let node = this._rows[aRow];
  1.1260 +    return node && PlacesUtils.nodeIsSeparator(node);
  1.1261 +  },
  1.1262 +
  1.1263 +  isSorted: function PTV_isSorted() {
  1.1264 +    return this._result.sortingMode !=
  1.1265 +           Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
  1.1266 +  },
  1.1267 +
  1.1268 +  canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) {
  1.1269 +    if (!this._result)
  1.1270 +      throw Cr.NS_ERROR_UNEXPECTED;
  1.1271 +
  1.1272 +    // Drop position into a sorted treeview would be wrong.
  1.1273 +    if (this.isSorted())
  1.1274 +      return false;
  1.1275 +
  1.1276 +    let ip = this._getInsertionPoint(aRow, aOrientation);
  1.1277 +    return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
  1.1278 +  },
  1.1279 +
  1.1280 +  _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) {
  1.1281 +    let container = this._result.root;
  1.1282 +    let dropNearItemId = -1;
  1.1283 +    // When there's no selection, assume the container is the container
  1.1284 +    // the view is populated from (i.e. the result's itemId).
  1.1285 +    if (index != -1) {
  1.1286 +      let lastSelected = this.nodeForTreeIndex(index);
  1.1287 +      if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
  1.1288 +        // If the last selected item is an open container, append _into_
  1.1289 +        // it, rather than insert adjacent to it.
  1.1290 +        container = lastSelected;
  1.1291 +        index = -1;
  1.1292 +      }
  1.1293 +      else if (lastSelected.containerOpen &&
  1.1294 +               orientation == Ci.nsITreeView.DROP_AFTER &&
  1.1295 +               lastSelected.hasChildren) {
  1.1296 +        // If the last selected node is an open container and the user is
  1.1297 +        // trying to drag into it as a first node, really insert into it.
  1.1298 +        container = lastSelected;
  1.1299 +        orientation = Ci.nsITreeView.DROP_ON;
  1.1300 +        index = 0;
  1.1301 +      }
  1.1302 +      else {
  1.1303 +        // Use the last-selected node's container.
  1.1304 +        container = lastSelected.parent;
  1.1305 +
  1.1306 +        // During its Drag & Drop operation, the tree code closes-and-opens
  1.1307 +        // containers very often (part of the XUL "spring-loaded folders"
  1.1308 +        // implementation).  And in certain cases, we may reach a closed
  1.1309 +        // container here.  However, we can simply bail out when this happens,
  1.1310 +        // because we would then be back here in less than a millisecond, when
  1.1311 +        // the container had been reopened.
  1.1312 +        if (!container || !container.containerOpen)
  1.1313 +          return null;
  1.1314 +
  1.1315 +        // Avoid the potentially expensive call to getChildIndex
  1.1316 +        // if we know this container doesn't allow insertion.
  1.1317 +        if (PlacesControllerDragHelper.disallowInsertion(container))
  1.1318 +          return null;
  1.1319 +
  1.1320 +        let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
  1.1321 +        if (queryOptions.sortingMode !=
  1.1322 +              Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
  1.1323 +          // If we are within a sorted view, insert at the end.
  1.1324 +          index = -1;
  1.1325 +        }
  1.1326 +        else if (queryOptions.excludeItems ||
  1.1327 +                 queryOptions.excludeQueries ||
  1.1328 +                 queryOptions.excludeReadOnlyFolders) {
  1.1329 +          // Some item may be invisible, insert near last selected one.
  1.1330 +          // We don't replace index here to avoid requests to the db,
  1.1331 +          // instead it will be calculated later by the controller.
  1.1332 +          index = -1;
  1.1333 +          dropNearItemId = lastSelected.itemId;
  1.1334 +        }
  1.1335 +        else {
  1.1336 +          let lsi = container.getChildIndex(lastSelected);
  1.1337 +          index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
  1.1338 +        }
  1.1339 +      }
  1.1340 +    }
  1.1341 +
  1.1342 +    if (PlacesControllerDragHelper.disallowInsertion(container))
  1.1343 +      return null;
  1.1344 +
  1.1345 +    return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
  1.1346 +                              index, orientation,
  1.1347 +                              PlacesUtils.nodeIsTagQuery(container),
  1.1348 +                              dropNearItemId);
  1.1349 +  },
  1.1350 +
  1.1351 +  drop: function PTV_drop(aRow, aOrientation, aDataTransfer) {
  1.1352 +    // We are responsible for translating the |index| and |orientation|
  1.1353 +    // parameters into a container id and index within the container,
  1.1354 +    // since this information is specific to the tree view.
  1.1355 +    let ip = this._getInsertionPoint(aRow, aOrientation);
  1.1356 +    if (ip)
  1.1357 +      PlacesControllerDragHelper.onDrop(ip, aDataTransfer);
  1.1358 +
  1.1359 +    PlacesControllerDragHelper.currentDropTarget = null;
  1.1360 +  },
  1.1361 +
  1.1362 +  getParentIndex: function PTV_getParentIndex(aRow) {
  1.1363 +    let [parentNode, parentRow] = this._getParentByChildRow(aRow);
  1.1364 +    return parentRow;
  1.1365 +  },
  1.1366 +
  1.1367 +  hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) {
  1.1368 +    if (aRow == this._rows.length - 1) {
  1.1369 +      // The last row has no sibling.
  1.1370 +      return false;
  1.1371 +    }
  1.1372 +
  1.1373 +    let node = this._rows[aRow];
  1.1374 +    if (node === undefined || this._isPlainContainer(node.parent)) {
  1.1375 +      // The node is a child of a plain container.
  1.1376 +      // If the next row is either unset or has the same parent,
  1.1377 +      // it's a sibling.
  1.1378 +      let nextNode = this._rows[aRow + 1];
  1.1379 +      return (nextNode == undefined || nextNode.parent == node.parent);
  1.1380 +    }
  1.1381 +
  1.1382 +    let thisLevel = node.indentLevel;
  1.1383 +    for (let i = aAfterIndex + 1; i < this._rows.length; ++i) {
  1.1384 +      let rowNode = this._getNodeForRow(i);
  1.1385 +      let nextLevel = rowNode.indentLevel;
  1.1386 +      if (nextLevel == thisLevel)
  1.1387 +        return true;
  1.1388 +      if (nextLevel < thisLevel)
  1.1389 +        break;
  1.1390 +    }
  1.1391 +
  1.1392 +    return false;
  1.1393 +  },
  1.1394 +
  1.1395 +  getLevel: function(aRow) this._getNodeForRow(aRow).indentLevel,
  1.1396 +
  1.1397 +  getImageSrc: function PTV_getImageSrc(aRow, aColumn) {
  1.1398 +    // Only the title column has an image.
  1.1399 +    if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE)
  1.1400 +      return "";
  1.1401 +
  1.1402 +    return this._getNodeForRow(aRow).icon;
  1.1403 +  },
  1.1404 +
  1.1405 +  getProgressMode: function(aRow, aColumn) { },
  1.1406 +  getCellValue: function(aRow, aColumn) { },
  1.1407 +
  1.1408 +  getCellText: function PTV_getCellText(aRow, aColumn) {
  1.1409 +    let node = this._getNodeForRow(aRow);
  1.1410 +    switch (this._getColumnType(aColumn)) {
  1.1411 +      case this.COLUMN_TYPE_TITLE:
  1.1412 +        // normally, this is just the title, but we don't want empty items in
  1.1413 +        // the tree view so return a special string if the title is empty.
  1.1414 +        // Do it here so that callers can still get at the 0 length title
  1.1415 +        // if they go through the "result" API.
  1.1416 +        if (PlacesUtils.nodeIsSeparator(node))
  1.1417 +          return "";
  1.1418 +        return PlacesUIUtils.getBestTitle(node, true);
  1.1419 +      case this.COLUMN_TYPE_TAGS:
  1.1420 +        return node.tags;
  1.1421 +      case this.COLUMN_TYPE_URI:
  1.1422 +        if (PlacesUtils.nodeIsURI(node))
  1.1423 +          return node.uri;
  1.1424 +        return "";
  1.1425 +      case this.COLUMN_TYPE_DATE:
  1.1426 +        let nodeTime = node.time;
  1.1427 +        if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) {
  1.1428 +          // hosts and days shouldn't have a value for the date column.
  1.1429 +          // Actually, you could argue this point, but looking at the
  1.1430 +          // results, seeing the most recently visited date is not what
  1.1431 +          // I expect, and gives me no information I know how to use.
  1.1432 +          // Only show this for URI-based items.
  1.1433 +          return "";
  1.1434 +        }
  1.1435 +
  1.1436 +        return this._convertPRTimeToString(nodeTime);
  1.1437 +      case this.COLUMN_TYPE_VISITCOUNT:
  1.1438 +        return node.accessCount;
  1.1439 +      case this.COLUMN_TYPE_KEYWORD:
  1.1440 +        if (PlacesUtils.nodeIsBookmark(node))
  1.1441 +          return PlacesUtils.bookmarks.getKeywordForBookmark(node.itemId);
  1.1442 +        return "";
  1.1443 +      case this.COLUMN_TYPE_DESCRIPTION:
  1.1444 +        if (node.itemId != -1) {
  1.1445 +          try {
  1.1446 +            return PlacesUtils.annotations.
  1.1447 +                               getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO);
  1.1448 +          }
  1.1449 +          catch (ex) { /* has no description */ }
  1.1450 +        }
  1.1451 +        return "";
  1.1452 +      case this.COLUMN_TYPE_DATEADDED:
  1.1453 +        if (node.dateAdded)
  1.1454 +          return this._convertPRTimeToString(node.dateAdded);
  1.1455 +        return "";
  1.1456 +      case this.COLUMN_TYPE_LASTMODIFIED:
  1.1457 +        if (node.lastModified)
  1.1458 +          return this._convertPRTimeToString(node.lastModified);
  1.1459 +        return "";
  1.1460 +    }
  1.1461 +    return "";
  1.1462 +  },
  1.1463 +
  1.1464 +  setTree: function PTV_setTree(aTree) {
  1.1465 +    // If we are replacing the tree during a batch, there is a concrete risk
  1.1466 +    // that the treeView goes out of sync, thus it's safer to end the batch now.
  1.1467 +    // This is a no-op if we are not batching.
  1.1468 +    this.batching(false);
  1.1469 +
  1.1470 +    let hasOldTree = this._tree != null;
  1.1471 +    this._tree = aTree;
  1.1472 +
  1.1473 +    if (this._result) {
  1.1474 +      if (hasOldTree) {
  1.1475 +        // detach from result when we are detaching from the tree.
  1.1476 +        // This breaks the reference cycle between us and the result.
  1.1477 +        if (!aTree) {
  1.1478 +          this._result.removeObserver(this);
  1.1479 +          this._rootNode.containerOpen = false;
  1.1480 +        }
  1.1481 +      }
  1.1482 +      if (aTree)
  1.1483 +        this._finishInit();
  1.1484 +    }
  1.1485 +  },
  1.1486 +
  1.1487 +  toggleOpenState: function PTV_toggleOpenState(aRow) {
  1.1488 +    if (!this._result)
  1.1489 +      throw Cr.NS_ERROR_UNEXPECTED;
  1.1490 +
  1.1491 +    let node = this._rows[aRow];
  1.1492 +    if (this._flatList && this._openContainerCallback) {
  1.1493 +      this._openContainerCallback(node);
  1.1494 +      return;
  1.1495 +    }
  1.1496 +
  1.1497 +    // Persist containers open status, but never persist livemarks.
  1.1498 +    if (!this._controller.hasCachedLivemarkInfo(node)) {
  1.1499 +      let resource = this._getResourceForNode(node);
  1.1500 +      if (resource) {
  1.1501 +        const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open");
  1.1502 +        const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true");
  1.1503 +
  1.1504 +        if (node.containerOpen)
  1.1505 +          PlacesUIUtils.localStore.Unassert(resource, openLiteral, trueLiteral);
  1.1506 +        else
  1.1507 +          PlacesUIUtils.localStore.Assert(resource, openLiteral, trueLiteral, true);
  1.1508 +      }
  1.1509 +    }
  1.1510 +
  1.1511 +    node.containerOpen = !node.containerOpen;
  1.1512 +  },
  1.1513 +
  1.1514 +  cycleHeader: function PTV_cycleHeader(aColumn) {
  1.1515 +    if (!this._result)
  1.1516 +      throw Cr.NS_ERROR_UNEXPECTED;
  1.1517 +
  1.1518 +    // Sometimes you want a tri-state sorting, and sometimes you don't. This
  1.1519 +    // rule allows tri-state sorting when the root node is a folder. This will
  1.1520 +    // catch the most common cases. When you are looking at folders, you want
  1.1521 +    // the third state to reset the sorting to the natural bookmark order. When
  1.1522 +    // you are looking at history, that third state has no meaning so we try
  1.1523 +    // to disallow it.
  1.1524 +    //
  1.1525 +    // The problem occurs when you have a query that results in bookmark
  1.1526 +    // folders. One example of this is the subscriptions view. In these cases,
  1.1527 +    // this rule doesn't allow you to sort those sub-folders by their natural
  1.1528 +    // order.
  1.1529 +    let allowTriState = PlacesUtils.nodeIsFolder(this._result.root);
  1.1530 +
  1.1531 +    let oldSort = this._result.sortingMode;
  1.1532 +    let oldSortingAnnotation = this._result.sortingAnnotation;
  1.1533 +    let newSort;
  1.1534 +    let newSortingAnnotation = "";
  1.1535 +    const NHQO = Ci.nsINavHistoryQueryOptions;
  1.1536 +    switch (this._getColumnType(aColumn)) {
  1.1537 +      case this.COLUMN_TYPE_TITLE:
  1.1538 +        if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING)
  1.1539 +          newSort = NHQO.SORT_BY_TITLE_DESCENDING;
  1.1540 +        else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING)
  1.1541 +          newSort = NHQO.SORT_BY_NONE;
  1.1542 +        else
  1.1543 +          newSort = NHQO.SORT_BY_TITLE_ASCENDING;
  1.1544 +
  1.1545 +        break;
  1.1546 +      case this.COLUMN_TYPE_URI:
  1.1547 +        if (oldSort == NHQO.SORT_BY_URI_ASCENDING)
  1.1548 +          newSort = NHQO.SORT_BY_URI_DESCENDING;
  1.1549 +        else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING)
  1.1550 +          newSort = NHQO.SORT_BY_NONE;
  1.1551 +        else
  1.1552 +          newSort = NHQO.SORT_BY_URI_ASCENDING;
  1.1553 +
  1.1554 +        break;
  1.1555 +      case this.COLUMN_TYPE_DATE:
  1.1556 +        if (oldSort == NHQO.SORT_BY_DATE_ASCENDING)
  1.1557 +          newSort = NHQO.SORT_BY_DATE_DESCENDING;
  1.1558 +        else if (allowTriState &&
  1.1559 +                 oldSort == NHQO.SORT_BY_DATE_DESCENDING)
  1.1560 +          newSort = NHQO.SORT_BY_NONE;
  1.1561 +        else
  1.1562 +          newSort = NHQO.SORT_BY_DATE_ASCENDING;
  1.1563 +
  1.1564 +        break;
  1.1565 +      case this.COLUMN_TYPE_VISITCOUNT:
  1.1566 +        // visit count default is unusual because we sort by descending
  1.1567 +        // by default because you are most likely to be looking for
  1.1568 +        // highly visited sites when you click it
  1.1569 +        if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING)
  1.1570 +          newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
  1.1571 +        else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING)
  1.1572 +          newSort = NHQO.SORT_BY_NONE;
  1.1573 +        else
  1.1574 +          newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
  1.1575 +
  1.1576 +        break;
  1.1577 +      case this.COLUMN_TYPE_KEYWORD:
  1.1578 +        if (oldSort == NHQO.SORT_BY_KEYWORD_ASCENDING)
  1.1579 +          newSort = NHQO.SORT_BY_KEYWORD_DESCENDING;
  1.1580 +        else if (allowTriState && oldSort == NHQO.SORT_BY_KEYWORD_DESCENDING)
  1.1581 +          newSort = NHQO.SORT_BY_NONE;
  1.1582 +        else
  1.1583 +          newSort = NHQO.SORT_BY_KEYWORD_ASCENDING;
  1.1584 +
  1.1585 +        break;
  1.1586 +      case this.COLUMN_TYPE_DESCRIPTION:
  1.1587 +        if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING &&
  1.1588 +            oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) {
  1.1589 +          newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING;
  1.1590 +          newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
  1.1591 +        }
  1.1592 +        else if (allowTriState &&
  1.1593 +                 oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING &&
  1.1594 +                 oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
  1.1595 +          newSort = NHQO.SORT_BY_NONE;
  1.1596 +        else {
  1.1597 +          newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING;
  1.1598 +          newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
  1.1599 +        }
  1.1600 +
  1.1601 +        break;
  1.1602 +      case this.COLUMN_TYPE_DATEADDED:
  1.1603 +        if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING)
  1.1604 +          newSort = NHQO.SORT_BY_DATEADDED_DESCENDING;
  1.1605 +        else if (allowTriState &&
  1.1606 +                 oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING)
  1.1607 +          newSort = NHQO.SORT_BY_NONE;
  1.1608 +        else
  1.1609 +          newSort = NHQO.SORT_BY_DATEADDED_ASCENDING;
  1.1610 +
  1.1611 +        break;
  1.1612 +      case this.COLUMN_TYPE_LASTMODIFIED:
  1.1613 +        if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING)
  1.1614 +          newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING;
  1.1615 +        else if (allowTriState &&
  1.1616 +                 oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING)
  1.1617 +          newSort = NHQO.SORT_BY_NONE;
  1.1618 +        else
  1.1619 +          newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING;
  1.1620 +
  1.1621 +        break;
  1.1622 +      case this.COLUMN_TYPE_TAGS:
  1.1623 +        if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING)
  1.1624 +          newSort = NHQO.SORT_BY_TAGS_DESCENDING;
  1.1625 +        else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING)
  1.1626 +          newSort = NHQO.SORT_BY_NONE;
  1.1627 +        else
  1.1628 +          newSort = NHQO.SORT_BY_TAGS_ASCENDING;
  1.1629 +
  1.1630 +        break;
  1.1631 +      default:
  1.1632 +        throw Cr.NS_ERROR_INVALID_ARG;
  1.1633 +    }
  1.1634 +    this._result.sortingAnnotation = newSortingAnnotation;
  1.1635 +    this._result.sortingMode = newSort;
  1.1636 +  },
  1.1637 +
  1.1638 +  isEditable: function PTV_isEditable(aRow, aColumn) {
  1.1639 +    // At this point we only support editing the title field.
  1.1640 +    if (aColumn.index != 0)
  1.1641 +      return false;
  1.1642 +
  1.1643 +    // Only bookmark-nodes are editable, and those are never built lazily
  1.1644 +    let node = this._rows[aRow];
  1.1645 +    if (!node || node.itemId == -1)
  1.1646 +      return false;
  1.1647 +
  1.1648 +    // The following items are never editable:
  1.1649 +    // * Read-only items.
  1.1650 +    // * places-roots
  1.1651 +    // * separators
  1.1652 +    if (PlacesUtils.nodeIsReadOnly(node) ||
  1.1653 +        PlacesUtils.nodeIsSeparator(node))
  1.1654 +      return false;
  1.1655 +
  1.1656 +    if (PlacesUtils.nodeIsFolder(node)) {
  1.1657 +      let itemId = PlacesUtils.getConcreteItemId(node);
  1.1658 +      if (PlacesUtils.isRootItem(itemId))
  1.1659 +        return false;
  1.1660 +    }
  1.1661 +
  1.1662 +    return true;
  1.1663 +  },
  1.1664 +
  1.1665 +  setCellText: function PTV_setCellText(aRow, aColumn, aText) {
  1.1666 +    // We may only get here if the cell is editable.
  1.1667 +    let node = this._rows[aRow];
  1.1668 +    if (node.title != aText) {
  1.1669 +      let txn = new PlacesEditItemTitleTransaction(node.itemId, aText);
  1.1670 +      PlacesUtils.transactionManager.doTransaction(txn);
  1.1671 +    }
  1.1672 +  },
  1.1673 +
  1.1674 +  toggleCutNode: function PTV_toggleCutNode(aNode, aValue) {
  1.1675 +    let currentVal = this._cuttingNodes.has(aNode);
  1.1676 +    if (currentVal != aValue) {
  1.1677 +      if (aValue)
  1.1678 +        this._cuttingNodes.add(aNode);
  1.1679 +      else
  1.1680 +        this._cuttingNodes.delete(aNode);
  1.1681 +
  1.1682 +      this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
  1.1683 +    }
  1.1684 +  },
  1.1685 +
  1.1686 +  selectionChanged: function() { },
  1.1687 +  cycleCell: function(aRow, aColumn) { },
  1.1688 +  isSelectable: function(aRow, aColumn) { return false; },
  1.1689 +  performAction: function(aAction) { },
  1.1690 +  performActionOnRow: function(aAction, aRow) { },
  1.1691 +  performActionOnCell: function(aAction, aRow, aColumn) { }
  1.1692 +};

mercurial