gecko/browser/base/content/sanitizeDialog.js
Drew Willcoxon 410dd5c0a6 Bug 480169 - Clear recent history refresh (sprint)
Item list hidden behind expander/progressive disclosure button, added a warning when clearing all history, added a tree view for selecting a fine-grained timespan.  We decided to remove the tree view at the last minute; code is still there but #ifdef'ed out.
r=mconnor
2009-04-15 10:59:07 -07:00

935 lines
32 KiB
JavaScript

/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Firefox Sanitizer.
*
* The Initial Developer of the Original Code is
* Ben Goodger.
* Portions created by the Initial Developer are Copyright (C) 2005
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Ben Goodger <ben@mozilla.org>
* Giorgio Maone <g.maone@informaction.com>
* Johnathan Nightingale <johnath@mozilla.com>
* Drew Willcoxon <adw@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
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;
this.checkPrefs();
var s = new Sanitizer();
s.prefDomain = "privacy.cpd.";
for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
var preference = this.sanitizePreferences.childNodes[i];
var name = s.getNameFromPreference(preference.name);
if (!s.canClearItem(name))
preference.disabled = true;
}
document.documentElement.getButton("accept").label =
this.bundleBrowser.getString("sanitizeButtonOK");
if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
this.ensureWarningIsInited();
this.warningBox.hidden = false;
}
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.ensureWarningIsInited();
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.
*/
ensureWarningIsInited: function ()
{
if (this._warningIsInited)
return;
this._warningIsInited = true;
// Get the number of items in history and the oldest item.
var histServ = Cc["@mozilla.org/browser/nav-history-service;1"].
getService(Ci.nsINavHistoryService);
var query = histServ.getNewQuery();
var opts = histServ.getNewQueryOptions();
opts.sortingMode = opts.SORT_BY_DATE_ASCENDING;
opts.queryType = opts.QUERY_TYPE_HISTORY;
var result = histServ.executeQuery(query, opts);
result.root.containerOpen = true;
var numItems = result.root.childCount;
var oldestTime = numItems > 0 ? result.root.getChild(0).time : null;
result.root.containerOpen = false;
var warningDesc = document.getElementById("sanitizeEverythingWarning");
warningDesc.textContent =
this.bundleBrowser.getString("sanitizeEverythingNoVisitsWarning");
},
checkPrefs : function ()
{
var prefService = Cc["@mozilla.org/preferences-service;1"].
getService(Ci.nsIPrefService);
var cpdBranch = prefService.getBranch("privacy.cpd.");
// If we don't have defaults for the privacy.cpd branch,
// clone the privacy.item (clear at shutdown) defaults
if (cpdBranch.prefHasUserValue("history"))
return;
var itemBranch = prefService.getBranch("privacy.item.");
var itemCount = { value: 0 };
var itemArray = itemBranch.getChildList("", itemCount);
itemArray.forEach(function (name) {
cpdBranch.setBoolPref(name, itemBranch.getBoolPref(name));
});
},
/**
* Called when the value of a preference element is synced from the actual
* pref. Enables or disables the OK button appropriately.
*/
onReadGeneric: function ()
{
// We don't update the separate history and downloads prefs until
// dialogaccept. So we need to handle the checked state of the combined
// history-downloads checkbox specially.
var combinedCb = document.getElementById("history-downloads-checkbox");
var found = combinedCb.checked;
// 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];
// We took into account history and downloads above; don't do it again.
found = !!preference.value &&
!preference.disabled &&
preference.id !== "privacy.cpd.history" &&
preference.id !== "privacy.cpd.downloads";
i++;
}
try {
document.documentElement.getButton("accept").disabled = !found;
}
catch (e) { }
return undefined;
},
/**
* Called when the values of the history and downloads preference elements are
* synced from the actual prefs. Sets the state of the combined history-
* downloads checkbox appropriately.
*/
onReadHistoryOrDownloads: function ()
{
// Call the common function that will update the accept button
this.onReadGeneric();
var historyPref = document.getElementById("privacy.cpd.history");
var downloadsPref = document.getElementById("privacy.cpd.downloads");
var combinedCb = document.getElementById("history-downloads-checkbox");
combinedCb.disabled = historyPref.disabled && downloadsPref.disabled;
combinedCb.checked = historyPref.value || downloadsPref.value;
},
/**
* 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);
// First set the values of the separate history and downloads pref
// elements based on the combined history-downloads checkbox.
var combinedCbChecked =
document.getElementById("history-downloads-checkbox").checked;
var historyPref = document.getElementById("privacy.cpd.history");
historyPref.value = !historyPref.disabled && combinedCbChecked;
var downloadsPref = document.getElementById("privacy.cpd.downloads");
downloadsPref.value = !downloadsPref.disabled && combinedCbChecked;
// 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);
}
},
/**
* Called by the item list expander button to toggle the list's visibility.
*/
toggleItemList: function ()
{
var itemList = document.getElementById("itemList");
var expanderButton = document.getElementById("detailsExpander");
// Showing item list
if (itemList.collapsed) {
expanderButton.className = "expander-up";
itemList.collapsed = false;
window.resizeBy(0, itemList.boxObject.height);
}
// Hiding item list
else {
expanderButton.className = "expander-down";
window.resizeBy(0, -itemList.boxObject.height);
itemList.collapsed = true;
}
}
#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;
this.checkPrefs();
var s = new Sanitizer();
s.prefDomain = "privacy.cpd.";
for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
var preference = this.sanitizePreferences.childNodes[i];
var name = s.getNameFromPreference(preference.name);
if (!s.canClearItem(name))
preference.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.viewer = view;
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.ensureWarningIsInited();
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 ()
{
var view = this.placesTree.view;
view.QueryInterface(Ci.nsINavHistoryResultViewer).result.viewer = null;
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.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