treeView.js 62 KB

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