summaryrefslogtreecommitdiffstats
path: root/comm/suite/chatzilla/xul/lib/tree-utils.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/suite/chatzilla/xul/lib/tree-utils.js1716
1 files changed, 1716 insertions, 0 deletions
diff --git a/comm/suite/chatzilla/xul/lib/tree-utils.js b/comm/suite/chatzilla/xul/lib/tree-utils.js
new file mode 100644
index 0000000000..1e47aebee6
--- /dev/null
+++ b/comm/suite/chatzilla/xul/lib/tree-utils.js
@@ -0,0 +1,1716 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ *
+ * 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/. */
+
+/**
+ * An implemention of |nsITreeView| for a tree whose elements have no children.
+ *
+ * Code using BasicOView can override |getRowProperties|, |getColumnProperties|,
+ * |getCellProperties|, etc., as needed.
+ *
+ * Code using |BasicOView| will need to make the appropriate |myTree.tree
+ * .invalidate| calls when |myTree.data| changes.
+ *
+ * @syntax
+ * var myTree = new BasicOView()
+ * myTree.setColumnNames(["col 1", "col 2"]);
+ * myTree.data = [["row 1, col 1", "row 1, col 2"],
+ * ["row 2, col 1", "row 2, col 2"]];
+ * treeBoxObject.view = myTree;
+ */
+function BasicOView()
+{
+ this.tree = null;
+}
+
+/* functions *you* should call to initialize and maintain the tree state */
+
+/* scroll the line specified by |line| to the center of the tree */
+BasicOView.prototype.centerLine =
+function bov_ctrln (line)
+{
+ var first = this.tree.getFirstVisibleRow();
+ var last = this.tree.getLastVisibleRow();
+ this.scrollToRow(line - (last - first + 1) / 2);
+}
+
+/* call this to set the association between column names and data columns */
+BasicOView.prototype.setColumnNames =
+function bov_setcn (aryNames)
+{
+ this.columnNames = new Object();
+ for (var i = 0; i < aryNames.length; ++i)
+ this.columnNames[aryNames[i]] = i;
+}
+
+/*
+ * scroll the source so |line| is at either the top, center, or bottom
+ * of the view, depending on the value of |align|.
+ *
+ * line is the one based target line.
+ * if align is negative, the line will be scrolled to the top, if align is
+ * zero the line will be centered, and if align is greater than 0 the line
+ * will be scrolled to the bottom. 0 is the default.
+ */
+BasicOView.prototype.scrollTo =
+function bov_scrollto (line, align)
+{
+ if (!this.tree)
+ return;
+
+ var headerRows = 1;
+
+ var first = this.tree.getFirstVisibleRow();
+ var last = this.tree.getLastVisibleRow();
+ var viz = last - first + 1 - headerRows; /* total number of visible rows */
+
+ /* all rows are visible, nothing to scroll */
+ if (first == 0 && last >= this.rowCount)
+ return;
+
+ /* tree lines are 0 based, we accept one based lines, deal with it */
+ --line;
+
+ /* safety clamp */
+ if (line < 0)
+ line = 0;
+ if (line >= this.rowCount)
+ line = this.rowCount - 1;
+
+ if (align < 0)
+ {
+ if (line > this.rowCount - viz) /* overscroll, can't put a row from */
+ line = this.rowCount - viz; /* last page at the top. */
+ this.tree.scrollToRow(line);
+ }
+ else if (align > 0)
+ {
+ if (line < viz) /* underscroll, can't put a row from the first page */
+ line = 0; /* at the bottom. */
+ else
+ line = line - viz + headerRows;
+
+ this.tree.scrollToRow(line);
+ }
+ else
+ {
+ var half_viz = viz / 2;
+ /* lines past this line can't be centered without causing the tree
+ * to show more rows than we have. */
+ var lastCenterable = this.rowCount - half_viz;
+ if (line > half_viz)
+ line = lastCenterable;
+ /* lines before this can't be centered without causing the tree
+ * to attempt to display negative rows. */
+ else if (line < half_viz)
+ line = half_viz;
+ else
+ /* round the vizible rows down to a whole number, or we try to end up
+ * on a N + 0.5 row! */
+ half_viz = Math.floor(half_viz);
+
+ this.tree.scrollToRow(line - half_viz);
+ }
+}
+
+BasicOView.prototype.__defineGetter__("selectedIndex", bov_getsel);
+function bov_getsel()
+{
+ if (!this.tree || this.tree.view.selection.getRangeCount() < 1)
+ return -1;
+
+ var min = new Object();
+ this.tree.view.selection.getRangeAt(0, min, {});
+ return min.value;
+}
+
+BasicOView.prototype.__defineSetter__("selectedIndex", bov_setsel);
+function bov_setsel(i)
+{
+ if (i == -1)
+ this.tree.view.selection.clearSelection();
+ else
+ this.tree.view.selection.timedSelect (i, 500);
+ return i;
+}
+
+/*
+ * functions the tree will call to retrieve the list state (nsITreeView.)
+ */
+
+BasicOView.prototype.rowCount = 0;
+
+BasicOView.prototype.getCellProperties =
+function bov_cellprops (row, col, properties)
+{
+ return "";
+}
+
+BasicOView.prototype.getColumnProperties =
+function bov_colprops (col, properties)
+{
+ return "";
+}
+
+BasicOView.prototype.getRowProperties =
+function bov_rowprops (index, properties)
+{
+ return "";
+}
+
+BasicOView.prototype.isContainer =
+function bov_isctr (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isContainerOpen =
+function bov_isctropen (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isContainerEmpty =
+function bov_isctrempt (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isSeparator =
+function bov_isseparator (index)
+{
+ return false;
+}
+
+BasicOView.prototype.isSorted =
+function bov_issorted (index)
+{
+ return false;
+}
+
+BasicOView.prototype.canDrop =
+function bov_drop (index, orientation)
+{
+ return false;
+}
+
+BasicOView.prototype.drop =
+function bov_drop (index, orientation)
+{
+ return false;
+}
+
+BasicOView.prototype.getParentIndex =
+function bov_getpi (index)
+{
+ if (index < 0)
+ return -1;
+
+ return 0;
+}
+
+BasicOView.prototype.hasNextSibling =
+function bov_hasnxtsib (rowIndex, afterIndex)
+{
+ return (afterIndex < (this.rowCount - 1));
+}
+
+BasicOView.prototype.getLevel =
+function bov_getlvl (index)
+{
+ return 0;
+}
+
+BasicOView.prototype.getImageSrc =
+function bov_getimgsrc (row, col)
+{
+}
+
+BasicOView.prototype.getProgressMode =
+function bov_getprgmode (row, col)
+{
+}
+
+BasicOView.prototype.getCellValue =
+function bov_getcellval (row, col)
+{
+}
+
+BasicOView.prototype.getCellText =
+function bov_getcelltxt (row, col)
+{
+ if (!this.columnNames)
+ return "";
+
+ if (typeof col == "object")
+ col = col.id;
+
+ var ary = col.match (/:(.*)/);
+ if (ary)
+ col = ary[1];
+
+ var colName = this.columnNames[col];
+
+ if (typeof colName == "undefined")
+ return "";
+
+ return this.data[row][colName];
+}
+
+BasicOView.prototype.setTree =
+function bov_seto (tree)
+{
+ this.tree = tree;
+}
+
+BasicOView.prototype.toggleOpenState =
+function bov_toggleopen (index)
+{
+}
+
+BasicOView.prototype.cycleHeader =
+function bov_cyclehdr (col)
+{
+}
+
+BasicOView.prototype.selectionChanged =
+function bov_selchg ()
+{
+}
+
+BasicOView.prototype.cycleCell =
+function bov_cyclecell (row, col)
+{
+}
+
+BasicOView.prototype.isEditable =
+function bov_isedit (row, col)
+{
+ return false;
+}
+
+BasicOView.prototype.isSelectable =
+function bov_isselect (row, col)
+{
+ return false;
+}
+
+BasicOView.prototype.setCellValue =
+function bov_setct (row, col, value)
+{
+}
+
+BasicOView.prototype.setCellText =
+function bov_setct (row, col, value)
+{
+}
+
+BasicOView.prototype.onRouteFocus =
+function bov_rfocus (event)
+{
+ if ("onFocus" in this)
+ this.onFocus(event);
+}
+
+BasicOView.prototype.onRouteBlur =
+function bov_rblur (event)
+{
+ if ("onBlur" in this)
+ this.onBlur(event);
+}
+
+BasicOView.prototype.onRouteDblClick =
+function bov_rdblclick (event)
+{
+ if (!("onRowCommand" in this) || event.target.localName != "treechildren")
+ return;
+
+ var rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ var rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+}
+
+BasicOView.prototype.onRouteKeyPress =
+function bov_rkeypress (event)
+{
+ var rec;
+ var rowIndex;
+
+ if ("onRowCommand" in this && (event.keyCode == 13 || event.charCode == 32))
+ {
+ if (!this.selection)
+ return;
+
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+ }
+ else if ("onKeyPress" in this)
+ {
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex != -1 && rowIndex < this.rowCount)
+ {
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+ }
+ else
+ {
+ rec = null;
+ }
+
+ this.onKeyPress(rec, event);
+ }
+}
+
+BasicOView.prototype.performAction =
+function bov_pact (action)
+{
+}
+
+BasicOView.prototype.performActionOnRow =
+function bov_pactrow (action)
+{
+}
+
+BasicOView.prototype.performActionOnCell =
+function bov_pactcell (action)
+{
+}
+
+/**
+ * A single entry in an |XULTreeView|.
+ *
+ * These things take care of keeping the |XULTreeView| properly informed of
+ * changes in value and child count. You shouldn't have to maintain tree state
+ * at all - just update the |XULTreeViewRecord| objects.
+ *
+ * @param share An otherwise empty object to store cache data. You should use
+ * the same object as the |share| for the |XULTreeView| that you
+ * indend to contain these records.
+ *
+ */
+function XULTreeViewRecord(share)
+{
+ this._share = share;
+ this.visualFootprint = 1;
+ this.isHidden = true; /* records are considered hidden until they are
+ * inserted into a live tree */
+}
+
+XULTreeViewRecord.prototype.isContainerOpen = false;
+
+/*
+ * walk the parent tree to find our tree container. return null if there is
+ * none
+ */
+XULTreeViewRecord.prototype.findContainerTree =
+function xtvr_gettree ()
+{
+ if (!("parentRecord" in this))
+ return null;
+ var parent = this.parentRecord;
+
+ while (parent)
+ {
+ if ("_treeView" in parent)
+ return parent._treeView;
+ if ("parentRecord" in parent)
+ parent = parent.parentRecord;
+ else
+ parent = null;
+ }
+
+ return null;
+}
+
+XULTreeViewRecord.prototype.__defineGetter__("childIndex", xtvr_getChildIndex);
+function xtvr_getChildIndex ()
+{
+ //dd ("getChildIndex {");
+
+ if (!("parentRecord" in this))
+ {
+ delete this._childIndex;
+ //dd ("} -1");
+ return -1;
+ }
+
+ if ("_childIndex" in this)
+ {
+ if ("childData" in this && this._childIndex in this.childData &&
+ this.childData[this._childIndex] == this)
+ {
+ //dd ("} " + this._childIndex);
+ return this._childIndex;
+ }
+ }
+
+ var childData = this.parentRecord.childData;
+ var len = childData.length;
+ for (var i = 0; i < len; ++i)
+ {
+ if (childData[i] == this)
+ {
+ this._childIndex = i;
+ //dd ("} " + this._childIndex);
+ return i;
+ }
+ }
+
+ delete this._childIndex;
+ //dd ("} -1");
+ return -1;
+}
+
+XULTreeViewRecord.prototype.__defineSetter__("childIndex", xtvr_setChildIndex);
+function xtvr_setChildIndex ()
+{
+ dd("xtvr: childIndex is read only, ignore attempt to write to it\n");
+ if (typeof getStackTrace == "function")
+ dd(getStackTrace());
+}
+
+/* count the number of parents, not including the root node */
+XULTreeViewRecord.prototype.__defineGetter__("level", xtvr_getLevel);
+function xtvr_getLevel ()
+{
+ if (!("parentRecord" in this))
+ return -1;
+
+ var rv = 0;
+ var parentRecord = this.parentRecord;
+ while ("parentRecord" in parentRecord &&
+ (parentRecord = parentRecord.parentRecord)) ++rv;
+ return rv;
+}
+
+/*
+ * associates a property name on this record, with a column in the tree. This
+ * method will set up a get/set pair for the property name you specify which
+ * will take care of updating the tree when the value changes. DO NOT try
+ * to change your mind later. Do not attach a different name to the same colID,
+ * and do not rename the colID. You have been warned.
+ */
+XULTreeViewRecord.prototype.setColumnPropertyName =
+function xtvr_setcol (colID, propertyName)
+{
+ function xtvr_getValueShim ()
+ {
+ return this._colValues[colID];
+ }
+ function xtvr_setValueShim (newValue)
+ {
+ this._colValues[colID] = newValue;
+ return newValue;
+ }
+
+ if (!("_colValues" in this))
+ this._colValues = new Object();
+
+ if (typeof propertyName == "function")
+ {
+ this._colValues.__defineGetter__(colID, propertyName);
+ }
+ else
+ {
+ this.__defineGetter__(propertyName, xtvr_getValueShim);
+ this.__defineSetter__(propertyName, xtvr_setValueShim);
+ }
+}
+
+XULTreeViewRecord.prototype.setColumnPropertyValue =
+function xtvr_setcolv (colID, value)
+{
+ this._colValues[colID] = value;
+}
+
+/*
+ * set the default sort column and reSort.
+ */
+XULTreeViewRecord.prototype.setSortColumn =
+function xtvr_setcol (colID, dir)
+{
+ //dd ("setting sort column to " + colID);
+ this._share.sortColumn = colID;
+ this._share.sortDirection = (typeof dir == "undefined") ? 1 : dir;
+ this.reSort();
+}
+
+/*
+ * set the default sort direction. 1 is ascending, -1 is descending, 0 is no
+ * sort. setting this to 0 will *not* recover the natural insertion order,
+ * it will only affect newly added items.
+ */
+XULTreeViewRecord.prototype.setSortDirection =
+function xtvr_setdir (dir)
+{
+ this._share.sortDirection = dir;
+}
+
+/*
+ * invalidate this row in the tree
+ */
+XULTreeViewRecord.prototype.invalidate =
+function xtvr_invalidate()
+{
+ var tree = this.findContainerTree();
+ if (tree)
+ {
+ var row = this.calculateVisualRow();
+ if (row != -1)
+ tree.tree.invalidateRow(row);
+ }
+}
+
+/*
+ * invalidate any data in the cache.
+ */
+XULTreeViewRecord.prototype.invalidateCache =
+function xtvr_killcache()
+{
+ this._share.rowCache = new Object();
+ this._share.lastComputedIndex = -1;
+ this._share.lastIndexOwner = null;
+}
+
+/*
+ * default comparator function for sorts. if you want a custom sort, override
+ * this method. We declare xtvr_sortcmp as a top level function, instead of
+ * a function expression so we can refer to it later.
+ */
+XULTreeViewRecord.prototype.sortCompare = xtvr_sortcmp;
+function xtvr_sortcmp (a, b)
+{
+ var sc = a._share.sortColumn;
+ var sd = a._share.sortDirection;
+
+ a = a[sc];
+ b = b[sc];
+
+ if (a < b)
+ return -1 * sd;
+
+ if (a > b)
+ return 1 * sd;
+
+ return 0;
+}
+
+/*
+ * this method will cause all child records to be reSorted. any records
+ * with the default sortCompare method will be sorted by the colID passed to
+ * setSortColumn.
+ *
+ * the local parameter is used internally to control whether or not the
+ * sorted rows are invalidated. don't use it yourself.
+ */
+XULTreeViewRecord.prototype.reSort =
+function xtvr_resort (leafSort)
+{
+ if (!("childData" in this) || this.childData.length < 1 ||
+ (this.childData[0].sortCompare == xtvr_sortcmp &&
+ !("sortColumn" in this._share) || this._share.sortDirection == 0))
+ {
+ /* if we have no children, or we have the default sort compare and no
+ * sort flags, then just exit */
+ return;
+ }
+
+ this.childData.sort(this.childData[0].sortCompare);
+
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ if ("isContainerOpen" in this.childData[i] &&
+ this.childData[i].isContainerOpen)
+ this.childData[i].reSort(true);
+ else
+ this.childData[i].sortIsInvalid = true;
+ }
+
+ if (!leafSort)
+ {
+ this.invalidateCache();
+ var tree = this.findContainerTree();
+ if (tree && tree.tree)
+ {
+ var rowIndex = this.calculateVisualRow();
+ /*
+ dd ("invalidating " + rowIndex + " - " +
+ (rowIndex + this.visualFootprint - 1));
+ */
+ tree.tree.invalidateRange (rowIndex,
+ rowIndex + this.visualFootprint - 1);
+ }
+ }
+ delete this.sortIsInvalid;
+}
+
+/*
+ * call this to indicate that this node may have children at one point. make
+ * sure to call it before adding your first child.
+ */
+XULTreeViewRecord.prototype.reserveChildren =
+function xtvr_rkids (always)
+{
+ if (!("childData" in this))
+ this.childData = new Array();
+ if (!("isContainerOpen" in this))
+ this.isContainerOpen = false;
+ if (always)
+ this.alwaysHasChildren = true;
+ else
+ delete this.alwaysHasChildren;
+}
+
+/*
+ * add a child to the end of the child list for this record. takes care of
+ * updating the tree as well.
+ */
+XULTreeViewRecord.prototype.appendChild =
+function xtvr_appchild (child)
+{
+ if (!isinstance(child, XULTreeViewRecord))
+ throw Components.results.NS_ERROR_INVALID_ARG;
+
+ child.isHidden = false;
+ child.parentRecord = this;
+ this.childData.push(child);
+
+ if ("isContainerOpen" in this && this.isContainerOpen)
+ {
+ //dd ("appendChild: " + xtv_formatRecord(child, ""));
+ if (this.calculateVisualRow() >= 0)
+ {
+ var tree = this.findContainerTree();
+ if (tree && tree.frozen)
+ this.needsReSort = true;
+ else
+ this.reSort(true); /* reSort, don't invalidate. we're going
+ * to do that in the
+ * onVisualFootprintChanged call. */
+ }
+ this.onVisualFootprintChanged(child.calculateVisualRow(),
+ child.visualFootprint);
+ }
+}
+
+/*
+ * add a list of children to the end of the child list for this record.
+ * faster than multiple appendChild() calls.
+ */
+XULTreeViewRecord.prototype.appendChildren =
+function xtvr_appchild (children)
+{
+ var delta = 0;
+ for (var i = 0; i < children.length; ++i)
+ {
+ var child = children[i];
+ child.isHidden = false;
+ child.parentRecord = this;
+ this.childData.push(child);
+ delta += child.visualFootprint;
+ }
+
+ if ("isContainerOpen" in this && this.isContainerOpen)
+ {
+ if (this.calculateVisualRow() >= 0)
+ {
+ this.reSort(true); /* reSort, don't invalidate. we're going to do
+ * that in the onVisualFootprintChanged call. */
+ }
+ this.onVisualFootprintChanged(this.childData[0].calculateVisualRow(),
+ delta);
+ }
+}
+
+/*
+ * Removes a single child from this record by index.
+ * @param index Index of the child record to remove.
+ */
+XULTreeViewRecord.prototype.removeChildAtIndex =
+function xtvr_remchild(index)
+{
+ var len = this.childData.length;
+ if (!ASSERT(index >= 0 && index < len, "index out of bounds"))
+ return;
+
+ var orphan = this.childData[index];
+ var delta = -orphan.visualFootprint;
+ var changeStart = orphan.calculateVisualRow();
+ delete orphan.parentRecord;
+ arrayRemoveAt(this.childData, index);
+
+ if (!orphan.isHidden && "isContainerOpen" in this && this.isContainerOpen)
+ this.onVisualFootprintChanged(changeStart, delta);
+}
+
+/*
+ * Removes a range of children from this record by index. Faster than multiple
+ * removeChildAtIndex() calls.
+ * @param index Index of the first child record to remove.
+ * @param count Number of child records to remove.
+ */
+XULTreeViewRecord.prototype.removeChildrenAtIndex =
+function xtvr_remchildren(index, count)
+{
+ var len = this.childData.length;
+ if (!ASSERT(index >= 0 && index < len, "index out of bounds"))
+ return;
+ if (!ASSERT(count > 0 && index + count <= len, "count out of bounds"))
+ return;
+
+ var delta = 0;
+ var changeStart = this.childData[index].calculateVisualRow();
+ for (var i = 0; i < count; ++i)
+ {
+ var orphan = this.childData[index + i];
+ if (!orphan.isHidden)
+ delta -= orphan.visualFootprint;
+ delete orphan.parentRecord;
+ }
+ this.childData.splice(index, count);
+
+ if ("isContainerOpen" in this && this.isContainerOpen)
+ this.onVisualFootprintChanged(changeStart, delta);
+}
+
+/*
+ * hide this record and all descendants.
+ */
+XULTreeViewRecord.prototype.hide =
+function xtvr_hide()
+{
+ if (this.isHidden)
+ return;
+
+ var tree = this.findContainerTree();
+ if (tree && tree.frozen)
+ {
+ this.isHidden = true;
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(0, -this.visualFootprint);
+ return;
+ }
+
+ /* get the row before hiding */
+ var row = this.calculateVisualRow();
+ this.invalidateCache();
+ this.isHidden = true;
+ /* go right to the parent so we don't muck with our own visualFootprint
+ * record, we'll need it to be correct if we're ever unHidden. */
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(row, -this.visualFootprint);
+}
+
+/*
+ * unhide this record and all descendants.
+ */
+XULTreeViewRecord.prototype.unHide =
+function xtvr_uhide()
+{
+ if (!this.isHidden)
+ return;
+
+ var tree = this.findContainerTree();
+ if (tree && tree.frozen)
+ {
+ this.isHidden = false;
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(0, this.visualFootprint);
+ return;
+ }
+
+ this.isHidden = false;
+ this.invalidateCache();
+ var row = this.calculateVisualRow();
+ if (this.parentRecord)
+ this.parentRecord.onVisualFootprintChanged(row, this.visualFootprint);
+}
+
+/*
+ * open this record, exposing it's children. DONT call this method if the
+ * record has no children.
+ */
+XULTreeViewRecord.prototype.open =
+function xtvr_open ()
+{
+ if (this.isContainerOpen)
+ return;
+
+ if ("onPreOpen" in this)
+ this.onPreOpen();
+
+ this.isContainerOpen = true;
+ var delta = 0;
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ if (!this.childData[i].isHidden)
+ delta += this.childData[i].visualFootprint;
+ }
+
+ /* this reSort should only happen if the sort column changed */
+ this.reSort(true);
+ this.visualFootprint += delta;
+ if ("parentRecord" in this)
+ {
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow(),
+ 0);
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow() +
+ 1, delta);
+ }
+}
+
+/*
+ * close this record, hiding it's children. DONT call this method if the record
+ * has no children, or if it is already closed.
+ */
+XULTreeViewRecord.prototype.close =
+function xtvr_close ()
+{
+ if (!this.isContainerOpen)
+ return;
+
+ this.isContainerOpen = false;
+ var delta = 1 - this.visualFootprint;
+ this.visualFootprint += delta;
+ if ("parentRecord" in this)
+ {
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow(),
+ 0);
+ this.parentRecord.onVisualFootprintChanged(this.calculateVisualRow() +
+ 1, delta);
+ }
+
+ if ("onPostClose" in this)
+ this.onPostClose();
+}
+
+/*
+ * called when a node above this one grows or shrinks. we need to adjust
+ * our own visualFootprint to match the change, and pass the message on.
+ */
+XULTreeViewRecord.prototype.onVisualFootprintChanged =
+function xtvr_vpchange (start, amount)
+{
+ /* if we're not hidden, but this notification came from a hidden node
+ * (start == -1), ignore it, it doesn't affect us. */
+ if (start == -1 && !this.isHidden)
+ {
+
+ //dd ("vfp change (" + amount + ") from hidden node ignored.");
+ return;
+ }
+
+ this.visualFootprint += amount;
+
+ if ("parentRecord" in this)
+ this.parentRecord.onVisualFootprintChanged(start, amount);
+}
+
+/*
+ * calculate the "visual" row for this record. If the record isn't actually
+ * visible return -1.
+ * eg.
+ * Name Visual Row
+ * node1 0
+ * node11 1
+ * node12 2
+ * node2 3
+ * node21 4
+ * node3 5
+ */
+XULTreeViewRecord.prototype.calculateVisualRow =
+function xtvr_calcrow ()
+{
+ /* if this is the second time in a row that someone asked us, fetch the last
+ * result from the cache. */
+ if (this._share.lastIndexOwner == this)
+ return this._share.lastComputedIndex;
+
+ var vrow;
+
+ /* if this is an uninserted or hidden node, or... */
+ if (!("parentRecord" in this) || (this.isHidden) ||
+ /* if parent isn't open, or... */
+ (!this.parentRecord.isContainerOpen) ||
+ /* parent isn't visible */
+ ((vrow = this.parentRecord.calculateVisualRow()) == -1))
+ {
+ /* then we're not visible, return -1 */
+ //dd ("cvr: returning -1");
+ return -1;
+ }
+
+ /* parent is the root node XXX parent is not visible */
+ if (vrow == null)
+ vrow = 0;
+ else
+ /* parent is not the root node, add one for the space they take up. */
+ ++vrow;
+
+ /* add in the footprint for all of the earlier siblings */
+ var ci = this.childIndex;
+ for (var i = 0; i < ci; ++i)
+ {
+ if (!this.parentRecord.childData[i].isHidden)
+ vrow += this.parentRecord.childData[i].visualFootprint;
+ }
+
+ /* save this calculation to the cache. */
+ this._share.lastIndexOwner = this;
+ this._share.lastComputedIndex = vrow;
+
+ return vrow;
+}
+
+/*
+ * locates the child record for the visible row |targetRow|. DO NOT call this
+ * with a targetRow less than this record's visual row, or greater than this
+ * record's visual row + the number of visible children it has.
+ */
+XULTreeViewRecord.prototype.locateChildByVisualRow =
+function xtvr_find (targetRow, myRow)
+{
+ if (targetRow in this._share.rowCache)
+ return this._share.rowCache[targetRow];
+
+ /* if this is true, we *are* the index */
+ if (targetRow == myRow)
+ return (this._share.rowCache[targetRow] = this);
+
+ /* otherwise, we've got to search the kids */
+ var childStart = myRow; /* childStart represents the starting visual row
+ * for the child we're examining. */
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ var child = this.childData[i];
+ /* ignore hidden children */
+ if (child.isHidden)
+ continue;
+ /* if this kid is the targetRow, we're done */
+ if (childStart == targetRow)
+ return (this._share.rowCache[targetRow] = child);
+ /* if this kid contains the index, ask *it* to find the record */
+ else if (targetRow <= childStart + child.visualFootprint) {
+ /* this *has* to succeed */
+ var rv = child.locateChildByVisualRow(targetRow, childStart + 1);
+ //XXXASSERT (rv, "Can't find a row that *has* to be there.");
+ /* don't cache this, the previous call to locateChildByVisualRow
+ * just did. */
+ return rv;
+ }
+
+ /* otherwise, get ready to ask the next kid */
+ childStart += child.visualFootprint;
+ }
+
+ return null;
+}
+
+/**
+ * Used to drop a label into an arbitrary place in an arbitrary tree.
+ *
+ * Normally, specializations of |XULTreeViewRecord| are tied to a specific
+ * tree because of implementation details. |XTLabelRecords| are specially
+ * designed (err, hacked) to work around these details - this makes them
+ * slower, but more generic.
+ *
+ * We set up a getter for |_share| that defers to the parent object. This lets
+ * |XTLabelRecords| work in any tree.
+ */
+function XTLabelRecord (columnName, label, blankCols)
+{
+ this.setColumnPropertyName (columnName, "label");
+ this.label = label;
+ this.property = null;
+
+ if (typeof blankCols == "object")
+ {
+ for (var i in blankCols)
+ this._colValues[blankCols[i]] = "";
+ }
+}
+
+XTLabelRecord.prototype = new XULTreeViewRecord (null);
+
+XTLabelRecord.prototype.__defineGetter__("_share", tolr_getshare);
+function tolr_getshare()
+{
+ if ("parentRecord" in this)
+ return this.parentRecord._share;
+
+ ASSERT (0, "XTLabelRecord cannot be the root of a visible tree.");
+ return null;
+}
+
+// @internal
+function XTRootRecord (tree, share)
+{
+ this._share = share;
+ this._treeView = tree;
+ this.visualFootprint = 0;
+ this.isHidden = false;
+ this.reserveChildren();
+ this.isContainerOpen = true;
+}
+
+/* no cache passed in here, we set it in the XTRootRecord contructor instead. */
+XTRootRecord.prototype = new XULTreeViewRecord (null);
+
+XTRootRecord.prototype.open =
+XTRootRecord.prototype.close =
+function torr_notimplemented()
+{
+ /* don't do this on a root node */
+}
+
+XTRootRecord.prototype.calculateVisualRow =
+function torr_calcrow ()
+{
+ return null;
+}
+
+XTRootRecord.prototype.reSort =
+function torr_resort ()
+{
+ if ("_treeView" in this && this._treeView.frozen)
+ {
+ this._treeView.needsReSort = true;
+ return;
+ }
+
+ if (!("childData" in this) || this.childData.length < 1 ||
+ (this.childData[0].sortCompare == xtvr_sortcmp &&
+ !("sortColumn" in this._share) || this._share.sortDirection == 0))
+ {
+ /* if we have no children, or we have the default sort compare but we're
+ * missing a sort flag, then just exit */
+ return;
+ }
+
+ this.childData.sort(this.childData[0].sortCompare);
+
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ if ("isContainerOpen" in this.childData[i] &&
+ this.childData[i].isContainerOpen)
+ this.childData[i].reSort(true);
+ else
+ this.childData[i].sortIsInvalid = true;
+ }
+
+ if ("_treeView" in this && this._treeView.tree)
+ {
+ /*
+ dd ("root node: invalidating 0 - " + this.visualFootprint +
+ " for sort");
+ */
+ this.invalidateCache();
+ this._treeView.tree.invalidateRange (0, this.visualFootprint);
+ }
+}
+
+XTRootRecord.prototype.locateChildByVisualRow =
+function torr_find (targetRow)
+{
+ if (targetRow in this._share.rowCache)
+ return this._share.rowCache[targetRow];
+
+ var childStart = -1; /* childStart represents the starting visual row
+ * for the child we're examining. */
+ for (var i = 0; i < this.childData.length; ++i)
+ {
+ var child = this.childData[i];
+ /* ignore hidden children */
+ if (child.isHidden)
+ continue;
+ /* if this kid is the targetRow, we're done */
+ if (childStart == targetRow)
+ return (this._share.rowCache[targetRow] = child);
+ /* if this kid contains the index, ask *it* to find the record */
+ else if (targetRow <= childStart + child.visualFootprint) {
+ /* this *has* to succeed */
+ var rv = child.locateChildByVisualRow(targetRow, childStart + 1);
+ //XXXASSERT (rv, "Can't find a row that *has* to be there.");
+ /* don't cache this, the previous call to locateChildByVisualRow
+ * just did. */
+ return rv;
+ }
+
+ /* otherwise, get ready to ask the next kid */
+ childStart += child.visualFootprint;
+ }
+
+ return null;
+}
+
+XTRootRecord.prototype.onVisualFootprintChanged =
+function torr_vfpchange (start, amount)
+{
+ if (!this._treeView.frozen)
+ {
+ this.invalidateCache();
+ this.visualFootprint += amount;
+ if ("_treeView" in this && "tree" in this._treeView &&
+ this._treeView.tree)
+ {
+ if (amount != 0)
+ this._treeView.tree.rowCountChanged (start, amount);
+ else
+ this._treeView.tree.invalidateRow (start);
+ }
+ }
+ else
+ {
+ if ("changeAmount" in this._treeView)
+ this._treeView.changeAmount += amount;
+ else
+ this._treeView.changeAmount = amount;
+ if ("changeStart" in this._treeView)
+ this._treeView.changeStart =
+ Math.min (start, this._treeView.changeStart);
+ else
+ this._treeView.changeStart = start;
+ }
+}
+
+/**
+ * An implemention of |nsITreeView| for a tree whose elements have multiple
+ * levels of children.
+ *
+ * Code using |XULTreeView| can override |getRowProperties|, |getColumnProperties|,
+ * |getCellProperties|, etc., as needed.
+ *
+ * @param share An otherwise empty object to store cache data.
+ */
+function XULTreeView(share)
+{
+ if (!share)
+ share = new Object();
+ this.childData = new XTRootRecord(this, share);
+ this.childData.invalidateCache();
+ this.tree = null;
+ this.share = share;
+ this.frozen = 0;
+}
+
+/* functions *you* should call to initialize and maintain the tree state */
+
+/*
+ * Changes to the tree contents will not cause the tree to be invalidated
+ * until |thaw()| is called. All changes will be pooled into a single invalidate
+ * call.
+ *
+ * Freeze/thaws are nestable, the tree will not update until the number of
+ * |thaw()| calls matches the number of freeze() calls.
+ */
+XULTreeView.prototype.freeze =
+function xtv_freeze ()
+{
+ if (++this.frozen == 1)
+ {
+ this.changeStart = 0;
+ this.changeAmount = 0;
+ }
+}
+
+/*
+ * Reflect any changes to the tree content since the last freeze.
+ */
+XULTreeView.prototype.thaw =
+function xtv_thaw ()
+{
+ if (this.frozen == 0)
+ {
+ ASSERT (0, "not frozen");
+ return;
+ }
+
+ if (--this.frozen == 0 && "changeStart" in this)
+ {
+ this.childData.onVisualFootprintChanged(this.changeStart,
+ this.changeAmount);
+ }
+
+ if ("needsReSort" in this) {
+ this.childData.reSort();
+ delete this.needsReSort;
+ }
+
+
+ delete this.changeStart;
+ delete this.changeAmount;
+}
+
+XULTreeView.prototype.saveBranchState =
+function xtv_savebranch (target, source, recurse)
+{
+ var len = source.length;
+ for (var i = 0; i < len; ++i)
+ {
+ if (source[i].isContainerOpen)
+ {
+ target[i] = new Object();
+ target[i].name = source[i]._colValues["col-0"];
+ if (recurse)
+ this.saveBranchState (target[i], source[i].childData, true);
+ }
+ }
+}
+
+XULTreeView.prototype.restoreBranchState =
+function xtv_restorebranch (target, source, recurse)
+{
+ for (var i in source)
+ {
+ if (typeof source[i] == "object")
+ {
+ var name = source[i].name;
+ var len = target.length;
+ for (var j = 0; j < len; ++j)
+ {
+ if (target[j]._colValues["col-0"] == name &&
+ "childData" in target[j])
+ {
+ //dd ("opening " + name);
+ target[j].open();
+ if (recurse)
+ {
+ this.restoreBranchState (target[j].childData,
+ source[i], true);
+ }
+ break;
+ }
+ }
+ }
+ }
+}
+
+/* scroll the line specified by |line| to the center of the tree */
+XULTreeView.prototype.centerLine =
+function xtv_ctrln (line)
+{
+ var first = this.tree.getFirstVisibleRow();
+ var last = this.tree.getLastVisibleRow();
+ this.scrollToRow(line - (last - first + 1) / 2);
+}
+
+/*
+ * functions the tree will call to retrieve the list state (nsITreeView.)
+ */
+
+// @internal
+XULTreeView.prototype.__defineGetter__("rowCount", xtv_getRowCount);
+function xtv_getRowCount ()
+{
+ if (!this.childData)
+ return 0;
+
+ return this.childData.visualFootprint;
+}
+
+// @internal
+XULTreeView.prototype.isContainer =
+function xtv_isctr (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+
+ return Boolean(row && ("alwaysHasChildren" in row || "childData" in row));
+}
+
+// @internal
+XULTreeView.prototype.__defineGetter__("selectedIndex", xtv_getsel);
+function xtv_getsel()
+{
+ if (!this.tree || this.tree.view.selection.getRangeCount() < 1)
+ return -1;
+
+ var min = new Object();
+ this.tree.view.selection.getRangeAt(0, min, {});
+ return min.value;
+}
+
+// @internal
+XULTreeView.prototype.__defineSetter__("selectedIndex", xtv_setsel);
+function xtv_setsel(i)
+{
+ this.tree.view.selection.clearSelection();
+ if (i != -1)
+ this.tree.view.selection.timedSelect (i, 500);
+ return i;
+}
+
+// @internal
+XULTreeView.prototype.scrollTo = BasicOView.prototype.scrollTo;
+
+// @internal
+XULTreeView.prototype.isContainerOpen =
+function xtv_isctropen (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ return row && row.isContainerOpen;
+}
+
+// @internal
+XULTreeView.prototype.toggleOpenState =
+function xtv_toggleopen (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row");
+ if (row)
+ {
+ if (row.isContainerOpen)
+ row.close();
+ else
+ row.open();
+ }
+}
+
+// @internal
+XULTreeView.prototype.isContainerEmpty =
+function xtv_isctrempt (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ if ("alwaysHasChildren" in row)
+ return false;
+
+ if (!row || !("childData" in row))
+ return true;
+
+ return !row.childData.length;
+}
+
+// @internal
+XULTreeView.prototype.isSeparator =
+function xtv_isseparator (index)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.getParentIndex =
+function xtv_getpi (index)
+{
+ if (index < 0)
+ return -1;
+
+ var row = this.childData.locateChildByVisualRow (index);
+
+ var rv = row.parentRecord.calculateVisualRow();
+ //dd ("getParentIndex: row " + index + " returning " + rv);
+ return (rv != null) ? rv : -1;
+}
+
+// @internal
+XULTreeView.prototype.hasNextSibling =
+function xtv_hasnxtsib (rowIndex, afterIndex)
+{
+ var row = this.childData.locateChildByVisualRow (rowIndex);
+ return row.childIndex < row.parentRecord.childData.length - 1;
+}
+
+// @internal
+XULTreeView.prototype.getLevel =
+function xtv_getlvl (index)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ if (!row)
+ return 0;
+
+ return row.level;
+}
+
+// @internal
+XULTreeView.prototype.getImageSrc =
+function xtv_getimgsrc (index, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.getProgressMode =
+function xtv_getprgmode (index, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.getCellValue =
+function xtv_getcellval (index, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.getCellText =
+function xtv_getcelltxt (index, col)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row " + index);
+
+ if (typeof col == "object")
+ col = col.id;
+
+ var ary = col.match (/:(.*)/);
+ if (ary)
+ col = ary[1];
+
+ if (row && row._colValues && col in row._colValues)
+ return row._colValues[col];
+ else
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.getCellProperties =
+function xtv_cellprops (row, col, properties)
+{
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.getColumnProperties =
+function xtv_colprops (col, properties)
+{
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.getRowProperties =
+function xtv_rowprops (index, properties)
+{
+ return "";
+}
+
+// @internal
+XULTreeView.prototype.isSorted =
+function xtv_issorted (index)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.canDrop =
+function xtv_drop (index, orientation)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row " + index);
+ return (row && ("canDrop" in row) && row.canDrop(orientation));
+}
+
+// @internal
+XULTreeView.prototype.drop =
+function xtv_drop (index, orientation)
+{
+ var row = this.childData.locateChildByVisualRow (index);
+ //ASSERT(row, "bogus row " + index);
+ return (row && ("drop" in row) && row.drop(orientation));
+}
+
+// @internal
+XULTreeView.prototype.setTree =
+function xtv_seto (tree)
+{
+ this.childData.invalidateCache();
+ this.tree = tree;
+}
+
+// @internal
+XULTreeView.prototype.cycleHeader =
+function xtv_cyclehdr (col)
+{
+}
+
+// @internal
+XULTreeView.prototype.selectionChanged =
+function xtv_selchg ()
+{
+}
+
+// @internal
+XULTreeView.prototype.cycleCell =
+function xtv_cyclecell (row, col)
+{
+}
+
+// @internal
+XULTreeView.prototype.isEditable =
+function xtv_isedit (row, col)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.isSelectable =
+function xtv_isselect (row, col)
+{
+ return false;
+}
+
+// @internal
+XULTreeView.prototype.setCellValue =
+function xtv_setct (row, col, value)
+{
+}
+
+// @internal
+XULTreeView.prototype.setCellText =
+function xtv_setct (row, col, value)
+{
+}
+
+// @internal
+XULTreeView.prototype.performAction =
+function xtv_pact (action)
+{
+}
+
+// @internal
+XULTreeView.prototype.performActionOnRow =
+function xtv_pactrow (action)
+{
+}
+
+// @internal
+XULTreeView.prototype.performActionOnCell =
+function xtv_pactcell (action)
+{
+}
+
+// @internal
+XULTreeView.prototype.onRouteFocus =
+function xtv_rfocus (event)
+{
+ if ("onFocus" in this)
+ this.onFocus(event);
+}
+
+// @internal
+XULTreeView.prototype.onRouteBlur =
+function xtv_rblur (event)
+{
+ if ("onBlur" in this)
+ this.onBlur(event);
+}
+
+// @internal
+XULTreeView.prototype.onRouteDblClick =
+function xtv_rdblclick (event)
+{
+ if (!("onRowCommand" in this) || event.target.localName != "treechildren")
+ return;
+
+ var rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ var rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+}
+
+// @internal
+XULTreeView.prototype.onRouteKeyPress =
+function xtv_rkeypress (event)
+{
+ var rec;
+ var rowIndex;
+
+ if ("onRowCommand" in this && (event.keyCode == 13 || event.charCode == 32))
+ {
+ if (!this.selection)
+ return;
+
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex == -1 || rowIndex > this.rowCount)
+ return;
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+
+ this.onRowCommand(rec, event);
+ }
+ else if ("onKeyPress" in this)
+ {
+ rowIndex = this.tree.view.selection.currentIndex;
+ if (rowIndex != -1 && rowIndex < this.rowCount)
+ {
+ rec = this.childData.locateChildByVisualRow(rowIndex);
+ if (!rec)
+ {
+ ASSERT (0, "bogus row index " + rowIndex);
+ return;
+ }
+ }
+ else
+ {
+ rec = null;
+ }
+
+ this.onKeyPress(rec, event);
+ }
+}
+
+/******************************************************************************/
+
+function xtv_formatRecord (rec, indent)
+{
+ var str = "";
+
+ for (var i in rec._colValues)
+ str += rec._colValues[i] + ", ";
+
+ str += "[";
+
+ str += rec.calculateVisualRow() + ", ";
+ str += rec.childIndex + ", ";
+ str += rec.level + ", ";
+ str += rec.visualFootprint + ", ";
+ str += rec.isHidden + "]";
+
+ return (indent + str);
+}
+
+function xtv_formatBranch (rec, indent, recurse)
+{
+ var str = "";
+ for (var i = 0; i < rec.childData.length; ++i)
+ {
+ str += xtv_formatRecord (rec.childData[i], indent) + "\n";
+ if (recurse)
+ {
+ if ("childData" in rec.childData[i])
+ str += xtv_formatBranch(rec.childData[i], indent + " ",
+ --recurse);
+ }
+ }
+
+ return str;
+}
+