Revision control

Copy as Markdown

Other Tools

/* -*- 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 (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 (aryNames) {
this.columnNames = {};
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 (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;
} else if (line < half_viz) {
/* lines before this can't be centered without causing the tree
* to attempt to display negative rows. */
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 = {};
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 (row, col, properties) {
return "";
};
BasicOView.prototype.getColumnProperties = function (col, properties) {
return "";
};
BasicOView.prototype.getRowProperties = function (index, properties) {
return "";
};
BasicOView.prototype.isContainer = function (index) {
return false;
};
BasicOView.prototype.isContainerOpen = function (index) {
return false;
};
BasicOView.prototype.isContainerEmpty = function (index) {
return false;
};
BasicOView.prototype.isSeparator = function (index) {
return false;
};
BasicOView.prototype.isSorted = function (index) {
return false;
};
BasicOView.prototype.canDrop = function (index, orientation) {
return false;
};
BasicOView.prototype.drop = function (index, orientation) {
return false;
};
BasicOView.prototype.getParentIndex = function (index) {
if (index < 0) {
return -1;
}
return 0;
};
BasicOView.prototype.hasNextSibling = function (rowIndex, afterIndex) {
return afterIndex < this.rowCount - 1;
};
BasicOView.prototype.getLevel = function (index) {
return 0;
};
BasicOView.prototype.getImageSrc = function (row, col) {};
BasicOView.prototype.getProgressMode = function (row, col) {};
BasicOView.prototype.getCellValue = function (row, col) {};
BasicOView.prototype.getCellText = function (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 (tree) {
this.tree = tree;
};
BasicOView.prototype.toggleOpenState = function (index) {};
BasicOView.prototype.cycleHeader = function (col) {};
BasicOView.prototype.selectionChanged = function () {};
BasicOView.prototype.cycleCell = function (row, col) {};
BasicOView.prototype.isEditable = function (row, col) {
return false;
};
BasicOView.prototype.isSelectable = function (row, col) {
return false;
};
BasicOView.prototype.setCellValue = function (row, col, value) {};
BasicOView.prototype.setCellText = function (row, col, value) {};
BasicOView.prototype.onRouteFocus = function (event) {
if ("onFocus" in this) {
this.onFocus(event);
}
};
BasicOView.prototype.onRouteBlur = function (event) {
if ("onBlur" in this) {
this.onBlur(event);
}
};
BasicOView.prototype.onRouteDblClick = function (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 (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 (action) {};
BasicOView.prototype.performActionOnRow = function (action) {};
BasicOView.prototype.performActionOnCell = function (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 () {
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 (
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 = {};
}
this.__defineGetter__(propertyName, xtvr_getValueShim);
this.__defineSetter__(propertyName, xtvr_setValueShim);
};
XULTreeViewRecord.prototype.setColumnPropertyValue = function (colID, value) {
this._colValues[colID] = value;
};
/*
* set the default sort column and reSort.
*/
XULTreeViewRecord.prototype.setSortColumn = function (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 (dir) {
this._share.sortDirection = dir;
};
/*
* invalidate this row in the tree
*/
XULTreeViewRecord.prototype.invalidate = function () {
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 () {
this._share.rowCache = {};
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 (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 (always) {
if (!("childData" in this)) {
this.childData = [];
}
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 (child) {
if (!isinstance(child, XULTreeViewRecord)) {
throw Components.Exception("", Cr.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 (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 (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;
this.childData.splice(index, 1);
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 (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 () {
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 () {
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 () {
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 () {
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 (
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 () {
/* 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 (
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);
} else if (targetRow <= childStart + child.visualFootprint) {
/* if this kid contains the index, ask *it* to find the record */
/* 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;
};
// @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 () {
/* don't do this on a root node */
};
XTRootRecord.prototype.calculateVisualRow = function () {
return null;
};
XTRootRecord.prototype.reSort = function () {
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 (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);
} else if (targetRow <= childStart + child.visualFootprint) {
/* if this kid contains the index, ask *it* to find the record */
/* 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 (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 = {};
}
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 () {
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 () {
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 (target, source, recurse) {
var len = source.length;
for (var i = 0; i < len; ++i) {
if (source[i].isContainerOpen) {
target[i] = {};
target[i].name = source[i]._colValues["col-0"];
if (recurse) {
this.saveBranchState(target[i], source[i].childData, true);
}
}
}
};
XULTreeView.prototype.restoreBranchState = function (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 (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 (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 = {};
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 (index) {
var row = this.childData.locateChildByVisualRow(index);
return row && row.isContainerOpen;
};
// @internal
XULTreeView.prototype.toggleOpenState = function (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 (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 (index) {
return false;
};
// @internal
XULTreeView.prototype.getParentIndex = function (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 (rowIndex, afterIndex) {
var row = this.childData.locateChildByVisualRow(rowIndex);
return row.childIndex < row.parentRecord.childData.length - 1;
};
// @internal
XULTreeView.prototype.getLevel = function (index) {
var row = this.childData.locateChildByVisualRow(index);
if (!row) {
return 0;
}
return row.level;
};
// @internal
XULTreeView.prototype.getImageSrc = function (index, col) {};
// @internal
XULTreeView.prototype.getProgressMode = function (index, col) {};
// @internal
XULTreeView.prototype.getCellValue = function (index, col) {};
// @internal
XULTreeView.prototype.getCellText = function (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];
}
return "";
};
// @internal
XULTreeView.prototype.getCellProperties = function (row, col, properties) {
return "";
};
// @internal
XULTreeView.prototype.getColumnProperties = function (col, properties) {
return "";
};
// @internal
XULTreeView.prototype.getRowProperties = function (index, properties) {
return "";
};
// @internal
XULTreeView.prototype.isSorted = function (index) {
return false;
};
// @internal
XULTreeView.prototype.canDrop = function (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 (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 (tree) {
this.childData.invalidateCache();
this.tree = tree;
};
// @internal
XULTreeView.prototype.cycleHeader = function (col) {};
// @internal
XULTreeView.prototype.selectionChanged = function () {};
// @internal
XULTreeView.prototype.cycleCell = function (row, col) {};
// @internal
XULTreeView.prototype.isEditable = function (row, col) {
return false;
};
// @internal
XULTreeView.prototype.isSelectable = function (row, col) {
return false;
};
// @internal
XULTreeView.prototype.setCellValue = function (row, col, value) {};
// @internal
XULTreeView.prototype.setCellText = function (row, col, value) {};
// @internal
XULTreeView.prototype.performAction = function (action) {};
// @internal
XULTreeView.prototype.performActionOnRow = function (action) {};
// @internal
XULTreeView.prototype.performActionOnCell = function (action) {};
// @internal
XULTreeView.prototype.onRouteFocus = function (event) {
if ("onFocus" in this) {
this.onFocus(event);
}
};
// @internal
XULTreeView.prototype.onRouteBlur = function (event) {
if ("onBlur" in this) {
this.onBlur(event);
}
};
// @internal
XULTreeView.prototype.onRouteDblClick = function (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 (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;
}