1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
- Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
- const PTV_interfaces = [Ci.nsITreeView,
- Ci.nsINavHistoryResultObserver,
- Ci.nsINavHistoryResultTreeViewer,
- Ci.nsISupportsWeakReference];
- function PlacesTreeView(aFlatList, aOnOpenFlatContainer, aController) {
- this._tree = null;
- this._result = null;
- this._selection = null;
- this._rootNode = null;
- this._rows = [];
- this._flatList = aFlatList;
- this._openContainerCallback = aOnOpenFlatContainer;
- this._controller = aController;
- }
- PlacesTreeView.prototype = {
- get wrappedJSObject() this,
- __dateService: null,
- get _dateService() {
- if (!this.__dateService) {
- this.__dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"].
- getService(Ci.nsIScriptableDateFormat);
- }
- return this.__dateService;
- },
- QueryInterface: XPCOMUtils.generateQI(PTV_interfaces),
- // Bug 761494:
- // ----------
- // Some addons use methods from nsINavHistoryResultObserver and
- // nsINavHistoryResultTreeViewer, without QIing to these interfaces first.
- // That's not a problem when the view is retrieved through the
- // <tree>.view getter (which returns the wrappedJSObject of this object),
- // it raises an issue when the view retrieved through the treeBoxObject.view
- // getter. Thus, to avoid breaking addons, the interfaces are prefetched.
- classInfo: XPCOMUtils.generateCI({ interfaces: PTV_interfaces }),
- /**
- * This is called once both the result and the tree are set.
- */
- _finishInit: function() {
- let selection = this.selection;
- if (selection)
- selection.selectEventsSuppressed = true;
- if (!this._rootNode.containerOpen) {
- // This triggers containerStateChanged which then builds the visible
- // section.
- this._rootNode.containerOpen = true;
- }
- else
- this.invalidateContainer(this._rootNode);
- // "Activate" the sorting column and update commands.
- this.sortingChanged(this._result.sortingMode);
- if (selection)
- selection.selectEventsSuppressed = false;
- },
- /**
- * Plain Container: container result nodes which may never include sub
- * hierarchies.
- *
- * When the rows array is constructed, we don't set the children of plain
- * containers. Instead, we keep placeholders for these children. We then
- * build these children lazily as the tree asks us for information about each
- * row. Luckily, the tree doesn't ask about rows outside the visible area.
- *
- * @see _getNodeForRow and _getRowForNode for the actual magic.
- *
- * @note It's guaranteed that all containers are listed in the rows
- * elements array. It's also guaranteed that separators (if they're not
- * filtered, see below) are listed in the visible elements array, because
- * bookmark folders are never built lazily, as described above.
- *
- * @param aContainer
- * A container result node.
- *
- * @return true if aContainer is a plain container, false otherwise.
- */
- _isPlainContainer: function(aContainer) {
- // Livemarks are always plain containers.
- if (this._controller.hasCachedLivemarkInfo(aContainer))
- return true;
- // We don't know enough about non-query containers.
- if (!(aContainer instanceof Ci.nsINavHistoryQueryResultNode))
- return false;
- switch (aContainer.queryOptions.resultType) {
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY:
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_SITE_QUERY:
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY:
- case Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAG_QUERY:
- return false;
- }
- // If it's a folder, it's not a plain container.
- let nodeType = aContainer.type;
- return nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER &&
- nodeType != Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT;
- },
- /**
- * Gets the row number for a given node. Assumes that the given node is
- * visible (i.e. it's not an obsolete node).
- *
- * @param aNode
- * A result node. Do not pass an obsolete node, or any
- * node which isn't supposed to be in the tree (e.g. separators in
- * sorted trees).
- * @param [optional] aForceBuild
- * @see _isPlainContainer.
- * If true, the row will be computed even if the node still isn't set
- * in our rows array.
- * @param [optional] aParentRow
- * The row of aNode's parent. Ignored for the root node.
- * @param [optional] aNodeIndex
- * The index of aNode in its parent. Only used if aParentRow is
- * set too.
- *
- * @throws if aNode is invisible.
- * @note If aParentRow and aNodeIndex are passed and parent is a plain
- * container, this method will just return a calculated row value, without
- * making assumptions on existence of the node at that position.
- * @return aNode's row if it's in the rows list or if aForceBuild is set, -1
- * otherwise.
- */
- _getRowForNode:
- function(aNode, aForceBuild, aParentRow, aNodeIndex) {
- if (aNode == this._rootNode)
- throw new Error("The root node is never visible");
- // A node is removed form the view either if it has no parent or if its
- // root-ancestor is not the root node (in which case that's the node
- // for which nodeRemoved was called).
- // Tycho: let ancestors = [x for (x of PlacesUtils.nodeAncestors(aNode))];
- let ancestors = [];
- for (let x of PlacesUtils.nodeAncestors(aNode)) {
- ancestors.push(x);
- }
- if (ancestors.length == 0 ||
- ancestors[ancestors.length - 1] != this._rootNode) {
- throw new Error("Removed node passed to _getRowForNode");
- }
- // Ensure that the entire chain is open, otherwise that node is invisible.
- for (let ancestor of ancestors) {
- if (!ancestor.containerOpen)
- throw new Error("Invisible node passed to _getRowForNode");
- }
- // Non-plain containers are initially built with their contents.
- let parent = aNode.parent;
- let parentIsPlain = this._isPlainContainer(parent);
- if (!parentIsPlain) {
- if (parent == this._rootNode)
- return this._rows.indexOf(aNode);
- return this._rows.indexOf(aNode, aParentRow);
- }
- let row = -1;
- let useNodeIndex = typeof(aNodeIndex) == "number";
- if (parent == this._rootNode) {
- if (aNode instanceof Ci.nsINavHistoryResultNode) {
- row = useNodeIndex ? aNodeIndex : this._rootNode.getChildIndex(aNode);
- }
- } else if (useNodeIndex && typeof(aParentRow) == "number") {
- // If we have both the row of the parent node, and the node's index, we
- // can avoid searching the rows array if the parent is a plain container.
- row = aParentRow + aNodeIndex + 1;
- } else {
- // Look for the node in the nodes array. Start the search at the parent
- // row. If the parent row isn't passed, we'll pass undefined to indexOf,
- // which is fine.
- row = this._rows.indexOf(aNode, aParentRow);
- if (row == -1 && aForceBuild) {
- let parentRow = typeof(aParentRow) == "number" ? aParentRow
- : this._getRowForNode(parent);
- row = parentRow + parent.getChildIndex(aNode) + 1;
- }
- }
- if (row != -1)
- this._rows[row] = aNode;
- return row;
- },
- /**
- * Given a row, finds and returns the parent details of the associated node.
- *
- * @param aChildRow
- * Row number.
- * @return [parentNode, parentRow]
- */
- _getParentByChildRow: function(aChildRow) {
- let node = this._getNodeForRow(aChildRow);
- let parent = (node === null) ? this._rootNode : node.parent;
- // The root node is never visible
- if (parent == this._rootNode)
- return [this._rootNode, -1];
- let parentRow = this._rows.lastIndexOf(parent, aChildRow - 1);
- return [parent, parentRow];
- },
- /**
- * Gets the node at a given row.
- */
- _getNodeForRow: function(aRow) {
- if (aRow < 0) {
- return null;
- }
-
- let node = this._rows[aRow];
- if (node !== undefined)
- return node;
- // Find the nearest node.
- let rowNode, row;
- for (let i = aRow - 1; i >= 0 && rowNode === undefined; i--) {
- rowNode = this._rows[i];
- row = i;
- }
- // If there's no container prior to the given row, it's a child of
- // the root node (remember: all containers are listed in the rows array).
- if (!rowNode)
- return this._rows[aRow] = this._rootNode.getChild(aRow);
- // Unset elements may exist only in plain containers. Thus, if the nearest
- // node is a container, it's the row's parent, otherwise, it's a sibling.
- if (rowNode instanceof Ci.nsINavHistoryContainerResultNode)
- return this._rows[aRow] = rowNode.getChild(aRow - row - 1);
- let [parent, parentRow] = this._getParentByChildRow(row);
- return this._rows[aRow] = parent.getChild(aRow - parentRow - 1);
- },
- /**
- * This takes a container and recursively appends our rows array per its
- * contents. Assumes that the rows arrays has no rows for the given
- * container.
- *
- * @param [in] aContainer
- * A container result node.
- * @param [in] aFirstChildRow
- * The first row at which nodes may be inserted to the row array.
- * In other words, that's aContainer's row + 1.
- * @param [out] aToOpen
- * An array of containers to open once the build is done.
- *
- * @return the number of rows which were inserted.
- */
- _buildVisibleSection:
- function(aContainer, aFirstChildRow, aToOpen)
- {
- // There's nothing to do if the container is closed.
- if (!aContainer.containerOpen)
- return 0;
- // Inserting the new elements into the rows array in one shot (by
- // Array.concat) is faster than resizing the array (by splice) on each loop
- // iteration.
- let cc = aContainer.childCount;
- let newElements = new Array(cc);
- this._rows = this._rows.splice(0, aFirstChildRow)
- .concat(newElements, this._rows);
- if (this._isPlainContainer(aContainer))
- return cc;
- const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open");
- const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true");
- let sortingMode = this._result.sortingMode;
- let rowsInserted = 0;
- for (let i = 0; i < cc; i++) {
- let curChild = aContainer.getChild(i);
- let curChildType = curChild.type;
- let row = aFirstChildRow + rowsInserted;
- // Don't display separators when sorted.
- if (curChildType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
- if (sortingMode != Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
- // Remove the element for the filtered separator.
- // Notice that the rows array was initially resized to include all
- // children.
- this._rows.splice(row, 1);
- continue;
- }
- }
- this._rows[row] = curChild;
- rowsInserted++;
- // Recursively do containers.
- if (!this._flatList &&
- curChild instanceof Ci.nsINavHistoryContainerResultNode &&
- !this._controller.hasCachedLivemarkInfo(curChild)) {
- let resource = this._getResourceForNode(curChild);
- let isopen = resource != null &&
- PlacesUIUtils.localStore.HasAssertion(resource,
- openLiteral,
- trueLiteral, true);
- if (isopen != curChild.containerOpen)
- aToOpen.push(curChild);
- else if (curChild.containerOpen && curChild.childCount > 0)
- rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen);
- }
- }
- return rowsInserted;
- },
- /**
- * This counts how many rows a node takes in the tree. For containers it
- * will count the node itself plus any child node following it.
- */
- _countVisibleRowsForNodeAtRow:
- function(aNodeRow) {
- let node = this._rows[aNodeRow];
- // If it's not listed yet, we know that it's a leaf node (instanceof also
- // null-checks).
- if (!(node instanceof Ci.nsINavHistoryContainerResultNode))
- return 1;
- let outerLevel = node.indentLevel;
- for (let i = aNodeRow + 1; i < this._rows.length; i++) {
- let rowNode = this._rows[i];
- if (rowNode && rowNode.indentLevel <= outerLevel)
- return i - aNodeRow;
- }
- // This node plus its children take up the bottom of the list.
- return this._rows.length - aNodeRow;
- },
- _getSelectedNodesInRange:
- function(aFirstRow, aLastRow) {
- let selection = this.selection;
- let rc = selection.getRangeCount();
- if (rc == 0)
- return [];
- // The visible-area borders are needed for checking whether a
- // selected row is also visible.
- let firstVisibleRow = this._tree.getFirstVisibleRow();
- let lastVisibleRow = this._tree.getLastVisibleRow();
- let nodesInfo = [];
- for (let rangeIndex = 0; rangeIndex < rc; rangeIndex++) {
- let min = { }, max = { };
- selection.getRangeAt(rangeIndex, min, max);
- // If this range does not overlap the replaced chunk, we don't need to
- // persist the selection.
- if (max.value < aFirstRow || min.value > aLastRow)
- continue;
- let firstRow = Math.max(min.value, aFirstRow);
- let lastRow = Math.min(max.value, aLastRow);
- for (let i = firstRow; i <= lastRow; i++) {
- nodesInfo.push({
- node: this._rows[i],
- oldRow: i,
- wasVisible: i >= firstVisibleRow && i <= lastVisibleRow
- });
- }
- }
- return nodesInfo;
- },
- /**
- * Tries to find an equivalent node for a node which was removed. We first
- * look for the original node, in case it was just relocated. Then, if we
- * that node was not found, we look for a node that has the same itemId, uri
- * and time values.
- *
- * @param aUpdatedContainer
- * An ancestor of the node which was removed. It does not have to be
- * its direct parent.
- * @param aOldNode
- * The node which was removed.
- *
- * @return the row number of an equivalent node for aOldOne, if one was
- * found, -1 otherwise.
- */
- _getNewRowForRemovedNode:
- function(aUpdatedContainer, aOldNode) {
- if (aOldNode == undefined) {
- return -1;
- }
- let parent = aOldNode.parent;
- if (parent) {
- // If the node's parent is still set, the node is not obsolete
- // and we should just find out its new position.
- // However, if any of the node's ancestor is closed, the node is
- // invisible.
- let ancestors = PlacesUtils.nodeAncestors(aOldNode);
- for (let ancestor of ancestors) {
- if (!ancestor.containerOpen)
- return -1;
- }
- return this._getRowForNode(aOldNode, true);
- }
- // There's a broken edge case here.
- // If a visit appears in two queries, and the second one was
- // the old node, we'll select the first one after refresh. There's
- // nothing we could do about that, because aOldNode.parent is
- // gone by the time invalidateContainer is called.
- let newNode = aUpdatedContainer.findNodeByDetails(aOldNode.uri,
- aOldNode.time,
- aOldNode.itemId,
- true);
- if (!newNode)
- return -1;
- return this._getRowForNode(newNode, true);
- },
- /**
- * Restores a given selection state as near as possible to the original
- * selection state.
- *
- * @param aNodesInfo
- * The persisted selection state as returned by
- * _getSelectedNodesInRange.
- * @param aUpdatedContainer
- * The container which was updated.
- */
- _restoreSelection:
- function(aNodesInfo, aUpdatedContainer) {
- if (aNodesInfo.length == 0)
- return;
- let selection = this.selection;
- // Attempt to ensure that previously-visible selection will be visible
- // if it's re-selected. However, we can only ensure that for one row.
- let scrollToRow = -1;
- for (let i = 0; i < aNodesInfo.length; i++) {
- let nodeInfo = aNodesInfo[i];
- let row = this._getNewRowForRemovedNode(aUpdatedContainer,
- nodeInfo.node);
- // Select the found node, if any.
- if (row != -1) {
- selection.rangedSelect(row, row, true);
- if (nodeInfo.wasVisible && scrollToRow == -1)
- scrollToRow = row;
- }
- }
- // If only one node was previously selected and there's no selection now,
- // select the node at its old row, if any.
- if (aNodesInfo.length == 1 && selection.count == 0) {
- let row = Math.min(aNodesInfo[0].oldRow, this._rows.length - 1);
- if (row != -1) {
- selection.rangedSelect(row, row, true);
- if (aNodesInfo[0].wasVisible && scrollToRow == -1)
- scrollToRow = aNodesInfo[0].oldRow;
- }
- }
- if (scrollToRow != -1)
- this._tree.ensureRowIsVisible(scrollToRow);
- },
- _convertPRTimeToString: function(aTime) {
- const MS_PER_MINUTE = 60000;
- const MS_PER_DAY = 86400000;
- let timeMs = aTime / 1000; // PRTime is in microseconds
- // Date is calculated starting from midnight, so the modulo with a day are
- // milliseconds from today's midnight.
- // getTimezoneOffset corrects that based on local time, notice midnight
- // can have a different offset during DST-change days.
- let dateObj = new Date();
- let now = dateObj.getTime() - dateObj.getTimezoneOffset() * MS_PER_MINUTE;
- let midnight = now - (now % MS_PER_DAY);
- midnight += new Date(midnight).getTimezoneOffset() * MS_PER_MINUTE;
- let dateFormat = timeMs >= midnight ?
- Ci.nsIScriptableDateFormat.dateFormatNone :
- Ci.nsIScriptableDateFormat.dateFormatShort;
- let timeObj = new Date(timeMs);
- return (this._dateService.FormatDateTime("", dateFormat,
- Ci.nsIScriptableDateFormat.timeFormatNoSeconds,
- timeObj.getFullYear(), timeObj.getMonth() + 1,
- timeObj.getDate(), timeObj.getHours(),
- timeObj.getMinutes(), timeObj.getSeconds()));
- },
- COLUMN_TYPE_UNKNOWN: 0,
- COLUMN_TYPE_TITLE: 1,
- COLUMN_TYPE_URI: 2,
- COLUMN_TYPE_DATE: 3,
- COLUMN_TYPE_VISITCOUNT: 4,
- COLUMN_TYPE_KEYWORD: 5,
- COLUMN_TYPE_DESCRIPTION: 6,
- COLUMN_TYPE_DATEADDED: 7,
- COLUMN_TYPE_LASTMODIFIED: 8,
- COLUMN_TYPE_TAGS: 9,
- COLUMN_TYPE_PARENTFOLDER: 10,
- COLUMN_TYPE_PARENTFOLDERPATH: 11,
- _getColumnType: function(aColumn) {
- let columnType = aColumn.element.getAttribute("anonid") || aColumn.id;
- switch (columnType) {
- case "title":
- return this.COLUMN_TYPE_TITLE;
- case "url":
- return this.COLUMN_TYPE_URI;
- case "date":
- return this.COLUMN_TYPE_DATE;
- case "visitCount":
- return this.COLUMN_TYPE_VISITCOUNT;
- case "keyword":
- return this.COLUMN_TYPE_KEYWORD;
- case "description":
- return this.COLUMN_TYPE_DESCRIPTION;
- case "dateAdded":
- return this.COLUMN_TYPE_DATEADDED;
- case "lastModified":
- return this.COLUMN_TYPE_LASTMODIFIED;
- case "tags":
- return this.COLUMN_TYPE_TAGS;
- case "parentFolder":
- return this.COLUMN_TYPE_PARENTFOLDER;
- case "parentFolderPath":
- return this.COLUMN_TYPE_PARENTFOLDERPATH;
- }
- return this.COLUMN_TYPE_UNKNOWN;
- },
- _sortTypeToColumnType: function(aSortType) {
- switch (aSortType) {
- case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING:
- return [this.COLUMN_TYPE_TITLE, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING:
- return [this.COLUMN_TYPE_TITLE, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING:
- return [this.COLUMN_TYPE_DATE, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING:
- return [this.COLUMN_TYPE_DATE, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_ASCENDING:
- return [this.COLUMN_TYPE_URI, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_URI_DESCENDING:
- return [this.COLUMN_TYPE_URI, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_ASCENDING:
- return [this.COLUMN_TYPE_VISITCOUNT, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING:
- return [this.COLUMN_TYPE_VISITCOUNT, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_ASCENDING:
- return [this.COLUMN_TYPE_KEYWORD, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_KEYWORD_DESCENDING:
- return [this.COLUMN_TYPE_KEYWORD, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_ASCENDING:
- if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
- return [this.COLUMN_TYPE_DESCRIPTION, false];
- break;
- case Ci.nsINavHistoryQueryOptions.SORT_BY_ANNOTATION_DESCENDING:
- if (this._result.sortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
- return [this.COLUMN_TYPE_DESCRIPTION, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_ASCENDING:
- return [this.COLUMN_TYPE_DATEADDED, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING:
- return [this.COLUMN_TYPE_DATEADDED, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_ASCENDING:
- return [this.COLUMN_TYPE_LASTMODIFIED, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING:
- return [this.COLUMN_TYPE_LASTMODIFIED, true];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_ASCENDING:
- return [this.COLUMN_TYPE_TAGS, false];
- case Ci.nsINavHistoryQueryOptions.SORT_BY_TAGS_DESCENDING:
- return [this.COLUMN_TYPE_TAGS, true];
- }
- return [this.COLUMN_TYPE_UNKNOWN, false];
- },
- // nsINavHistoryResultObserver
- nodeInserted: function(aParentNode, aNode, aNewIndex) {
- NS_ASSERT(this._result, "Got a notification but have no result!");
- if (!this._tree || !this._result)
- return;
- // Bail out for hidden separators.
- if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
- return;
- let parentRow;
- if (aParentNode != this._rootNode) {
- parentRow = this._getRowForNode(aParentNode);
- // Update parent when inserting the first item, since twisty has changed.
- if (aParentNode.childCount == 1)
- this._tree.invalidateRow(parentRow);
- }
- // Compute the new row number of the node.
- let row = -1;
- let cc = aParentNode.childCount;
- if (aNewIndex == 0 || this._isPlainContainer(aParentNode) || cc == 0) {
- // We don't need to worry about sub hierarchies of the parent node
- // if it's a plain container, or if the new node is its first child.
- if (aParentNode == this._rootNode)
- row = aNewIndex;
- else
- row = parentRow + aNewIndex + 1;
- }
- else {
- // Here, we try to find the next visible element in the child list so we
- // can set the new visible index to be right before that. Note that we
- // have to search down instead of up, because some siblings could have
- // children themselves that would be in the way.
- let separatorsAreHidden = PlacesUtils.nodeIsSeparator(aNode) &&
- this.isSorted();
- for (let i = aNewIndex + 1; i < cc; i++) {
- let node = aParentNode.getChild(i);
- if (!separatorsAreHidden || PlacesUtils.nodeIsSeparator(node)) {
- // The children have not been shifted so the next item will have what
- // should be our index.
- row = this._getRowForNode(node, false, parentRow, i);
- break;
- }
- }
- if (row < 0) {
- // At the end of the child list without finding a visible sibling. This
- // is a little harder because we don't know how many rows the last item
- // in our list takes up (it could be a container with many children).
- let prevChild = aParentNode.getChild(aNewIndex - 1);
- let prevIndex = this._getRowForNode(prevChild, false, parentRow,
- aNewIndex - 1);
- row = prevIndex + this._countVisibleRowsForNodeAtRow(prevIndex);
- }
- }
- this._rows.splice(row, 0, aNode);
- this._tree.rowCountChanged(row, 1);
- if (PlacesUtils.nodeIsContainer(aNode) &&
- PlacesUtils.asContainer(aNode).containerOpen) {
- this.invalidateContainer(aNode);
- }
- },
- /**
- * THIS FUNCTION DOES NOT HANDLE cases where a collapsed node is being
- * removed but the node it is collapsed with is not being removed (this then
- * just swap out the removee with its collapsing partner). The only time
- * when we really remove things is when deleting URIs, which will apply to
- * all collapsees. This function is called sometimes when resorting items.
- * However, we won't do this when sorted by date because dates will never
- * change for visits, and date sorting is the only time things are collapsed.
- */
- nodeRemoved: function(aParentNode, aNode, aOldIndex) {
- NS_ASSERT(this._result, "Got a notification but have no result!");
- if (!this._tree || !this._result)
- return;
- // XXX bug 517701: We don't know what to do when the root node is removed.
- if (aNode == this._rootNode)
- throw Cr.NS_ERROR_NOT_IMPLEMENTED;
- // Bail out for hidden separators.
- if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
- return;
- let parentRow = aParentNode == this._rootNode ?
- undefined : this._getRowForNode(aParentNode, true);
- let oldRow = this._getRowForNode(aNode, true, parentRow, aOldIndex);
- if (oldRow < 0)
- throw Cr.NS_ERROR_UNEXPECTED;
- // If the node was exclusively selected, the node next to it will be
- // selected.
- let selectNext = false;
- let selection = this.selection;
- if (selection.getRangeCount() == 1) {
- let min = { }, max = { };
- selection.getRangeAt(0, min, max);
- if (min.value == max.value &&
- this.nodeForTreeIndex(min.value) == aNode)
- selectNext = true;
- }
- // Remove the node and its children, if any.
- let count = this._countVisibleRowsForNodeAtRow(oldRow);
- this._rows.splice(oldRow, count);
- this._tree.rowCountChanged(oldRow, -count);
- // Redraw the parent if its twisty state has changed.
- if (aParentNode != this._rootNode && !aParentNode.hasChildren) {
- let parentRow = oldRow - 1;
- this._tree.invalidateRow(parentRow);
- }
- // Restore selection if the node was exclusively selected.
- if (!selectNext)
- return;
- // Restore selection.
- let rowToSelect = Math.min(oldRow, this._rows.length - 1);
- if (rowToSelect != -1)
- this.selection.rangedSelect(rowToSelect, rowToSelect, true);
- },
- nodeMoved:
- function(aNode, aOldParent, aOldIndex, aNewParent, aNewIndex) {
- NS_ASSERT(this._result, "Got a notification but have no result!");
- if (!this._tree || !this._result)
- return;
- // Bail out for hidden separators.
- if (PlacesUtils.nodeIsSeparator(aNode) && this.isSorted())
- return;
- // Note that at this point the node has already been moved by the backend,
- // so we must give hints to _getRowForNode to get the old row position.
- let oldParentRow = aOldParent == this._rootNode ?
- undefined : this._getRowForNode(aOldParent, true);
- let oldRow = this._getRowForNode(aNode, true, oldParentRow, aOldIndex);
- if (oldRow < 0)
- throw Cr.NS_ERROR_UNEXPECTED;
- // If this node is a container it could take up more than one row.
- let count = this._countVisibleRowsForNodeAtRow(oldRow);
- // Persist selection state.
- let nodesToReselect =
- this._getSelectedNodesInRange(oldRow, oldRow + count);
- if (nodesToReselect.length > 0)
- this.selection.selectEventsSuppressed = true;
- // Redraw the parent if its twisty state has changed.
- if (aOldParent != this._rootNode && !aOldParent.hasChildren) {
- let parentRow = oldRow - 1;
- this._tree.invalidateRow(parentRow);
- }
- // Remove node and its children, if any, from the old position.
- this._rows.splice(oldRow, count);
- this._tree.rowCountChanged(oldRow, -count);
- // Insert the node into the new position.
- this.nodeInserted(aNewParent, aNode, aNewIndex);
- // Restore selection.
- if (nodesToReselect.length > 0) {
- this._restoreSelection(nodesToReselect, aNewParent);
- this.selection.selectEventsSuppressed = false;
- }
- },
- _invalidateCellValue: function(aNode,
- aColumnType) {
- NS_ASSERT(this._result, "Got a notification but have no result!");
- if (!this._tree || !this._result)
- return;
- // Nothing to do for the root node.
- if (aNode == this._rootNode)
- return;
- let row = this._getRowForNode(aNode);
- if (row == -1)
- return;
- let column = this._findColumnByType(aColumnType);
- if (column && !column.element.hidden)
- this._tree.invalidateCell(row, column);
- // Last modified time is altered for almost all node changes.
- if (aColumnType != this.COLUMN_TYPE_LASTMODIFIED) {
- let lastModifiedColumn =
- this._findColumnByType(this.COLUMN_TYPE_LASTMODIFIED);
- if (lastModifiedColumn && !lastModifiedColumn.hidden)
- this._tree.invalidateCell(row, lastModifiedColumn);
- }
- },
- _populateLivemarkContainer: function(aNode) {
- PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
- .then(aLivemark => {
- let placesNode = aNode;
- // Need to check containerOpen since getLivemark is async.
- if (!placesNode.containerOpen)
- return;
- let children = aLivemark.getNodesForContainer(placesNode);
- for (let i = 0; i < children.length; i++) {
- let child = children[i];
- this.nodeInserted(placesNode, child, i);
- }
- }, Components.utils.reportError);
- },
- nodeTitleChanged: function(aNode, aNewTitle) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
- },
- nodeURIChanged: function(aNode, aNewURI) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_URI);
- },
- nodeIconChanged: function(aNode) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
- },
- nodeHistoryDetailsChanged:
- function(aNode, aUpdatedVisitDate,
- aUpdatedVisitCount) {
- if (aNode.parent && this._controller.hasCachedLivemarkInfo(aNode.parent)) {
- // Find the node in the parent.
- let parentRow = this._flatList ? 0 : this._getRowForNode(aNode.parent);
- for (let i = parentRow; i < this._rows.length; i++) {
- let child = this.nodeForTreeIndex(i);
- if (child.uri == aNode.uri) {
- this._cellProperties.delete(child);
- this._invalidateCellValue(child, this.COLUMN_TYPE_TITLE);
- break;
- }
- }
- return;
- }
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATE);
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_VISITCOUNT);
- },
- nodeTagsChanged: function(aNode) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_TAGS);
- },
- nodeKeywordChanged: function(aNode, aNewKeyword) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_KEYWORD);
- },
- nodeAnnotationChanged: function(aNode, aAnno) {
- if (aAnno == PlacesUIUtils.DESCRIPTION_ANNO) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_DESCRIPTION);
- }
- else if (aAnno == PlacesUtils.LMANNO_FEEDURI) {
- PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
- .then(aLivemark => {
- this._controller.cacheLivemarkInfo(aNode, aLivemark);
- let properties = this._cellProperties.get(aNode);
- this._cellProperties.set(aNode, properties += " livemark");
- // The livemark attribute is set as a cell property on the title cell.
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
- }, Components.utils.reportError);
- }
- },
- nodeDateAddedChanged: function(aNode, aNewValue) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_DATEADDED);
- },
- nodeLastModifiedChanged:
- function(aNode, aNewValue) {
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_LASTMODIFIED);
- },
- containerStateChanged:
- function(aNode, aOldState, aNewState) {
- this.invalidateContainer(aNode);
- if (PlacesUtils.nodeIsFolder(aNode) ||
- (this._flatList && aNode == this._rootNode)) {
- let queryOptions = PlacesUtils.asQuery(this._rootNode).queryOptions;
- if (queryOptions.excludeItems) {
- return;
- }
- if (aNode.itemId != -1) { // run when there's a valid node id
- PlacesUtils.livemarks.getLivemark({ id: aNode.itemId })
- .then(aLivemark => {
- let shouldInvalidate =
- !this._controller.hasCachedLivemarkInfo(aNode);
- this._controller.cacheLivemarkInfo(aNode, aLivemark);
- if (aNewState == Components.interfaces.nsINavHistoryContainerResultNode.STATE_OPENED) {
- aLivemark.registerForUpdates(aNode, this);
- // Prioritize the current livemark.
- aLivemark.reload();
- PlacesUtils.livemarks.reloadLivemarks();
- if (shouldInvalidate)
- this.invalidateContainer(aNode);
- }
- else {
- aLivemark.unregisterForUpdates(aNode);
- }
- }, () => undefined);
- }
- }
- },
- invalidateContainer: function(aContainer) {
- NS_ASSERT(this._result, "Need to have a result to update");
- if (!this._tree)
- return;
- let startReplacement, replaceCount;
- if (aContainer == this._rootNode) {
- startReplacement = 0;
- replaceCount = this._rows.length;
- // If the root node is now closed, the tree is empty.
- if (!this._rootNode.containerOpen) {
- this._rows = [];
- if (replaceCount)
- this._tree.rowCountChanged(startReplacement, -replaceCount);
- return;
- }
- }
- else {
- // Update the twisty state.
- let row = this._getRowForNode(aContainer);
- this._tree.invalidateRow(row);
- // We don't replace the container node itself, so we should decrease the
- // replaceCount by 1.
- startReplacement = row + 1;
- replaceCount = this._countVisibleRowsForNodeAtRow(row) - 1;
- }
- // Persist selection state.
- let nodesToReselect =
- this._getSelectedNodesInRange(startReplacement,
- startReplacement + replaceCount);
- // Now update the number of elements.
- this.selection.selectEventsSuppressed = true;
- // First remove the old elements
- this._rows.splice(startReplacement, replaceCount);
- // If the container is now closed, we're done.
- if (!aContainer.containerOpen) {
- let oldSelectionCount = this.selection.count;
- if (replaceCount)
- this._tree.rowCountChanged(startReplacement, -replaceCount);
- // Select the row next to the closed container if any of its
- // children were selected, and nothing else is selected.
- if (nodesToReselect.length > 0 &&
- nodesToReselect.length == oldSelectionCount) {
- this.selection.rangedSelect(startReplacement, startReplacement, true);
- this._tree.ensureRowIsVisible(startReplacement);
- }
- this.selection.selectEventsSuppressed = false;
- return;
- }
- // Otherwise, start a batch first.
- this._tree.beginUpdateBatch();
- if (replaceCount)
- this._tree.rowCountChanged(startReplacement, -replaceCount);
- let toOpenElements = [];
- let elementsAddedCount = this._buildVisibleSection(aContainer,
- startReplacement,
- toOpenElements);
- if (elementsAddedCount)
- this._tree.rowCountChanged(startReplacement, elementsAddedCount);
- if (!this._flatList) {
- // Now, open any containers that were persisted.
- for (let i = 0; i < toOpenElements.length; i++) {
- let item = toOpenElements[i];
- let parent = item.parent;
- // Avoid recursively opening containers.
- while (parent) {
- if (parent.uri == item.uri)
- break;
- parent = parent.parent;
- }
- // If we don't have a parent, we made it all the way to the root
- // and didn't find a match, so we can open our item.
- if (!parent && !item.containerOpen)
- item.containerOpen = true;
- }
- }
- if (this._controller.hasCachedLivemarkInfo(aContainer)) {
- let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
- if (!queryOptions.excludeItems) {
- this._populateLivemarkContainer(aContainer);
- }
- }
- this._tree.endUpdateBatch();
- // Restore selection.
- this._restoreSelection(nodesToReselect, aContainer);
- this.selection.selectEventsSuppressed = false;
- },
- _columns: [],
- _findColumnByType: function(aColumnType) {
- if (this._columns[aColumnType])
- return this._columns[aColumnType];
- let columns = this._tree.columns;
- let colCount = columns.count;
- for (let i = 0; i < colCount; i++) {
- let column = columns.getColumnAt(i);
- let columnType = this._getColumnType(column);
- this._columns[columnType] = column;
- if (columnType == aColumnType)
- return column;
- }
- // That's completely valid. Most of our trees actually include just the
- // title column.
- return null;
- },
- sortingChanged: function(aSortingMode) {
- if (!this._tree || !this._result)
- return;
- // Depending on the sort mode, certain commands may be disabled.
- window.updateCommands("sort");
- let columns = this._tree.columns;
- // Clear old sorting indicator.
- let sortedColumn = columns.getSortedColumn();
- if (sortedColumn)
- sortedColumn.element.removeAttribute("sortDirection");
- // Set new sorting indicator by looking through all columns for ours.
- if (aSortingMode == Ci.nsINavHistoryQueryOptions.SORT_BY_NONE)
- return;
- let [desiredColumn, desiredIsDescending] =
- this._sortTypeToColumnType(aSortingMode);
- let colCount = columns.count;
- let column = this._findColumnByType(desiredColumn);
- if (column) {
- let sortDir = desiredIsDescending ? "descending" : "ascending";
- column.element.setAttribute("sortDirection", sortDir);
- }
- },
- _inBatchMode: false,
- batching: function(aToggleMode) {
- if (this._inBatchMode != aToggleMode) {
- this._inBatchMode = this.selection.selectEventsSuppressed = aToggleMode;
- if (this._inBatchMode) {
- this._tree.beginUpdateBatch();
- }
- else {
- this._tree.endUpdateBatch();
- }
- }
- },
- get result() this._result,
- set result(val) {
- if (this._result) {
- this._result.removeObserver(this);
- this._rootNode.containerOpen = false;
- }
- if (val) {
- this._result = val;
- this._rootNode = this._result.root;
- this._cellProperties = new Map();
- this._cuttingNodes = new Set();
- }
- else if (this._result) {
- delete this._result;
- delete this._rootNode;
- delete this._cellProperties;
- delete this._cuttingNodes;
- }
- // If the tree is not set yet, setTree will call finishInit.
- if (this._tree && val)
- this._finishInit();
- return val;
- },
- nodeForTreeIndex: function(aIndex) {
- if (aIndex > this._rows.length)
- throw Cr.NS_ERROR_INVALID_ARG;
- return this._getNodeForRow(aIndex);
- },
- treeIndexForNode: function(aNode) {
- // The API allows passing invisible nodes.
- try {
- return this._getRowForNode(aNode, true);
- }
- catch(ex) { }
- return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE;
- },
- _getResourceForNode: function(aNode)
- {
- let uri = aNode.uri;
- NS_ASSERT(uri, "if there is no uri, we can't persist the open state");
- return uri ? PlacesUIUtils.RDF.GetResource(uri) : null;
- },
- // nsITreeView
- get rowCount() this._rows.length,
- get selection() this._selection,
- set selection(val) this._selection = val,
- getRowProperties: function() { return ""; },
- getCellProperties:
- function(aRow, aColumn) {
- // for anonid-trees, we need to add the column-type manually
- var props = "";
- let columnType = aColumn.element.getAttribute("anonid");
- if (columnType)
- props += columnType;
- else
- columnType = aColumn.id;
- // Set the "ltr" property on url cells
- if (columnType == "url")
- props += " ltr";
- if (columnType != "title")
- return props;
- let node = this._getNodeForRow(aRow);
- if (this._cuttingNodes.has(node)) {
- props += " cutting";
- }
- let properties = this._cellProperties.get(node);
- if (properties === undefined) {
- properties = "";
- let itemId = node.itemId;
- let nodeType = node.type;
- if (PlacesUtils.containerTypes.indexOf(nodeType) != -1) {
- if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
- properties += " query";
- if (PlacesUtils.nodeIsTagQuery(node))
- properties += " tagContainer";
- else if (PlacesUtils.nodeIsDay(node))
- properties += " dayContainer";
- else if (PlacesUtils.nodeIsHost(node))
- properties += " hostContainer";
- }
- else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER ||
- nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT) {
- if (this._controller.hasCachedLivemarkInfo(node)) {
- properties += " livemark";
- }
- else {
- PlacesUtils.livemarks.getLivemark({ id: node.itemId })
- .then(aLivemark => {
- this._controller.cacheLivemarkInfo(node, aLivemark);
- let props = this._cellProperties.get(node);
- this._cellProperties.set(node, props += " livemark");
- // The livemark attribute is set as a cell property on the title cell.
- this._invalidateCellValue(node, this.COLUMN_TYPE_TITLE);
- }, () => undefined);
- }
- }
- if (itemId != -1) {
- let queryName = PlacesUIUtils.getLeftPaneQueryNameFromId(itemId);
- if (queryName)
- properties += " OrganizerQuery_" + queryName;
- }
- }
- else if (nodeType == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR)
- properties += " separator";
- else if (PlacesUtils.nodeIsURI(node)) {
- properties += " " + PlacesUIUtils.guessUrlSchemeForUI(node.uri);
- if (this._controller.hasCachedLivemarkInfo(node.parent)) {
- properties += " livemarkItem";
- if (node.accessCount) {
- properties += " visited";
- }
- }
- }
- this._cellProperties.set(node, properties);
- }
- return props + " " + properties;
- },
- getColumnProperties: function(aColumn) { return ""; },
- isContainer: function(aRow) {
- // Only leaf nodes aren't listed in the rows array.
- let node = this._rows[aRow];
- if (node === undefined)
- return false;
- if (PlacesUtils.nodeIsContainer(node)) {
- // Flat-lists may ignore expandQueries and other query options when
- // they are asked to open a container.
- if (this._flatList)
- return true;
- // treat non-expandable childless queries as non-containers
- if (PlacesUtils.nodeIsQuery(node)) {
- let parent = node.parent;
- if ((PlacesUtils.nodeIsQuery(parent) ||
- PlacesUtils.nodeIsFolder(parent)) &&
- !PlacesUtils.asQuery(node).hasChildren)
- return PlacesUtils.asQuery(parent).queryOptions.expandQueries;
- }
- return true;
- }
- return false;
- },
- isContainerOpen: function(aRow) {
- if (this._flatList)
- return false;
- // All containers are listed in the rows array.
- return this._rows[aRow].containerOpen;
- },
- isContainerEmpty: function(aRow) {
- if (this._flatList)
- return true;
- let node = this._rows[aRow];
- if (this._controller.hasCachedLivemarkInfo(node)) {
- let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
- return queryOptions.excludeItems;
- }
- // All containers are listed in the rows array.
- return !node.hasChildren;
- },
- isSeparator: function(aRow) {
- // All separators are listed in the rows array.
- let node = this._rows[aRow];
- return node && PlacesUtils.nodeIsSeparator(node);
- },
- isSorted: function() {
- return this._result.sortingMode !=
- Ci.nsINavHistoryQueryOptions.SORT_BY_NONE;
- },
- canDrop: function(aRow, aOrientation, aDataTransfer) {
- if (!this._result)
- throw Cr.NS_ERROR_UNEXPECTED;
- // Drop position into a sorted treeview would be wrong.
- if (this.isSorted())
- return false;
- let ip = this._getInsertionPoint(aRow, aOrientation);
- return ip && PlacesControllerDragHelper.canDrop(ip, aDataTransfer);
- },
- _getInsertionPoint: function(index, orientation) {
- let container = this._result.root;
- let dropNearItemId = -1;
- // When there's no selection, assume the container is the container
- // the view is populated from (i.e. the result's itemId).
- if (index != -1) {
- let lastSelected = this.nodeForTreeIndex(index);
- if (this.isContainer(index) && orientation == Ci.nsITreeView.DROP_ON) {
- // If the last selected item is an open container, append _into_
- // it, rather than insert adjacent to it.
- container = lastSelected;
- index = -1;
- }
- else if (lastSelected.containerOpen &&
- orientation == Ci.nsITreeView.DROP_AFTER &&
- lastSelected.hasChildren) {
- // If the last selected node is an open container and the user is
- // trying to drag into it as a first node, really insert into it.
- container = lastSelected;
- orientation = Ci.nsITreeView.DROP_ON;
- index = 0;
- }
- else {
- // Use the last-selected node's container.
- container = lastSelected.parent;
- // During its Drag & Drop operation, the tree code closes-and-opens
- // containers very often (part of the XUL "spring-loaded folders"
- // implementation). And in certain cases, we may reach a closed
- // container here. However, we can simply bail out when this happens,
- // because we would then be back here in less than a millisecond, when
- // the container had been reopened.
- if (!container || !container.containerOpen)
- return null;
- // Avoid the potentially expensive call to getChildIndex
- // if we know this container doesn't allow insertion.
- if (PlacesControllerDragHelper.disallowInsertion(container))
- return null;
- let queryOptions = PlacesUtils.asQuery(this._result.root).queryOptions;
- if (queryOptions.sortingMode !=
- Ci.nsINavHistoryQueryOptions.SORT_BY_NONE) {
- // If we are within a sorted view, insert at the end.
- index = -1;
- }
- else if (queryOptions.excludeItems ||
- queryOptions.excludeQueries ||
- queryOptions.excludeReadOnlyFolders) {
- // Some item may be invisible, insert near last selected one.
- // We don't replace index here to avoid requests to the db,
- // instead it will be calculated later by the controller.
- index = -1;
- dropNearItemId = lastSelected.itemId;
- }
- else {
- let lsi = container.getChildIndex(lastSelected);
- index = orientation == Ci.nsITreeView.DROP_BEFORE ? lsi : lsi + 1;
- }
- }
- }
- if (PlacesControllerDragHelper.disallowInsertion(container))
- return null;
- return new InsertionPoint(PlacesUtils.getConcreteItemId(container),
- index, orientation,
- PlacesUtils.nodeIsTagQuery(container),
- dropNearItemId);
- },
- drop: function(aRow, aOrientation, aDataTransfer) {
- // We are responsible for translating the |index| and |orientation|
- // parameters into a container id and index within the container,
- // since this information is specific to the tree view.
- let ip = this._getInsertionPoint(aRow, aOrientation);
- if (ip)
- PlacesControllerDragHelper.onDrop(ip, aDataTransfer);
- PlacesControllerDragHelper.currentDropTarget = null;
- },
- getParentIndex: function(aRow) {
- let [parentNode, parentRow] = this._getParentByChildRow(aRow);
- return parentRow;
- },
- hasNextSibling: function(aRow, aAfterIndex) {
- if (aRow == this._rows.length - 1) {
- // The last row has no sibling.
- return false;
- }
- let node = this._rows[aRow];
- if (node === undefined || this._isPlainContainer(node.parent)) {
- // The node is a child of a plain container.
- // If the next row is either unset or has the same parent,
- // it's a sibling.
- let nextNode = this._rows[aRow + 1];
- return (nextNode == undefined || nextNode.parent == node.parent);
- }
- let thisLevel = node.indentLevel;
- for (let i = aAfterIndex + 1; i < this._rows.length; ++i) {
- let rowNode = this._getNodeForRow(i);
- let nextLevel = rowNode.indentLevel;
- if (nextLevel == thisLevel)
- return true;
- if (nextLevel < thisLevel)
- break;
- }
- return false;
- },
- getLevel: function(aRow) this._getNodeForRow(aRow).indentLevel,
- getImageSrc: function(aRow, aColumn) {
- // Only the title column has an image.
- if (this._getColumnType(aColumn) != this.COLUMN_TYPE_TITLE)
- return "";
- return this._getNodeForRow(aRow).icon;
- },
- getProgressMode: function(aRow, aColumn) { },
- getCellValue: function(aRow, aColumn) { },
- getCellText: function(aRow, aColumn) {
- let node = this._getNodeForRow(aRow);
- switch (this._getColumnType(aColumn)) {
- case this.COLUMN_TYPE_TITLE:
- // normally, this is just the title, but we don't want empty items in
- // the tree view so return a special string if the title is empty.
- // Do it here so that callers can still get at the 0 length title
- // if they go through the "result" API.
- if (PlacesUtils.nodeIsSeparator(node))
- return "";
- return PlacesUIUtils.getBestTitle(node, true);
- case this.COLUMN_TYPE_TAGS:
- return node.tags;
- case this.COLUMN_TYPE_URI:
- if (PlacesUtils.nodeIsURI(node))
- return node.uri;
- return "";
- case this.COLUMN_TYPE_DATE:
- let nodeTime = node.time;
- if (nodeTime == 0 || !PlacesUtils.nodeIsURI(node)) {
- // hosts and days shouldn't have a value for the date column.
- // Actually, you could argue this point, but looking at the
- // results, seeing the most recently visited date is not what
- // I expect, and gives me no information I know how to use.
- // Only show this for URI-based items.
- return "";
- }
- return this._convertPRTimeToString(nodeTime);
- case this.COLUMN_TYPE_VISITCOUNT:
- return node.accessCount;
- case this.COLUMN_TYPE_KEYWORD:
- if (PlacesUtils.nodeIsBookmark(node))
- return PlacesUtils.bookmarks.getKeywordForBookmark(node.itemId);
- return "";
- case this.COLUMN_TYPE_DESCRIPTION:
- if (node.itemId != -1) {
- try {
- return PlacesUtils.annotations.
- getItemAnnotation(node.itemId, PlacesUIUtils.DESCRIPTION_ANNO);
- }
- catch (ex) { /* has no description */ }
- }
- return "";
- case this.COLUMN_TYPE_DATEADDED:
- if (node.dateAdded)
- return this._convertPRTimeToString(node.dateAdded);
- return "";
- case this.COLUMN_TYPE_LASTMODIFIED:
- if (node.lastModified)
- return this._convertPRTimeToString(node.lastModified);
- return "";
- case this.COLUMN_TYPE_PARENTFOLDER:
- if (PlacesUtils.nodeIsQuery(node.parent) &&
- PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
- Components.interfaces.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY && node.uri)
- return "";
- var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"].
- getService(Components.interfaces.nsINavBookmarksService);
- var rowId = node.itemId;
- try {
- var parentFolderId = bmsvc.getFolderIdForItem(rowId);
- var folderTitle = bmsvc.getItemTitle(parentFolderId);
- } catch(ex) {
- var folderTitle = "";
- }
- return folderTitle;
- case this.COLUMN_TYPE_PARENTFOLDERPATH:
- if (PlacesUtils.nodeIsQuery(node.parent) &&
- PlacesUtils.asQuery(node.parent).queryOptions.queryType ==
- Components.interfaces.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY && node.uri)
- return "";
- var bmsvc = Components.classes["@mozilla.org/browser/nav-bookmarks-service;1"].
- getService(Components.interfaces.nsINavBookmarksService);
- var rowId = node.itemId;
- try {
- var FolderId;
- var parentFolderId = bmsvc.getFolderIdForItem(rowId);
- var folderTitle = bmsvc.getItemTitle(parentFolderId);
- while ((FolderId = bmsvc.getFolderIdForItem(parentFolderId))) {
- if (FolderId == parentFolderId)
- break;
- parentFolderId = FolderId;
- var text = bmsvc.getItemTitle(parentFolderId);
- if (!text)
- break;
- folderTitle = text + " /"+ folderTitle;
- }
- folderTitle = folderTitle.replace(/^\s/,"");
- } catch(ex) {
- var folderTitle = "";
- }
- return folderTitle;
- }
- return "";
- },
- setTree: function(aTree) {
- // If we are replacing the tree during a batch, there is a concrete risk
- // that the treeView goes out of sync, thus it's safer to end the batch now.
- // This is a no-op if we are not batching.
- this.batching(false);
- let hasOldTree = this._tree != null;
- this._tree = aTree;
- if (this._result) {
- if (hasOldTree) {
- // detach from result when we are detaching from the tree.
- // This breaks the reference cycle between us and the result.
- if (!aTree) {
- this._result.removeObserver(this);
- this._rootNode.containerOpen = false;
- }
- }
- if (aTree)
- this._finishInit();
- }
- },
- toggleOpenState: function(aRow) {
- if (!this._result)
- throw Cr.NS_ERROR_UNEXPECTED;
- let node = this._rows[aRow];
- if (this._flatList && this._openContainerCallback) {
- this._openContainerCallback(node);
- return;
- }
- // Persist containers open status, but never persist livemarks.
- if (!this._controller.hasCachedLivemarkInfo(node)) {
- let resource = this._getResourceForNode(node);
- if (resource) {
- const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open");
- const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true");
- if (node.containerOpen)
- PlacesUIUtils.localStore.Unassert(resource, openLiteral, trueLiteral);
- else
- PlacesUIUtils.localStore.Assert(resource, openLiteral, trueLiteral, true);
- }
- }
- node.containerOpen = !node.containerOpen;
- },
- cycleHeader: function(aColumn) {
- if (!this._result)
- throw Cr.NS_ERROR_UNEXPECTED;
- // Sometimes you want a tri-state sorting, and sometimes you don't. This
- // rule allows tri-state sorting when the root node is a folder. This will
- // catch the most common cases. When you are looking at folders, you want
- // the third state to reset the sorting to the natural bookmark order. When
- // you are looking at history, that third state has no meaning so we try
- // to disallow it.
- //
- // The problem occurs when you have a query that results in bookmark
- // folders. One example of this is the subscriptions view. In these cases,
- // this rule doesn't allow you to sort those sub-folders by their natural
- // order.
- let allowTriState = PlacesUtils.nodeIsFolder(this._result.root);
- let oldSort = this._result.sortingMode;
- let oldSortingAnnotation = this._result.sortingAnnotation;
- let newSort;
- let newSortingAnnotation = "";
- const NHQO = Ci.nsINavHistoryQueryOptions;
- switch (this._getColumnType(aColumn)) {
- case this.COLUMN_TYPE_TITLE:
- if (oldSort == NHQO.SORT_BY_TITLE_ASCENDING)
- newSort = NHQO.SORT_BY_TITLE_DESCENDING;
- else if (allowTriState && oldSort == NHQO.SORT_BY_TITLE_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_TITLE_ASCENDING;
- break;
- case this.COLUMN_TYPE_URI:
- if (oldSort == NHQO.SORT_BY_URI_ASCENDING)
- newSort = NHQO.SORT_BY_URI_DESCENDING;
- else if (allowTriState && oldSort == NHQO.SORT_BY_URI_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_URI_ASCENDING;
- break;
- case this.COLUMN_TYPE_DATE:
- if (oldSort == NHQO.SORT_BY_DATE_ASCENDING)
- newSort = NHQO.SORT_BY_DATE_DESCENDING;
- else if (allowTriState &&
- oldSort == NHQO.SORT_BY_DATE_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_DATE_ASCENDING;
- break;
- case this.COLUMN_TYPE_VISITCOUNT:
- // visit count default is unusual because we sort by descending
- // by default because you are most likely to be looking for
- // highly visited sites when you click it
- if (oldSort == NHQO.SORT_BY_VISITCOUNT_DESCENDING)
- newSort = NHQO.SORT_BY_VISITCOUNT_ASCENDING;
- else if (allowTriState && oldSort == NHQO.SORT_BY_VISITCOUNT_ASCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_VISITCOUNT_DESCENDING;
- break;
- case this.COLUMN_TYPE_KEYWORD:
- if (oldSort == NHQO.SORT_BY_KEYWORD_ASCENDING)
- newSort = NHQO.SORT_BY_KEYWORD_DESCENDING;
- else if (allowTriState && oldSort == NHQO.SORT_BY_KEYWORD_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_KEYWORD_ASCENDING;
- break;
- case this.COLUMN_TYPE_DESCRIPTION:
- if (oldSort == NHQO.SORT_BY_ANNOTATION_ASCENDING &&
- oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO) {
- newSort = NHQO.SORT_BY_ANNOTATION_DESCENDING;
- newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
- }
- else if (allowTriState &&
- oldSort == NHQO.SORT_BY_ANNOTATION_DESCENDING &&
- oldSortingAnnotation == PlacesUIUtils.DESCRIPTION_ANNO)
- newSort = NHQO.SORT_BY_NONE;
- else {
- newSort = NHQO.SORT_BY_ANNOTATION_ASCENDING;
- newSortingAnnotation = PlacesUIUtils.DESCRIPTION_ANNO;
- }
- break;
- case this.COLUMN_TYPE_DATEADDED:
- if (oldSort == NHQO.SORT_BY_DATEADDED_ASCENDING)
- newSort = NHQO.SORT_BY_DATEADDED_DESCENDING;
- else if (allowTriState &&
- oldSort == NHQO.SORT_BY_DATEADDED_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_DATEADDED_ASCENDING;
- break;
- case this.COLUMN_TYPE_LASTMODIFIED:
- if (oldSort == NHQO.SORT_BY_LASTMODIFIED_ASCENDING)
- newSort = NHQO.SORT_BY_LASTMODIFIED_DESCENDING;
- else if (allowTriState &&
- oldSort == NHQO.SORT_BY_LASTMODIFIED_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_LASTMODIFIED_ASCENDING;
- break;
- case this.COLUMN_TYPE_TAGS:
- if (oldSort == NHQO.SORT_BY_TAGS_ASCENDING)
- newSort = NHQO.SORT_BY_TAGS_DESCENDING;
- else if (allowTriState && oldSort == NHQO.SORT_BY_TAGS_DESCENDING)
- newSort = NHQO.SORT_BY_NONE;
- else
- newSort = NHQO.SORT_BY_TAGS_ASCENDING;
- break;
- case this.COLUMN_TYPE_PARENTFOLDER:
- return;
- break;
- case this.COLUMN_TYPE_PARENTFOLDERPATH:
- return;
- break;
- default:
- throw Cr.NS_ERROR_INVALID_ARG;
- }
- this._result.sortingAnnotation = newSortingAnnotation;
- this._result.sortingMode = newSort;
- },
- isEditable: function(aRow, aColumn) {
- // At this point we only support editing the title field.
- if (aColumn.index != 0)
- return false;
- let node = this._rows[aRow];
- if (!node) {
- Cu.reportError("isEditable called for an unbuilt row.");
- return false;
- }
- let itemId = node.itemId;
- // Only bookmark-nodes are editable. Fortunately, this check also takes
- // care of livemark children.
- if (itemId == -1)
- return false;
- // The following items are also not editable, even though they are bookmark
- // items.
- // * places-roots
- // * the left pane special folders and queries (those are place: uri
- // bookmarks)
- // * separators
- //
- // Note that concrete itemIds aren't used intentionally. For example, we
- // have no reason to disallow renaming a shortcut to the Bookmarks Toolbar,
- // except for the one under All Bookmarks.
- if (PlacesUtils.nodeIsSeparator(node) || PlacesUtils.isRootItem(itemId))
- return false;
- let parentId = PlacesUtils.getConcreteItemId(node.parent);
- if (parentId == PlacesUIUtils.leftPaneFolderId ||
- parentId == PlacesUIUtils.allBookmarksFolderId) {
- // Note that the for the time being this is the check that actually
- // blocks renaming places "roots", and not the isRootItem check above.
- // That's because places root are only exposed through folder shortcuts
- // descendants of the left pane folder.
- return false;
- }
- return true;
- },
- setCellText: function(aRow, aColumn, aText) {
- // We may only get here if the cell is editable.
- let node = this._rows[aRow];
- if (node.title != aText) {
- let txn = new PlacesEditItemTitleTransaction(node.itemId, aText);
- PlacesUtils.transactionManager.doTransaction(txn);
- }
- },
- toggleCutNode: function(aNode, aValue) {
- let currentVal = this._cuttingNodes.has(aNode);
- if (currentVal != aValue) {
- if (aValue)
- this._cuttingNodes.add(aNode);
- else
- this._cuttingNodes.delete(aNode);
- this._invalidateCellValue(aNode, this.COLUMN_TYPE_TITLE);
- }
- },
- selectionChanged: function() { },
- cycleCell: function(aRow, aColumn) { },
- isSelectable: function(aRow, aColumn) { return false; },
- performAction: function(aAction) { },
- performActionOnRow: function(aAction, aRow) { },
- performActionOnCell: function(aAction, aRow, aColumn) { }
- };
|