browser/components/places/content/treeView.js

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

mercurial