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