gecko/browser/base/content/sanitizeDialog.js
Ehsan Akhgari 45fe6d3ae2 Bug 722872 - Part 1: Add nsITransferable::Init(nsILoadContext*), enforce that it's called in debug builds, and add nsIDOMDocument* arguments to nsIClipboardHelper methods; r=roc
This patch does the following:

* It adds nsITransferable::Init(nsILoadContext*).  The load context
  might be null, which means that the transferable is non-private, but
  if it's non-null, we extract the boolean value for the privacy mode
  and store it in the transferable.
* It adds checks in debug builds to make sure that Init is always
  called, in form of fatal assertions.
* It adds nsIDOMDocument* agruments to nsIClipboardHelper methods which
  represent the document that the string is coming from.
  nsIClipboardHelper implementation internally gets the nsILoadContext
  from that and passes it on to the transferable upon creation.  The
  reason that I did this was that nsIClipboardHelper is supposed to be a
  high-level helper, and in most of its call sites, we have easy access
  to a document object.
* It modifies all of the call sites of the above interfaces according to
  this change.
* It adds a GetLoadContext helper to nsIDocument to help with changing
  the call sites.
2012-04-16 22:14:01 -04:00

898 lines
29 KiB
JavaScript

/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
const Cc = Components.classes;
const Ci = Components.interfaces;
var gSanitizePromptDialog = {
get bundleBrowser()
{
if (!this._bundleBrowser)
this._bundleBrowser = document.getElementById("bundleBrowser");
return this._bundleBrowser;
},
get selectedTimespan()
{
var durList = document.getElementById("sanitizeDurationChoice");
return parseInt(durList.value);
},
get sanitizePreferences()
{
if (!this._sanitizePreferences) {
this._sanitizePreferences =
document.getElementById("sanitizePreferences");
}
return this._sanitizePreferences;
},
get warningBox()
{
return document.getElementById("sanitizeEverythingWarningBox");
},
init: function ()
{
// This is used by selectByTimespan() to determine if the window has loaded.
this._inited = true;
var s = new Sanitizer();
s.prefDomain = "privacy.cpd.";
let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
for (let i = 0; i < sanitizeItemList.length; i++) {
let prefItem = sanitizeItemList[i];
let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
if (!s.canClearItem(name)) {
prefItem.preference = null;
prefItem.checked = false;
prefItem.disabled = true;
}
}
document.documentElement.getButton("accept").label =
this.bundleBrowser.getString("sanitizeButtonOK");
if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
this.prepareWarning();
this.warningBox.hidden = false;
document.title =
this.bundleBrowser.getString("sanitizeDialog2.everything.title");
}
else
this.warningBox.hidden = true;
},
selectByTimespan: function ()
{
// This method is the onselect handler for the duration dropdown. As a
// result it's called a couple of times before onload calls init().
if (!this._inited)
return;
var warningBox = this.warningBox;
// If clearing everything
if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
this.prepareWarning();
if (warningBox.hidden) {
warningBox.hidden = false;
window.resizeBy(0, warningBox.boxObject.height);
}
window.document.title =
this.bundleBrowser.getString("sanitizeDialog2.everything.title");
return;
}
// If clearing a specific time range
if (!warningBox.hidden) {
window.resizeBy(0, -warningBox.boxObject.height);
warningBox.hidden = true;
}
window.document.title =
window.document.documentElement.getAttribute("noneverythingtitle");
},
sanitize: function ()
{
// Update pref values before handing off to the sanitizer (bug 453440)
this.updatePrefs();
var s = new Sanitizer();
s.prefDomain = "privacy.cpd.";
s.range = Sanitizer.getClearRange(this.selectedTimespan);
s.ignoreTimespan = !s.range;
try {
s.sanitize();
} catch (er) {
Components.utils.reportError("Exception during sanitize: " + er);
}
return true;
},
/**
* If the panel that displays a warning when the duration is "Everything" is
* not set up, sets it up. Otherwise does nothing.
*
* @param aDontShowItemList Whether only the warning message should be updated.
* True means the item list visibility status should not
* be changed.
*/
prepareWarning: function (aDontShowItemList) {
// If the date and time-aware locale warning string is ever used again,
// initialize it here. Currently we use the no-visits warning string,
// which does not include date and time. See bug 480169 comment 48.
var warningStringID;
if (this.hasNonSelectedItems()) {
warningStringID = "sanitizeSelectedWarning";
if (!aDontShowItemList)
this.showItemList();
}
else {
warningStringID = "sanitizeEverythingWarning2";
}
var warningDesc = document.getElementById("sanitizeEverythingWarning");
warningDesc.textContent =
this.bundleBrowser.getString(warningStringID);
},
/**
* Called when the value of a preference element is synced from the actual
* pref. Enables or disables the OK button appropriately.
*/
onReadGeneric: function ()
{
var found = false;
// Find any other pref that's checked and enabled.
var i = 0;
while (!found && i < this.sanitizePreferences.childNodes.length) {
var preference = this.sanitizePreferences.childNodes[i];
found = !!preference.value &&
!preference.disabled;
i++;
}
try {
document.documentElement.getButton("accept").disabled = !found;
}
catch (e) { }
// Update the warning prompt if needed
this.prepareWarning(true);
return undefined;
},
/**
* Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
* Because the type of this prefwindow is "child" -- and that's needed because
* without it the dialog has no OK and Cancel buttons -- the prefs are not
* updated on dialogaccept on platforms that don't support instant-apply
* (i.e., Windows). We must therefore manually set the prefs from their
* corresponding preference elements.
*/
updatePrefs : function ()
{
var tsPref = document.getElementById("privacy.sanitize.timeSpan");
Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan);
// Keep the pref for the download history in sync with the history pref.
document.getElementById("privacy.cpd.downloads").value =
document.getElementById("privacy.cpd.history").value;
// Now manually set the prefs from their corresponding preference
// elements.
var prefs = this.sanitizePreferences.rootBranch;
for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
var p = this.sanitizePreferences.childNodes[i];
prefs.setBoolPref(p.name, p.value);
}
},
/**
* Check if all of the history items have been selected like the default status.
*/
hasNonSelectedItems: function () {
let checkboxes = document.querySelectorAll("#itemList > [preference]");
for (let i = 0; i < checkboxes.length; ++i) {
let pref = document.getElementById(checkboxes[i].getAttribute("preference"));
if (!pref.value)
return true;
}
return false;
},
/**
* Show the history items list.
*/
showItemList: function () {
var itemList = document.getElementById("itemList");
var expanderButton = document.getElementById("detailsExpander");
if (itemList.collapsed) {
expanderButton.className = "expander-up";
itemList.setAttribute("collapsed", "false");
if (document.documentElement.boxObject.height)
window.resizeBy(0, itemList.boxObject.height);
}
},
/**
* Hide the history items list.
*/
hideItemList: function () {
var itemList = document.getElementById("itemList");
var expanderButton = document.getElementById("detailsExpander");
if (!itemList.collapsed) {
expanderButton.className = "expander-down";
window.resizeBy(0, -itemList.boxObject.height);
itemList.setAttribute("collapsed", "true");
}
},
/**
* Called by the item list expander button to toggle the list's visibility.
*/
toggleItemList: function ()
{
var itemList = document.getElementById("itemList");
if (itemList.collapsed)
this.showItemList();
else
this.hideItemList();
}
#ifdef CRH_DIALOG_TREE_VIEW
// A duration value; used in the same context as Sanitizer.TIMESPAN_HOUR,
// Sanitizer.TIMESPAN_2HOURS, et al. This should match the value attribute
// of the sanitizeDurationCustom menuitem.
get TIMESPAN_CUSTOM()
{
return -1;
},
get placesTree()
{
if (!this._placesTree)
this._placesTree = document.getElementById("placesTree");
return this._placesTree;
},
init: function ()
{
// This is used by selectByTimespan() to determine if the window has loaded.
this._inited = true;
var s = new Sanitizer();
s.prefDomain = "privacy.cpd.";
let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
for (let i = 0; i < sanitizeItemList.length; i++) {
let prefItem = sanitizeItemList[i];
let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
if (!s.canClearItem(name)) {
prefItem.preference = null;
prefItem.checked = false;
prefItem.disabled = true;
}
}
document.documentElement.getButton("accept").label =
this.bundleBrowser.getString("sanitizeButtonOK");
this.selectByTimespan();
},
/**
* Sets up the hashes this.durationValsToRows, which maps duration values
* to rows in the tree, this.durationRowsToVals, which maps rows in
* the tree to duration values, and this.durationStartTimes, which maps
* duration values to their corresponding start times.
*/
initDurationDropdown: function ()
{
// First, calculate the start times for each duration.
this.durationStartTimes = {};
var durVals = [];
var durPopup = document.getElementById("sanitizeDurationPopup");
var durMenuitems = durPopup.childNodes;
for (let i = 0; i < durMenuitems.length; i++) {
let durMenuitem = durMenuitems[i];
let durVal = parseInt(durMenuitem.value);
if (durMenuitem.localName === "menuitem" &&
durVal !== Sanitizer.TIMESPAN_EVERYTHING &&
durVal !== this.TIMESPAN_CUSTOM) {
durVals.push(durVal);
let durTimes = Sanitizer.getClearRange(durVal);
this.durationStartTimes[durVal] = durTimes[0];
}
}
// Sort the duration values ascending. Because one tree index can map to
// more than one duration, this ensures that this.durationRowsToVals maps
// a row index to the largest duration possible in the code below.
durVals.sort();
// Now calculate the rows in the tree of the durations' start times. For
// each duration, we are looking for the node in the tree whose time is the
// smallest time greater than or equal to the duration's start time.
this.durationRowsToVals = {};
this.durationValsToRows = {};
var view = this.placesTree.view;
// For all rows in the tree except the grippy row...
for (let i = 0; i < view.rowCount - 1; i++) {
let unfoundDurVals = [];
let nodeTime = view.QueryInterface(Ci.nsINavHistoryResultTreeViewer).
nodeForTreeIndex(i).time;
// For all durations whose rows have not yet been found in the tree, see
// if index i is their index. An index may map to more than one duration,
// in which case the final duration (the largest) wins.
for (let j = 0; j < durVals.length; j++) {
let durVal = durVals[j];
let durStartTime = this.durationStartTimes[durVal];
if (nodeTime < durStartTime) {
this.durationValsToRows[durVal] = i - 1;
this.durationRowsToVals[i - 1] = durVal;
}
else
unfoundDurVals.push(durVal);
}
durVals = unfoundDurVals;
}
// If any durations were not found above, then every node in the tree has a
// time greater than or equal to the duration. In other words, those
// durations include the entire tree (except the grippy row).
for (let i = 0; i < durVals.length; i++) {
let durVal = durVals[i];
this.durationValsToRows[durVal] = view.rowCount - 2;
this.durationRowsToVals[view.rowCount - 2] = durVal;
}
},
/**
* If the Places tree is not set up, sets it up. Otherwise does nothing.
*/
ensurePlacesTreeIsInited: function ()
{
if (this._placesTreeIsInited)
return;
this._placesTreeIsInited = true;
// Either "Last Four Hours" or "Today" will have the most history. If
// it's been more than 4 hours since today began, "Today" will. Otherwise
// "Last Four Hours" will.
var times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_TODAY);
// If it's been less than 4 hours since today began, use the past 4 hours.
if (times[1] - times[0] < 14400000000) { // 4*60*60*1000000
times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_4HOURS);
}
var histServ = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsINavHistoryService);
var query = histServ.getNewQuery();
query.beginTimeReference = query.TIME_RELATIVE_EPOCH;
query.beginTime = times[0];
query.endTimeReference = query.TIME_RELATIVE_EPOCH;
query.endTime = times[1];
var opts = histServ.getNewQueryOptions();
opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
opts.queryType = opts.QUERY_TYPE_HISTORY;
var result = histServ.executeQuery(query, opts);
var view = gContiguousSelectionTreeHelper.setTree(this.placesTree,
new PlacesTreeView());
result.addObserver(view, false);
this.initDurationDropdown();
},
/**
* Called on select of the duration dropdown and when grippyMoved() sets a
* duration based on the location of the grippy row. Selects all the nodes in
* the tree that are contained in the selected duration. If clearing
* everything, the warning panel is shown instead.
*/
selectByTimespan: function ()
{
// This method is the onselect handler for the duration dropdown. As a
// result it's called a couple of times before onload calls init().
if (!this._inited)
return;
var durDeck = document.getElementById("durationDeck");
var durList = document.getElementById("sanitizeDurationChoice");
var durVal = parseInt(durList.value);
var durCustom = document.getElementById("sanitizeDurationCustom");
// If grippy row is not at a duration boundary, show the custom menuitem;
// otherwise, hide it. Since the user cannot specify a custom duration by
// using the dropdown, this conditional is true only when this method is
// called onselect from grippyMoved(), so no selection need be made.
if (durVal === this.TIMESPAN_CUSTOM) {
durCustom.hidden = false;
return;
}
durCustom.hidden = true;
// If clearing everything, show the warning and change the dialog's title.
if (durVal === Sanitizer.TIMESPAN_EVERYTHING) {
this.prepareWarning();
durDeck.selectedIndex = 1;
window.document.title =
this.bundleBrowser.getString("sanitizeDialog2.everything.title");
document.documentElement.getButton("accept").disabled = false;
return;
}
// Otherwise -- if clearing a specific time range -- select that time range
// in the tree.
this.ensurePlacesTreeIsInited();
durDeck.selectedIndex = 0;
window.document.title =
window.document.documentElement.getAttribute("noneverythingtitle");
var durRow = this.durationValsToRows[durVal];
gContiguousSelectionTreeHelper.rangedSelect(durRow);
gContiguousSelectionTreeHelper.scrollToGrippy();
// If duration is empty (there are no selected rows), disable the dialog's
// OK button.
document.documentElement.getButton("accept").disabled = durRow < 0;
},
sanitize: function ()
{
// Update pref values before handing off to the sanitizer (bug 453440)
this.updatePrefs();
var s = new Sanitizer();
s.prefDomain = "privacy.cpd.";
var durList = document.getElementById("sanitizeDurationChoice");
var durValue = parseInt(durList.value);
s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING;
// Set the sanitizer's time range if we're not clearing everything.
if (!s.ignoreTimespan) {
// If user selected a custom timespan, use that.
if (durValue === this.TIMESPAN_CUSTOM) {
var view = this.placesTree.view;
var now = Date.now() * 1000;
// We disable the dialog's OK button if there's no selection, but we'll
// handle that case just in... case.
if (view.selection.getRangeCount() === 0)
s.range = [now, now];
else {
var startIndexRef = {};
// Tree sorted by visit date DEscending, so start time time comes last.
view.selection.getRangeAt(0, {}, startIndexRef);
view.QueryInterface(Ci.nsINavHistoryResultTreeViewer);
var startNode = view.nodeForTreeIndex(startIndexRef.value);
s.range = [startNode.time, now];
}
}
// Otherwise use the predetermined range.
else
s.range = [this.durationStartTimes[durValue], Date.now() * 1000];
}
try {
s.sanitize();
} catch (er) {
Components.utils.reportError("Exception during sanitize: " + er);
}
return true;
},
/**
* In order to mark the custom Places tree view and its nsINavHistoryResult
* for garbage collection, we need to break the reference cycle between the
* two.
*/
unload: function ()
{
let result = this.placesTree.getResult();
result.removeObserver(this.placesTree.view);
this.placesTree.view = null;
},
/**
* Called when the user moves the grippy by dragging it, clicking in the tree,
* or on keypress. Updates the duration dropdown so that it displays the
* appropriate specific or custom duration.
*
* @param aEventName
* The name of the event whose handler called this method, e.g.,
* "ondragstart", "onkeypress", etc.
* @param aEvent
* The event captured in the event handler.
*/
grippyMoved: function (aEventName, aEvent)
{
gContiguousSelectionTreeHelper[aEventName](aEvent);
var lastSelRow = gContiguousSelectionTreeHelper.getGrippyRow() - 1;
var durList = document.getElementById("sanitizeDurationChoice");
var durValue = parseInt(durList.value);
// Multiple durations can map to the same row. Don't update the dropdown
// if the current duration is valid for lastSelRow.
if ((durValue !== this.TIMESPAN_CUSTOM ||
lastSelRow in this.durationRowsToVals) &&
(durValue === this.TIMESPAN_CUSTOM ||
this.durationValsToRows[durValue] !== lastSelRow)) {
// Setting durList.value causes its onselect handler to fire, which calls
// selectByTimespan().
if (lastSelRow in this.durationRowsToVals)
durList.value = this.durationRowsToVals[lastSelRow];
else
durList.value = this.TIMESPAN_CUSTOM;
}
// If there are no selected rows, disable the dialog's OK button.
document.documentElement.getButton("accept").disabled = lastSelRow < 0;
}
#endif
};
#ifdef CRH_DIALOG_TREE_VIEW
/**
* A helper for handling contiguous selection in the tree.
*/
var gContiguousSelectionTreeHelper = {
/**
* Gets the tree associated with this helper.
*/
get tree()
{
return this._tree;
},
/**
* Sets the tree that this module handles. The tree is assigned a new view
* that is equipped to handle contiguous selection. You can pass in an
* object that will be used as the prototype of the new view. Otherwise
* the tree's current view is used as the prototype.
*
* @param aTreeElement
* The tree element
* @param aProtoTreeView
* If defined, this will be used as the prototype of the tree's new
* view
* @return The new view
*/
setTree: function CSTH_setTree(aTreeElement, aProtoTreeView)
{
this._tree = aTreeElement;
var newView = this._makeTreeView(aProtoTreeView || aTreeElement.view);
aTreeElement.view = newView;
return newView;
},
/**
* The index of the row that the grippy occupies. Note that the index of the
* last selected row is getGrippyRow() - 1. If getGrippyRow() is 0, then
* no selection exists.
*
* @return The row index of the grippy
*/
getGrippyRow: function CSTH_getGrippyRow()
{
var sel = this.tree.view.selection;
var rangeCount = sel.getRangeCount();
if (rangeCount === 0)
return 0;
if (rangeCount !== 1) {
throw "contiguous selection tree helper: getGrippyRow called with " +
"multiple selection ranges";
}
var max = {};
sel.getRangeAt(0, {}, max);
return max.value + 1;
},
/**
* Helper function for the dragover event. Your dragover listener should
* call this. It updates the selection in the tree under the mouse.
*
* @param aEvent
* The observed dragover event
*/
ondragover: function CSTH_ondragover(aEvent)
{
// Without this when dragging on Windows the mouse cursor is a "no" sign.
// This makes it a drop symbol.
var ds = Cc["@mozilla.org/widget/dragservice;1"].
getService(Ci.nsIDragService).
getCurrentSession();
ds.canDrop = true;
ds.dragAction = 0;
var tbo = this.tree.treeBoxObject;
aEvent.QueryInterface(Ci.nsIDOMMouseEvent);
var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
if (hoverRow < 0)
return;
this.rangedSelect(hoverRow - 1);
},
/**
* Helper function for the dragstart event. Your dragstart listener should
* call this. It starts a drag session.
*
* @param aEvent
* The observed dragstart event
*/
ondragstart: function CSTH_ondragstart(aEvent)
{
var tbo = this.tree.treeBoxObject;
var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
if (clickedRow !== this.getGrippyRow())
return;
// This part is a hack. What we really want is a grab and slide, not
// drag and drop. Start a move drag session with dummy data and a
// dummy region. Set the region's coordinates to (Infinity, Infinity)
// so it's drawn offscreen and its size to (1, 1).
var arr = Cc["@mozilla.org/supports-array;1"].
createInstance(Ci.nsISupportsArray);
var trans = Cc["@mozilla.org/widget/transferable;1"].
createInstance(Ci.nsITransferable);
trans.init(null);
trans.setTransferData('dummy-flavor', null, 0);
arr.AppendElement(trans);
var reg = Cc["@mozilla.org/gfx/region;1"].
createInstance(Ci.nsIScriptableRegion);
reg.setToRect(Infinity, Infinity, 1, 1);
var ds = Cc["@mozilla.org/widget/dragservice;1"].
getService(Ci.nsIDragService);
ds.invokeDragSession(aEvent.target, arr, reg, ds.DRAGDROP_ACTION_MOVE);
},
/**
* Helper function for the keypress event. Your keypress listener should
* call this. Users can use Up, Down, Page Up/Down, Home, and End to move
* the bottom of the selection window.
*
* @param aEvent
* The observed keypress event
*/
onkeypress: function CSTH_onkeypress(aEvent)
{
var grippyRow = this.getGrippyRow();
var tbo = this.tree.treeBoxObject;
var rangeEnd;
switch (aEvent.keyCode) {
case aEvent.DOM_VK_HOME:
rangeEnd = 0;
break;
case aEvent.DOM_VK_PAGE_UP:
rangeEnd = grippyRow - tbo.getPageLength();
break;
case aEvent.DOM_VK_UP:
rangeEnd = grippyRow - 2;
break;
case aEvent.DOM_VK_DOWN:
rangeEnd = grippyRow;
break;
case aEvent.DOM_VK_PAGE_DOWN:
rangeEnd = grippyRow + tbo.getPageLength();
break;
case aEvent.DOM_VK_END:
rangeEnd = this.tree.view.rowCount - 2;
break;
default:
return;
break;
}
aEvent.stopPropagation();
// First, clip rangeEnd. this.rangedSelect() doesn't clip the range if we
// select past the ends of the tree.
if (rangeEnd < 0)
rangeEnd = -1;
else if (this.tree.view.rowCount - 2 < rangeEnd)
rangeEnd = this.tree.view.rowCount - 2;
// Next, (de)select.
this.rangedSelect(rangeEnd);
// Finally, scroll the tree. We always want one row above and below the
// grippy row to be visible if possible.
if (rangeEnd < grippyRow) // moved up
tbo.ensureRowIsVisible(rangeEnd < 0 ? 0 : rangeEnd);
else { // moved down
if (rangeEnd + 2 < this.tree.view.rowCount)
tbo.ensureRowIsVisible(rangeEnd + 2);
else if (rangeEnd + 1 < this.tree.view.rowCount)
tbo.ensureRowIsVisible(rangeEnd + 1);
}
},
/**
* Helper function for the mousedown event. Your mousedown listener should
* call this. Users can click on individual rows to make the selection
* jump to them immediately.
*
* @param aEvent
* The observed mousedown event
*/
onmousedown: function CSTH_onmousedown(aEvent)
{
var tbo = this.tree.treeBoxObject;
var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount)
return;
if (clickedRow < this.getGrippyRow())
this.rangedSelect(clickedRow);
else if (clickedRow > this.getGrippyRow())
this.rangedSelect(clickedRow - 1);
},
/**
* Selects range [0, aEndRow] in the tree. The grippy row will then be at
* index aEndRow + 1. aEndRow may be -1, in which case the selection is
* cleared and the grippy row will be at index 0.
*
* @param aEndRow
* The range [0, aEndRow] will be selected.
*/
rangedSelect: function CSTH_rangedSelect(aEndRow)
{
var tbo = this.tree.treeBoxObject;
if (aEndRow < 0)
this.tree.view.selection.clearSelection();
else
this.tree.view.selection.rangedSelect(0, aEndRow, false);
tbo.invalidateRange(tbo.getFirstVisibleRow(), tbo.getLastVisibleRow());
},
/**
* Scrolls the tree so that the grippy row is in the center of the view.
*/
scrollToGrippy: function CSTH_scrollToGrippy()
{
var rowCount = this.tree.view.rowCount;
var tbo = this.tree.treeBoxObject;
var pageLen = tbo.getPageLength() ||
parseInt(this.tree.getAttribute("rows")) ||
10;
// All rows fit on a single page.
if (rowCount <= pageLen)
return;
var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0);
// Grippy row is in first half of first page.
if (scrollToRow < 0)
scrollToRow = 0;
// Grippy row is in last half of last page.
else if (rowCount < scrollToRow + pageLen)
scrollToRow = rowCount - pageLen;
tbo.scrollToRow(scrollToRow);
},
/**
* Creates a new tree view suitable for contiguous selection. If
* aProtoTreeView is specified, it's used as the new view's prototype.
* Otherwise the tree's current view is used as the prototype.
*
* @param aProtoTreeView
* Used as the new view's prototype if specified
*/
_makeTreeView: function CSTH__makeTreeView(aProtoTreeView)
{
var atomServ = Cc["@mozilla.org/atom-service;1"].
getService(Ci.nsIAtomService);
var view = aProtoTreeView;
var that = this;
//XXXadw: When Alex gets the grippy icon done, this may or may not change,
// depending on how we style it.
view.isSeparator = function CSTH_View_isSeparator(aRow)
{
return aRow === that.getGrippyRow();
};
// rowCount includes the grippy row.
view.__defineGetter__("_rowCount", view.__lookupGetter__("rowCount"));
view.__defineGetter__("rowCount",
function CSTH_View_rowCount()
{
return this._rowCount + 1;
});
// This has to do with visual feedback in the view itself, e.g., drawing
// a small line underneath the dropzone. Not what we want.
view.canDrop = function CSTH_View_canDrop() { return false; };
// No clicking headers to sort the tree or sort feedback on columns.
view.cycleHeader = function CSTH_View_cycleHeader() {};
view.sortingChanged = function CSTH_View_sortingChanged() {};
// Override a bunch of methods to account for the grippy row.
view._getCellProperties = view.getCellProperties;
view.getCellProperties =
function CSTH_View_getCellProperties(aRow, aCol, aProps)
{
var grippyRow = that.getGrippyRow();
if (aRow === grippyRow)
aProps.AppendElement(atomServ.getAtom("grippyRow"));
else if (aRow < grippyRow)
this._getCellProperties(aRow, aCol, aProps);
else
this._getCellProperties(aRow - 1, aCol, aProps);
};
view._getRowProperties = view.getRowProperties;
view.getRowProperties =
function CSTH_View_getRowProperties(aRow, aProps)
{
var grippyRow = that.getGrippyRow();
if (aRow === grippyRow)
aProps.AppendElement(atomServ.getAtom("grippyRow"));
else if (aRow < grippyRow)
this._getRowProperties(aRow, aProps);
else
this._getRowProperties(aRow - 1, aProps);
};
view._getCellText = view.getCellText;
view.getCellText =
function CSTH_View_getCellText(aRow, aCol)
{
var grippyRow = that.getGrippyRow();
if (aRow === grippyRow)
return "";
aRow = aRow < grippyRow ? aRow : aRow - 1;
return this._getCellText(aRow, aCol);
};
view._getImageSrc = view.getImageSrc;
view.getImageSrc =
function CSTH_View_getImageSrc(aRow, aCol)
{
var grippyRow = that.getGrippyRow();
if (aRow === grippyRow)
return "";
aRow = aRow < grippyRow ? aRow : aRow - 1;
return this._getImageSrc(aRow, aCol);
};
view.isContainer = function CSTH_View_isContainer(aRow) { return false; };
view.getParentIndex = function CSTH_View_getParentIndex(aRow) { return -1; };
view.getLevel = function CSTH_View_getLevel(aRow) { return 0; };
view.hasNextSibling = function CSTH_View_hasNextSibling(aRow, aAfterIndex)
{
return aRow < this.rowCount - 1;
};
return view;
}
};
#endif