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