browser/base/content/sanitizeDialog.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 const Cc = Components.classes;
     7 const Ci = Components.interfaces;
     9 var gSanitizePromptDialog = {
    11   get bundleBrowser()
    12   {
    13     if (!this._bundleBrowser)
    14       this._bundleBrowser = document.getElementById("bundleBrowser");
    15     return this._bundleBrowser;
    16   },
    18   get selectedTimespan()
    19   {
    20     var durList = document.getElementById("sanitizeDurationChoice");
    21     return parseInt(durList.value);
    22   },
    24   get sanitizePreferences()
    25   {
    26     if (!this._sanitizePreferences) {
    27       this._sanitizePreferences =
    28         document.getElementById("sanitizePreferences");
    29     }
    30     return this._sanitizePreferences;
    31   },
    33   get warningBox()
    34   {
    35     return document.getElementById("sanitizeEverythingWarningBox");
    36   },
    38   init: function ()
    39   {
    40     // This is used by selectByTimespan() to determine if the window has loaded.
    41     this._inited = true;
    43     var s = new Sanitizer();
    44     s.prefDomain = "privacy.cpd.";
    46     let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
    47     for (let i = 0; i < sanitizeItemList.length; i++) {
    48       let prefItem = sanitizeItemList[i];
    49       let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
    50       s.canClearItem(name, function canClearCallback(aItem, aCanClear, aPrefItem) {
    51         if (!aCanClear) {
    52           aPrefItem.preference = null;
    53           aPrefItem.checked = false;
    54           aPrefItem.disabled = true;
    55         }
    56       }, prefItem);
    57     }
    59     document.documentElement.getButton("accept").label =
    60       this.bundleBrowser.getString("sanitizeButtonOK");
    62     if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
    63       this.prepareWarning();
    64       this.warningBox.hidden = false;
    65       document.title =
    66         this.bundleBrowser.getString("sanitizeDialog2.everything.title");
    67     }
    68     else
    69       this.warningBox.hidden = true;
    70   },
    72   selectByTimespan: function ()
    73   {
    74     // This method is the onselect handler for the duration dropdown.  As a
    75     // result it's called a couple of times before onload calls init().
    76     if (!this._inited)
    77       return;
    79     var warningBox = this.warningBox;
    81     // If clearing everything
    82     if (this.selectedTimespan === Sanitizer.TIMESPAN_EVERYTHING) {
    83       this.prepareWarning();
    84       if (warningBox.hidden) {
    85         warningBox.hidden = false;
    86         window.resizeBy(0, warningBox.boxObject.height);
    87       }
    88       window.document.title =
    89         this.bundleBrowser.getString("sanitizeDialog2.everything.title");
    90       return;
    91     }
    93     // If clearing a specific time range
    94     if (!warningBox.hidden) {
    95       window.resizeBy(0, -warningBox.boxObject.height);
    96       warningBox.hidden = true;
    97     }
    98     window.document.title =
    99       window.document.documentElement.getAttribute("noneverythingtitle");
   100   },
   102   sanitize: function ()
   103   {
   104     // Update pref values before handing off to the sanitizer (bug 453440)
   105     this.updatePrefs();
   106     var s = new Sanitizer();
   107     s.prefDomain = "privacy.cpd.";
   109     s.range = Sanitizer.getClearRange(this.selectedTimespan);
   110     s.ignoreTimespan = !s.range;
   112     // As the sanitize is async, we disable the buttons, update the label on
   113     // the 'accept' button to indicate things are happening and return false -
   114     // once the async operation completes (either with or without errors)
   115     // we close the window.
   116     let docElt = document.documentElement;
   117     let acceptButton = docElt.getButton("accept");
   118     acceptButton.disabled = true;
   119     acceptButton.setAttribute("label",
   120                               this.bundleBrowser.getString("sanitizeButtonClearing"));
   121     docElt.getButton("cancel").disabled = true;
   122     try {
   123       s.sanitize().then(null, Components.utils.reportError)
   124                   .then(() => window.close())
   125                   .then(null, Components.utils.reportError);
   126     } catch (er) {
   127       Components.utils.reportError("Exception during sanitize: " + er);
   128       return true; // We *do* want to close immediately on error.
   129     }
   130     return false;
   131   },
   133   /**
   134    * If the panel that displays a warning when the duration is "Everything" is
   135    * not set up, sets it up.  Otherwise does nothing.
   136    *
   137    * @param aDontShowItemList Whether only the warning message should be updated.
   138    *                          True means the item list visibility status should not
   139    *                          be changed.
   140    */
   141   prepareWarning: function (aDontShowItemList) {
   142     // If the date and time-aware locale warning string is ever used again,
   143     // initialize it here.  Currently we use the no-visits warning string,
   144     // which does not include date and time.  See bug 480169 comment 48.
   146     var warningStringID;
   147     if (this.hasNonSelectedItems()) {
   148       warningStringID = "sanitizeSelectedWarning";
   149       if (!aDontShowItemList)
   150         this.showItemList();
   151     }
   152     else {
   153       warningStringID = "sanitizeEverythingWarning2";
   154     }
   156     var warningDesc = document.getElementById("sanitizeEverythingWarning");
   157     warningDesc.textContent =
   158       this.bundleBrowser.getString(warningStringID);
   159   },
   161   /**
   162    * Called when the value of a preference element is synced from the actual
   163    * pref.  Enables or disables the OK button appropriately.
   164    */
   165   onReadGeneric: function ()
   166   {
   167     var found = false;
   169     // Find any other pref that's checked and enabled.
   170     var i = 0;
   171     while (!found && i < this.sanitizePreferences.childNodes.length) {
   172       var preference = this.sanitizePreferences.childNodes[i];
   174       found = !!preference.value &&
   175               !preference.disabled;
   176       i++;
   177     }
   179     try {
   180       document.documentElement.getButton("accept").disabled = !found;
   181     }
   182     catch (e) { }
   184     // Update the warning prompt if needed
   185     this.prepareWarning(true);
   187     return undefined;
   188   },
   190   /**
   191    * Sanitizer.prototype.sanitize() requires the prefs to be up-to-date.
   192    * Because the type of this prefwindow is "child" -- and that's needed because
   193    * without it the dialog has no OK and Cancel buttons -- the prefs are not
   194    * updated on dialogaccept on platforms that don't support instant-apply
   195    * (i.e., Windows).  We must therefore manually set the prefs from their
   196    * corresponding preference elements.
   197    */
   198   updatePrefs : function ()
   199   {
   200     var tsPref = document.getElementById("privacy.sanitize.timeSpan");
   201     Sanitizer.prefs.setIntPref("timeSpan", this.selectedTimespan);
   203     // Keep the pref for the download history in sync with the history pref.
   204     document.getElementById("privacy.cpd.downloads").value =
   205       document.getElementById("privacy.cpd.history").value;
   207     // Now manually set the prefs from their corresponding preference
   208     // elements.
   209     var prefs = this.sanitizePreferences.rootBranch;
   210     for (let i = 0; i < this.sanitizePreferences.childNodes.length; ++i) {
   211       var p = this.sanitizePreferences.childNodes[i];
   212       prefs.setBoolPref(p.name, p.value);
   213     }
   214   },
   216   /**
   217    * Check if all of the history items have been selected like the default status.
   218    */
   219   hasNonSelectedItems: function () {
   220     let checkboxes = document.querySelectorAll("#itemList > [preference]");
   221     for (let i = 0; i < checkboxes.length; ++i) {
   222       let pref = document.getElementById(checkboxes[i].getAttribute("preference"));
   223       if (!pref.value)
   224         return true;
   225     }
   226     return false;
   227   },
   229   /**
   230    * Show the history items list.
   231    */
   232   showItemList: function () {
   233     var itemList = document.getElementById("itemList");
   234     var expanderButton = document.getElementById("detailsExpander");
   236     if (itemList.collapsed) {
   237       expanderButton.className = "expander-up";
   238       itemList.setAttribute("collapsed", "false");
   239       if (document.documentElement.boxObject.height)
   240         window.resizeBy(0, itemList.boxObject.height);
   241     }
   242   },
   244   /**
   245    * Hide the history items list.
   246    */
   247   hideItemList: function () {
   248     var itemList = document.getElementById("itemList");
   249     var expanderButton = document.getElementById("detailsExpander");
   251     if (!itemList.collapsed) {
   252       expanderButton.className = "expander-down";
   253       window.resizeBy(0, -itemList.boxObject.height);
   254       itemList.setAttribute("collapsed", "true");
   255     }
   256   },
   258   /**
   259    * Called by the item list expander button to toggle the list's visibility.
   260    */
   261   toggleItemList: function ()
   262   {
   263     var itemList = document.getElementById("itemList");
   265     if (itemList.collapsed)
   266       this.showItemList();
   267     else
   268       this.hideItemList();
   269   }
   271 #ifdef CRH_DIALOG_TREE_VIEW
   272   // A duration value; used in the same context as Sanitizer.TIMESPAN_HOUR,
   273   // Sanitizer.TIMESPAN_2HOURS, et al.  This should match the value attribute
   274   // of the sanitizeDurationCustom menuitem.
   275   get TIMESPAN_CUSTOM()
   276   {
   277     return -1;
   278   },
   280   get placesTree()
   281   {
   282     if (!this._placesTree)
   283       this._placesTree = document.getElementById("placesTree");
   284     return this._placesTree;
   285   },
   287   init: function ()
   288   {
   289     // This is used by selectByTimespan() to determine if the window has loaded.
   290     this._inited = true;
   292     var s = new Sanitizer();
   293     s.prefDomain = "privacy.cpd.";
   295     let sanitizeItemList = document.querySelectorAll("#itemList > [preference]");
   296     for (let i = 0; i < sanitizeItemList.length; i++) {
   297       let prefItem = sanitizeItemList[i];
   298       let name = s.getNameFromPreference(prefItem.getAttribute("preference"));
   299       s.canClearItem(name, function canClearCallback(aCanClear) {
   300         if (!aCanClear) {
   301           prefItem.preference = null;
   302           prefItem.checked = false;
   303           prefItem.disabled = true;
   304         }
   305       });
   306     }
   308     document.documentElement.getButton("accept").label =
   309       this.bundleBrowser.getString("sanitizeButtonOK");
   311     this.selectByTimespan();
   312   },
   314   /**
   315    * Sets up the hashes this.durationValsToRows, which maps duration values
   316    * to rows in the tree, this.durationRowsToVals, which maps rows in
   317    * the tree to duration values, and this.durationStartTimes, which maps
   318    * duration values to their corresponding start times.
   319    */
   320   initDurationDropdown: function ()
   321   {
   322     // First, calculate the start times for each duration.
   323     this.durationStartTimes = {};
   324     var durVals = [];
   325     var durPopup = document.getElementById("sanitizeDurationPopup");
   326     var durMenuitems = durPopup.childNodes;
   327     for (let i = 0; i < durMenuitems.length; i++) {
   328       let durMenuitem = durMenuitems[i];
   329       let durVal = parseInt(durMenuitem.value);
   330       if (durMenuitem.localName === "menuitem" &&
   331           durVal !== Sanitizer.TIMESPAN_EVERYTHING &&
   332           durVal !== this.TIMESPAN_CUSTOM) {
   333         durVals.push(durVal);
   334         let durTimes = Sanitizer.getClearRange(durVal);
   335         this.durationStartTimes[durVal] = durTimes[0];
   336       }
   337     }
   339     // Sort the duration values ascending.  Because one tree index can map to
   340     // more than one duration, this ensures that this.durationRowsToVals maps
   341     // a row index to the largest duration possible in the code below.
   342     durVals.sort();
   344     // Now calculate the rows in the tree of the durations' start times.  For
   345     // each duration, we are looking for the node in the tree whose time is the
   346     // smallest time greater than or equal to the duration's start time.
   347     this.durationRowsToVals = {};
   348     this.durationValsToRows = {};
   349     var view = this.placesTree.view;
   350     // For all rows in the tree except the grippy row...
   351     for (let i = 0; i < view.rowCount - 1; i++) {
   352       let unfoundDurVals = [];
   353       let nodeTime = view.QueryInterface(Ci.nsINavHistoryResultTreeViewer).
   354                      nodeForTreeIndex(i).time;
   355       // For all durations whose rows have not yet been found in the tree, see
   356       // if index i is their index.  An index may map to more than one duration,
   357       // in which case the final duration (the largest) wins.
   358       for (let j = 0; j < durVals.length; j++) {
   359         let durVal = durVals[j];
   360         let durStartTime = this.durationStartTimes[durVal];
   361         if (nodeTime < durStartTime) {
   362           this.durationValsToRows[durVal] = i - 1;
   363           this.durationRowsToVals[i - 1] = durVal;
   364         }
   365         else
   366           unfoundDurVals.push(durVal);
   367       }
   368       durVals = unfoundDurVals;
   369     }
   371     // If any durations were not found above, then every node in the tree has a
   372     // time greater than or equal to the duration.  In other words, those
   373     // durations include the entire tree (except the grippy row).
   374     for (let i = 0; i < durVals.length; i++) {
   375       let durVal = durVals[i];
   376       this.durationValsToRows[durVal] = view.rowCount - 2;
   377       this.durationRowsToVals[view.rowCount - 2] = durVal;
   378     }
   379   },
   381   /**
   382    * If the Places tree is not set up, sets it up.  Otherwise does nothing.
   383    */
   384   ensurePlacesTreeIsInited: function ()
   385   {
   386     if (this._placesTreeIsInited)
   387       return;
   389     this._placesTreeIsInited = true;
   391     // Either "Last Four Hours" or "Today" will have the most history.  If
   392     // it's been more than 4 hours since today began, "Today" will. Otherwise
   393     // "Last Four Hours" will.
   394     var times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_TODAY);
   396     // If it's been less than 4 hours since today began, use the past 4 hours.
   397     if (times[1] - times[0] < 14400000000) { // 4*60*60*1000000
   398       times = Sanitizer.getClearRange(Sanitizer.TIMESPAN_4HOURS);
   399     }
   401     var histServ = Cc["@mozilla.org/browser/nav-history-service;1"].
   402                    getService(Ci.nsINavHistoryService);
   403     var query = histServ.getNewQuery();
   404     query.beginTimeReference = query.TIME_RELATIVE_EPOCH;
   405     query.beginTime = times[0];
   406     query.endTimeReference = query.TIME_RELATIVE_EPOCH;
   407     query.endTime = times[1];
   408     var opts = histServ.getNewQueryOptions();
   409     opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
   410     opts.queryType = opts.QUERY_TYPE_HISTORY;
   411     var result = histServ.executeQuery(query, opts);
   413     var view = gContiguousSelectionTreeHelper.setTree(this.placesTree,
   414                                                       new PlacesTreeView());
   415     result.addObserver(view, false);
   416     this.initDurationDropdown();
   417   },
   419   /**
   420    * Called on select of the duration dropdown and when grippyMoved() sets a
   421    * duration based on the location of the grippy row.  Selects all the nodes in
   422    * the tree that are contained in the selected duration.  If clearing
   423    * everything, the warning panel is shown instead.
   424    */
   425   selectByTimespan: function ()
   426   {
   427     // This method is the onselect handler for the duration dropdown.  As a
   428     // result it's called a couple of times before onload calls init().
   429     if (!this._inited)
   430       return;
   432     var durDeck = document.getElementById("durationDeck");
   433     var durList = document.getElementById("sanitizeDurationChoice");
   434     var durVal = parseInt(durList.value);
   435     var durCustom = document.getElementById("sanitizeDurationCustom");
   437     // If grippy row is not at a duration boundary, show the custom menuitem;
   438     // otherwise, hide it.  Since the user cannot specify a custom duration by
   439     // using the dropdown, this conditional is true only when this method is
   440     // called onselect from grippyMoved(), so no selection need be made.
   441     if (durVal === this.TIMESPAN_CUSTOM) {
   442       durCustom.hidden = false;
   443       return;
   444     }
   445     durCustom.hidden = true;
   447     // If clearing everything, show the warning and change the dialog's title.
   448     if (durVal === Sanitizer.TIMESPAN_EVERYTHING) {
   449       this.prepareWarning();
   450       durDeck.selectedIndex = 1;
   451       window.document.title =
   452         this.bundleBrowser.getString("sanitizeDialog2.everything.title");
   453       document.documentElement.getButton("accept").disabled = false;
   454       return;
   455     }
   457     // Otherwise -- if clearing a specific time range -- select that time range
   458     // in the tree.
   459     this.ensurePlacesTreeIsInited();
   460     durDeck.selectedIndex = 0;
   461     window.document.title =
   462       window.document.documentElement.getAttribute("noneverythingtitle");
   463     var durRow = this.durationValsToRows[durVal];
   464     gContiguousSelectionTreeHelper.rangedSelect(durRow);
   465     gContiguousSelectionTreeHelper.scrollToGrippy();
   467     // If duration is empty (there are no selected rows), disable the dialog's
   468     // OK button.
   469     document.documentElement.getButton("accept").disabled = durRow < 0;
   470   },
   472   sanitize: function ()
   473   {
   474     // Update pref values before handing off to the sanitizer (bug 453440)
   475     this.updatePrefs();
   476     var s = new Sanitizer();
   477     s.prefDomain = "privacy.cpd.";
   479     var durList = document.getElementById("sanitizeDurationChoice");
   480     var durValue = parseInt(durList.value);
   481     s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING;
   483     // Set the sanitizer's time range if we're not clearing everything.
   484     if (!s.ignoreTimespan) {
   485       // If user selected a custom timespan, use that.
   486       if (durValue === this.TIMESPAN_CUSTOM) {
   487         var view = this.placesTree.view;
   488         var now = Date.now() * 1000;
   489         // We disable the dialog's OK button if there's no selection, but we'll
   490         // handle that case just in... case.
   491         if (view.selection.getRangeCount() === 0)
   492           s.range = [now, now];
   493         else {
   494           var startIndexRef = {};
   495           // Tree sorted by visit date DEscending, so start time time comes last.
   496           view.selection.getRangeAt(0, {}, startIndexRef);
   497           view.QueryInterface(Ci.nsINavHistoryResultTreeViewer);
   498           var startNode = view.nodeForTreeIndex(startIndexRef.value);
   499           s.range = [startNode.time, now];
   500         }
   501       }
   502       // Otherwise use the predetermined range.
   503       else
   504         s.range = [this.durationStartTimes[durValue], Date.now() * 1000];
   505     }
   507     try {
   508       s.sanitize();
   509     } catch (er) {
   510       Components.utils.reportError("Exception during sanitize: " + er);
   511     }
   512     return true;
   513   },
   515   /**
   516    * In order to mark the custom Places tree view and its nsINavHistoryResult
   517    * for garbage collection, we need to break the reference cycle between the
   518    * two.
   519    */
   520   unload: function ()
   521   {
   522     let result = this.placesTree.getResult();
   523     result.removeObserver(this.placesTree.view);
   524     this.placesTree.view = null;
   525   },
   527   /**
   528    * Called when the user moves the grippy by dragging it, clicking in the tree,
   529    * or on keypress.  Updates the duration dropdown so that it displays the
   530    * appropriate specific or custom duration.
   531    *
   532    * @param aEventName
   533    *        The name of the event whose handler called this method, e.g.,
   534    *        "ondragstart", "onkeypress", etc.
   535    * @param aEvent
   536    *        The event captured in the event handler.
   537    */
   538   grippyMoved: function (aEventName, aEvent)
   539   {
   540     gContiguousSelectionTreeHelper[aEventName](aEvent);
   541     var lastSelRow = gContiguousSelectionTreeHelper.getGrippyRow() - 1;
   542     var durList = document.getElementById("sanitizeDurationChoice");
   543     var durValue = parseInt(durList.value);
   545     // Multiple durations can map to the same row.  Don't update the dropdown
   546     // if the current duration is valid for lastSelRow.
   547     if ((durValue !== this.TIMESPAN_CUSTOM ||
   548          lastSelRow in this.durationRowsToVals) &&
   549         (durValue === this.TIMESPAN_CUSTOM ||
   550          this.durationValsToRows[durValue] !== lastSelRow)) {
   551       // Setting durList.value causes its onselect handler to fire, which calls
   552       // selectByTimespan().
   553       if (lastSelRow in this.durationRowsToVals)
   554         durList.value = this.durationRowsToVals[lastSelRow];
   555       else
   556         durList.value = this.TIMESPAN_CUSTOM;
   557     }
   559     // If there are no selected rows, disable the dialog's OK button.
   560     document.documentElement.getButton("accept").disabled = lastSelRow < 0;
   561   }
   562 #endif
   564 };
   567 #ifdef CRH_DIALOG_TREE_VIEW
   568 /**
   569  * A helper for handling contiguous selection in the tree.
   570  */
   571 var gContiguousSelectionTreeHelper = {
   573   /**
   574    * Gets the tree associated with this helper.
   575    */
   576   get tree()
   577   {
   578     return this._tree;
   579   },
   581   /**
   582    * Sets the tree that this module handles.  The tree is assigned a new view
   583    * that is equipped to handle contiguous selection.  You can pass in an
   584    * object that will be used as the prototype of the new view.  Otherwise
   585    * the tree's current view is used as the prototype.
   586    *
   587    * @param  aTreeElement
   588    *         The tree element
   589    * @param  aProtoTreeView
   590    *         If defined, this will be used as the prototype of the tree's new
   591    *         view
   592    * @return The new view
   593    */
   594   setTree: function CSTH_setTree(aTreeElement, aProtoTreeView)
   595   {
   596     this._tree = aTreeElement;
   597     var newView = this._makeTreeView(aProtoTreeView || aTreeElement.view);
   598     aTreeElement.view = newView;
   599     return newView;
   600   },
   602   /**
   603    * The index of the row that the grippy occupies.  Note that the index of the
   604    * last selected row is getGrippyRow() - 1.  If getGrippyRow() is 0, then
   605    * no selection exists.
   606    *
   607    * @return The row index of the grippy
   608    */
   609   getGrippyRow: function CSTH_getGrippyRow()
   610   {
   611     var sel = this.tree.view.selection;
   612     var rangeCount = sel.getRangeCount();
   613     if (rangeCount === 0)
   614       return 0;
   615     if (rangeCount !== 1) {
   616       throw "contiguous selection tree helper: getGrippyRow called with " +
   617             "multiple selection ranges";
   618     }
   619     var max = {};
   620     sel.getRangeAt(0, {}, max);
   621     return max.value + 1;
   622   },
   624   /**
   625    * Helper function for the dragover event.  Your dragover listener should
   626    * call this.  It updates the selection in the tree under the mouse.
   627    *
   628    * @param aEvent
   629    *        The observed dragover event
   630    */
   631   ondragover: function CSTH_ondragover(aEvent)
   632   {
   633     // Without this when dragging on Windows the mouse cursor is a "no" sign.
   634     // This makes it a drop symbol.
   635     var ds = Cc["@mozilla.org/widget/dragservice;1"].
   636              getService(Ci.nsIDragService).
   637              getCurrentSession();
   638     ds.canDrop = true;
   639     ds.dragAction = 0;
   641     var tbo = this.tree.treeBoxObject;
   642     aEvent.QueryInterface(Ci.nsIDOMMouseEvent);
   643     var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
   645     if (hoverRow < 0)
   646       return;
   648     this.rangedSelect(hoverRow - 1);
   649   },
   651   /**
   652    * Helper function for the dragstart event.  Your dragstart listener should
   653    * call this.  It starts a drag session.
   654    *
   655    * @param aEvent
   656    *        The observed dragstart event
   657    */
   658   ondragstart: function CSTH_ondragstart(aEvent)
   659   {
   660     var tbo = this.tree.treeBoxObject;
   661     var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
   663     if (clickedRow !== this.getGrippyRow())
   664       return;
   666     // This part is a hack.  What we really want is a grab and slide, not
   667     // drag and drop.  Start a move drag session with dummy data and a
   668     // dummy region.  Set the region's coordinates to (Infinity, Infinity)
   669     // so it's drawn offscreen and its size to (1, 1).
   670     var arr = Cc["@mozilla.org/supports-array;1"].
   671               createInstance(Ci.nsISupportsArray);
   672     var trans = Cc["@mozilla.org/widget/transferable;1"].
   673                 createInstance(Ci.nsITransferable);
   674     trans.init(null);
   675     trans.setTransferData('dummy-flavor', null, 0);
   676     arr.AppendElement(trans);
   677     var reg = Cc["@mozilla.org/gfx/region;1"].
   678               createInstance(Ci.nsIScriptableRegion);
   679     reg.setToRect(Infinity, Infinity, 1, 1);
   680     var ds = Cc["@mozilla.org/widget/dragservice;1"].
   681              getService(Ci.nsIDragService);
   682     ds.invokeDragSession(aEvent.target, arr, reg, ds.DRAGDROP_ACTION_MOVE);
   683   },
   685   /**
   686    * Helper function for the keypress event.  Your keypress listener should
   687    * call this.  Users can use Up, Down, Page Up/Down, Home, and End to move
   688    * the bottom of the selection window.
   689    *
   690    * @param aEvent
   691    *        The observed keypress event
   692    */
   693   onkeypress: function CSTH_onkeypress(aEvent)
   694   {
   695     var grippyRow = this.getGrippyRow();
   696     var tbo = this.tree.treeBoxObject;
   697     var rangeEnd;
   698     switch (aEvent.keyCode) {
   699     case aEvent.DOM_VK_HOME:
   700       rangeEnd = 0;
   701       break;
   702     case aEvent.DOM_VK_PAGE_UP:
   703       rangeEnd = grippyRow - tbo.getPageLength();
   704       break;
   705     case aEvent.DOM_VK_UP:
   706       rangeEnd = grippyRow - 2;
   707       break;
   708     case aEvent.DOM_VK_DOWN:
   709       rangeEnd = grippyRow;
   710       break;
   711     case aEvent.DOM_VK_PAGE_DOWN:
   712       rangeEnd = grippyRow + tbo.getPageLength();
   713       break;
   714     case aEvent.DOM_VK_END:
   715       rangeEnd = this.tree.view.rowCount - 2;
   716       break;
   717     default:
   718       return;
   719       break;
   720     }
   722     aEvent.stopPropagation();
   724     // First, clip rangeEnd.  this.rangedSelect() doesn't clip the range if we
   725     // select past the ends of the tree.
   726     if (rangeEnd < 0)
   727       rangeEnd = -1;
   728     else if (this.tree.view.rowCount - 2 < rangeEnd)
   729       rangeEnd = this.tree.view.rowCount - 2;
   731     // Next, (de)select.
   732     this.rangedSelect(rangeEnd);
   734     // Finally, scroll the tree.  We always want one row above and below the
   735     // grippy row to be visible if possible.
   736     if (rangeEnd < grippyRow) // moved up
   737       tbo.ensureRowIsVisible(rangeEnd < 0 ? 0 : rangeEnd);
   738     else {                    // moved down
   739       if (rangeEnd + 2 < this.tree.view.rowCount)
   740         tbo.ensureRowIsVisible(rangeEnd + 2);
   741       else if (rangeEnd + 1 < this.tree.view.rowCount)
   742         tbo.ensureRowIsVisible(rangeEnd + 1);
   743     }
   744   },
   746   /**
   747    * Helper function for the mousedown event.  Your mousedown listener should
   748    * call this.  Users can click on individual rows to make the selection
   749    * jump to them immediately.
   750    *
   751    * @param aEvent
   752    *        The observed mousedown event
   753    */
   754   onmousedown: function CSTH_onmousedown(aEvent)
   755   {
   756     var tbo = this.tree.treeBoxObject;
   757     var clickedRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
   759     if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount)
   760       return;
   762     if (clickedRow < this.getGrippyRow())
   763       this.rangedSelect(clickedRow);
   764     else if (clickedRow > this.getGrippyRow())
   765       this.rangedSelect(clickedRow - 1);
   766   },
   768   /**
   769    * Selects range [0, aEndRow] in the tree.  The grippy row will then be at
   770    * index aEndRow + 1.  aEndRow may be -1, in which case the selection is
   771    * cleared and the grippy row will be at index 0.
   772    *
   773    * @param aEndRow
   774    *        The range [0, aEndRow] will be selected.
   775    */
   776   rangedSelect: function CSTH_rangedSelect(aEndRow)
   777   {
   778     var tbo = this.tree.treeBoxObject;
   779     if (aEndRow < 0)
   780       this.tree.view.selection.clearSelection();
   781     else
   782       this.tree.view.selection.rangedSelect(0, aEndRow, false);
   783     tbo.invalidateRange(tbo.getFirstVisibleRow(), tbo.getLastVisibleRow());
   784   },
   786   /**
   787    * Scrolls the tree so that the grippy row is in the center of the view.
   788    */
   789   scrollToGrippy: function CSTH_scrollToGrippy()
   790   {
   791     var rowCount = this.tree.view.rowCount;
   792     var tbo = this.tree.treeBoxObject;
   793     var pageLen = tbo.getPageLength() ||
   794                   parseInt(this.tree.getAttribute("rows")) ||
   795                   10;
   797     // All rows fit on a single page.
   798     if (rowCount <= pageLen)
   799       return;
   801     var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0);
   803     // Grippy row is in first half of first page.
   804     if (scrollToRow < 0)
   805       scrollToRow = 0;
   807     // Grippy row is in last half of last page.
   808     else if (rowCount < scrollToRow + pageLen)
   809       scrollToRow = rowCount - pageLen;
   811     tbo.scrollToRow(scrollToRow);
   812   },
   814   /**
   815    * Creates a new tree view suitable for contiguous selection.  If
   816    * aProtoTreeView is specified, it's used as the new view's prototype.
   817    * Otherwise the tree's current view is used as the prototype.
   818    *
   819    * @param aProtoTreeView
   820    *        Used as the new view's prototype if specified
   821    */
   822   _makeTreeView: function CSTH__makeTreeView(aProtoTreeView)
   823   {
   824     var view = aProtoTreeView;
   825     var that = this;
   827     //XXXadw: When Alex gets the grippy icon done, this may or may not change,
   828     //        depending on how we style it.
   829     view.isSeparator = function CSTH_View_isSeparator(aRow)
   830     {
   831       return aRow === that.getGrippyRow();
   832     };
   834     // rowCount includes the grippy row.
   835     view.__defineGetter__("_rowCount", view.__lookupGetter__("rowCount"));
   836     view.__defineGetter__("rowCount",
   837       function CSTH_View_rowCount()
   838       {
   839         return this._rowCount + 1;
   840       });
   842     // This has to do with visual feedback in the view itself, e.g., drawing
   843     // a small line underneath the dropzone.  Not what we want.
   844     view.canDrop = function CSTH_View_canDrop() { return false; };
   846     // No clicking headers to sort the tree or sort feedback on columns.
   847     view.cycleHeader = function CSTH_View_cycleHeader() {};
   848     view.sortingChanged = function CSTH_View_sortingChanged() {};
   850     // Override a bunch of methods to account for the grippy row.
   852     view._getCellProperties = view.getCellProperties;
   853     view.getCellProperties =
   854       function CSTH_View_getCellProperties(aRow, aCol)
   855       {
   856         var grippyRow = that.getGrippyRow();
   857         if (aRow === grippyRow)
   858           return "grippyRow";
   859         if (aRow < grippyRow)
   860           return this._getCellProperties(aRow, aCol);
   862         return this._getCellProperties(aRow - 1, aCol);
   863       };
   865     view._getRowProperties = view.getRowProperties;
   866     view.getRowProperties =
   867       function CSTH_View_getRowProperties(aRow)
   868       {
   869         var grippyRow = that.getGrippyRow();
   870         if (aRow === grippyRow)
   871           return "grippyRow";
   873         if (aRow < grippyRow)
   874           return this._getRowProperties(aRow);
   876         return this._getRowProperties(aRow - 1);
   877       };
   879     view._getCellText = view.getCellText;
   880     view.getCellText =
   881       function CSTH_View_getCellText(aRow, aCol)
   882       {
   883         var grippyRow = that.getGrippyRow();
   884         if (aRow === grippyRow)
   885           return "";
   886         aRow = aRow < grippyRow ? aRow : aRow - 1;
   887         return this._getCellText(aRow, aCol);
   888       };
   890     view._getImageSrc = view.getImageSrc;
   891     view.getImageSrc =
   892       function CSTH_View_getImageSrc(aRow, aCol)
   893       {
   894         var grippyRow = that.getGrippyRow();
   895         if (aRow === grippyRow)
   896           return "";
   897         aRow = aRow < grippyRow ? aRow : aRow - 1;
   898         return this._getImageSrc(aRow, aCol);
   899       };
   901     view.isContainer = function CSTH_View_isContainer(aRow) { return false; };
   902     view.getParentIndex = function CSTH_View_getParentIndex(aRow) { return -1; };
   903     view.getLevel = function CSTH_View_getLevel(aRow) { return 0; };
   904     view.hasNextSibling = function CSTH_View_hasNextSibling(aRow, aAfterIndex)
   905     {
   906       return aRow < this.rowCount - 1;
   907     };
   909     return view;
   910   }
   911 };
   912 #endif

mercurial