browser/components/places/content/treeView.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
     7 const PTV_interfaces = [Ci.nsITreeView,
     8                         Ci.nsINavHistoryResultObserver,
     9                         Ci.nsINavHistoryResultTreeViewer,
    10                         Ci.nsISupportsWeakReference];
    12 function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) {
    13   this._tree = null;
    14   this._result = null;
    15   this._selection = null;
    16   this._rootNode = null;
    17   this._rows = [];
    18   this._flatList = aFlatList;
    19   this._openContainerCallback = aOnOpenFlatContainer;
    20   this._controller = aController;
    21 }
    23 PlacesTreeView.prototype = {
    24   get wrappedJSObject() this,
    26   __dateService: null,
    27   get _dateService() {
    28     if (!this.__dateService) {
    29       this.__dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"].
    30                            getService(Ci.nsIScriptableDateFormat);
    31     }
    32     return this.__dateService;
    33   },
    35   QueryInterface: XPCOMUtils.generateQI(PTV_interfaces),
    37   // Bug 761494:
    38   // ----------
    39   // Some addons use methods from nsINavHistoryResultObserver and
    40   // nsINavHistoryResultTreeViewer, without QIing to these interfaces first.
    41   // That's not a problem when the view is retrieved through the
    42   // <tree>.view getter (which returns the wrappedJSObject of this object),
    43   // it raises an issue when the view retrieved through the treeBoxObject.view
    44   // getter.  Thus, to avoid breaking addons, the interfaces are prefetched.
    45   classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }),
    47   /**
    48    * This is called once both the result and the tree are set.
    49    */
    50   _finishInit: function PTV__finishInit() {
    51     let selection = this.selection;
    52     if (selection)
    53       selection.selectEventsSuppressed = true;
    55     if (!this._rootNode.containerOpen) {
    56       // This triggers containerStateChanged which then builds the visible
    57       // section.
    58       this._rootNode.containerOpen = true;
    59     }
    60     else
    61       this.invalidateContainer(this._rootNode);
    63     // "Activate" the sorting column and update commands.
    64     this.sortingChanged(this._result.sortingMode);
    66     if (selection)
    67       selection.selectEventsSuppressed = false;
    68   },
    70   /**
    71    * Plain Container: container result nodes which may never include sub
    72    * hierarchies.
    73    *
    74    * When the rows array is constructed, we don't set the children of plain
    75    * containers.  Instead, we keep placeholders for these children.  We then
    76    * build these children lazily as the tree asks us for information about each
    77    * row.  Luckily, the tree doesn't ask about rows outside the visible area.
    78    *
    79    * @see _getNodeForRow and _getRowForNode for the actual magic.
    80    *
    81    * @note It's guaranteed that all containers are listed in the rows
    82    * elements array.  It's also guaranteed that separators (if they're not
    83    * filtered, see below) are listed in the visible elements array, because
    84    * bookmark folders are never built lazily, as described above.
    85    *
    86    * @param aContainer
    87    *        A container result node.
    88    *
    89    * @return true if aContainer is a plain container, false otherwise.
    90    */
    91   _isPlainContainer: function PTV__isPlainContainer(aContainer) {
    92     // Livemarks are always plain containers.
    93     if (this._controller.hasCachedLivemarkInfo(aContainer))
    94       return true;
    96     // We don't know enough about non-query containers.
    97     if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode))
    98       return false;
   100     switch (aContainer.queryOptions.resultType) {
   101       case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
   102       case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
   103       case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
   104       case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY:
   105         return false;
   106     }
   108     // If it's a folder, it's not a plain container.
   109     let nodeType = aContainer.type;
   110     return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER &&
   111            nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
   112   },
   114   /**
   115    * Gets the row number for a given node.  Assumes that the given node is
   116    * visible (i.e. it's not an obsolete node).
   117    *
   118    * @param aNode
   119    *        A result node.  Do not pass an obsolete node, or any
   120    *        node which isn't supposed to be in the tree (e.g. separators in
   121    *        sorted trees).
   122    * @param [optional] aForceBuild
   123    *        @see _isPlainContainer.
   124    *        If true, the row will be computed even if the node still isn't set
   125    *        in our rows array.
   126    * @param [optional] aParentRow
   127    *        The row of aNode's parent. Ignored for the root node.
   128    * @param [optional] aNodeIndex
   129    *        The index of aNode in its parent.  Only used if aParentRow is
   130    *        set too.
   131    *
   132    * @throws if aNode is invisible.
   133    * @note If aParentRow and aNodeIndex are passed and parent is a plain
   134    * container, this method will just return a calculated row value, without
   135    * making assumptions on existence of the node at that position.
   136    * @return aNode's row if it's in the rows list or if aForceBuild is set, -1
   137    *         otherwise.
   138    */
   139   _getRowForNode:
   140   function PTV__getRowForNode(aNode, aForceBuild, aParentRow, aNodeIndex) {
   141     if (aNode == this._rootNode)
   142       throw new Error("The root node is never visible");
   144     // A node is removed form the view either if it has no parent or if its
   145     // root-ancestor is not the root node (in which case that's the node
   146     // for which nodeRemoved was called).
   147     let ancestors = [x for each (x in PlacesUtils.nodeAncestors(aNode))];
   148     if (ancestors.length == 0 ||
   149         ancestors[ancestors.length - 1] != this._rootNode) {
   150       throw new Error("Removed node passed to _getRowForNode");
   151     }
   153     // Ensure that the entire chain is open, otherwise that node is invisible.
   154     for (let ancestor of ancestors) {
   155       if (!ancestor.containerOpen)
   156         throw new Error("Invisible node passed to _getRowForNode");
   157     }
   159     // Non-plain containers are initially built with their contents.
   160     let parent = aNode.parent;
   161     let parentIsPlain = this._isPlainContainer(parent);
   162     if (!parentIsPlain) {
   163       if (parent == this._rootNode)
   164         return this._rows.indexOf(aNode);
   166       return this._rows.indexOf(aNode, aParentRow);
   167     }
   169     let row = -1;
   170     let useNodeIndex = typeof(aNodeIndex) == "number";
   171     if (parent == this._rootNode)
   172       row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode);
   173     else if (useNodeIndex && typeof(aParentRow) == "number") {
   174       // If we have both the row of the parent node, and the node's index, we
   175       // can avoid searching the rows array if the parent is a plain container.
   176       row = aParentRow + aNodeIndex + 1;
   177     }
   178     else {
   179       // Look for the node in the nodes array.  Start the search at the parent
   180       // row.  If the parent row isn't passed, we'll pass undefined to indexOf,
   181       // which is fine.
   182       row = this._rows.indexOf(aNode, aParentRow);
   183       if (row == -1 && aForceBuild) {
   184         let parentRow = typeof(aParentRow) == "number" ? aParentRow
   185                                                        : this._getRowForNode(parent);
   186         row = parentRow + parent.getChildIndex(aNode) + 1;
   187       }
   188     }
   190     if (row != -1)
   191       this._rows[row] = aNode;
   193     return row;
   194   },
   196   /**
   197    * Given a row, finds and returns the parent details of the associated node.
   198    *
   199    * @param aChildRow
   200    *        Row number.
   201    * @return [parentNode, parentRow]
   202    */
   203   _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) {
   204     let node = this._getNodeForRow(aChildRow);
   205     let parent = (node === null) ? this._rootNode : node.parent;
   207     // The root node is never visible
   208     if (parent == this._rootNode)
   209       return [this._rootNode, -1];
   211     let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1);
   212     return [parent, parentRow];
   213   },
   215   /**
   216    * Gets the node at a given row.
   217    */
   218   _getNodeForRow: function PTV__getNodeForRow(aRow) {
   219     if (aRow < 0) {
   220       return null;
   221     }
   223     let node = this._rows[aRow];
   224     if (node !== undefined)
   225       return node;
   227     // Find the nearest node.
   228     let rowNode, row;
   229     for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) {
   230       rowNode = this._rows[i];
   231       row = i;
   232     }
   234     // If there's no container prior to the given row, it's a child of
   235     // the root node (remember: all containers are listed in the rows array).
   236     if (!rowNode)
   237       return this._rows[aRow] = this._rootNode.getChild(aRow);
   239     // Unset elements may exist only in plain containers.  Thus, if the nearest
   240     // node is a container, it's the row's parent, otherwise, it's a sibling.
   241     if (rowNode instanceof Ci.nsINavHistoryContainerResultNode)
   242       return this._rows[aRow] = rowNode.getChild(aRow - row - 1);
   244     let [parent, parentRow] = this._getParentByChildRow(row);
   245     return this._rows[aRow] = parent.getChild(aRow - parentRow - 1);
   246   },
   248   /**
   249    * This takes a container and recursively appends our rows array per its
   250    * contents.  Assumes that the rows arrays has no rows for the given
   251    * container.
   252    *
   253    * @param [in] aContainer
   254    *        A container result node.
   255    * @param [in] aFirstChildRow
   256    *        The first row at which nodes may be inserted to the row array.
   257    *        In other words, that's aContainer's row + 1.
   258    * @param [out] aToOpen
   259    *        An array of containers to open once the build is done.
   260    *
   261    * @return the number of rows which were inserted.
   262    */
   263   _buildVisibleSection:
   264   function PTV__buildVisibleSection(aContainer, aFirstChildRow, aToOpen)
   265   {
   266     // There's nothing to do if the container is closed.
   267     if (!aContainer.containerOpen)
   268       return 0;
   270     // Inserting the new elements into the rows array in one shot (by
   271     // Array.concat) is faster than resizing the array (by splice) on each loop
   272     // iteration.
   273     let cc = aContainer.childCount;
   274     let newElements = new Array(cc);
   275     this._rows = this._rows.splice(0, aFirstChildRow)
   276                      .concat(newElements, this._rows);
   278     if (this._isPlainContainer(aContainer))
   279       return cc;
   281     const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open");
   282     const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true");
   283     let sortingMode = this._result.sortingMode;
   285     let rowsInserted = 0;
   286     for (let i = 0; i < cc; i++) {
   287       let curChild = aContainer.getChild(i);
   288       let curChildType = curChild.type;
   290       let row = aFirstChildRow + rowsInserted;
   292       // Don't display separators when sorted.
   293       if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
   294         if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
   295           // Remove the element for the filtered separator.
   296           // Notice that the rows array was initially resized to include all
   297           // children.
   298           this._rows.splice(row, 1);
   299           continue;
   300         }
   301       }
   303       this._rows[row] = curChild;
   304       rowsInserted++;
   306       // Recursively do containers.
   307       if (!this._flatList &&
   308           curChild instanceof Ci.nsINavHistoryContainerResultNode &&
   309           !this._controller.hasCachedLivemarkInfo(curChild)) {
   310         let resource = this._getResourceForNode(curChild);
   311         let isopen = resource != null &&
   312                      PlacesUIUtils.localStore.HasAssertion(resource,
   313                                                            openLiteral,
   314                                                            trueLiteral, true);
   315         if (isopen != curChild.containerOpen)
   316           aToOpen.push(curChild);
   317         else if (curChild.containerOpen && curChild.childCount > 0)
   318           rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen);
   319       }
   320     }
   322     return rowsInserted;
   323   },
   325   /**
   326    * This counts how many rows a node takes in the tree.  For containers it
   327    * will count the node itself plus any child node following it.
   328    */
   329   _countVisibleRowsForNodeAtRow:
   330   function PTV__countVisibleRowsForNodeAtRow(aNodeRow) {
   331     let node = this._rows[aNodeRow];
   333     // If it's not listed yet, we know that it's a leaf node (instanceof also
   334     // null-checks).
   335     if (!(node instanceof Ci.nsINavHistoryContainerResultNode))
   336       return 1;
   338     let outerLevel = node.indentLevel;
   339     for (let i = aNodeRow + 1; i < this._rows.length; i++) {
   340       let rowNode = this._rows[i];
   341       if (rowNode && rowNode.indentLevel <= outerLevel)
   342         return i - aNodeRow;
   343     }
   345     // This node plus its children take up the bottom of the list.
   346     return this._rows.length - aNodeRow;
   347   },
   349   _getSelectedNodesInRange:
   350   function PTV__getSelectedNodesInRange(aFirstRow, aLastRow) {
   351     let selection = this.selection;
   352     let rc = selection.getRangeCount();
   353     if (rc == 0)
   354       return [];
   356     // The visible-area borders are needed for checking whether a
   357     // selected row is also visible.
   358     let firstVisibleRow = this._tree.getFirstVisibleRow();
   359     let lastVisibleRow = this._tree.getLastVisibleRow();
   361     let nodesInfo = [];
   362     for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) {
   363       let min = { }, max = { };
   364       selection.getRangeAt(rangeIndex, min, max);
   366       // If this range does not overlap the replaced chunk, we don't need to
   367       // persist the selection.
   368       if (max.value < aFirstRow || min.value > aLastRow)
   369         continue;
   371       let firstRow = Math.max(min.value, aFirstRow);
   372       let lastRow = Math.min(max.value, aLastRow);
   373       for (let i = firstRow; i <= lastRow; i++) {
   374         nodesInfo.push({
   375           node: this._rows[i],
   376           oldRow: i,
   377           wasVisible: i >= firstVisibleRow && i <= lastVisibleRow
   378         });
   379       }
   380     }
   382     return nodesInfo;
   383   },
   385   /**
   386    * Tries to find an equivalent node for a node which was removed.  We first
   387    * look for the original node, in case it was just relocated.  Then, if we
   388    * that node was not found, we look for a node that has the same itemId, uri
   389    * and time values.
   390    *
   391    * @param aUpdatedContainer
   392    *        An ancestor of the node which was removed.  It does not have to be
   393    *        its direct parent.
   394    * @param aOldNode
   395    *        The node which was removed.
   396    *
   397    * @return the row number of an equivalent node for aOldOne, if one was
   398    *         found, -1 otherwise.
   399    */
   400   _getNewRowForRemovedNode:
   401   function PTV__getNewRowForRemovedNode(aUpdatedContainer, aOldNode) {
   402     let parent = aOldNode.parent;
   403     if (parent) {
   404       // If the node's parent is still set, the node is not obsolete
   405       // and we should just find out its new position.
   406       // However, if any of the node's ancestor is closed, the node is
   407       // invisible.
   408       let ancestors = PlacesUtils.nodeAncestors(aOldNode);
   409       for (let ancestor in ancestors) {
   410         if (!ancestor.containerOpen)
   411           return -1;
   412       }
   414       return this._getRowForNode(aOldNode, true);
   415     }
   417     // There's a broken edge case here.
   418     // If a visit appears in two queries, and the second one was
   419     // the old node, we'll select the first one after refresh.  There's
   420     // nothing we could do about that, because aOldNode.parent is
   421     // gone by the time invalidateContainer is called.
   422     let newNode = aUpdatedContainer.findNodeByDetails(aOldNode.uri,
   423                                                       aOldNode.time,
   424                                                       aOldNode.itemId,
   425                                                       true);
   426     if (!newNode)
   427       return -1;
   429     return this._getRowForNode(newNode, true);
   430   },
   432   /**
   433    * Restores a given selection state as near as possible to the original
   434    * selection state.
   435    *
   436    * @param aNodesInfo
   437    *        The persisted selection state as returned by
   438    *        _getSelectedNodesInRange.
   439    * @param aUpdatedContainer
   440    *        The container which was updated.
   441    */
   442   _restoreSelection:
   443   function PTV__restoreSelection(aNodesInfo, aUpdatedContainer) {
   444     if (aNodesInfo.length == 0)
   445       return;
   447     let selection = this.selection;
   449     // Attempt to ensure that previously-visible selection will be visible
   450     // if it's re-selected.  However, we can only ensure that for one row.
   451     let scrollToRow = -1;
   452     for (let i = 0; i < aNodesInfo.length; i++) {
   453       let nodeInfo = aNodesInfo[i];
   454       let row = this._getNewRowForRemovedNode(aUpdatedContainer,
   455                                               nodeInfo.node);
   456       // Select the found node, if any.
   457       if (row != -1) {
   458         selection.rangedSelect(row, row, true);
   459         if (nodeInfo.wasVisible && scrollToRow == -1)
   460           scrollToRow = row;
   461       }
   462     }
   464     // If only one node was previously selected and there's no selection now,
   465     // select the node at its old row, if any.
   466     if (aNodesInfo.length == 1 && selection.count == 0) {
   467       let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1);
   468       if (row != -1) {
   469         selection.rangedSelect(row, row, true);
   470         if (aNodesInfo[0].wasVisible && scrollToRow == -1)
   471           scrollToRow = aNodesInfo[0].oldRow;
   472       }
   473     }
   475     if (scrollToRow != -1)
   476       this._tree.ensureRowIsVisible(scrollToRow);
   477   },
   479   _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) {
   480     const MS_PER_MINUTE = 60000;
   481     const MS_PER_DAY = 86400000;
   482     let timeMs = aTime / 1000; // PRTime is in microseconds
   484     // Date is calculated starting from midnight, so the modulo with a day are
   485     // milliseconds from today's midnight.
   486     // getTimezoneOffset corrects that based on local time, notice midnight
   487     // can have a different offset during DST-change days.
   488     let dateObj = new Date();
   489     let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
   490     let midnight = now - (now % MS_PER_DAY);
   491     midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
   493     let dateFormat = timeMs >= midnight ?
   494                       Ci.nsIScriptableDateFormat.dateFormatNone :
   495                       Ci.nsIScriptableDateFormat.dateFormatShort;
   497     let timeObj = new Date(timeMs);
   498     return (this._dateService.FormatDateTime("", dateFormat,
   499       Ci.nsIScriptableDateFormat.timeFormatNoSeconds,
   500       timeObj.getFullYear(), timeObj.getMonth() + 1,
   501       timeObj.getDate(), timeObj.getHours(),
   502       timeObj.getMinutes(), timeObj.getSeconds()));
   503   },
   505   COLUMN_TYPE_UNKNOWN: 0,
   506   COLUMN_TYPE_TITLE: 1,
   507   COLUMN_TYPE_URI: 2,
   508   COLUMN_TYPE_DATE: 3,
   509   COLUMN_TYPE_VISITCOUNT: 4,
   510   COLUMN_TYPE_KEYWORD: 5,
   511   COLUMN_TYPE_DESCRIPTION: 6,
   512   COLUMN_TYPE_DATEADDED: 7,
   513   COLUMN_TYPE_LASTMODIFIED: 8,
   514   COLUMN_TYPE_TAGS: 9,
   516   _getColumnType: function PTV__getColumnType(aColumn) {
   517     let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
   519     switch (columnType) {
   520       case "title":
   521         return this.COLUMN_TYPE_TITLE;
   522       case "url":
   523         return this.COLUMN_TYPE_URI;
   524       case "date":
   525         return this.COLUMN_TYPE_DATE;
   526       case "visitCount":
   527         return this.COLUMN_TYPE_VISITCOUNT;
   528       case "keyword":
   529         return this.COLUMN_TYPE_KEYWORD;
   530       case "description":
   531         return this.COLUMN_TYPE_DESCRIPTION;
   532       case "dateAdded":
   533         return this.COLUMN_TYPE_DATEADDED;
   534       case "lastModified":
   535         return this.COLUMN_TYPE_LASTMODIFIED;
   536       case "tags":
   537         return this.COLUMN_TYPE_TAGS;
   538     }
   539     return this.COLUMN_TYPE_UNKNOWN;
   540   },
   542   _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) {
   543     switch (aSortType) {
   544       case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
   545         return [this.COLUMN_TYPE_TITLE, false];
   546       case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
   547         return [this.COLUMN_TYPE_TITLE, true];
   548       case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
   549         return [this.COLUMN_TYPE_DATE, false];
   550       case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
   551         return [this.COLUMN_TYPE_DATE, true];
   552       case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
   553         return [this.COLUMN_TYPE_URI, false];
   554       case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
   555         return [this.COLUMN_TYPE_URI, true];
   556       case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
   557         return [this.COLUMN_TYPE_VISITCOUNT, false];
   558       case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
   559         return [this.COLUMN_TYPE_VISITCOUNT, true];
   560       case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING:
   561         return [this.COLUMN_TYPE_KEYWORD, false];
   562       case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING:
   563         return [this.COLUMN_TYPE_KEYWORD, true];
   564       case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING:
   565         if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
   566           return [this.COLUMN_TYPE_DESCRIPTION, false];
   567         break;
   568       case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING:
   569         if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
   570           return [this.COLUMN_TYPE_DESCRIPTION, true];
   571       case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
   572         return [this.COLUMN_TYPE_DATEADDED, false];
   573       case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
   574         return [this.COLUMN_TYPE_DATEADDED, true];
   575       case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING:
   576         return [this.COLUMN_TYPE_LASTMODIFIED, false];
   577       case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING:
   578         return [this.COLUMN_TYPE_LASTMODIFIED, true];
   579       case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING:
   580         return [this.COLUMN_TYPE_TAGS, false];
   581       case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING:
   582         return [this.COLUMN_TYPE_TAGS, true];
   583     }
   584     return [this.COLUMN_TYPE_UNKNOWN, false];
   585   },
   587   // nsINavHistoryResultObserver
   588   nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) {
   589     NS_ASSERT(this._result, "Got a notification but have no result!");
   590     if (!this._tree || !this._result)
   591       return;
   593     // Bail out for hidden separators.
   594     if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
   595       return;
   597     let parentRow;
   598     if (aParentNode != this._rootNode) {
   599       parentRow = this._getRowForNode(aParentNode);
   601       // Update parent when inserting the first item, since twisty has changed.
   602       if (aParentNode.childCount == 1)
   603         this._tree.invalidateRow(parentRow);
   604     }
   606     // Compute the new row number of the node.
   607     let row = -1;
   608     let cc = aParentNode.childCount;
   609     if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) {
   610       // We don't need to worry about sub hierarchies of the parent node
   611       // if it's a plain container, or if the new node is its first child.
   612       if (aParentNode == this._rootNode)
   613         row = aNewIndex;
   614       else
   615         row = parentRow + aNewIndex + 1;
   616     }
   617     else {
   618       // Here, we try to find the next visible element in the child list so we
   619       // can set the new visible index to be right before that.  Note that we
   620       // have to search down instead of up, because some siblings could have
   621       // children themselves that would be in the way.
   622       let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) &&
   623                                 this.isSorted();
   624       for (let i = aNewIndex + 1; i < cc; i++) {
   625         let node = aParentNode.getChild(i);
   626         if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) {
   627           // The children have not been shifted so the next item will have what
   628           // should be our index.
   629           row = this._getRowForNode(node, false, parentRow, i);
   630           break;
   631         }
   632       }
   633       if (row < 0) {
   634         // At the end of the child list without finding a visible sibling. This
   635         // is a little harder because we don't know how many rows the last item
   636         // in our list takes up (it could be a container with many children).
   637         let prevChild = aParentNode.getChild(aNewIndex - 1);
   638         let prevIndex = this._getRowForNode(prevChild, false, parentRow,
   639                                             aNewIndex - 1);
   640         row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex);
   641       }
   642     }
   644     this._rows.splice(row, 0, aNode);
   645     this._tree.rowCountChanged(row, 1);
   647     if (PlacesUtils.nodeIsContainer(aNode) &&
   648         PlacesUtils.asContainer(aNode).containerOpen) {
   649       this.invalidateContainer(aNode);
   650     }
   651   },
   653   /**
   654    * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being
   655    * removed but the node it is collapsed with is not being removed (this then
   656    * just swap out the removee with its collapsing partner). The only time
   657    * when we really remove things is when deleting URIs, which will apply to
   658    * all collapsees. This function is called sometimes when resorting items.
   659    * However, we won't do this when sorted by date because dates will never
   660    * change for visits, and date sorting is the only time things are collapsed.
   661    */
   662   nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) {
   663     NS_ASSERT(this._result, "Got a notification but have no result!");
   664     if (!this._tree || !this._result)
   665       return;
   667     // XXX bug 517701: We don't know what to do when the root node is removed.
   668     if (aNode == this._rootNode)
   669       throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   671     // Bail out for hidden separators.
   672     if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
   673       return;
   675     let parentRow = aParentNode == this._rootNode ?
   676                     undefined : this._getRowForNode(aParentNode, true);
   677     let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex);
   678     if (oldRow < 0)
   679       throw Cr.NS_ERROR_UNEXPECTED;
   681     // If the node was exclusively selected, the node next to it will be
   682     // selected.
   683     let selectNext = false;
   684     let selection = this.selection;
   685     if (selection.getRangeCount() == 1) {
   686       let min = { }, max = { };
   687       selection.getRangeAt(0, min, max);
   688       if (min.value == max.value &&
   689           this.nodeForTreeIndex(min.value) == aNode)
   690         selectNext = true;
   691     }
   693     // Remove the node and its children, if any.
   694     let count = this._countVisibleRowsForNodeAtRow(oldRow);
   695     this._rows.splice(oldRow, count);
   696     this._tree.rowCountChanged(oldRow, -count);
   698     // Redraw the parent if its twisty state has changed.
   699     if (aParentNode != this._rootNode && !aParentNode.hasChildren) {
   700       let parentRow = oldRow - 1;
   701       this._tree.invalidateRow(parentRow);
   702     }
   704     // Restore selection if the node was exclusively selected.
   705     if (!selectNext)
   706       return;
   708     // Restore selection.
   709     let rowToSelect = Math.min(oldRow, this._rows.length - 1);
   710     if (rowToSelect != -1)
   711       this.selection.rangedSelect(rowToSelect, rowToSelect, true);
   712   },
   714   nodeMoved:
   715   function PTV_nodeMoved(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) {
   716     NS_ASSERT(this._result, "Got a notification but have no result!");
   717     if (!this._tree || !this._result)
   718       return;
   720     // Bail out for hidden separators.
   721     if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
   722       return;
   724     // Note that at this point the node has already been moved by the backend,
   725     // so we must give hints to _getRowForNode to get the old row position.
   726     let oldParentRow = aOldParent == this._rootNode ?
   727                          undefined : this._getRowForNode(aOldParent, true);
   728     let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex);
   729     if (oldRow < 0)
   730       throw Cr.NS_ERROR_UNEXPECTED;
   732     // If this node is a container it could take up more than one row.
   733     let count = this._countVisibleRowsForNodeAtRow(oldRow);
   735     // Persist selection state.
   736     let nodesToReselect =
   737       this._getSelectedNodesInRange(oldRow, oldRow + count);
   738     if (nodesToReselect.length > 0)
   739       this.selection.selectEventsSuppressed = true;
   741     // Redraw the parent if its twisty state has changed.
   742     if (aOldParent != this._rootNode && !aOldParent.hasChildren) {
   743       let parentRow = oldRow - 1;
   744       this._tree.invalidateRow(parentRow);
   745     }
   747     // Remove node and its children, if any, from the old position.
   748     this._rows.splice(oldRow, count);
   749     this._tree.rowCountChanged(oldRow, -count);
   751     // Insert the node into the new position.
   752     this.nodeInserted(aNewParent, aNode, aNewIndex);
   754     // Restore selection.
   755     if (nodesToReselect.length > 0) {
   756       this._restoreSelection(nodesToReselect, aNewParent);
   757       this.selection.selectEventsSuppressed = false;
   758     }
   759   },
   761   _invalidateCellValue: function PTV__invalidateCellValue(aNode,
   762                                                           aColumnType) {
   763     NS_ASSERT(this._result, "Got a notification but have no result!");
   764     if (!this._tree || !this._result)
   765       return;
   767     // Nothing to do for the root node.
   768     if (aNode == this._rootNode)
   769       return;
   771     let row = this._getRowForNode(aNode);
   772     if (row == -1)
   773       return;
   775     let column = this._findColumnByType(aColumnType);
   776     if (column && !column.element.hidden)
   777       this._tree.invalidateCell(row, column);
   779     // Last modified time is altered for almost all node changes.
   780     if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) {
   781       let lastModifiedColumn =
   782         this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED);
   783       if (lastModifiedColumn && !lastModifiedColumn.hidden)
   784         this._tree.invalidateCell(row, lastModifiedColumn);
   785     }
   786   },
   788   _populateLivemarkContainer: function PTV__populateLivemarkContainer(aNode) {
   789     PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
   790       .then(aLivemark => {
   791         let placesNode = aNode;
   792         // Need to check containerOpen since getLivemark is async.
   793         if (!placesNode.containerOpen)
   794           return;
   796         let children = aLivemark.getNodesForContainer(placesNode);
   797         for (let i = 0; i < children.length; i++) {
   798           let child = children[i];
   799           this.nodeInserted(placesNode, child, i);
   800         }
   801       }, Components.utils.reportError);
   802   },
   804   nodeTitleChanged: function PTV_nodeTitleChanged(aNode, aNewTitle) {
   805     this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   806   },
   808   nodeURIChanged: function PTV_nodeURIChanged(aNode, aNewURI) {
   809     this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
   810   },
   812   nodeIconChanged: function PTV_nodeIconChanged(aNode) {
   813     this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   814   },
   816   nodeHistoryDetailsChanged:
   817   function PTV_nodeHistoryDetailsChanged(aNode, aUpdatedVisitDate,
   818                                          aUpdatedVisitCount) {
   819     if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) {
   820       // Find the node in the parent.
   821       let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent);
   822       for (let i = parentRow; i < this._rows.length; i++) {
   823         let child = this.nodeForTreeIndex(i);
   824         if (child.uri == aNode.uri) {
   825           this._cellProperties.delete(child);
   826           this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE);
   827           break;
   828         }
   829       }
   830       return;
   831     }
   833     this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
   834     this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
   835   },
   837   nodeTagsChanged: function PTV_nodeTagsChanged(aNode) {
   838     this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
   839   },
   841   nodeKeywordChanged: function PTV_nodeKeywordChanged(aNode, aNewKeyword) {
   842     this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD);
   843   },
   845   nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) {
   846     if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
   847       this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
   848     }
   849     else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
   850       PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
   851         .then(aLivemark => {
   852           this._controller.cacheLivemarkInfo(aNode, aLivemark);
   853           let properties = this._cellProperties.get(aNode);
   854           this._cellProperties.set(aNode, properties += " livemark ");
   856           // The livemark attribute is set as a cell property on the title cell.
   857           this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
   858         }, Components.utils.reportError);
   859     }
   860   },
   862   nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode, aNewValue) {
   863     this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
   864   },
   866   nodeLastModifiedChanged:
   867   function PTV_nodeLastModifiedChanged(aNode, aNewValue) {
   868     this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED);
   869   },
   871   containerStateChanged:
   872   function PTV_containerStateChanged(aNode, aOldState, aNewState) {
   873     this.invalidateContainer(aNode);
   875     if (PlacesUtils.nodeIsFolder(aNode) ||
   876         (this._flatList && aNode == this._rootNode)) {
   877       let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions;
   878       if (queryOptions.excludeItems) {
   879         return;
   880       }
   882       PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
   883         .then(aLivemark => {
   884           let shouldInvalidate = 
   885             !this._controller.hasCachedLivemarkInfo(aNode);
   886           this._controller.cacheLivemarkInfo(aNode, aLivemark);
   887           if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) {
   888             aLivemark.registerForUpdates(aNode, this);
   889             // Prioritize the current livemark.
   890             aLivemark.reload();
   891             PlacesUtils.livemarks.reloadLivemarks();
   892             if (shouldInvalidate)
   893               this.invalidateContainer(aNode);
   894           }
   895           else {
   896             aLivemark.unregisterForUpdates(aNode);
   897           }
   898         }, () => undefined);
   899     }
   900   },
   902   invalidateContainer: function PTV_invalidateContainer(aContainer) {
   903     NS_ASSERT(this._result, "Need to have a result to update");
   904     if (!this._tree)
   905       return;
   907     let startReplacement, replaceCount;
   908     if (aContainer == this._rootNode) {
   909       startReplacement = 0;
   910       replaceCount = this._rows.length;
   912       // If the root node is now closed, the tree is empty.
   913       if (!this._rootNode.containerOpen) {
   914         this._rows = [];
   915         if (replaceCount)
   916           this._tree.rowCountChanged(startReplacement, -replaceCount);
   918         return;
   919       }
   920     }
   921     else {
   922       // Update the twisty state.
   923       let row = this._getRowForNode(aContainer);
   924       this._tree.invalidateRow(row);
   926       // We don't replace the container node itself, so we should decrease the
   927       // replaceCount by 1.
   928       startReplacement = row + 1;
   929       replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1;
   930     }
   932     // Persist selection state.
   933     let nodesToReselect =
   934       this._getSelectedNodesInRange(startReplacement,
   935                                     startReplacement + replaceCount);
   937     // Now update the number of elements.
   938     this.selection.selectEventsSuppressed = true;
   940     // First remove the old elements
   941     this._rows.splice(startReplacement, replaceCount);
   943     // If the container is now closed, we're done.
   944     if (!aContainer.containerOpen) {
   945       let oldSelectionCount = this.selection.count;
   946       if (replaceCount)
   947         this._tree.rowCountChanged(startReplacement, -replaceCount);
   949       // Select the row next to the closed container if any of its
   950       // children were selected, and nothing else is selected.
   951       if (nodesToReselect.length > 0 &&
   952           nodesToReselect.length == oldSelectionCount) {
   953         this.selection.rangedSelect(startReplacement, startReplacement, true);
   954         this._tree.ensureRowIsVisible(startReplacement);
   955       }
   957       this.selection.selectEventsSuppressed = false;
   958       return;
   959     }
   961     // Otherwise, start a batch first.
   962     this._tree.beginUpdateBatch();
   963     if (replaceCount)
   964       this._tree.rowCountChanged(startReplacement, -replaceCount);
   966     let toOpenElements = [];
   967     let elementsAddedCount = this._buildVisibleSection(aContainer,
   968                                                        startReplacement,
   969                                                        toOpenElements);
   970     if (elementsAddedCount)
   971       this._tree.rowCountChanged(startReplacement, elementsAddedCount);
   973     if (!this._flatList) {
   974       // Now, open any containers that were persisted.
   975       for (let i = 0; i < toOpenElements.length; i++) {
   976         let item = toOpenElements[i];
   977         let parent = item.parent;
   979         // Avoid recursively opening containers.
   980         while (parent) {
   981           if (parent.uri == item.uri)
   982             break;
   983           parent = parent.parent;
   984         }
   986         // If we don't have a parent, we made it all the way to the root
   987         // and didn't find a match, so we can open our item.
   988         if (!parent && !item.containerOpen)
   989           item.containerOpen = true;
   990       }
   991     }
   993     if (this._controller.hasCachedLivemarkInfo(aContainer)) {
   994       let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
   995       if (!queryOptions.excludeItems) {
   996         this._populateLivemarkContainer(aContainer);
   997       }
   998     }
  1000     this._tree.endUpdateBatch();
  1002     // Restore selection.
  1003     this._restoreSelection(nodesToReselect, aContainer);
  1004     this.selection.selectEventsSuppressed = false;
  1005   },
  1007   _columns: [],
  1008   _findColumnByType: function PTV__findColumnByType(aColumnType) {
  1009     if (this._columns[aColumnType])
  1010       return this._columns[aColumnType];
  1012     let columns = this._tree.columns;
  1013     let colCount = columns.count;
  1014     for (let i = 0; i < colCount; i++) {
  1015       let column = columns.getColumnAt(i);
  1016       let columnType = this._getColumnType(column);
  1017       this._columns[columnType] = column;
  1018       if (columnType == aColumnType)
  1019         return column;
  1022     // That's completely valid.  Most of our trees actually include just the
  1023     // title column.
  1024     return null;
  1025   },
  1027   sortingChanged: function PTV__sortingChanged(aSortingMode) {
  1028     if (!this._tree || !this._result)
  1029       return;
  1031     // Depending on the sort mode, certain commands may be disabled.
  1032     window.updateCommands("sort");
  1034     let columns = this._tree.columns;
  1036     // Clear old sorting indicator.
  1037     let sortedColumn = columns.getSortedColumn();
  1038     if (sortedColumn)
  1039       sortedColumn.element.removeAttribute("sortDirection");
  1041     // Set new sorting indicator by looking through all columns for ours.
  1042     if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE)
  1043       return;
  1045     let [desiredColumn, desiredIsDescending] =
  1046       this._sortTypeToColumnType(aSortingMode);
  1047     let colCount = columns.count;
  1048     let column = this._findColumnByType(desiredColumn);
  1049     if (column) {
  1050       let sortDir = desiredIsDescending ? "descending" : "ascending";
  1051       column.element.setAttribute("sortDirection", sortDir);
  1053   },
  1055   _inBatchMode: false,
  1056   batching: function PTV__batching(aToggleMode) {
  1057     if (this._inBatchMode != aToggleMode) {
  1058       this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode;
  1059       if (this._inBatchMode) {
  1060         this._tree.beginUpdateBatch();
  1062       else {
  1063         this._tree.endUpdateBatch();
  1066   },
  1068   get result() this._result,
  1069   set result(val) {
  1070     if (this._result) {
  1071       this._result.removeObserver(this);
  1072       this._rootNode.containerOpen = false;
  1075     if (val) {
  1076       this._result = val;
  1077       this._rootNode = this._result.root;
  1078       this._cellProperties = new Map();
  1079       this._cuttingNodes = new Set();
  1081     else if (this._result) {
  1082       delete this._result;
  1083       delete this._rootNode;
  1084       delete this._cellProperties;
  1085       delete this._cuttingNodes;
  1088     // If the tree is not set yet, setTree will call finishInit.
  1089     if (this._tree && val)
  1090       this._finishInit();
  1092     return val;
  1093   },
  1095   nodeForTreeIndex: function PTV_nodeForTreeIndex(aIndex) {
  1096     if (aIndex > this._rows.length)
  1097       throw Cr.NS_ERROR_INVALID_ARG;
  1099     return this._getNodeForRow(aIndex);
  1100   },
  1102   treeIndexForNode: function PTV_treeNodeForIndex(aNode) {
  1103     // The API allows passing invisible nodes.
  1104     try {
  1105       return this._getRowForNode(aNode, true);
  1107     catch(ex) { }
  1109     return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE;
  1110   },
  1112   _getResourceForNode: function PTV_getResourceForNode(aNode)
  1114     let uri = aNode.uri;
  1115     NS_ASSERT(uri, "if there is no uri, we can't persist the open state");
  1116     return uri ? PlacesUIUtils.RDF.GetResource(uri) : null;
  1117   },
  1119   // nsITreeView
  1120   get rowCount() this._rows.length,
  1121   get selection() this._selection,
  1122   set selection(val) this._selection = val,
  1124   getRowProperties: function() { return ""; },
  1126   getCellProperties:
  1127   function PTV_getCellProperties(aRow, aColumn) {
  1128     // for anonid-trees, we need to add the column-type manually
  1129     var props = "";
  1130     let columnType = aColumn.element.getAttribute("anonid");
  1131     if (columnType)
  1132       props += columnType;
  1133     else
  1134       columnType = aColumn.id;
  1136     // Set the "ltr" property on url cells
  1137     if (columnType == "url")
  1138       props += " ltr";
  1140     if (columnType != "title")
  1141       return props;
  1143     let node = this._getNodeForRow(aRow);
  1145     if (this._cuttingNodes.has(node)) {
  1146       props += " cutting";
  1149     let properties = this._cellProperties.get(node);
  1150     if (properties === undefined) {
  1151       properties = "";
  1152       let itemId = node.itemId;
  1153       let nodeType = node.type;
  1154       if (PlacesUtils.containerTypes.indexOf(nodeType) != -1) {
  1155         if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
  1156           properties += " query";
  1157           if (PlacesUtils.nodeIsTagQuery(node))
  1158             properties += " tagContainer";
  1159           else if (PlacesUtils.nodeIsDay(node))
  1160             properties += " dayContainer";
  1161           else if (PlacesUtils.nodeIsHost(node))
  1162             properties += " hostContainer";
  1164         else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
  1165                  nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
  1166           if (this._controller.hasCachedLivemarkInfo(node)) {
  1167             properties += " livemark";
  1169           else {
  1170             PlacesUtils.livemarks.getLivemark({ id: node.itemId })
  1171               .then(aLivemark => {
  1172                 this._controller.cacheLivemarkInfo(node, aLivemark);
  1173                 properties += " livemark";
  1174                 // The livemark attribute is set as a cell property on the title cell.
  1175                 this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
  1176               }, () => undefined);
  1180         if (itemId != -1) {
  1181           let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId);
  1182           if (queryName)
  1183             properties += " OrganizerQuery_" + queryName;
  1186       else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR)
  1187         properties += " separator";
  1188       else if (PlacesUtils.nodeIsURI(node)) {
  1189         properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri);
  1191         if (this._controller.hasCachedLivemarkInfo(node.parent)) {
  1192           properties += " livemarkItem";
  1193           if (node.accessCount) {
  1194             properties += " visited";
  1199       this._cellProperties.set(node, properties);
  1202     return props + " " + properties;
  1203   },
  1205   getColumnProperties: function(aColumn) { return ""; },
  1207   isContainer: function PTV_isContainer(aRow) {
  1208     // Only leaf nodes aren't listed in the rows array.
  1209     let node = this._rows[aRow];
  1210     if (node === undefined)
  1211       return false;
  1213     if (PlacesUtils.nodeIsContainer(node)) {
  1214       // Flat-lists may ignore expandQueries and other query options when
  1215       // they are asked to open a container.
  1216       if (this._flatList)
  1217         return true;
  1219       // treat non-expandable childless queries as non-containers
  1220       if (PlacesUtils.nodeIsQuery(node)) {
  1221         let parent = node.parent;
  1222         if ((PlacesUtils.nodeIsQuery(parent) ||
  1223              PlacesUtils.nodeIsFolder(parent)) &&
  1224             !PlacesUtils.asQuery(node).hasChildren)
  1225           return PlacesUtils.asQuery(parent).queryOptions.expandQueries;
  1227       return true;
  1229     return false;
  1230   },
  1232   isContainerOpen: function PTV_isContainerOpen(aRow) {
  1233     if (this._flatList)
  1234       return false;
  1236     // All containers are listed in the rows array.
  1237     return this._rows[aRow].containerOpen;
  1238   },
  1240   isContainerEmpty: function PTV_isContainerEmpty(aRow) {
  1241     if (this._flatList)
  1242       return true;
  1244     let node = this._rows[aRow];
  1245     if (this._controller.hasCachedLivemarkInfo(node)) {
  1246       let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
  1247       return queryOptions.excludeItems;
  1250     // All containers are listed in the rows array.
  1251     return !node.hasChildren;
  1252   },
  1254   isSeparator: function PTV_isSeparator(aRow) {
  1255     // All separators are listed in the rows array.
  1256     let node = this._rows[aRow];
  1257     return node && PlacesUtils.nodeIsSeparator(node);
  1258   },
  1260   isSorted: function PTV_isSorted() {
  1261     return this._result.sortingMode !=
  1262            Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
  1263   },
  1265   canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) {
  1266     if (!this._result)
  1267       throw Cr.NS_ERROR_UNEXPECTED;
  1269     // Drop position into a sorted treeview would be wrong.
  1270     if (this.isSorted())
  1271       return false;
  1273     let ip = this._getInsertionPoint(aRow, aOrientation);
  1274     return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
  1275   },
  1277   _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) {
  1278     let container = this._result.root;
  1279     let dropNearItemId = -1;
  1280     // When there's no selection, assume the container is the container
  1281     // the view is populated from (i.e. the result's itemId).
  1282     if (index != -1) {
  1283       let lastSelected = this.nodeForTreeIndex(index);
  1284       if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
  1285         // If the last selected item is an open container, append _into_
  1286         // it, rather than insert adjacent to it.
  1287         container = lastSelected;
  1288         index = -1;
  1290       else if (lastSelected.containerOpen &&
  1291                orientation == Ci.nsITreeView.DROP_AFTER &&
  1292                lastSelected.hasChildren) {
  1293         // If the last selected node is an open container and the user is
  1294         // trying to drag into it as a first node, really insert into it.
  1295         container = lastSelected;
  1296         orientation = Ci.nsITreeView.DROP_ON;
  1297         index = 0;
  1299       else {
  1300         // Use the last-selected node's container.
  1301         container = lastSelected.parent;
  1303         // During its Drag & Drop operation, the tree code closes-and-opens
  1304         // containers very often (part of the XUL "spring-loaded folders"
  1305         // implementation).  And in certain cases, we may reach a closed
  1306         // container here.  However, we can simply bail out when this happens,
  1307         // because we would then be back here in less than a millisecond, when
  1308         // the container had been reopened.
  1309         if (!container || !container.containerOpen)
  1310           return null;
  1312         // Avoid the potentially expensive call to getChildIndex
  1313         // if we know this container doesn't allow insertion.
  1314         if (PlacesControllerDragHelper.disallowInsertion(container))
  1315           return null;
  1317         let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
  1318         if (queryOptions.sortingMode !=
  1319               Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
  1320           // If we are within a sorted view, insert at the end.
  1321           index = -1;
  1323         else if (queryOptions.excludeItems ||
  1324                  queryOptions.excludeQueries ||
  1325                  queryOptions.excludeReadOnlyFolders) {
  1326           // Some item may be invisible, insert near last selected one.
  1327           // We don't replace index here to avoid requests to the db,
  1328           // instead it will be calculated later by the controller.
  1329           index = -1;
  1330           dropNearItemId = lastSelected.itemId;
  1332         else {
  1333           let lsi = container.getChildIndex(lastSelected);
  1334           index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
  1339     if (PlacesControllerDragHelper.disallowInsertion(container))
  1340       return null;
  1342     return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
  1343                               index, orientation,
  1344                               PlacesUtils.nodeIsTagQuery(container),
  1345                               dropNearItemId);
  1346   },
  1348   drop: function PTV_drop(aRow, aOrientation, aDataTransfer) {
  1349     // We are responsible for translating the |index| and |orientation|
  1350     // parameters into a container id and index within the container,
  1351     // since this information is specific to the tree view.
  1352     let ip = this._getInsertionPoint(aRow, aOrientation);
  1353     if (ip)
  1354       PlacesControllerDragHelper.onDrop(ip, aDataTransfer);
  1356     PlacesControllerDragHelper.currentDropTarget = null;
  1357   },
  1359   getParentIndex: function PTV_getParentIndex(aRow) {
  1360     let [parentNode, parentRow] = this._getParentByChildRow(aRow);
  1361     return parentRow;
  1362   },
  1364   hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) {
  1365     if (aRow == this._rows.length - 1) {
  1366       // The last row has no sibling.
  1367       return false;
  1370     let node = this._rows[aRow];
  1371     if (node === undefined || this._isPlainContainer(node.parent)) {
  1372       // The node is a child of a plain container.
  1373       // If the next row is either unset or has the same parent,
  1374       // it's a sibling.
  1375       let nextNode = this._rows[aRow + 1];
  1376       return (nextNode == undefined || nextNode.parent == node.parent);
  1379     let thisLevel = node.indentLevel;
  1380     for (let i = aAfterIndex + 1; i < this._rows.length; ++i) {
  1381       let rowNode = this._getNodeForRow(i);
  1382       let nextLevel = rowNode.indentLevel;
  1383       if (nextLevel == thisLevel)
  1384         return true;
  1385       if (nextLevel < thisLevel)
  1386         break;
  1389     return false;
  1390   },
  1392   getLevel: function(aRow) this._getNodeForRow(aRow).indentLevel,
  1394   getImageSrc: function PTV_getImageSrc(aRow, aColumn) {
  1395     // Only the title column has an image.
  1396     if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE)
  1397       return "";
  1399     return this._getNodeForRow(aRow).icon;
  1400   },
  1402   getProgressMode: function(aRow, aColumn) { },
  1403   getCellValue: function(aRow, aColumn) { },
  1405   getCellText: function PTV_getCellText(aRow, aColumn) {
  1406     let node = this._getNodeForRow(aRow);
  1407     switch (this._getColumnType(aColumn)) {
  1408       case this.COLUMN_TYPE_TITLE:
  1409         // normally, this is just the title, but we don't want empty items in
  1410         // the tree view so return a special string if the title is empty.
  1411         // Do it here so that callers can still get at the 0 length title
  1412         // if they go through the "result" API.
  1413         if (PlacesUtils.nodeIsSeparator(node))
  1414           return "";
  1415         return PlacesUIUtils.getBestTitle(node, true);
  1416       case this.COLUMN_TYPE_TAGS:
  1417         return node.tags;
  1418       case this.COLUMN_TYPE_URI:
  1419         if (PlacesUtils.nodeIsURI(node))
  1420           return node.uri;
  1421         return "";
  1422       case this.COLUMN_TYPE_DATE:
  1423         let nodeTime = node.time;
  1424         if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) {
  1425           // hosts and days shouldn't have a value for the date column.
  1426           // Actually, you could argue this point, but looking at the
  1427           // results, seeing the most recently visited date is not what
  1428           // I expect, and gives me no information I know how to use.
  1429           // Only show this for URI-based items.
  1430           return "";
  1433         return this._convertPRTimeToString(nodeTime);
  1434       case this.COLUMN_TYPE_VISITCOUNT:
  1435         return node.accessCount;
  1436       case this.COLUMN_TYPE_KEYWORD:
  1437         if (PlacesUtils.nodeIsBookmark(node))
  1438           return PlacesUtils.bookmarks.getKeywordForBookmark(node.itemId);
  1439         return "";
  1440       case this.COLUMN_TYPE_DESCRIPTION:
  1441         if (node.itemId != -1) {
  1442           try {
  1443             return PlacesUtils.annotations.
  1444                                getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO);
  1446           catch (ex) { /* has no description */ }
  1448         return "";
  1449       case this.COLUMN_TYPE_DATEADDED:
  1450         if (node.dateAdded)
  1451           return this._convertPRTimeToString(node.dateAdded);
  1452         return "";
  1453       case this.COLUMN_TYPE_LASTMODIFIED:
  1454         if (node.lastModified)
  1455           return this._convertPRTimeToString(node.lastModified);
  1456         return "";
  1458     return "";
  1459   },
  1461   setTree: function PTV_setTree(aTree) {
  1462     // If we are replacing the tree during a batch, there is a concrete risk
  1463     // that the treeView goes out of sync, thus it's safer to end the batch now.
  1464     // This is a no-op if we are not batching.
  1465     this.batching(false);
  1467     let hasOldTree = this._tree != null;
  1468     this._tree = aTree;
  1470     if (this._result) {
  1471       if (hasOldTree) {
  1472         // detach from result when we are detaching from the tree.
  1473         // This breaks the reference cycle between us and the result.
  1474         if (!aTree) {
  1475           this._result.removeObserver(this);
  1476           this._rootNode.containerOpen = false;
  1479       if (aTree)
  1480         this._finishInit();
  1482   },
  1484   toggleOpenState: function PTV_toggleOpenState(aRow) {
  1485     if (!this._result)
  1486       throw Cr.NS_ERROR_UNEXPECTED;
  1488     let node = this._rows[aRow];
  1489     if (this._flatList && this._openContainerCallback) {
  1490       this._openContainerCallback(node);
  1491       return;
  1494     // Persist containers open status, but never persist livemarks.
  1495     if (!this._controller.hasCachedLivemarkInfo(node)) {
  1496       let resource = this._getResourceForNode(node);
  1497       if (resource) {
  1498         const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open");
  1499         const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true");
  1501         if (node.containerOpen)
  1502           PlacesUIUtils.localStore.Unassert(resource, openLiteral, trueLiteral);
  1503         else
  1504           PlacesUIUtils.localStore.Assert(resource, openLiteral, trueLiteral, true);
  1508     node.containerOpen = !node.containerOpen;
  1509   },
  1511   cycleHeader: function PTV_cycleHeader(aColumn) {
  1512     if (!this._result)
  1513       throw Cr.NS_ERROR_UNEXPECTED;
  1515     // Sometimes you want a tri-state sorting, and sometimes you don't. This
  1516     // rule allows tri-state sorting when the root node is a folder. This will
  1517     // catch the most common cases. When you are looking at folders, you want
  1518     // the third state to reset the sorting to the natural bookmark order. When
  1519     // you are looking at history, that third state has no meaning so we try
  1520     // to disallow it.
  1521     //
  1522     // The problem occurs when you have a query that results in bookmark
  1523     // folders. One example of this is the subscriptions view. In these cases,
  1524     // this rule doesn't allow you to sort those sub-folders by their natural
  1525     // order.
  1526     let allowTriState = PlacesUtils.nodeIsFolder(this._result.root);
  1528     let oldSort = this._result.sortingMode;
  1529     let oldSortingAnnotation = this._result.sortingAnnotation;
  1530     let newSort;
  1531     let newSortingAnnotation = "";
  1532     const NHQO = Ci.nsINavHistoryQueryOptions;
  1533     switch (this._getColumnType(aColumn)) {
  1534       case this.COLUMN_TYPE_TITLE:
  1535         if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING)
  1536           newSort = NHQO.SORT_BY_TITLE_DESCENDING;
  1537         else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING)
  1538           newSort = NHQO.SORT_BY_NONE;
  1539         else
  1540           newSort = NHQO.SORT_BY_TITLE_ASCENDING;
  1542         break;
  1543       case this.COLUMN_TYPE_URI:
  1544         if (oldSort == NHQO.SORT_BY_URI_ASCENDING)
  1545           newSort = NHQO.SORT_BY_URI_DESCENDING;
  1546         else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING)
  1547           newSort = NHQO.SORT_BY_NONE;
  1548         else
  1549           newSort = NHQO.SORT_BY_URI_ASCENDING;
  1551         break;
  1552       case this.COLUMN_TYPE_DATE:
  1553         if (oldSort == NHQO.SORT_BY_DATE_ASCENDING)
  1554           newSort = NHQO.SORT_BY_DATE_DESCENDING;
  1555         else if (allowTriState &&
  1556                  oldSort == NHQO.SORT_BY_DATE_DESCENDING)
  1557           newSort = NHQO.SORT_BY_NONE;
  1558         else
  1559           newSort = NHQO.SORT_BY_DATE_ASCENDING;
  1561         break;
  1562       case this.COLUMN_TYPE_VISITCOUNT:
  1563         // visit count default is unusual because we sort by descending
  1564         // by default because you are most likely to be looking for
  1565         // highly visited sites when you click it
  1566         if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING)
  1567           newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
  1568         else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING)
  1569           newSort = NHQO.SORT_BY_NONE;
  1570         else
  1571           newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
  1573         break;
  1574       case this.COLUMN_TYPE_KEYWORD:
  1575         if (oldSort == NHQO.SORT_BY_KEYWORD_ASCENDING)
  1576           newSort = NHQO.SORT_BY_KEYWORD_DESCENDING;
  1577         else if (allowTriState && oldSort == NHQO.SORT_BY_KEYWORD_DESCENDING)
  1578           newSort = NHQO.SORT_BY_NONE;
  1579         else
  1580           newSort = NHQO.SORT_BY_KEYWORD_ASCENDING;
  1582         break;
  1583       case this.COLUMN_TYPE_DESCRIPTION:
  1584         if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING &&
  1585             oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) {
  1586           newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING;
  1587           newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
  1589         else if (allowTriState &&
  1590                  oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING &&
  1591                  oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
  1592           newSort = NHQO.SORT_BY_NONE;
  1593         else {
  1594           newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING;
  1595           newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
  1598         break;
  1599       case this.COLUMN_TYPE_DATEADDED:
  1600         if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING)
  1601           newSort = NHQO.SORT_BY_DATEADDED_DESCENDING;
  1602         else if (allowTriState &&
  1603                  oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING)
  1604           newSort = NHQO.SORT_BY_NONE;
  1605         else
  1606           newSort = NHQO.SORT_BY_DATEADDED_ASCENDING;
  1608         break;
  1609       case this.COLUMN_TYPE_LASTMODIFIED:
  1610         if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING)
  1611           newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING;
  1612         else if (allowTriState &&
  1613                  oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING)
  1614           newSort = NHQO.SORT_BY_NONE;
  1615         else
  1616           newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING;
  1618         break;
  1619       case this.COLUMN_TYPE_TAGS:
  1620         if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING)
  1621           newSort = NHQO.SORT_BY_TAGS_DESCENDING;
  1622         else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING)
  1623           newSort = NHQO.SORT_BY_NONE;
  1624         else
  1625           newSort = NHQO.SORT_BY_TAGS_ASCENDING;
  1627         break;
  1628       default:
  1629         throw Cr.NS_ERROR_INVALID_ARG;
  1631     this._result.sortingAnnotation = newSortingAnnotation;
  1632     this._result.sortingMode = newSort;
  1633   },
  1635   isEditable: function PTV_isEditable(aRow, aColumn) {
  1636     // At this point we only support editing the title field.
  1637     if (aColumn.index != 0)
  1638       return false;
  1640     // Only bookmark-nodes are editable, and those are never built lazily
  1641     let node = this._rows[aRow];
  1642     if (!node || node.itemId == -1)
  1643       return false;
  1645     // The following items are never editable:
  1646     // * Read-only items.
  1647     // * places-roots
  1648     // * separators
  1649     if (PlacesUtils.nodeIsReadOnly(node) ||
  1650         PlacesUtils.nodeIsSeparator(node))
  1651       return false;
  1653     if (PlacesUtils.nodeIsFolder(node)) {
  1654       let itemId = PlacesUtils.getConcreteItemId(node);
  1655       if (PlacesUtils.isRootItem(itemId))
  1656         return false;
  1659     return true;
  1660   },
  1662   setCellText: function PTV_setCellText(aRow, aColumn, aText) {
  1663     // We may only get here if the cell is editable.
  1664     let node = this._rows[aRow];
  1665     if (node.title != aText) {
  1666       let txn = new PlacesEditItemTitleTransaction(node.itemId, aText);
  1667       PlacesUtils.transactionManager.doTransaction(txn);
  1669   },
  1671   toggleCutNode: function PTV_toggleCutNode(aNode, aValue) {
  1672     let currentVal = this._cuttingNodes.has(aNode);
  1673     if (currentVal != aValue) {
  1674       if (aValue)
  1675         this._cuttingNodes.add(aNode);
  1676       else
  1677         this._cuttingNodes.delete(aNode);
  1679       this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
  1681   },
  1683   selectionChanged: function() { },
  1684   cycleCell: function(aRow, aColumn) { },
  1685   isSelectable: function(aRow, aColumn) { return false; },
  1686   performAction: function(aAction) { },
  1687   performActionOnRow: function(aAction, aRow) { },
  1688   performActionOnCell: function(aAction, aRow, aColumn) { }
  1689 };

mercurial