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