browser/components/places/content/treeView.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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 };

mercurial