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