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