michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import('resource://gre/modules/XPCOMUtils.jsm'); michael@0: michael@0: const PTV_interfaces = [Ci.nsITreeView, michael@0: Ci.nsINavHistoryResultObserver, michael@0: Ci.nsINavHistoryResultTreeViewer, michael@0: Ci.nsISupportsWeakReference]; michael@0: michael@0: function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) { michael@0: this._tree = null; michael@0: this._result = null; michael@0: this._selection = null; michael@0: this._rootNode = null; michael@0: this._rows = []; michael@0: this._flatList = aFlatList; michael@0: this._openContainerCallback = aOnOpenFlatContainer; michael@0: this._controller = aController; michael@0: } michael@0: michael@0: PlacesTreeView.prototype = { michael@0: get wrappedJSObject() this, michael@0: michael@0: __dateService: null, michael@0: get _dateService() { michael@0: if (!this.__dateService) { michael@0: this.__dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"]. michael@0: getService(Ci.nsIScriptableDateFormat); michael@0: } michael@0: return this.__dateService; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI(PTV_interfaces), michael@0: michael@0: // Bug 761494: michael@0: // ---------- michael@0: // Some addons use methods from nsINavHistoryResultObserver and michael@0: // nsINavHistoryResultTreeViewer, without QIing to these interfaces first. michael@0: // That's not a problem when the view is retrieved through the michael@0: // .view getter (which returns the wrappedJSObject of this object), michael@0: // it raises an issue when the view retrieved through the treeBoxObject.view michael@0: // getter. Thus, to avoid breaking addons, the interfaces are prefetched. michael@0: classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }), michael@0: michael@0: /** michael@0: * This is called once both the result and the tree are set. michael@0: */ michael@0: _finishInit: function PTV__finishInit() { michael@0: let selection = this.selection; michael@0: if (selection) michael@0: selection.selectEventsSuppressed = true; michael@0: michael@0: if (!this._rootNode.containerOpen) { michael@0: // This triggers containerStateChanged which then builds the visible michael@0: // section. michael@0: this._rootNode.containerOpen = true; michael@0: } michael@0: else michael@0: this.invalidateContainer(this._rootNode); michael@0: michael@0: // "Activate" the sorting column and update commands. michael@0: this.sortingChanged(this._result.sortingMode); michael@0: michael@0: if (selection) michael@0: selection.selectEventsSuppressed = false; michael@0: }, michael@0: michael@0: /** michael@0: * Plain Container: container result nodes which may never include sub michael@0: * hierarchies. michael@0: * michael@0: * When the rows array is constructed, we don't set the children of plain michael@0: * containers. Instead, we keep placeholders for these children. We then michael@0: * build these children lazily as the tree asks us for information about each michael@0: * row. Luckily, the tree doesn't ask about rows outside the visible area. michael@0: * michael@0: * @see _getNodeForRow and _getRowForNode for the actual magic. michael@0: * michael@0: * @note It's guaranteed that all containers are listed in the rows michael@0: * elements array. It's also guaranteed that separators (if they're not michael@0: * filtered, see below) are listed in the visible elements array, because michael@0: * bookmark folders are never built lazily, as described above. michael@0: * michael@0: * @param aContainer michael@0: * A container result node. michael@0: * michael@0: * @return true if aContainer is a plain container, false otherwise. michael@0: */ michael@0: _isPlainContainer: function PTV__isPlainContainer(aContainer) { michael@0: // Livemarks are always plain containers. michael@0: if (this._controller.hasCachedLivemarkInfo(aContainer)) michael@0: return true; michael@0: michael@0: // We don't know enough about non-query containers. michael@0: if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode)) michael@0: return false; michael@0: michael@0: switch (aContainer.queryOptions.resultType) { michael@0: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY: michael@0: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY: michael@0: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY: michael@0: case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY: michael@0: return false; michael@0: } michael@0: michael@0: // If it's a folder, it's not a plain container. michael@0: let nodeType = aContainer.type; michael@0: return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER && michael@0: nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the row number for a given node. Assumes that the given node is michael@0: * visible (i.e. it's not an obsolete node). michael@0: * michael@0: * @param aNode michael@0: * A result node. Do not pass an obsolete node, or any michael@0: * node which isn't supposed to be in the tree (e.g. separators in michael@0: * sorted trees). michael@0: * @param [optional] aForceBuild michael@0: * @see _isPlainContainer. michael@0: * If true, the row will be computed even if the node still isn't set michael@0: * in our rows array. michael@0: * @param [optional] aParentRow michael@0: * The row of aNode's parent. Ignored for the root node. michael@0: * @param [optional] aNodeIndex michael@0: * The index of aNode in its parent. Only used if aParentRow is michael@0: * set too. michael@0: * michael@0: * @throws if aNode is invisible. michael@0: * @note If aParentRow and aNodeIndex are passed and parent is a plain michael@0: * container, this method will just return a calculated row value, without michael@0: * making assumptions on existence of the node at that position. michael@0: * @return aNode's row if it's in the rows list or if aForceBuild is set, -1 michael@0: * otherwise. michael@0: */ michael@0: _getRowForNode: michael@0: function PTV__getRowForNode(aNode, aForceBuild, aParentRow, aNodeIndex) { michael@0: if (aNode == this._rootNode) michael@0: throw new Error("The root node is never visible"); michael@0: michael@0: // A node is removed form the view either if it has no parent or if its michael@0: // root-ancestor is not the root node (in which case that's the node michael@0: // for which nodeRemoved was called). michael@0: let ancestors = [x for each (x in PlacesUtils.nodeAncestors(aNode))]; michael@0: if (ancestors.length == 0 || michael@0: ancestors[ancestors.length - 1] != this._rootNode) { michael@0: throw new Error("Removed node passed to _getRowForNode"); michael@0: } michael@0: michael@0: // Ensure that the entire chain is open, otherwise that node is invisible. michael@0: for (let ancestor of ancestors) { michael@0: if (!ancestor.containerOpen) michael@0: throw new Error("Invisible node passed to _getRowForNode"); michael@0: } michael@0: michael@0: // Non-plain containers are initially built with their contents. michael@0: let parent = aNode.parent; michael@0: let parentIsPlain = this._isPlainContainer(parent); michael@0: if (!parentIsPlain) { michael@0: if (parent == this._rootNode) michael@0: return this._rows.indexOf(aNode); michael@0: michael@0: return this._rows.indexOf(aNode, aParentRow); michael@0: } michael@0: michael@0: let row = -1; michael@0: let useNodeIndex = typeof(aNodeIndex) == "number"; michael@0: if (parent == this._rootNode) michael@0: row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode); michael@0: else if (useNodeIndex && typeof(aParentRow) == "number") { michael@0: // If we have both the row of the parent node, and the node's index, we michael@0: // can avoid searching the rows array if the parent is a plain container. michael@0: row = aParentRow + aNodeIndex + 1; michael@0: } michael@0: else { michael@0: // Look for the node in the nodes array. Start the search at the parent michael@0: // row. If the parent row isn't passed, we'll pass undefined to indexOf, michael@0: // which is fine. michael@0: row = this._rows.indexOf(aNode, aParentRow); michael@0: if (row == -1 && aForceBuild) { michael@0: let parentRow = typeof(aParentRow) == "number" ? aParentRow michael@0: : this._getRowForNode(parent); michael@0: row = parentRow + parent.getChildIndex(aNode) + 1; michael@0: } michael@0: } michael@0: michael@0: if (row != -1) michael@0: this._rows[row] = aNode; michael@0: michael@0: return row; michael@0: }, michael@0: michael@0: /** michael@0: * Given a row, finds and returns the parent details of the associated node. michael@0: * michael@0: * @param aChildRow michael@0: * Row number. michael@0: * @return [parentNode, parentRow] michael@0: */ michael@0: _getParentByChildRow: function PTV__getParentByChildRow(aChildRow) { michael@0: let node = this._getNodeForRow(aChildRow); michael@0: let parent = (node === null) ? this._rootNode : node.parent; michael@0: michael@0: // The root node is never visible michael@0: if (parent == this._rootNode) michael@0: return [this._rootNode, -1]; michael@0: michael@0: let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1); michael@0: return [parent, parentRow]; michael@0: }, michael@0: michael@0: /** michael@0: * Gets the node at a given row. michael@0: */ michael@0: _getNodeForRow: function PTV__getNodeForRow(aRow) { michael@0: if (aRow < 0) { michael@0: return null; michael@0: } michael@0: michael@0: let node = this._rows[aRow]; michael@0: if (node !== undefined) michael@0: return node; michael@0: michael@0: // Find the nearest node. michael@0: let rowNode, row; michael@0: for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) { michael@0: rowNode = this._rows[i]; michael@0: row = i; michael@0: } michael@0: michael@0: // If there's no container prior to the given row, it's a child of michael@0: // the root node (remember: all containers are listed in the rows array). michael@0: if (!rowNode) michael@0: return this._rows[aRow] = this._rootNode.getChild(aRow); michael@0: michael@0: // Unset elements may exist only in plain containers. Thus, if the nearest michael@0: // node is a container, it's the row's parent, otherwise, it's a sibling. michael@0: if (rowNode instanceof Ci.nsINavHistoryContainerResultNode) michael@0: return this._rows[aRow] = rowNode.getChild(aRow - row - 1); michael@0: michael@0: let [parent, parentRow] = this._getParentByChildRow(row); michael@0: return this._rows[aRow] = parent.getChild(aRow - parentRow - 1); michael@0: }, michael@0: michael@0: /** michael@0: * This takes a container and recursively appends our rows array per its michael@0: * contents. Assumes that the rows arrays has no rows for the given michael@0: * container. michael@0: * michael@0: * @param [in] aContainer michael@0: * A container result node. michael@0: * @param [in] aFirstChildRow michael@0: * The first row at which nodes may be inserted to the row array. michael@0: * In other words, that's aContainer's row + 1. michael@0: * @param [out] aToOpen michael@0: * An array of containers to open once the build is done. michael@0: * michael@0: * @return the number of rows which were inserted. michael@0: */ michael@0: _buildVisibleSection: michael@0: function PTV__buildVisibleSection(aContainer, aFirstChildRow, aToOpen) michael@0: { michael@0: // There's nothing to do if the container is closed. michael@0: if (!aContainer.containerOpen) michael@0: return 0; michael@0: michael@0: // Inserting the new elements into the rows array in one shot (by michael@0: // Array.concat) is faster than resizing the array (by splice) on each loop michael@0: // iteration. michael@0: let cc = aContainer.childCount; michael@0: let newElements = new Array(cc); michael@0: this._rows = this._rows.splice(0, aFirstChildRow) michael@0: .concat(newElements, this._rows); michael@0: michael@0: if (this._isPlainContainer(aContainer)) michael@0: return cc; michael@0: michael@0: const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open"); michael@0: const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true"); michael@0: let sortingMode = this._result.sortingMode; michael@0: michael@0: let rowsInserted = 0; michael@0: for (let i = 0; i < cc; i++) { michael@0: let curChild = aContainer.getChild(i); michael@0: let curChildType = curChild.type; michael@0: michael@0: let row = aFirstChildRow + rowsInserted; michael@0: michael@0: // Don't display separators when sorted. michael@0: if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { michael@0: if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { michael@0: // Remove the element for the filtered separator. michael@0: // Notice that the rows array was initially resized to include all michael@0: // children. michael@0: this._rows.splice(row, 1); michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: this._rows[row] = curChild; michael@0: rowsInserted++; michael@0: michael@0: // Recursively do containers. michael@0: if (!this._flatList && michael@0: curChild instanceof Ci.nsINavHistoryContainerResultNode && michael@0: !this._controller.hasCachedLivemarkInfo(curChild)) { michael@0: let resource = this._getResourceForNode(curChild); michael@0: let isopen = resource != null && michael@0: PlacesUIUtils.localStore.HasAssertion(resource, michael@0: openLiteral, michael@0: trueLiteral, true); michael@0: if (isopen != curChild.containerOpen) michael@0: aToOpen.push(curChild); michael@0: else if (curChild.containerOpen && curChild.childCount > 0) michael@0: rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen); michael@0: } michael@0: } michael@0: michael@0: return rowsInserted; michael@0: }, michael@0: michael@0: /** michael@0: * This counts how many rows a node takes in the tree. For containers it michael@0: * will count the node itself plus any child node following it. michael@0: */ michael@0: _countVisibleRowsForNodeAtRow: michael@0: function PTV__countVisibleRowsForNodeAtRow(aNodeRow) { michael@0: let node = this._rows[aNodeRow]; michael@0: michael@0: // If it's not listed yet, we know that it's a leaf node (instanceof also michael@0: // null-checks). michael@0: if (!(node instanceof Ci.nsINavHistoryContainerResultNode)) michael@0: return 1; michael@0: michael@0: let outerLevel = node.indentLevel; michael@0: for (let i = aNodeRow + 1; i < this._rows.length; i++) { michael@0: let rowNode = this._rows[i]; michael@0: if (rowNode && rowNode.indentLevel <= outerLevel) michael@0: return i - aNodeRow; michael@0: } michael@0: michael@0: // This node plus its children take up the bottom of the list. michael@0: return this._rows.length - aNodeRow; michael@0: }, michael@0: michael@0: _getSelectedNodesInRange: michael@0: function PTV__getSelectedNodesInRange(aFirstRow, aLastRow) { michael@0: let selection = this.selection; michael@0: let rc = selection.getRangeCount(); michael@0: if (rc == 0) michael@0: return []; michael@0: michael@0: // The visible-area borders are needed for checking whether a michael@0: // selected row is also visible. michael@0: let firstVisibleRow = this._tree.getFirstVisibleRow(); michael@0: let lastVisibleRow = this._tree.getLastVisibleRow(); michael@0: michael@0: let nodesInfo = []; michael@0: for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) { michael@0: let min = { }, max = { }; michael@0: selection.getRangeAt(rangeIndex, min, max); michael@0: michael@0: // If this range does not overlap the replaced chunk, we don't need to michael@0: // persist the selection. michael@0: if (max.value < aFirstRow || min.value > aLastRow) michael@0: continue; michael@0: michael@0: let firstRow = Math.max(min.value, aFirstRow); michael@0: let lastRow = Math.min(max.value, aLastRow); michael@0: for (let i = firstRow; i <= lastRow; i++) { michael@0: nodesInfo.push({ michael@0: node: this._rows[i], michael@0: oldRow: i, michael@0: wasVisible: i >= firstVisibleRow && i <= lastVisibleRow michael@0: }); michael@0: } michael@0: } michael@0: michael@0: return nodesInfo; michael@0: }, michael@0: michael@0: /** michael@0: * Tries to find an equivalent node for a node which was removed. We first michael@0: * look for the original node, in case it was just relocated. Then, if we michael@0: * that node was not found, we look for a node that has the same itemId, uri michael@0: * and time values. michael@0: * michael@0: * @param aUpdatedContainer michael@0: * An ancestor of the node which was removed. It does not have to be michael@0: * its direct parent. michael@0: * @param aOldNode michael@0: * The node which was removed. michael@0: * michael@0: * @return the row number of an equivalent node for aOldOne, if one was michael@0: * found, -1 otherwise. michael@0: */ michael@0: _getNewRowForRemovedNode: michael@0: function PTV__getNewRowForRemovedNode(aUpdatedContainer, aOldNode) { michael@0: let parent = aOldNode.parent; michael@0: if (parent) { michael@0: // If the node's parent is still set, the node is not obsolete michael@0: // and we should just find out its new position. michael@0: // However, if any of the node's ancestor is closed, the node is michael@0: // invisible. michael@0: let ancestors = PlacesUtils.nodeAncestors(aOldNode); michael@0: for (let ancestor in ancestors) { michael@0: if (!ancestor.containerOpen) michael@0: return -1; michael@0: } michael@0: michael@0: return this._getRowForNode(aOldNode, true); michael@0: } michael@0: michael@0: // There's a broken edge case here. michael@0: // If a visit appears in two queries, and the second one was michael@0: // the old node, we'll select the first one after refresh. There's michael@0: // nothing we could do about that, because aOldNode.parent is michael@0: // gone by the time invalidateContainer is called. michael@0: let newNode = aUpdatedContainer.findNodeByDetails(aOldNode.uri, michael@0: aOldNode.time, michael@0: aOldNode.itemId, michael@0: true); michael@0: if (!newNode) michael@0: return -1; michael@0: michael@0: return this._getRowForNode(newNode, true); michael@0: }, michael@0: michael@0: /** michael@0: * Restores a given selection state as near as possible to the original michael@0: * selection state. michael@0: * michael@0: * @param aNodesInfo michael@0: * The persisted selection state as returned by michael@0: * _getSelectedNodesInRange. michael@0: * @param aUpdatedContainer michael@0: * The container which was updated. michael@0: */ michael@0: _restoreSelection: michael@0: function PTV__restoreSelection(aNodesInfo, aUpdatedContainer) { michael@0: if (aNodesInfo.length == 0) michael@0: return; michael@0: michael@0: let selection = this.selection; michael@0: michael@0: // Attempt to ensure that previously-visible selection will be visible michael@0: // if it's re-selected. However, we can only ensure that for one row. michael@0: let scrollToRow = -1; michael@0: for (let i = 0; i < aNodesInfo.length; i++) { michael@0: let nodeInfo = aNodesInfo[i]; michael@0: let row = this._getNewRowForRemovedNode(aUpdatedContainer, michael@0: nodeInfo.node); michael@0: // Select the found node, if any. michael@0: if (row != -1) { michael@0: selection.rangedSelect(row, row, true); michael@0: if (nodeInfo.wasVisible && scrollToRow == -1) michael@0: scrollToRow = row; michael@0: } michael@0: } michael@0: michael@0: // If only one node was previously selected and there's no selection now, michael@0: // select the node at its old row, if any. michael@0: if (aNodesInfo.length == 1 && selection.count == 0) { michael@0: let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1); michael@0: if (row != -1) { michael@0: selection.rangedSelect(row, row, true); michael@0: if (aNodesInfo[0].wasVisible && scrollToRow == -1) michael@0: scrollToRow = aNodesInfo[0].oldRow; michael@0: } michael@0: } michael@0: michael@0: if (scrollToRow != -1) michael@0: this._tree.ensureRowIsVisible(scrollToRow); michael@0: }, michael@0: michael@0: _convertPRTimeToString: function PTV__convertPRTimeToString(aTime) { michael@0: const MS_PER_MINUTE = 60000; michael@0: const MS_PER_DAY = 86400000; michael@0: let timeMs = aTime / 1000; // PRTime is in microseconds michael@0: michael@0: // Date is calculated starting from midnight, so the modulo with a day are michael@0: // milliseconds from today's midnight. michael@0: // getTimezoneOffset corrects that based on local time, notice midnight michael@0: // can have a different offset during DST-change days. michael@0: let dateObj = new Date(); michael@0: let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE; michael@0: let midnight = now - (now % MS_PER_DAY); michael@0: midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE; michael@0: michael@0: let dateFormat = timeMs >= midnight ? michael@0: Ci.nsIScriptableDateFormat.dateFormatNone : michael@0: Ci.nsIScriptableDateFormat.dateFormatShort; michael@0: michael@0: let timeObj = new Date(timeMs); michael@0: return (this._dateService.FormatDateTime("", dateFormat, michael@0: Ci.nsIScriptableDateFormat.timeFormatNoSeconds, michael@0: timeObj.getFullYear(), timeObj.getMonth() + 1, michael@0: timeObj.getDate(), timeObj.getHours(), michael@0: timeObj.getMinutes(), timeObj.getSeconds())); michael@0: }, michael@0: michael@0: COLUMN_TYPE_UNKNOWN: 0, michael@0: COLUMN_TYPE_TITLE: 1, michael@0: COLUMN_TYPE_URI: 2, michael@0: COLUMN_TYPE_DATE: 3, michael@0: COLUMN_TYPE_VISITCOUNT: 4, michael@0: COLUMN_TYPE_KEYWORD: 5, michael@0: COLUMN_TYPE_DESCRIPTION: 6, michael@0: COLUMN_TYPE_DATEADDED: 7, michael@0: COLUMN_TYPE_LASTMODIFIED: 8, michael@0: COLUMN_TYPE_TAGS: 9, michael@0: michael@0: _getColumnType: function PTV__getColumnType(aColumn) { michael@0: let columnType = aColumn.element.getAttribute("anonid") || aColumn.id; michael@0: michael@0: switch (columnType) { michael@0: case "title": michael@0: return this.COLUMN_TYPE_TITLE; michael@0: case "url": michael@0: return this.COLUMN_TYPE_URI; michael@0: case "date": michael@0: return this.COLUMN_TYPE_DATE; michael@0: case "visitCount": michael@0: return this.COLUMN_TYPE_VISITCOUNT; michael@0: case "keyword": michael@0: return this.COLUMN_TYPE_KEYWORD; michael@0: case "description": michael@0: return this.COLUMN_TYPE_DESCRIPTION; michael@0: case "dateAdded": michael@0: return this.COLUMN_TYPE_DATEADDED; michael@0: case "lastModified": michael@0: return this.COLUMN_TYPE_LASTMODIFIED; michael@0: case "tags": michael@0: return this.COLUMN_TYPE_TAGS; michael@0: } michael@0: return this.COLUMN_TYPE_UNKNOWN; michael@0: }, michael@0: michael@0: _sortTypeToColumnType: function PTV__sortTypeToColumnType(aSortType) { michael@0: switch (aSortType) { michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING: michael@0: return [this.COLUMN_TYPE_TITLE, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING: michael@0: return [this.COLUMN_TYPE_TITLE, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING: michael@0: return [this.COLUMN_TYPE_DATE, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING: michael@0: return [this.COLUMN_TYPE_DATE, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING: michael@0: return [this.COLUMN_TYPE_URI, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING: michael@0: return [this.COLUMN_TYPE_URI, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING: michael@0: return [this.COLUMN_TYPE_VISITCOUNT, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING: michael@0: return [this.COLUMN_TYPE_VISITCOUNT, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING: michael@0: return [this.COLUMN_TYPE_KEYWORD, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING: michael@0: return [this.COLUMN_TYPE_KEYWORD, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING: michael@0: if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) michael@0: return [this.COLUMN_TYPE_DESCRIPTION, false]; michael@0: break; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING: michael@0: if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) michael@0: return [this.COLUMN_TYPE_DESCRIPTION, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING: michael@0: return [this.COLUMN_TYPE_DATEADDED, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING: michael@0: return [this.COLUMN_TYPE_DATEADDED, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING: michael@0: return [this.COLUMN_TYPE_LASTMODIFIED, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING: michael@0: return [this.COLUMN_TYPE_LASTMODIFIED, true]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING: michael@0: return [this.COLUMN_TYPE_TAGS, false]; michael@0: case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING: michael@0: return [this.COLUMN_TYPE_TAGS, true]; michael@0: } michael@0: return [this.COLUMN_TYPE_UNKNOWN, false]; michael@0: }, michael@0: michael@0: // nsINavHistoryResultObserver michael@0: nodeInserted: function PTV_nodeInserted(aParentNode, aNode, aNewIndex) { michael@0: NS_ASSERT(this._result, "Got a notification but have no result!"); michael@0: if (!this._tree || !this._result) michael@0: return; michael@0: michael@0: // Bail out for hidden separators. michael@0: if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) michael@0: return; michael@0: michael@0: let parentRow; michael@0: if (aParentNode != this._rootNode) { michael@0: parentRow = this._getRowForNode(aParentNode); michael@0: michael@0: // Update parent when inserting the first item, since twisty has changed. michael@0: if (aParentNode.childCount == 1) michael@0: this._tree.invalidateRow(parentRow); michael@0: } michael@0: michael@0: // Compute the new row number of the node. michael@0: let row = -1; michael@0: let cc = aParentNode.childCount; michael@0: if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) { michael@0: // We don't need to worry about sub hierarchies of the parent node michael@0: // if it's a plain container, or if the new node is its first child. michael@0: if (aParentNode == this._rootNode) michael@0: row = aNewIndex; michael@0: else michael@0: row = parentRow + aNewIndex + 1; michael@0: } michael@0: else { michael@0: // Here, we try to find the next visible element in the child list so we michael@0: // can set the new visible index to be right before that. Note that we michael@0: // have to search down instead of up, because some siblings could have michael@0: // children themselves that would be in the way. michael@0: let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) && michael@0: this.isSorted(); michael@0: for (let i = aNewIndex + 1; i < cc; i++) { michael@0: let node = aParentNode.getChild(i); michael@0: if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) { michael@0: // The children have not been shifted so the next item will have what michael@0: // should be our index. michael@0: row = this._getRowForNode(node, false, parentRow, i); michael@0: break; michael@0: } michael@0: } michael@0: if (row < 0) { michael@0: // At the end of the child list without finding a visible sibling. This michael@0: // is a little harder because we don't know how many rows the last item michael@0: // in our list takes up (it could be a container with many children). michael@0: let prevChild = aParentNode.getChild(aNewIndex - 1); michael@0: let prevIndex = this._getRowForNode(prevChild, false, parentRow, michael@0: aNewIndex - 1); michael@0: row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex); michael@0: } michael@0: } michael@0: michael@0: this._rows.splice(row, 0, aNode); michael@0: this._tree.rowCountChanged(row, 1); michael@0: michael@0: if (PlacesUtils.nodeIsContainer(aNode) && michael@0: PlacesUtils.asContainer(aNode).containerOpen) { michael@0: this.invalidateContainer(aNode); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being michael@0: * removed but the node it is collapsed with is not being removed (this then michael@0: * just swap out the removee with its collapsing partner). The only time michael@0: * when we really remove things is when deleting URIs, which will apply to michael@0: * all collapsees. This function is called sometimes when resorting items. michael@0: * However, we won't do this when sorted by date because dates will never michael@0: * change for visits, and date sorting is the only time things are collapsed. michael@0: */ michael@0: nodeRemoved: function PTV_nodeRemoved(aParentNode, aNode, aOldIndex) { michael@0: NS_ASSERT(this._result, "Got a notification but have no result!"); michael@0: if (!this._tree || !this._result) michael@0: return; michael@0: michael@0: // XXX bug 517701: We don't know what to do when the root node is removed. michael@0: if (aNode == this._rootNode) michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED; michael@0: michael@0: // Bail out for hidden separators. michael@0: if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) michael@0: return; michael@0: michael@0: let parentRow = aParentNode == this._rootNode ? michael@0: undefined : this._getRowForNode(aParentNode, true); michael@0: let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex); michael@0: if (oldRow < 0) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: michael@0: // If the node was exclusively selected, the node next to it will be michael@0: // selected. michael@0: let selectNext = false; michael@0: let selection = this.selection; michael@0: if (selection.getRangeCount() == 1) { michael@0: let min = { }, max = { }; michael@0: selection.getRangeAt(0, min, max); michael@0: if (min.value == max.value && michael@0: this.nodeForTreeIndex(min.value) == aNode) michael@0: selectNext = true; michael@0: } michael@0: michael@0: // Remove the node and its children, if any. michael@0: let count = this._countVisibleRowsForNodeAtRow(oldRow); michael@0: this._rows.splice(oldRow, count); michael@0: this._tree.rowCountChanged(oldRow, -count); michael@0: michael@0: // Redraw the parent if its twisty state has changed. michael@0: if (aParentNode != this._rootNode && !aParentNode.hasChildren) { michael@0: let parentRow = oldRow - 1; michael@0: this._tree.invalidateRow(parentRow); michael@0: } michael@0: michael@0: // Restore selection if the node was exclusively selected. michael@0: if (!selectNext) michael@0: return; michael@0: michael@0: // Restore selection. michael@0: let rowToSelect = Math.min(oldRow, this._rows.length - 1); michael@0: if (rowToSelect != -1) michael@0: this.selection.rangedSelect(rowToSelect, rowToSelect, true); michael@0: }, michael@0: michael@0: nodeMoved: michael@0: function PTV_nodeMoved(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) { michael@0: NS_ASSERT(this._result, "Got a notification but have no result!"); michael@0: if (!this._tree || !this._result) michael@0: return; michael@0: michael@0: // Bail out for hidden separators. michael@0: if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted()) michael@0: return; michael@0: michael@0: // Note that at this point the node has already been moved by the backend, michael@0: // so we must give hints to _getRowForNode to get the old row position. michael@0: let oldParentRow = aOldParent == this._rootNode ? michael@0: undefined : this._getRowForNode(aOldParent, true); michael@0: let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex); michael@0: if (oldRow < 0) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: michael@0: // If this node is a container it could take up more than one row. michael@0: let count = this._countVisibleRowsForNodeAtRow(oldRow); michael@0: michael@0: // Persist selection state. michael@0: let nodesToReselect = michael@0: this._getSelectedNodesInRange(oldRow, oldRow + count); michael@0: if (nodesToReselect.length > 0) michael@0: this.selection.selectEventsSuppressed = true; michael@0: michael@0: // Redraw the parent if its twisty state has changed. michael@0: if (aOldParent != this._rootNode && !aOldParent.hasChildren) { michael@0: let parentRow = oldRow - 1; michael@0: this._tree.invalidateRow(parentRow); michael@0: } michael@0: michael@0: // Remove node and its children, if any, from the old position. michael@0: this._rows.splice(oldRow, count); michael@0: this._tree.rowCountChanged(oldRow, -count); michael@0: michael@0: // Insert the node into the new position. michael@0: this.nodeInserted(aNewParent, aNode, aNewIndex); michael@0: michael@0: // Restore selection. michael@0: if (nodesToReselect.length > 0) { michael@0: this._restoreSelection(nodesToReselect, aNewParent); michael@0: this.selection.selectEventsSuppressed = false; michael@0: } michael@0: }, michael@0: michael@0: _invalidateCellValue: function PTV__invalidateCellValue(aNode, michael@0: aColumnType) { michael@0: NS_ASSERT(this._result, "Got a notification but have no result!"); michael@0: if (!this._tree || !this._result) michael@0: return; michael@0: michael@0: // Nothing to do for the root node. michael@0: if (aNode == this._rootNode) michael@0: return; michael@0: michael@0: let row = this._getRowForNode(aNode); michael@0: if (row == -1) michael@0: return; michael@0: michael@0: let column = this._findColumnByType(aColumnType); michael@0: if (column && !column.element.hidden) michael@0: this._tree.invalidateCell(row, column); michael@0: michael@0: // Last modified time is altered for almost all node changes. michael@0: if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) { michael@0: let lastModifiedColumn = michael@0: this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED); michael@0: if (lastModifiedColumn && !lastModifiedColumn.hidden) michael@0: this._tree.invalidateCell(row, lastModifiedColumn); michael@0: } michael@0: }, michael@0: michael@0: _populateLivemarkContainer: function PTV__populateLivemarkContainer(aNode) { michael@0: PlacesUtils.livemarks.getLivemark({ id: aNode.itemId }) michael@0: .then(aLivemark => { michael@0: let placesNode = aNode; michael@0: // Need to check containerOpen since getLivemark is async. michael@0: if (!placesNode.containerOpen) michael@0: return; michael@0: michael@0: let children = aLivemark.getNodesForContainer(placesNode); michael@0: for (let i = 0; i < children.length; i++) { michael@0: let child = children[i]; michael@0: this.nodeInserted(placesNode, child, i); michael@0: } michael@0: }, Components.utils.reportError); michael@0: }, michael@0: michael@0: nodeTitleChanged: function PTV_nodeTitleChanged(aNode, aNewTitle) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); michael@0: }, michael@0: michael@0: nodeURIChanged: function PTV_nodeURIChanged(aNode, aNewURI) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI); michael@0: }, michael@0: michael@0: nodeIconChanged: function PTV_nodeIconChanged(aNode) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); michael@0: }, michael@0: michael@0: nodeHistoryDetailsChanged: michael@0: function PTV_nodeHistoryDetailsChanged(aNode, aUpdatedVisitDate, michael@0: aUpdatedVisitCount) { michael@0: if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) { michael@0: // Find the node in the parent. michael@0: let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent); michael@0: for (let i = parentRow; i < this._rows.length; i++) { michael@0: let child = this.nodeForTreeIndex(i); michael@0: if (child.uri == aNode.uri) { michael@0: this._cellProperties.delete(child); michael@0: this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE); michael@0: break; michael@0: } michael@0: } michael@0: return; michael@0: } michael@0: michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE); michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT); michael@0: }, michael@0: michael@0: nodeTagsChanged: function PTV_nodeTagsChanged(aNode) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS); michael@0: }, michael@0: michael@0: nodeKeywordChanged: function PTV_nodeKeywordChanged(aNode, aNewKeyword) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD); michael@0: }, michael@0: michael@0: nodeAnnotationChanged: function PTV_nodeAnnotationChanged(aNode, aAnno) { michael@0: if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION); michael@0: } michael@0: else if (aAnno == PlacesUtils.LMANNO_FEEDURI) { michael@0: PlacesUtils.livemarks.getLivemark({ id: aNode.itemId }) michael@0: .then(aLivemark => { michael@0: this._controller.cacheLivemarkInfo(aNode, aLivemark); michael@0: let properties = this._cellProperties.get(aNode); michael@0: this._cellProperties.set(aNode, properties += " livemark "); michael@0: michael@0: // The livemark attribute is set as a cell property on the title cell. michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); michael@0: }, Components.utils.reportError); michael@0: } michael@0: }, michael@0: michael@0: nodeDateAddedChanged: function PTV_nodeDateAddedChanged(aNode, aNewValue) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED); michael@0: }, michael@0: michael@0: nodeLastModifiedChanged: michael@0: function PTV_nodeLastModifiedChanged(aNode, aNewValue) { michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED); michael@0: }, michael@0: michael@0: containerStateChanged: michael@0: function PTV_containerStateChanged(aNode, aOldState, aNewState) { michael@0: this.invalidateContainer(aNode); michael@0: michael@0: if (PlacesUtils.nodeIsFolder(aNode) || michael@0: (this._flatList && aNode == this._rootNode)) { michael@0: let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions; michael@0: if (queryOptions.excludeItems) { michael@0: return; michael@0: } michael@0: michael@0: PlacesUtils.livemarks.getLivemark({ id: aNode.itemId }) michael@0: .then(aLivemark => { michael@0: let shouldInvalidate = michael@0: !this._controller.hasCachedLivemarkInfo(aNode); michael@0: this._controller.cacheLivemarkInfo(aNode, aLivemark); michael@0: if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) { michael@0: aLivemark.registerForUpdates(aNode, this); michael@0: // Prioritize the current livemark. michael@0: aLivemark.reload(); michael@0: PlacesUtils.livemarks.reloadLivemarks(); michael@0: if (shouldInvalidate) michael@0: this.invalidateContainer(aNode); michael@0: } michael@0: else { michael@0: aLivemark.unregisterForUpdates(aNode); michael@0: } michael@0: }, () => undefined); michael@0: } michael@0: }, michael@0: michael@0: invalidateContainer: function PTV_invalidateContainer(aContainer) { michael@0: NS_ASSERT(this._result, "Need to have a result to update"); michael@0: if (!this._tree) michael@0: return; michael@0: michael@0: let startReplacement, replaceCount; michael@0: if (aContainer == this._rootNode) { michael@0: startReplacement = 0; michael@0: replaceCount = this._rows.length; michael@0: michael@0: // If the root node is now closed, the tree is empty. michael@0: if (!this._rootNode.containerOpen) { michael@0: this._rows = []; michael@0: if (replaceCount) michael@0: this._tree.rowCountChanged(startReplacement, -replaceCount); michael@0: michael@0: return; michael@0: } michael@0: } michael@0: else { michael@0: // Update the twisty state. michael@0: let row = this._getRowForNode(aContainer); michael@0: this._tree.invalidateRow(row); michael@0: michael@0: // We don't replace the container node itself, so we should decrease the michael@0: // replaceCount by 1. michael@0: startReplacement = row + 1; michael@0: replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1; michael@0: } michael@0: michael@0: // Persist selection state. michael@0: let nodesToReselect = michael@0: this._getSelectedNodesInRange(startReplacement, michael@0: startReplacement + replaceCount); michael@0: michael@0: // Now update the number of elements. michael@0: this.selection.selectEventsSuppressed = true; michael@0: michael@0: // First remove the old elements michael@0: this._rows.splice(startReplacement, replaceCount); michael@0: michael@0: // If the container is now closed, we're done. michael@0: if (!aContainer.containerOpen) { michael@0: let oldSelectionCount = this.selection.count; michael@0: if (replaceCount) michael@0: this._tree.rowCountChanged(startReplacement, -replaceCount); michael@0: michael@0: // Select the row next to the closed container if any of its michael@0: // children were selected, and nothing else is selected. michael@0: if (nodesToReselect.length > 0 && michael@0: nodesToReselect.length == oldSelectionCount) { michael@0: this.selection.rangedSelect(startReplacement, startReplacement, true); michael@0: this._tree.ensureRowIsVisible(startReplacement); michael@0: } michael@0: michael@0: this.selection.selectEventsSuppressed = false; michael@0: return; michael@0: } michael@0: michael@0: // Otherwise, start a batch first. michael@0: this._tree.beginUpdateBatch(); michael@0: if (replaceCount) michael@0: this._tree.rowCountChanged(startReplacement, -replaceCount); michael@0: michael@0: let toOpenElements = []; michael@0: let elementsAddedCount = this._buildVisibleSection(aContainer, michael@0: startReplacement, michael@0: toOpenElements); michael@0: if (elementsAddedCount) michael@0: this._tree.rowCountChanged(startReplacement, elementsAddedCount); michael@0: michael@0: if (!this._flatList) { michael@0: // Now, open any containers that were persisted. michael@0: for (let i = 0; i < toOpenElements.length; i++) { michael@0: let item = toOpenElements[i]; michael@0: let parent = item.parent; michael@0: michael@0: // Avoid recursively opening containers. michael@0: while (parent) { michael@0: if (parent.uri == item.uri) michael@0: break; michael@0: parent = parent.parent; michael@0: } michael@0: michael@0: // If we don't have a parent, we made it all the way to the root michael@0: // and didn't find a match, so we can open our item. michael@0: if (!parent && !item.containerOpen) michael@0: item.containerOpen = true; michael@0: } michael@0: } michael@0: michael@0: if (this._controller.hasCachedLivemarkInfo(aContainer)) { michael@0: let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; michael@0: if (!queryOptions.excludeItems) { michael@0: this._populateLivemarkContainer(aContainer); michael@0: } michael@0: } michael@0: michael@0: this._tree.endUpdateBatch(); michael@0: michael@0: // Restore selection. michael@0: this._restoreSelection(nodesToReselect, aContainer); michael@0: this.selection.selectEventsSuppressed = false; michael@0: }, michael@0: michael@0: _columns: [], michael@0: _findColumnByType: function PTV__findColumnByType(aColumnType) { michael@0: if (this._columns[aColumnType]) michael@0: return this._columns[aColumnType]; michael@0: michael@0: let columns = this._tree.columns; michael@0: let colCount = columns.count; michael@0: for (let i = 0; i < colCount; i++) { michael@0: let column = columns.getColumnAt(i); michael@0: let columnType = this._getColumnType(column); michael@0: this._columns[columnType] = column; michael@0: if (columnType == aColumnType) michael@0: return column; michael@0: } michael@0: michael@0: // That's completely valid. Most of our trees actually include just the michael@0: // title column. michael@0: return null; michael@0: }, michael@0: michael@0: sortingChanged: function PTV__sortingChanged(aSortingMode) { michael@0: if (!this._tree || !this._result) michael@0: return; michael@0: michael@0: // Depending on the sort mode, certain commands may be disabled. michael@0: window.updateCommands("sort"); michael@0: michael@0: let columns = this._tree.columns; michael@0: michael@0: // Clear old sorting indicator. michael@0: let sortedColumn = columns.getSortedColumn(); michael@0: if (sortedColumn) michael@0: sortedColumn.element.removeAttribute("sortDirection"); michael@0: michael@0: // Set new sorting indicator by looking through all columns for ours. michael@0: if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) michael@0: return; michael@0: michael@0: let [desiredColumn, desiredIsDescending] = michael@0: this._sortTypeToColumnType(aSortingMode); michael@0: let colCount = columns.count; michael@0: let column = this._findColumnByType(desiredColumn); michael@0: if (column) { michael@0: let sortDir = desiredIsDescending ? "descending" : "ascending"; michael@0: column.element.setAttribute("sortDirection", sortDir); michael@0: } michael@0: }, michael@0: michael@0: _inBatchMode: false, michael@0: batching: function PTV__batching(aToggleMode) { michael@0: if (this._inBatchMode != aToggleMode) { michael@0: this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode; michael@0: if (this._inBatchMode) { michael@0: this._tree.beginUpdateBatch(); michael@0: } michael@0: else { michael@0: this._tree.endUpdateBatch(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: get result() this._result, michael@0: set result(val) { michael@0: if (this._result) { michael@0: this._result.removeObserver(this); michael@0: this._rootNode.containerOpen = false; michael@0: } michael@0: michael@0: if (val) { michael@0: this._result = val; michael@0: this._rootNode = this._result.root; michael@0: this._cellProperties = new Map(); michael@0: this._cuttingNodes = new Set(); michael@0: } michael@0: else if (this._result) { michael@0: delete this._result; michael@0: delete this._rootNode; michael@0: delete this._cellProperties; michael@0: delete this._cuttingNodes; michael@0: } michael@0: michael@0: // If the tree is not set yet, setTree will call finishInit. michael@0: if (this._tree && val) michael@0: this._finishInit(); michael@0: michael@0: return val; michael@0: }, michael@0: michael@0: nodeForTreeIndex: function PTV_nodeForTreeIndex(aIndex) { michael@0: if (aIndex > this._rows.length) michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: michael@0: return this._getNodeForRow(aIndex); michael@0: }, michael@0: michael@0: treeIndexForNode: function PTV_treeNodeForIndex(aNode) { michael@0: // The API allows passing invisible nodes. michael@0: try { michael@0: return this._getRowForNode(aNode, true); michael@0: } michael@0: catch(ex) { } michael@0: michael@0: return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE; michael@0: }, michael@0: michael@0: _getResourceForNode: function PTV_getResourceForNode(aNode) michael@0: { michael@0: let uri = aNode.uri; michael@0: NS_ASSERT(uri, "if there is no uri, we can't persist the open state"); michael@0: return uri ? PlacesUIUtils.RDF.GetResource(uri) : null; michael@0: }, michael@0: michael@0: // nsITreeView michael@0: get rowCount() this._rows.length, michael@0: get selection() this._selection, michael@0: set selection(val) this._selection = val, michael@0: michael@0: getRowProperties: function() { return ""; }, michael@0: michael@0: getCellProperties: michael@0: function PTV_getCellProperties(aRow, aColumn) { michael@0: // for anonid-trees, we need to add the column-type manually michael@0: var props = ""; michael@0: let columnType = aColumn.element.getAttribute("anonid"); michael@0: if (columnType) michael@0: props += columnType; michael@0: else michael@0: columnType = aColumn.id; michael@0: michael@0: // Set the "ltr" property on url cells michael@0: if (columnType == "url") michael@0: props += " ltr"; michael@0: michael@0: if (columnType != "title") michael@0: return props; michael@0: michael@0: let node = this._getNodeForRow(aRow); michael@0: michael@0: if (this._cuttingNodes.has(node)) { michael@0: props += " cutting"; michael@0: } michael@0: michael@0: let properties = this._cellProperties.get(node); michael@0: if (properties === undefined) { michael@0: properties = ""; michael@0: let itemId = node.itemId; michael@0: let nodeType = node.type; michael@0: if (PlacesUtils.containerTypes.indexOf(nodeType) != -1) { michael@0: if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) { michael@0: properties += " query"; michael@0: if (PlacesUtils.nodeIsTagQuery(node)) michael@0: properties += " tagContainer"; michael@0: else if (PlacesUtils.nodeIsDay(node)) michael@0: properties += " dayContainer"; michael@0: else if (PlacesUtils.nodeIsHost(node)) michael@0: properties += " hostContainer"; michael@0: } michael@0: else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER || michael@0: nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) { michael@0: if (this._controller.hasCachedLivemarkInfo(node)) { michael@0: properties += " livemark"; michael@0: } michael@0: else { michael@0: PlacesUtils.livemarks.getLivemark({ id: node.itemId }) michael@0: .then(aLivemark => { michael@0: this._controller.cacheLivemarkInfo(node, aLivemark); michael@0: properties += " livemark"; michael@0: // The livemark attribute is set as a cell property on the title cell. michael@0: this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE); michael@0: }, () => undefined); michael@0: } michael@0: } michael@0: michael@0: if (itemId != -1) { michael@0: let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId); michael@0: if (queryName) michael@0: properties += " OrganizerQuery_" + queryName; michael@0: } michael@0: } michael@0: else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) michael@0: properties += " separator"; michael@0: else if (PlacesUtils.nodeIsURI(node)) { michael@0: properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri); michael@0: michael@0: if (this._controller.hasCachedLivemarkInfo(node.parent)) { michael@0: properties += " livemarkItem"; michael@0: if (node.accessCount) { michael@0: properties += " visited"; michael@0: } michael@0: } michael@0: } michael@0: michael@0: this._cellProperties.set(node, properties); michael@0: } michael@0: michael@0: return props + " " + properties; michael@0: }, michael@0: michael@0: getColumnProperties: function(aColumn) { return ""; }, michael@0: michael@0: isContainer: function PTV_isContainer(aRow) { michael@0: // Only leaf nodes aren't listed in the rows array. michael@0: let node = this._rows[aRow]; michael@0: if (node === undefined) michael@0: return false; michael@0: michael@0: if (PlacesUtils.nodeIsContainer(node)) { michael@0: // Flat-lists may ignore expandQueries and other query options when michael@0: // they are asked to open a container. michael@0: if (this._flatList) michael@0: return true; michael@0: michael@0: // treat non-expandable childless queries as non-containers michael@0: if (PlacesUtils.nodeIsQuery(node)) { michael@0: let parent = node.parent; michael@0: if ((PlacesUtils.nodeIsQuery(parent) || michael@0: PlacesUtils.nodeIsFolder(parent)) && michael@0: !PlacesUtils.asQuery(node).hasChildren) michael@0: return PlacesUtils.asQuery(parent).queryOptions.expandQueries; michael@0: } michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: isContainerOpen: function PTV_isContainerOpen(aRow) { michael@0: if (this._flatList) michael@0: return false; michael@0: michael@0: // All containers are listed in the rows array. michael@0: return this._rows[aRow].containerOpen; michael@0: }, michael@0: michael@0: isContainerEmpty: function PTV_isContainerEmpty(aRow) { michael@0: if (this._flatList) michael@0: return true; michael@0: michael@0: let node = this._rows[aRow]; michael@0: if (this._controller.hasCachedLivemarkInfo(node)) { michael@0: let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; michael@0: return queryOptions.excludeItems; michael@0: } michael@0: michael@0: // All containers are listed in the rows array. michael@0: return !node.hasChildren; michael@0: }, michael@0: michael@0: isSeparator: function PTV_isSeparator(aRow) { michael@0: // All separators are listed in the rows array. michael@0: let node = this._rows[aRow]; michael@0: return node && PlacesUtils.nodeIsSeparator(node); michael@0: }, michael@0: michael@0: isSorted: function PTV_isSorted() { michael@0: return this._result.sortingMode != michael@0: Ci.nsINavHistoryQueryOptions.SORT_BY_NONE; michael@0: }, michael@0: michael@0: canDrop: function PTV_canDrop(aRow, aOrientation, aDataTransfer) { michael@0: if (!this._result) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: michael@0: // Drop position into a sorted treeview would be wrong. michael@0: if (this.isSorted()) michael@0: return false; michael@0: michael@0: let ip = this._getInsertionPoint(aRow, aOrientation); michael@0: return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer); michael@0: }, michael@0: michael@0: _getInsertionPoint: function PTV__getInsertionPoint(index, orientation) { michael@0: let container = this._result.root; michael@0: let dropNearItemId = -1; michael@0: // When there's no selection, assume the container is the container michael@0: // the view is populated from (i.e. the result's itemId). michael@0: if (index != -1) { michael@0: let lastSelected = this.nodeForTreeIndex(index); michael@0: if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) { michael@0: // If the last selected item is an open container, append _into_ michael@0: // it, rather than insert adjacent to it. michael@0: container = lastSelected; michael@0: index = -1; michael@0: } michael@0: else if (lastSelected.containerOpen && michael@0: orientation == Ci.nsITreeView.DROP_AFTER && michael@0: lastSelected.hasChildren) { michael@0: // If the last selected node is an open container and the user is michael@0: // trying to drag into it as a first node, really insert into it. michael@0: container = lastSelected; michael@0: orientation = Ci.nsITreeView.DROP_ON; michael@0: index = 0; michael@0: } michael@0: else { michael@0: // Use the last-selected node's container. michael@0: container = lastSelected.parent; michael@0: michael@0: // During its Drag & Drop operation, the tree code closes-and-opens michael@0: // containers very often (part of the XUL "spring-loaded folders" michael@0: // implementation). And in certain cases, we may reach a closed michael@0: // container here. However, we can simply bail out when this happens, michael@0: // because we would then be back here in less than a millisecond, when michael@0: // the container had been reopened. michael@0: if (!container || !container.containerOpen) michael@0: return null; michael@0: michael@0: // Avoid the potentially expensive call to getChildIndex michael@0: // if we know this container doesn't allow insertion. michael@0: if (PlacesControllerDragHelper.disallowInsertion(container)) michael@0: return null; michael@0: michael@0: let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions; michael@0: if (queryOptions.sortingMode != michael@0: Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) { michael@0: // If we are within a sorted view, insert at the end. michael@0: index = -1; michael@0: } michael@0: else if (queryOptions.excludeItems || michael@0: queryOptions.excludeQueries || michael@0: queryOptions.excludeReadOnlyFolders) { michael@0: // Some item may be invisible, insert near last selected one. michael@0: // We don't replace index here to avoid requests to the db, michael@0: // instead it will be calculated later by the controller. michael@0: index = -1; michael@0: dropNearItemId = lastSelected.itemId; michael@0: } michael@0: else { michael@0: let lsi = container.getChildIndex(lastSelected); michael@0: index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (PlacesControllerDragHelper.disallowInsertion(container)) michael@0: return null; michael@0: michael@0: return new InsertionPoint(PlacesUtils.getConcreteItemId(container), michael@0: index, orientation, michael@0: PlacesUtils.nodeIsTagQuery(container), michael@0: dropNearItemId); michael@0: }, michael@0: michael@0: drop: function PTV_drop(aRow, aOrientation, aDataTransfer) { michael@0: // We are responsible for translating the |index| and |orientation| michael@0: // parameters into a container id and index within the container, michael@0: // since this information is specific to the tree view. michael@0: let ip = this._getInsertionPoint(aRow, aOrientation); michael@0: if (ip) michael@0: PlacesControllerDragHelper.onDrop(ip, aDataTransfer); michael@0: michael@0: PlacesControllerDragHelper.currentDropTarget = null; michael@0: }, michael@0: michael@0: getParentIndex: function PTV_getParentIndex(aRow) { michael@0: let [parentNode, parentRow] = this._getParentByChildRow(aRow); michael@0: return parentRow; michael@0: }, michael@0: michael@0: hasNextSibling: function PTV_hasNextSibling(aRow, aAfterIndex) { michael@0: if (aRow == this._rows.length - 1) { michael@0: // The last row has no sibling. michael@0: return false; michael@0: } michael@0: michael@0: let node = this._rows[aRow]; michael@0: if (node === undefined || this._isPlainContainer(node.parent)) { michael@0: // The node is a child of a plain container. michael@0: // If the next row is either unset or has the same parent, michael@0: // it's a sibling. michael@0: let nextNode = this._rows[aRow + 1]; michael@0: return (nextNode == undefined || nextNode.parent == node.parent); michael@0: } michael@0: michael@0: let thisLevel = node.indentLevel; michael@0: for (let i = aAfterIndex + 1; i < this._rows.length; ++i) { michael@0: let rowNode = this._getNodeForRow(i); michael@0: let nextLevel = rowNode.indentLevel; michael@0: if (nextLevel == thisLevel) michael@0: return true; michael@0: if (nextLevel < thisLevel) michael@0: break; michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: getLevel: function(aRow) this._getNodeForRow(aRow).indentLevel, michael@0: michael@0: getImageSrc: function PTV_getImageSrc(aRow, aColumn) { michael@0: // Only the title column has an image. michael@0: if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE) michael@0: return ""; michael@0: michael@0: return this._getNodeForRow(aRow).icon; michael@0: }, michael@0: michael@0: getProgressMode: function(aRow, aColumn) { }, michael@0: getCellValue: function(aRow, aColumn) { }, michael@0: michael@0: getCellText: function PTV_getCellText(aRow, aColumn) { michael@0: let node = this._getNodeForRow(aRow); michael@0: switch (this._getColumnType(aColumn)) { michael@0: case this.COLUMN_TYPE_TITLE: michael@0: // normally, this is just the title, but we don't want empty items in michael@0: // the tree view so return a special string if the title is empty. michael@0: // Do it here so that callers can still get at the 0 length title michael@0: // if they go through the "result" API. michael@0: if (PlacesUtils.nodeIsSeparator(node)) michael@0: return ""; michael@0: return PlacesUIUtils.getBestTitle(node, true); michael@0: case this.COLUMN_TYPE_TAGS: michael@0: return node.tags; michael@0: case this.COLUMN_TYPE_URI: michael@0: if (PlacesUtils.nodeIsURI(node)) michael@0: return node.uri; michael@0: return ""; michael@0: case this.COLUMN_TYPE_DATE: michael@0: let nodeTime = node.time; michael@0: if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) { michael@0: // hosts and days shouldn't have a value for the date column. michael@0: // Actually, you could argue this point, but looking at the michael@0: // results, seeing the most recently visited date is not what michael@0: // I expect, and gives me no information I know how to use. michael@0: // Only show this for URI-based items. michael@0: return ""; michael@0: } michael@0: michael@0: return this._convertPRTimeToString(nodeTime); michael@0: case this.COLUMN_TYPE_VISITCOUNT: michael@0: return node.accessCount; michael@0: case this.COLUMN_TYPE_KEYWORD: michael@0: if (PlacesUtils.nodeIsBookmark(node)) michael@0: return PlacesUtils.bookmarks.getKeywordForBookmark(node.itemId); michael@0: return ""; michael@0: case this.COLUMN_TYPE_DESCRIPTION: michael@0: if (node.itemId != -1) { michael@0: try { michael@0: return PlacesUtils.annotations. michael@0: getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO); michael@0: } michael@0: catch (ex) { /* has no description */ } michael@0: } michael@0: return ""; michael@0: case this.COLUMN_TYPE_DATEADDED: michael@0: if (node.dateAdded) michael@0: return this._convertPRTimeToString(node.dateAdded); michael@0: return ""; michael@0: case this.COLUMN_TYPE_LASTMODIFIED: michael@0: if (node.lastModified) michael@0: return this._convertPRTimeToString(node.lastModified); michael@0: return ""; michael@0: } michael@0: return ""; michael@0: }, michael@0: michael@0: setTree: function PTV_setTree(aTree) { michael@0: // If we are replacing the tree during a batch, there is a concrete risk michael@0: // that the treeView goes out of sync, thus it's safer to end the batch now. michael@0: // This is a no-op if we are not batching. michael@0: this.batching(false); michael@0: michael@0: let hasOldTree = this._tree != null; michael@0: this._tree = aTree; michael@0: michael@0: if (this._result) { michael@0: if (hasOldTree) { michael@0: // detach from result when we are detaching from the tree. michael@0: // This breaks the reference cycle between us and the result. michael@0: if (!aTree) { michael@0: this._result.removeObserver(this); michael@0: this._rootNode.containerOpen = false; michael@0: } michael@0: } michael@0: if (aTree) michael@0: this._finishInit(); michael@0: } michael@0: }, michael@0: michael@0: toggleOpenState: function PTV_toggleOpenState(aRow) { michael@0: if (!this._result) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: michael@0: let node = this._rows[aRow]; michael@0: if (this._flatList && this._openContainerCallback) { michael@0: this._openContainerCallback(node); michael@0: return; michael@0: } michael@0: michael@0: // Persist containers open status, but never persist livemarks. michael@0: if (!this._controller.hasCachedLivemarkInfo(node)) { michael@0: let resource = this._getResourceForNode(node); michael@0: if (resource) { michael@0: const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open"); michael@0: const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true"); michael@0: michael@0: if (node.containerOpen) michael@0: PlacesUIUtils.localStore.Unassert(resource, openLiteral, trueLiteral); michael@0: else michael@0: PlacesUIUtils.localStore.Assert(resource, openLiteral, trueLiteral, true); michael@0: } michael@0: } michael@0: michael@0: node.containerOpen = !node.containerOpen; michael@0: }, michael@0: michael@0: cycleHeader: function PTV_cycleHeader(aColumn) { michael@0: if (!this._result) michael@0: throw Cr.NS_ERROR_UNEXPECTED; michael@0: michael@0: // Sometimes you want a tri-state sorting, and sometimes you don't. This michael@0: // rule allows tri-state sorting when the root node is a folder. This will michael@0: // catch the most common cases. When you are looking at folders, you want michael@0: // the third state to reset the sorting to the natural bookmark order. When michael@0: // you are looking at history, that third state has no meaning so we try michael@0: // to disallow it. michael@0: // michael@0: // The problem occurs when you have a query that results in bookmark michael@0: // folders. One example of this is the subscriptions view. In these cases, michael@0: // this rule doesn't allow you to sort those sub-folders by their natural michael@0: // order. michael@0: let allowTriState = PlacesUtils.nodeIsFolder(this._result.root); michael@0: michael@0: let oldSort = this._result.sortingMode; michael@0: let oldSortingAnnotation = this._result.sortingAnnotation; michael@0: let newSort; michael@0: let newSortingAnnotation = ""; michael@0: const NHQO = Ci.nsINavHistoryQueryOptions; michael@0: switch (this._getColumnType(aColumn)) { michael@0: case this.COLUMN_TYPE_TITLE: michael@0: if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING) michael@0: newSort = NHQO.SORT_BY_TITLE_DESCENDING; michael@0: else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_TITLE_ASCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_URI: michael@0: if (oldSort == NHQO.SORT_BY_URI_ASCENDING) michael@0: newSort = NHQO.SORT_BY_URI_DESCENDING; michael@0: else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_URI_ASCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_DATE: michael@0: if (oldSort == NHQO.SORT_BY_DATE_ASCENDING) michael@0: newSort = NHQO.SORT_BY_DATE_DESCENDING; michael@0: else if (allowTriState && michael@0: oldSort == NHQO.SORT_BY_DATE_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_DATE_ASCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_VISITCOUNT: michael@0: // visit count default is unusual because we sort by descending michael@0: // by default because you are most likely to be looking for michael@0: // highly visited sites when you click it michael@0: if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING) michael@0: newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING; michael@0: else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_KEYWORD: michael@0: if (oldSort == NHQO.SORT_BY_KEYWORD_ASCENDING) michael@0: newSort = NHQO.SORT_BY_KEYWORD_DESCENDING; michael@0: else if (allowTriState && oldSort == NHQO.SORT_BY_KEYWORD_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_KEYWORD_ASCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_DESCRIPTION: michael@0: if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING && michael@0: oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) { michael@0: newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING; michael@0: newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO; michael@0: } michael@0: else if (allowTriState && michael@0: oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING && michael@0: oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else { michael@0: newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING; michael@0: newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO; michael@0: } michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_DATEADDED: michael@0: if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING) michael@0: newSort = NHQO.SORT_BY_DATEADDED_DESCENDING; michael@0: else if (allowTriState && michael@0: oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_DATEADDED_ASCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_LASTMODIFIED: michael@0: if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING) michael@0: newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING; michael@0: else if (allowTriState && michael@0: oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING; michael@0: michael@0: break; michael@0: case this.COLUMN_TYPE_TAGS: michael@0: if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING) michael@0: newSort = NHQO.SORT_BY_TAGS_DESCENDING; michael@0: else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING) michael@0: newSort = NHQO.SORT_BY_NONE; michael@0: else michael@0: newSort = NHQO.SORT_BY_TAGS_ASCENDING; michael@0: michael@0: break; michael@0: default: michael@0: throw Cr.NS_ERROR_INVALID_ARG; michael@0: } michael@0: this._result.sortingAnnotation = newSortingAnnotation; michael@0: this._result.sortingMode = newSort; michael@0: }, michael@0: michael@0: isEditable: function PTV_isEditable(aRow, aColumn) { michael@0: // At this point we only support editing the title field. michael@0: if (aColumn.index != 0) michael@0: return false; michael@0: michael@0: // Only bookmark-nodes are editable, and those are never built lazily michael@0: let node = this._rows[aRow]; michael@0: if (!node || node.itemId == -1) michael@0: return false; michael@0: michael@0: // The following items are never editable: michael@0: // * Read-only items. michael@0: // * places-roots michael@0: // * separators michael@0: if (PlacesUtils.nodeIsReadOnly(node) || michael@0: PlacesUtils.nodeIsSeparator(node)) michael@0: return false; michael@0: michael@0: if (PlacesUtils.nodeIsFolder(node)) { michael@0: let itemId = PlacesUtils.getConcreteItemId(node); michael@0: if (PlacesUtils.isRootItem(itemId)) michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: setCellText: function PTV_setCellText(aRow, aColumn, aText) { michael@0: // We may only get here if the cell is editable. michael@0: let node = this._rows[aRow]; michael@0: if (node.title != aText) { michael@0: let txn = new PlacesEditItemTitleTransaction(node.itemId, aText); michael@0: PlacesUtils.transactionManager.doTransaction(txn); michael@0: } michael@0: }, michael@0: michael@0: toggleCutNode: function PTV_toggleCutNode(aNode, aValue) { michael@0: let currentVal = this._cuttingNodes.has(aNode); michael@0: if (currentVal != aValue) { michael@0: if (aValue) michael@0: this._cuttingNodes.add(aNode); michael@0: else michael@0: this._cuttingNodes.delete(aNode); michael@0: michael@0: this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE); michael@0: } michael@0: }, michael@0: michael@0: selectionChanged: function() { }, michael@0: cycleCell: function(aRow, aColumn) { }, michael@0: isSelectable: function(aRow, aColumn) { return false; }, michael@0: performAction: function(aAction) { }, michael@0: performActionOnRow: function(aAction, aRow) { }, michael@0: performActionOnCell: function(aAction, aRow, aColumn) { } michael@0: };