/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from controller.js */ /** * This returns the key for any node/details object. * * @param {object} nodeOrDetails * A node, or an object containing the following properties: * - uri * - time * - itemId * In case any of these is missing, an empty string will be returned. This is * to facilitate easy delete statements which occur due to assignment to items in `this._rows`, * since the item we are deleting may be undefined in the array. * * @returns {string} key or empty string. */ function makeNodeDetailsKey(nodeOrDetails) { if ( nodeOrDetails && typeof nodeOrDetails === "object" && "uri" in nodeOrDetails && "time" in nodeOrDetails && "itemId" in nodeOrDetails ) { return `${nodeOrDetails.uri}*${nodeOrDetails.time}*${nodeOrDetails.itemId}`; } return ""; } function PlacesTreeView(aContainer) { this._tree = null; this._result = null; this._selection = null; this._rootNode = null; this._rows = []; this._flatList = aContainer.flatList; this._nodeDetails = new Map(); this._element = aContainer; this._controller = aContainer._controller; } PlacesTreeView.prototype = { get wrappedJSObject() { return this; }, QueryInterface: ChromeUtils.generateQI([ "nsITreeView", "nsINavHistoryResultObserver", "nsISupportsWeakReference", ]), /** * This is called once both the result and the tree are set. */ _finishInit: function PTV__finishInit() { let selection = this.selection; if (selection) { selection.selectEventsSuppressed = true; } if (!this._rootNode.containerOpen) { // This triggers containerStateChanged which then builds the visible // section. this._rootNode.containerOpen = true; } else { this.invalidateContainer(this._rootNode); } // "Activate" the sorting column and update commands. this.sortingChanged(this._result.sortingMode); if (selection) { selection.selectEventsSuppressed = false; } }, uninit() { if (this._editingObservers) { for (let observer of this._editingObservers.values()) { observer.disconnect(); } delete this._editingObservers; } // Break the reference cycle between the PlacesTreeView and result. if (this._result) { this._result.removeObserver(this); } }, /** * Plain Container: container result nodes which may never include sub * hierarchies. * * When the rows array is constructed, we don't set the children of plain * containers. Instead, we keep placeholders for these children. We then * build these children lazily as the tree asks us for information about each * row. Luckily, the tree doesn't ask about rows outside the visible area. * * It's guaranteed that all containers are listed in the rows * elements array. It's also guaranteed that separators (if they're not * filtered, see below) are listed in the visible elements array, because * bookmark folders are never built lazily, as described above. * * @see {@link PlacesTreeView._getNodeForRow} and * {@link PlacesTreeView._getRowForNode} for the actual magic. * * @param {object} aContainer * A container result node. * * @returns {boolean} true if aContainer is a plain container, false otherwise. */ _isPlainContainer: function PTV__isPlainContainer(aContainer) { // We don't know enough about non-query containers. if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode)) { return false; } switch (aContainer.queryOptions.resultType) { case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_LEFT_PANE_QUERY: return false; } // If it's a folder, it's not a plain container. let nodeType = aContainer.type; return ( nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER && nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT ); }, /** * Gets the row number for a given node. Assumes that the given node is * visible (i.e. it's not an obsolete node). * * If aParentRow and aNodeIndex are passed and parent is a plain * container, this method will just return a calculated row value, without * making assumptions on existence of the node at that position. * * @param {object} aNode * A result node. Do not pass an obsolete node, or any * node which isn't supposed to be in the tree (e.g. separators in * sorted trees). * @param {boolean} [aForceBuild] * See {@link _isPlainContainer}. * If true, the row will be computed even if the node still isn't set * in our rows array. * @param {object} [aParentRow] * The row of aNode's parent. Ignored for the root node. * @param {number} [aNodeIndex] * The index of aNode in its parent. Only used if aParentRow is * set too. * * @throws if aNode is invisible. * @returns {object} aNode's row if it's in the rows list or if aForceBuild is set, -1 * otherwise. */ _getRowForNode: function PTV__getRowForNode( aNode, aForceBuild, aParentRow, aNodeIndex ) { if (aNode == this._rootNode) { throw new Error("The root node is never visible"); } // A node is removed form the view either if it has no parent or if its // root-ancestor is not the root node (in which case that's the node // for which nodeRemoved was called). let ancestors = Array.from(PlacesUtils.nodeAncestors(aNode)); if ( !ancestors.length || ancestors[ancestors.length - 1] != this._rootNode ) { throw new Error("Removed node passed to _getRowForNode"); } // Ensure that the entire chain is open, otherwise that node is invisible. for (let ancestor of ancestors) { if (!ancestor.containerOpen) { throw new Error("Invisible node passed to _getRowForNode"); } } // Non-plain containers are initially built with their contents. let parent = aNode.parent; let parentIsPlain = this._isPlainContainer(parent); if (!parentIsPlain) { if (parent == this._rootNode) { return this._rows.indexOf(aNode); } return this._rows.indexOf(aNode, aParentRow); } let row = -1; let useNodeIndex = typeof aNodeIndex == "number"; if (parent == this._rootNode) { row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode); } else if (useNodeIndex && typeof aParentRow == "number") { // If we have both the row of the parent node, and the node's index, we // can avoid searching the rows array if the parent is a plain container. row = aParentRow + aNodeIndex + 1; } else { // Look for the node in the nodes array. Start the search at the parent // row. If the parent row isn't passed, we'll pass undefined to indexOf, // which is fine. row = this._rows.indexOf(aNode, aParentRow); if (row == -1 && aForceBuild) { let parentRow = typeof aParentRow == "number" ? aParentRow : this._getRowForNode(parent); row = parentRow + parent.getChildIndex(aNode) + 1; } } if (row != -1) { this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row])); this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode); this._rows[row] = aNode; } return row; }, /** * Given a row, finds and returns the parent details of the associated node. * * @param {number} aChildRow * Row number. * @returns {Array} [parentNode, parentRow] */ _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) { let node = this._getNodeForRow(aChildRow); let parent = node === null ? this._rootNode : node.parent; // The root node is never visible if (parent == this._rootNode) { return [this._rootNode, -1]; } let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1); return [parent, parentRow]; }, /** * Gets the node at a given row. * * @param {number} aRow * The index of the row to set * @returns {object} */ _getNodeForRow: function PTV__getNodeForRow(aRow) { if (aRow < 0) { return null; } let node = this._rows[aRow]; if (node !== undefined) { return node; } // Find the nearest node. let rowNode, row; for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) { rowNode = this._rows[i]; row = i; } // If there's no container prior to the given row, it's a child of // the root node (remember: all containers are listed in the rows array). if (!rowNode) { let newNode = this._rootNode.getChild(aRow); this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow])); this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode); return (this._rows[aRow] = newNode); } // Unset elements may exist only in plain containers. Thus, if the nearest // node is a container, it's the row's parent, otherwise, it's a sibling. if (rowNode instanceof Ci.nsINavHistoryContainerResultNode) { let newNode = rowNode.getChild(aRow - row - 1); this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow])); this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode); return (this._rows[aRow] = newNode); } let [parent, parentRow] = this._getParentByChildRow(row); let newNode = parent.getChild(aRow - parentRow - 1); this._nodeDetails.delete(makeNodeDetailsKey(this._rows[aRow])); this._nodeDetails.set(makeNodeDetailsKey(newNode), newNode); return (this._rows[aRow] = newNode); }, /** * This takes a container and recursively appends our rows array per its * contents. Assumes that the rows arrays has no rows for the given * container. * * @param {object} aContainer * A container result node. * @param {object} aFirstChildRow * The first row at which nodes may be inserted to the row array. * In other words, that's aContainer's row + 1. * @param {Array} aToOpen * An array of containers to open once the build is done (out param) * * @returns {number} the number of rows which were inserted. */ _buildVisibleSection: function PTV__buildVisibleSection( aContainer, aFirstChildRow, aToOpen ) { // There's nothing to do if the container is closed. if (!aContainer.containerOpen) { return 0; } // Inserting the new elements into the rows array in one shot (by // Array.prototype.concat) is faster than resizing the array (by splice) on each loop // iteration. let cc = aContainer.childCount; let newElements = new Array(cc); // We need to clean up the node details from aFirstChildRow + 1 to the end of rows. for (let i = aFirstChildRow + 1; i < this._rows.length; i++) { this._nodeDetails.delete(makeNodeDetailsKey(this._rows[i])); } this._rows = this._rows .splice(0, aFirstChildRow) .concat(newElements, this._rows); if (this._isPlainContainer(aContainer)) { return cc; } let sortingMode = this._result.sortingMode; let rowsInserted = 0; for (let i = 0; i < cc; i++) { let curChild = aContainer.getChild(i); let curChildType = curChild.type; let row = aFirstChildRow + rowsInserted; // Don't display separators when sorted. if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { // Remove the element for the filtered separator. // Notice that the rows array was initially resized to include all // children. this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row])); this._rows.splice(row, 1); continue; } } this._nodeDetails.delete(makeNodeDetailsKey(this._rows[row])); this._nodeDetails.set(makeNodeDetailsKey(curChild), curChild); this._rows[row] = curChild; rowsInserted++; // Recursively do containers. if ( !this._flatList && curChild instanceof Ci.nsINavHistoryContainerResultNode ) { let uri = curChild.uri; let isopen = false; if (uri) { let val = Services.xulStore.getValue( document.documentURI, PlacesUIUtils.obfuscateUrlForXulStore(uri), "open" ); isopen = val == "true"; } if (isopen != curChild.containerOpen) { aToOpen.push(curChild); } else if (curChild.containerOpen && curChild.childCount > 0) { rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen); } } } return rowsInserted; }, /** * This counts how many rows a node takes in the tree. For containers it * will count the node itself plus any child node following it. * * @param {number} aNodeRow * The row of the node to count * @returns {number} */ _countVisibleRowsForNodeAtRow: function PTV__countVisibleRowsForNodeAtRow( aNodeRow ) { let node = this._rows[aNodeRow]; // If it's not listed yet, we know that it's a leaf node (instanceof also // null-checks). if (!(node instanceof Ci.nsINavHistoryContainerResultNode)) { return 1; } let outerLevel = node.indentLevel; for (let i = aNodeRow + 1; i < this._rows.length; i++) { let rowNode = this._rows[i]; if (rowNode && rowNode.indentLevel <= outerLevel) { return i - aNodeRow; } } // This node plus its children take up the bottom of the list. return this._rows.length - aNodeRow; }, _getSelectedNodesInRange: function PTV__getSelectedNodesInRange( aFirstRow, aLastRow ) { let selection = this.selection; let rc = selection.getRangeCount(); if (rc == 0) { return []; } // The visible-area borders are needed for checking whether a // selected row is also visible. let firstVisibleRow = this._tree.getFirstVisibleRow(); let lastVisibleRow = this._tree.getLastVisibleRow(); let nodesInfo = []; for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) { let min = {}, max = {}; selection.getRangeAt(rangeIndex, min, max); // If this range does not overlap the replaced chunk, we don't need to // persist the selection. if (max.value < aFirstRow || min.value > aLastRow) { continue; } let firstRow = Math.max(min.value, aFirstRow); let lastRow = Math.min(max.value, aLastRow); for (let i = firstRow; i <= lastRow; i++) { nodesInfo.push({ node: this._rows[i], oldRow: i, wasVisible: i >= firstVisibleRow && i <= lastVisibleRow, }); } } return nodesInfo; }, /** * Tries to find an equivalent node for a node which was removed. We first * look for the original node, in case it was just relocated. Then, if we * that node was not found, we look for a node that has the same itemId, uri * and time values. * * @param {object} aOldNode * The node which was removed. * * @returns {number} the row number of an equivalent node for aOldOne, if one was * found, -1 otherwise. */ _getNewRowForRemovedNode: function PTV__getNewRowForRemovedNode(aOldNode) { let parent = aOldNode.parent; if (parent) { // If the node's parent is still set, the node is not obsolete // and we should just find out its new position. // However, if any of the node's ancestor is closed, the node is // invisible. let ancestors = PlacesUtils.nodeAncestors(aOldNode); for (let ancestor of ancestors) { if (!ancestor.containerOpen) { return -1; } } return this._getRowForNode(aOldNode, true); } // There's a broken edge case here. // If a visit appears in two queries, and the second one was // the old node, we'll select the first one after refresh. There's // nothing we could do about that, because aOldNode.parent is // gone by the time invalidateContainer is called. let newNode = this._nodeDetails.get(makeNodeDetailsKey(aOldNode)); if (!newNode) { return -1; } return this._getRowForNode(newNode, true); }, /** * Restores a given selection state as near as possible to the original * selection state. * * @param {Array} aNodesInfo * The persisted selection state as returned by * _getSelectedNodesInRange. */ _restoreSelection: function PTV__restoreSelection(aNodesInfo) { if (!aNodesInfo.length) { return; } let selection = this.selection; // Attempt to ensure that previously-visible selection will be visible // if it's re-selected. However, we can only ensure that for one row. let scrollToRow = -1; for (let i = 0; i < aNodesInfo.length; i++) { let nodeInfo = aNodesInfo[i]; let row = this._getNewRowForRemovedNode(nodeInfo.node); // Select the found node, if any. if (row != -1) { selection.rangedSelect(row, row, true); if (nodeInfo.wasVisible && scrollToRow == -1) { scrollToRow = row; } } } // If only one node was previously selected and there's no selection now, // select the node at its old row, if any. if (aNodesInfo.length == 1 && selection.count == 0) { let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1); if (row != -1) { selection.rangedSelect(row, row, true); if (aNodesInfo[0].wasVisible && scrollToRow == -1) { scrollToRow = aNodesInfo[0].oldRow; } } } if (scrollToRow != -1) { this._tree.ensureRowIsVisible(scrollToRow); } }, _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) { const MS_PER_MINUTE = 60000; const MS_PER_DAY = 86400000; let timeMs = aTime / 1000; // PRTime is in microseconds // Date is calculated starting from midnight, so the modulo with a day are // milliseconds from today's midnight. // getTimezoneOffset corrects that based on local time, notice midnight // can have a different offset during DST-change days. let dateObj = new Date(); let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE; let midnight = now - (now % MS_PER_DAY); midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE; let timeObj = new Date(timeMs); return timeMs >= midnight ? this._todayFormatter.format(timeObj) : this._dateFormatter.format(timeObj); }, // We use a different formatter for times within the current day, // so we cache both a "today" formatter and a general date formatter. __todayFormatter: null, get _todayFormatter() { if (!this.__todayFormatter) { const dtOptions = { timeStyle: "short" }; this.__todayFormatter = new Services.intl.DateTimeFormat( undefined, dtOptions ); } return this.__todayFormatter; }, __dateFormatter: null, get _dateFormatter() { if (!this.__dateFormatter) { const dtOptions = { dateStyle: "short", timeStyle: "short", }; this.__dateFormatter = new Services.intl.DateTimeFormat( undefined, dtOptions ); } return this.__dateFormatter; }, COLUMN_TYPE_UNKNOWN: 0, COLUMN_TYPE_TITLE: 1, COLUMN_TYPE_URI: 2, COLUMN_TYPE_DATE: 3, COLUMN_TYPE_VISITCOUNT: 4, COLUMN_TYPE_DATEADDED: 5, COLUMN_TYPE_LASTMODIFIED: 6, COLUMN_TYPE_TAGS: 7, _getColumnType: function PTV__getColumnType(aColumn) { let columnType = aColumn.element.getAttribute("anonid") || aColumn.id; switch (columnType) { case "title": return this.COLUMN_TYPE_TITLE; case "url": return this.COLUMN_TYPE_URI; case "date": return this.COLUMN_TYPE_DATE; case "visitCount": return this.COLUMN_TYPE_VISITCOUNT; case "dateAdded": return this.COLUMN_TYPE_DATEADDED; case "lastModified": return this.COLUMN_TYPE_LASTMODIFIED; case "tags": return this.COLUMN_TYPE_TAGS; } return this.COLUMN_TYPE_UNKNOWN; }, _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) { switch (aSortType) { case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING: return [this.COLUMN_TYPE_TITLE, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING: return [this.COLUMN_TYPE_TITLE, true]; case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING: return [this.COLUMN_TYPE_DATE, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING: return [this.COLUMN_TYPE_DATE, true]; case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING: return [this.COLUMN_TYPE_URI, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING: return [this.COLUMN_TYPE_URI, true]; case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING: return [this.COLUMN_TYPE_VISITCOUNT, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING: return [this.COLUMN_TYPE_VISITCOUNT, true]; case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING: return [this.COLUMN_TYPE_DATEADDED, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING: return [this.COLUMN_TYPE_DATEADDED, true]; case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING: return [this.COLUMN_TYPE_LASTMODIFIED, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING: return [this.COLUMN_TYPE_LASTMODIFIED, true]; case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING: return [this.COLUMN_TYPE_TAGS, false]; case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING: return [this.COLUMN_TYPE_TAGS, true]; } return [this.COLUMN_TYPE_UNKNOWN, false]; }, // nsINavHistoryResultObserver nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) { console.assert(this._result, "Got a notification but have no result!"); if (!this._tree || !this._result) { return; } // Bail out for hidden separators. if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) { return; } let parentRow; if (aParentNode != this._rootNode) { parentRow = this._getRowForNode(aParentNode); // Update parent when inserting the first item, since twisty has changed. if (aParentNode.childCount == 1) { this._tree.invalidateRow(parentRow); } } // Compute the new row number of the node. let row = -1; let cc = aParentNode.childCount; if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) { // We don't need to worry about sub hierarchies of the parent node // if it's a plain container, or if the new node is its first child. if (aParentNode == this._rootNode) { row = aNewIndex; } else { row = parentRow + aNewIndex + 1; } } else { // Here, we try to find the next visible element in the child list so we // can set the new visible index to be right before that. Note that we // have to search down instead of up, because some siblings could have // children themselves that would be in the way. let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) && this.isSorted(); for (let i = aNewIndex + 1; i < cc; i++) { let node = aParentNode.getChild(i); if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) { // The children have not been shifted so the next item will have what // should be our index. row = this._getRowForNode(node, false, parentRow, i); break; } } if (row < 0) { // At the end of the child list without finding a visible sibling. This // is a little harder because we don't know how many rows the last item // in our list takes up (it could be a container with many children). let prevChild = aParentNode.getChild(aNewIndex - 1); let prevIndex = this._getRowForNode( prevChild, false, parentRow, aNewIndex - 1 ); row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex); } } this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode); this._rows.splice(row, 0, aNode); this._tree.rowCountChanged(row, 1); if ( PlacesUtils.nodeIsContainer(aNode) && PlacesUtils.asContainer(aNode).containerOpen ) { this.invalidateContainer(aNode); } }, /** * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being * removed but the node it is collapsed with is not being removed (this then * just swap out the removee with its collapsing partner). The only time * when we really remove things is when deleting URIs, which will apply to * all collapsees. This function is called sometimes when resorting items. * However, we won't do this when sorted by date because dates will never * change for visits, and date sorting is the only time things are collapsed. * * @param {object} aParentNode * The parent node of the node being removed. * @param {object} aNode * The node to remove from the tree. * @param {number} aOldIndex * The old index of the node in the parent. */ nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) { console.assert(this._result, "Got a notification but have no result!"); if (!this._tree || !this._result) { return; } // XXX bug 517701: We don't know what to do when the root node is removed. if (aNode == this._rootNode) { throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); } // Bail out for hidden separators. if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) { return; } let parentRow = aParentNode == this._rootNode ? undefined : this._getRowForNode(aParentNode, true); let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex); if (oldRow < 0) { throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); } // If the node was exclusively selected, the node next to it will be // selected. let selectNext = false; let selection = this.selection; if (selection.getRangeCount() == 1) { let min = {}, max = {}; selection.getRangeAt(0, min, max); if (min.value == max.value && this.nodeForTreeIndex(min.value) == aNode) { selectNext = true; } } // Remove the node and its children, if any. let count = this._countVisibleRowsForNodeAtRow(oldRow); for (let splicedNode of this._rows.splice(oldRow, count)) { this._nodeDetails.delete(makeNodeDetailsKey(splicedNode)); } this._tree.rowCountChanged(oldRow, -count); // Redraw the parent if its twisty state has changed. if (aParentNode != this._rootNode && !aParentNode.hasChildren) { parentRow = oldRow - 1; this._tree.invalidateRow(parentRow); } // Restore selection if the node was exclusively selected. if (!selectNext) { return; } // Restore selection. let rowToSelect = Math.min(oldRow, this._rows.length - 1); if (rowToSelect != -1) { this.selection.rangedSelect(rowToSelect, rowToSelect, true); } }, nodeMoved: function PTV_nodeMoved( aNode, aOldParent, aOldIndex, aNewParent, aNewIndex ) { console.assert(this._result, "Got a notification but have no result!"); if (!this._tree || !this._result) { return; } // Bail out for hidden separators. if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) { return; } // Note that at this point the node has already been moved by the backend, // so we must give hints to _getRowForNode to get the old row position. let oldParentRow = aOldParent == this._rootNode ? undefined : this._getRowForNode(aOldParent, true); let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex); if (oldRow < 0) { throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); } // If this node is a container it could take up more than one row. let count = this._countVisibleRowsForNodeAtRow(oldRow); // Persist selection state. let nodesToReselect = this._getSelectedNodesInRange( oldRow, oldRow + count - 1 ); if (nodesToReselect.length) { this.selection.selectEventsSuppressed = true; } // Redraw the parent if its twisty state has changed. if (aOldParent != this._rootNode && !aOldParent.hasChildren) { let parentRow = oldRow - 1; this._tree.invalidateRow(parentRow); } // Remove node and its children, if any, from the old position. for (let splicedNode of this._rows.splice(oldRow, count)) { this._nodeDetails.delete(makeNodeDetailsKey(splicedNode)); } this._tree.rowCountChanged(oldRow, -count); // Insert the node into the new position. this.nodeInserted(aNewParent, aNode, aNewIndex); // Restore selection. if (nodesToReselect.length) { this._restoreSelection(nodesToReselect); this.selection.selectEventsSuppressed = false; } }, _invalidateCellValue: function PTV__invalidateCellValue(aNode, aColumnType) { console.assert(this._result, "Got a notification but have no result!"); if (!this._tree || !this._result) { return; } // Nothing to do for the root node. if (aNode == this._rootNode) { return; } let row = this._getRowForNode(aNode); if (row == -1) { return; } let column = this._findColumnByType(aColumnType); if (column && !column.element.hidden) { if (aColumnType == this.COLUMN_TYPE_TITLE) { this._tree.removeImageCacheEntry(row, column); } this._tree.invalidateCell(row, column); } // Last modified time is altered for almost all node changes. if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) { let lastModifiedColumn = this._findColumnByType( this.COLUMN_TYPE_LASTMODIFIED ); if (lastModifiedColumn && !lastModifiedColumn.hidden) { this._tree.invalidateCell(row, lastModifiedColumn); } } }, nodeTitleChanged: function PTV_nodeTitleChanged(aNode) { this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); }, nodeURIChanged: function PTV_nodeURIChanged(aNode, aOldURI) { this._nodeDetails.delete( makeNodeDetailsKey({ uri: aOldURI, itemId: aNode.itemId, time: aNode.time, }) ); this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode); this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI); }, nodeIconChanged: function PTV_nodeIconChanged(aNode) { this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); }, nodeHistoryDetailsChanged: function PTV_nodeHistoryDetailsChanged( aNode, aOldVisitDate ) { this._nodeDetails.delete( makeNodeDetailsKey({ uri: aNode.uri, itemId: aNode.itemId, time: aOldVisitDate, }) ); this._nodeDetails.set(makeNodeDetailsKey(aNode), aNode); this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE); this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT); }, nodeTagsChanged: function PTV_nodeTagsChanged(aNode) { this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS); }, nodeKeywordChanged() {}, nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode) { this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED); }, nodeLastModifiedChanged: function PTV_nodeLastModifiedChanged(aNode) { this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED); }, containerStateChanged: function PTV_containerStateChanged(aNode) { this.invalidateContainer(aNode); }, invalidateContainer: function PTV_invalidateContainer(aContainer) { console.assert(this._result, "Need to have a result to update"); if (!this._tree) { return; } // If we are currently editing, don't invalidate the container until we // finish. if (this._tree.getAttribute("editing")) { if (!this._editingObservers) { this._editingObservers = new Map(); } if (!this._editingObservers.has(aContainer)) { let mutationObserver = new MutationObserver(() => { Services.tm.dispatchToMainThread(() => this.invalidateContainer(aContainer) ); let observer = this._editingObservers.get(aContainer); observer.disconnect(); this._editingObservers.delete(aContainer); }); mutationObserver.observe(this._tree, { attributes: true, attributeFilter: ["editing"], }); this._editingObservers.set(aContainer, mutationObserver); } return; } let startReplacement, replaceCount; if (aContainer == this._rootNode) { startReplacement = 0; replaceCount = this._rows.length; // If the root node is now closed, the tree is empty. if (!this._rootNode.containerOpen) { this._nodeDetails.clear(); this._rows = []; if (replaceCount) { this._tree.rowCountChanged(startReplacement, -replaceCount); } return; } } else { // Update the twisty state. let row = this._getRowForNode(aContainer); this._tree.invalidateRow(row); // We don't replace the container node itself, so we should decrease the // replaceCount by 1. startReplacement = row + 1; replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1; } // Persist selection state. let nodesToReselect = this._getSelectedNodesInRange( startReplacement, startReplacement + replaceCount ); // Now update the number of elements. this.selection.selectEventsSuppressed = true; // First remove the old elements for (let splicedNode of this._rows.splice(startReplacement, replaceCount)) { this._nodeDetails.delete(makeNodeDetailsKey(splicedNode)); } // If the container is now closed, we're done. if (!aContainer.containerOpen) { let oldSelectionCount = this.selection.count; if (replaceCount) { this._tree.rowCountChanged(startReplacement, -replaceCount); } // Select the row next to the closed container if any of its // children were selected, and nothing else is selected. if ( nodesToReselect.length && nodesToReselect.length == oldSelectionCount ) { this.selection.rangedSelect(startReplacement, startReplacement, true); this._tree.ensureRowIsVisible(startReplacement); } this.selection.selectEventsSuppressed = false; return; } // Otherwise, start a batch first. this._tree.beginUpdateBatch(); if (replaceCount) { this._tree.rowCountChanged(startReplacement, -replaceCount); } let toOpenElements = []; let elementsAddedCount = this._buildVisibleSection( aContainer, startReplacement, toOpenElements ); if (elementsAddedCount) { this._tree.rowCountChanged(startReplacement, elementsAddedCount); } if (!this._flatList) { // Now, open any containers that were persisted. for (let i = 0; i < toOpenElements.length; i++) { let item = toOpenElements[i]; let parent = item.parent; // Avoid recursively opening containers. while (parent) { if (parent.uri == item.uri) { break; } parent = parent.parent; } // If we don't have a parent, we made it all the way to the root // and didn't find a match, so we can open our item. if (!parent && !item.containerOpen) { item.containerOpen = true; } } } this._tree.endUpdateBatch(); // Restore selection. this._restoreSelection(nodesToReselect); this.selection.selectEventsSuppressed = false; }, _columns: [], _findColumnByType: function PTV__findColumnByType(aColumnType) { if (this._columns[aColumnType]) { return this._columns[aColumnType]; } let columns = this._tree.columns; let colCount = columns.count; for (let i = 0; i < colCount; i++) { let column = columns.getColumnAt(i); let columnType = this._getColumnType(column); this._columns[columnType] = column; if (columnType == aColumnType) { return column; } } // That's completely valid. Most of our trees actually include just the // title column. return null; }, sortingChanged: function PTV__sortingChanged(aSortingMode) { if (!this._tree || !this._result) { return; } // Depending on the sort mode, certain commands may be disabled. window.updateCommands("sort"); let columns = this._tree.columns; // Clear old sorting indicator. let sortedColumn = columns.getSortedColumn(); if (sortedColumn) { sortedColumn.element.removeAttribute("sortDirection"); } // Set new sorting indicator by looking through all columns for ours. if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { return; } let [desiredColumn, desiredIsDescending] = this._sortTypeToColumnType(aSortingMode); let column = this._findColumnByType(desiredColumn); if (column) { let sortDir = desiredIsDescending ? "descending" : "ascending"; column.element.setAttribute("sortDirection", sortDir); } }, _inBatchMode: false, batching: function PTV__batching(aToggleMode) { if (this._inBatchMode != aToggleMode) { this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode; if (this._inBatchMode) { this._tree.beginUpdateBatch(); } else { this._tree.endUpdateBatch(); } } }, get result() { return this._result; }, set result(val) { if (this._result) { this._result.removeObserver(this); this._rootNode.containerOpen = false; } if (val) { this._result = val; this._rootNode = this._result.root; this._cellProperties = new Map(); this._cuttingNodes = new Set(); } else if (this._result) { delete this._result; delete this._rootNode; delete this._cellProperties; delete this._cuttingNodes; } // If the tree is not set yet, setTree will call finishInit. if (this._tree && val) { this._finishInit(); } }, /** * This allows you to get at the real node for a given row index. This is * only valid when a tree is attached. * * @param {Integer} aIndex The index for the node to get. * @returns {Ci.nsINavHistoryResultNode} The node. * @throws Cr.NS_ERROR_INVALID_ARG if the index is greater than the number of * rows. */ nodeForTreeIndex(aIndex) { if (aIndex > this._rows.length) { throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); } return this._getNodeForRow(aIndex); }, /** * Reverse of nodeForTreeIndex, returns the row index for a given result node. * The node should be part of the tree. * * @param {Ci.nsINavHistoryResultNode} aNode The node to look for in the tree. * @returns {Integer} The found index, or -1 if the item is not visible or not found. */ treeIndexForNode(aNode) { // The API allows passing invisible nodes. try { return this._getRowForNode(aNode, true); } catch (ex) {} return -1; }, // nsITreeView get rowCount() { return this._rows.length; }, get selection() { return this._selection; }, set selection(val) { this._selection = val; }, getRowProperties() { return ""; }, getCellProperties: function PTV_getCellProperties(aRow, aColumn) { // for anonid-trees, we need to add the column-type manually var props = ""; let columnType = aColumn.element.getAttribute("anonid"); if (columnType) { props += columnType; } else { columnType = aColumn.id; } // Set the "ltr" property on url cells if (columnType == "url") { props += " ltr"; } if (columnType != "title") { return props; } let node = this._getNodeForRow(aRow); if (this._cuttingNodes.has(node)) { props += " cutting"; } let properties = this._cellProperties.get(node); if (properties === undefined) { properties = ""; let itemId = node.itemId; let nodeType = node.type; if (PlacesUtils.containerTypes.includes(nodeType)) { if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) { properties += " query"; if (PlacesUtils.nodeIsTagQuery(node)) { properties += " tagContainer"; } else if (PlacesUtils.nodeIsDay(node)) { properties += " dayContainer"; } else if (PlacesUtils.nodeIsHost(node)) { properties += " hostContainer"; } } if (itemId == -1) { switch (node.bookmarkGuid) { case PlacesUtils.bookmarks.virtualToolbarGuid: properties += ` queryFolder_${PlacesUtils.bookmarks.toolbarGuid}`; break; case PlacesUtils.bookmarks.virtualMenuGuid: properties += ` queryFolder_${PlacesUtils.bookmarks.menuGuid}`; break; case PlacesUtils.bookmarks.virtualUnfiledGuid: properties += ` queryFolder_${PlacesUtils.bookmarks.unfiledGuid}`; break; case PlacesUtils.virtualAllBookmarksGuid: case PlacesUtils.virtualHistoryGuid: case PlacesUtils.virtualDownloadsGuid: case PlacesUtils.virtualTagsGuid: properties += ` OrganizerQuery_${node.bookmarkGuid}`; break; } } } else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { properties += " separator"; } else if (PlacesUtils.nodeIsURI(node)) { properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri); } this._cellProperties.set(node, properties); } return props + " " + properties; }, getColumnProperties() { return ""; }, isContainer: function PTV_isContainer(aRow) { // Only leaf nodes aren't listed in the rows array. let node = this._rows[aRow]; if (node === undefined || !PlacesUtils.nodeIsContainer(node)) { return false; } // Flat-lists may ignore expandQueries and other query options when // they are asked to open a container. if (this._flatList) { return true; } // Treat non-expandable childless queries as non-containers, unless they // are tags. if (PlacesUtils.nodeIsQuery(node) && !PlacesUtils.nodeIsTagQuery(node)) { return ( PlacesUtils.asQuery(node).queryOptions.expandQueries || node.hasChildren ); } return true; }, isContainerOpen: function PTV_isContainerOpen(aRow) { if (this._flatList) { return false; } // All containers are listed in the rows array. return this._rows[aRow].containerOpen; }, isContainerEmpty: function PTV_isContainerEmpty(aRow) { if (this._flatList) { return true; } // All containers are listed in the rows array. return !this._rows[aRow].hasChildren; }, isSeparator: function PTV_isSeparator(aRow) { // All separators are listed in the rows array. let node = this._rows[aRow]; return node && PlacesUtils.nodeIsSeparator(node); }, isSorted: function PTV_isSorted() { return ( this._result.sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE ); }, canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) { if (!this._result) { throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); } if (this._controller.disableUserActions) { return false; } // Drop position into a sorted treeview would be wrong. if (this.isSorted()) { return false; } let ip = this._getInsertionPoint(aRow, aOrientation); return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer); }, _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) { let container = this._result.root; let dropNearNode = null; // When there's no selection, assume the container is the container // the view is populated from (i.e. the result's itemId). if (index != -1) { let lastSelected = this.nodeForTreeIndex(index); if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) { // If the last selected item is an open container, append _into_ // it, rather than insert adjacent to it. container = lastSelected; index = -1; } else if ( lastSelected.containerOpen && orientation == Ci.nsITreeView.DROP_AFTER && lastSelected.hasChildren ) { // If the last selected node is an open container and the user is // trying to drag into it as a first node, really insert into it. container = lastSelected; orientation = Ci.nsITreeView.DROP_ON; index = 0; } else { // Use the last-selected node's container. container = lastSelected.parent; // During its Drag & Drop operation, the tree code closes-and-opens // containers very often (part of the XUL "spring-loaded folders" // implementation). And in certain cases, we may reach a closed // container here. However, we can simply bail out when this happens, // because we would then be back here in less than a millisecond, when // the container had been reopened. if (!container || !container.containerOpen) { return null; } // Don't show an insertion point if the index is contained // within the selection and drag source is the same if ( this._element.isDragSource && this._element.view.selection.isSelected(index) ) { return null; } // Avoid the potentially expensive call to getChildIndex // if we know this container doesn't allow insertion. if (this._controller.disallowInsertion(container)) { return null; } let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; if ( queryOptions.sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE ) { // If we are within a sorted view, insert at the end. index = -1; } else if (queryOptions.excludeItems || queryOptions.excludeQueries) { // Some item may be invisible, insert near last selected one. // We don't replace index here to avoid requests to the db, // instead it will be calculated later by the controller. index = -1; dropNearNode = lastSelected; } else { let lastSelectedIndex = container.getChildIndex(lastSelected); index = orientation == Ci.nsITreeView.DROP_BEFORE ? lastSelectedIndex : lastSelectedIndex + 1; } } } if (this._controller.disallowInsertion(container)) { return null; } let tagName = PlacesUtils.nodeIsTagQuery(container) ? PlacesUtils.asQuery(container).query.tags[0] : null; return new PlacesInsertionPoint({ parentGuid: PlacesUtils.getConcreteItemGuid(container), index, orientation, tagName, dropNearNode, }); }, async drop(aRow, aOrientation, aDataTransfer) { if (this._controller.disableUserActions) { return; } // We are responsible for translating the |index| and |orientation| // parameters into a container id and index within the container, // since this information is specific to the tree view. let ip = this._getInsertionPoint(aRow, aOrientation); if (ip) { try { await PlacesControllerDragHelper.onDrop(ip, aDataTransfer, this._tree); } catch (ex) { console.error(ex); } finally { // We should only clear the drop target once // the onDrop is complete, as it is an async function. PlacesControllerDragHelper.currentDropTarget = null; } } }, getParentIndex: function PTV_getParentIndex(aRow) { let [, parentRow] = this._getParentByChildRow(aRow); return parentRow; }, hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) { if (aRow == this._rows.length - 1) { // The last row has no sibling. return false; } let node = this._rows[aRow]; if (node === undefined || this._isPlainContainer(node.parent)) { // The node is a child of a plain container. // If the next row is either unset or has the same parent, // it's a sibling. let nextNode = this._rows[aRow + 1]; return nextNode == undefined || nextNode.parent == node.parent; } let thisLevel = node.indentLevel; for (let i = aAfterIndex + 1; i < this._rows.length; ++i) { let rowNode = this._getNodeForRow(i); let nextLevel = rowNode.indentLevel; if (nextLevel == thisLevel) { return true; } if (nextLevel < thisLevel) { break; } } return false; }, getLevel(aRow) { return this._getNodeForRow(aRow).indentLevel; }, getImageSrc: function PTV_getImageSrc(aRow, aColumn) { // Only the title column has an image. if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE) { return ""; } let node = this._getNodeForRow(aRow); return node.icon; }, getCellValue() {}, getCellText: function PTV_getCellText(aRow, aColumn) { let node = this._getNodeForRow(aRow); switch (this._getColumnType(aColumn)) { case this.COLUMN_TYPE_TITLE: // normally, this is just the title, but we don't want empty items in // the tree view so return a special string if the title is empty. // Do it here so that callers can still get at the 0 length title // if they go through the "result" API. if (PlacesUtils.nodeIsSeparator(node)) { return ""; } return PlacesUIUtils.getBestTitle(node, true); case this.COLUMN_TYPE_TAGS: return node.tags?.replace(",", ", "); case this.COLUMN_TYPE_URI: if (PlacesUtils.nodeIsURI(node)) { return node.uri; } return ""; case this.COLUMN_TYPE_DATE: let nodeTime = node.time; if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) { // hosts and days shouldn't have a value for the date column. // Actually, you could argue this point, but looking at the // results, seeing the most recently visited date is not what // I expect, and gives me no information I know how to use. // Only show this for URI-based items. return ""; } return this._convertPRTimeToString(nodeTime); case this.COLUMN_TYPE_VISITCOUNT: return node.accessCount; case this.COLUMN_TYPE_DATEADDED: if (node.dateAdded) { return this._convertPRTimeToString(node.dateAdded); } return ""; case this.COLUMN_TYPE_LASTMODIFIED: if (node.lastModified) { return this._convertPRTimeToString(node.lastModified); } return ""; } return ""; }, setTree: function PTV_setTree(aTree) { // If we are replacing the tree during a batch, there is a concrete risk // that the treeView goes out of sync, thus it's safer to end the batch now. // This is a no-op if we are not batching. this.batching(false); let hasOldTree = this._tree != null; this._tree = aTree; if (this._result) { if (hasOldTree) { // detach from result when we are detaching from the tree. // This breaks the reference cycle between us and the result. if (!aTree) { // Close the root container to free up memory and stop live updates. this._rootNode.containerOpen = false; } } if (aTree) { this._finishInit(); } } }, toggleOpenState: function PTV_toggleOpenState(aRow) { if (!this._result) { throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); } let node = this._rows[aRow]; if (this._flatList && this._element) { let event = new CustomEvent("onOpenFlatContainer", { detail: node }); this._element.dispatchEvent(event); return; } let uri = node.uri; if (uri) { let docURI = document.documentURI; if (node.containerOpen) { Services.xulStore.removeValue( docURI, PlacesUIUtils.obfuscateUrlForXulStore(uri), "open" ); } else { Services.xulStore.setValue( docURI, PlacesUIUtils.obfuscateUrlForXulStore(uri), "open", "true" ); } } node.containerOpen = !node.containerOpen; }, cycleHeader: function PTV_cycleHeader(aColumn) { if (!this._result) { throw Components.Exception("", Cr.NS_ERROR_UNEXPECTED); } // Sometimes you want a tri-state sorting, and sometimes you don't. This // rule allows tri-state sorting when the root node is a folder. This will // catch the most common cases. When you are looking at folders, you want // the third state to reset the sorting to the natural bookmark order. When // you are looking at history, that third state has no meaning so we try // to disallow it. // // The problem occurs when you have a query that results in bookmark // folders. One example of this is the subscriptions view. In these cases, // this rule doesn't allow you to sort those sub-folders by their natural // order. let allowTriState = PlacesUtils.nodeIsFolder(this._result.root); let oldSort = this._result.sortingMode; let newSort; const NHQO = Ci.nsINavHistoryQueryOptions; switch (this._getColumnType(aColumn)) { case this.COLUMN_TYPE_TITLE: if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING) { newSort = NHQO.SORT_BY_TITLE_DESCENDING; } else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_TITLE_ASCENDING; } break; case this.COLUMN_TYPE_URI: if (oldSort == NHQO.SORT_BY_URI_ASCENDING) { newSort = NHQO.SORT_BY_URI_DESCENDING; } else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_URI_ASCENDING; } break; case this.COLUMN_TYPE_DATE: if (oldSort == NHQO.SORT_BY_DATE_ASCENDING) { newSort = NHQO.SORT_BY_DATE_DESCENDING; } else if (allowTriState && oldSort == NHQO.SORT_BY_DATE_DESCENDING) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_DATE_ASCENDING; } break; case this.COLUMN_TYPE_VISITCOUNT: // visit count default is unusual because we sort by descending // by default because you are most likely to be looking for // highly visited sites when you click it if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING) { newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING; } else if ( allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING ) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING; } break; case this.COLUMN_TYPE_DATEADDED: if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING) { newSort = NHQO.SORT_BY_DATEADDED_DESCENDING; } else if ( allowTriState && oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING ) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_DATEADDED_ASCENDING; } break; case this.COLUMN_TYPE_LASTMODIFIED: if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING) { newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING; } else if ( allowTriState && oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING ) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING; } break; case this.COLUMN_TYPE_TAGS: if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING) { newSort = NHQO.SORT_BY_TAGS_DESCENDING; } else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING) { newSort = NHQO.SORT_BY_NONE; } else { newSort = NHQO.SORT_BY_TAGS_ASCENDING; } break; default: throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG); } this._result.sortingMode = newSort; }, isEditable: function PTV_isEditable(aRow, aColumn) { // At this point we only support editing the title field. if (aColumn.index != 0) { return false; } let node = this._rows[aRow]; if (!node) { console.error("isEditable called for an unbuilt row."); return false; } let itemGuid = node.bookmarkGuid; // Only bookmark-nodes are editable. if (!itemGuid) { return false; } // The following items are also not editable, even though they are bookmark // items. // * places-roots // * the left pane special folders and queries (those are place: uri // bookmarks) // * separators // // Note that concrete itemIds aren't used intentionally. For example, we // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar, // except for the one under All Bookmarks. if ( PlacesUtils.nodeIsSeparator(node) || PlacesUtils.isRootItem(itemGuid) || PlacesUtils.isQueryGeneratedFolder(node) ) { return false; } return true; }, setCellText: function PTV_setCellText(aRow, aColumn, aText) { // We may only get here if the cell is editable. let node = this._rows[aRow]; if (node.title != aText) { PlacesTransactions.EditTitle({ guid: node.bookmarkGuid, title: aText }) .transact() .catch(console.error); } }, toggleCutNode: function PTV_toggleCutNode(aNode, aValue) { let currentVal = this._cuttingNodes.has(aNode); if (currentVal != aValue) { if (aValue) { this._cuttingNodes.add(aNode); } else { this._cuttingNodes.delete(aNode); } this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); } }, selectionChanged() {}, cycleCell() {}, };