browser/base/content/sanitizeDialog.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:3b16bbd768ee
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/. */
5
6 const Cc = Components.classes;
7 const Ci = Components.interfaces;
8
9 var gSanitizePromptDialog = {
10
11 get bundleBrowser()
12 {
13 if (!this._bundleBrowser)
14 this._bundleBrowser = document.getElementById("bundleBrowser");
15 return this._bundleBrowser;
16 },
17
18 get selectedTimespan()
19 {
20 var durList = document.getElementById("sanitizeDurationChoice");
21 return parseInt(durList.value);
22 },
23
24 get sanitizePreferences()
25 {
26 if (!this._sanitizePreferences) {
27 this._sanitizePreferences =
28 document.getElementById("sanitizePreferences");
29 }
30 return this._sanitizePreferences;
31 },
32
33 get warningBox()
34 {
35 return document.getElementById("sanitizeEverythingWarningBox");
36 },
37
38 init: function ()
39 {
40 // This is used by selectByTimespan() to determine if the window has loaded.
41 this._inited = true;
42
43 var s = new Sanitizer();
44 s.prefDomain = "privacy.cpd.";
45
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 }
58
59 document.documentElement.getButton("accept").label =
60 this.bundleBrowser.getString("sanitizeButtonOK");
61
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 },
71
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;
78
79 var warningBox = this.warningBox;
80
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 }
92
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 },
101
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.";
108
109 s.range = Sanitizer.getClearRange(this.selectedTimespan);
110 s.ignoreTimespan = !s.range;
111
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 },
132
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.
145
146 var warningStringID;
147 if (this.hasNonSelectedItems()) {
148 warningStringID = "sanitizeSelectedWarning";
149 if (!aDontShowItemList)
150 this.showItemList();
151 }
152 else {
153 warningStringID = "sanitizeEverythingWarning2";
154 }
155
156 var warningDesc = document.getElementById("sanitizeEverythingWarning");
157 warningDesc.textContent =
158 this.bundleBrowser.getString(warningStringID);
159 },
160
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;
168
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];
173
174 found = !!preference.value &&
175 !preference.disabled;
176 i++;
177 }
178
179 try {
180 document.documentElement.getButton("accept").disabled = !found;
181 }
182 catch (e) { }
183
184 // Update the warning prompt if needed
185 this.prepareWarning(true);
186
187 return undefined;
188 },
189
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);
202
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;
206
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 },
215
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 },
228
229 /**
230 * Show the history items list.
231 */
232 showItemList: function () {
233 var itemList = document.getElementById("itemList");
234 var expanderButton = document.getElementById("detailsExpander");
235
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 },
243
244 /**
245 * Hide the history items list.
246 */
247 hideItemList: function () {
248 var itemList = document.getElementById("itemList");
249 var expanderButton = document.getElementById("detailsExpander");
250
251 if (!itemList.collapsed) {
252 expanderButton.className = "expander-down";
253 window.resizeBy(0, -itemList.boxObject.height);
254 itemList.setAttribute("collapsed", "true");
255 }
256 },
257
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");
264
265 if (itemList.collapsed)
266 this.showItemList();
267 else
268 this.hideItemList();
269 }
270
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 },
279
280 get placesTree()
281 {
282 if (!this._placesTree)
283 this._placesTree = document.getElementById("placesTree");
284 return this._placesTree;
285 },
286
287 init: function ()
288 {
289 // This is used by selectByTimespan() to determine if the window has loaded.
290 this._inited = true;
291
292 var s = new Sanitizer();
293 s.prefDomain = "privacy.cpd.";
294
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 }
307
308 document.documentElement.getButton("accept").label =
309 this.bundleBrowser.getString("sanitizeButtonOK");
310
311 this.selectByTimespan();
312 },
313
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 }
338
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();
343
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 }
370
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 },
380
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;
388
389 this._placesTreeIsInited = true;
390
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);
395
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 }
400
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);
412
413 var view = gContiguousSelectionTreeHelper.setTree(this.placesTree,
414 new PlacesTreeView());
415 result.addObserver(view, false);
416 this.initDurationDropdown();
417 },
418
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;
431
432 var durDeck = document.getElementById("durationDeck");
433 var durList = document.getElementById("sanitizeDurationChoice");
434 var durVal = parseInt(durList.value);
435 var durCustom = document.getElementById("sanitizeDurationCustom");
436
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;
446
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 }
456
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();
466
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 },
471
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.";
478
479 var durList = document.getElementById("sanitizeDurationChoice");
480 var durValue = parseInt(durList.value);
481 s.ignoreTimespan = durValue === Sanitizer.TIMESPAN_EVERYTHING;
482
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 }
506
507 try {
508 s.sanitize();
509 } catch (er) {
510 Components.utils.reportError("Exception during sanitize: " + er);
511 }
512 return true;
513 },
514
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 },
526
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);
544
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 }
558
559 // If there are no selected rows, disable the dialog's OK button.
560 document.documentElement.getButton("accept").disabled = lastSelRow < 0;
561 }
562 #endif
563
564 };
565
566
567 #ifdef CRH_DIALOG_TREE_VIEW
568 /**
569 * A helper for handling contiguous selection in the tree.
570 */
571 var gContiguousSelectionTreeHelper = {
572
573 /**
574 * Gets the tree associated with this helper.
575 */
576 get tree()
577 {
578 return this._tree;
579 },
580
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 },
601
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 },
623
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;
640
641 var tbo = this.tree.treeBoxObject;
642 aEvent.QueryInterface(Ci.nsIDOMMouseEvent);
643 var hoverRow = tbo.getRowAt(aEvent.clientX, aEvent.clientY);
644
645 if (hoverRow < 0)
646 return;
647
648 this.rangedSelect(hoverRow - 1);
649 },
650
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);
662
663 if (clickedRow !== this.getGrippyRow())
664 return;
665
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 },
684
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 }
721
722 aEvent.stopPropagation();
723
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;
730
731 // Next, (de)select.
732 this.rangedSelect(rangeEnd);
733
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 },
745
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);
758
759 if (clickedRow < 0 || clickedRow >= this.tree.view.rowCount)
760 return;
761
762 if (clickedRow < this.getGrippyRow())
763 this.rangedSelect(clickedRow);
764 else if (clickedRow > this.getGrippyRow())
765 this.rangedSelect(clickedRow - 1);
766 },
767
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 },
785
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;
796
797 // All rows fit on a single page.
798 if (rowCount <= pageLen)
799 return;
800
801 var scrollToRow = this.getGrippyRow() - Math.ceil(pageLen / 2.0);
802
803 // Grippy row is in first half of first page.
804 if (scrollToRow < 0)
805 scrollToRow = 0;
806
807 // Grippy row is in last half of last page.
808 else if (rowCount < scrollToRow + pageLen)
809 scrollToRow = rowCount - pageLen;
810
811 tbo.scrollToRow(scrollToRow);
812 },
813
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;
826
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 };
833
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 });
841
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; };
845
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() {};
849
850 // Override a bunch of methods to account for the grippy row.
851
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);
861
862 return this._getCellProperties(aRow - 1, aCol);
863 };
864
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";
872
873 if (aRow < grippyRow)
874 return this._getRowProperties(aRow);
875
876 return this._getRowProperties(aRow - 1);
877 };
878
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 };
889
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 };
900
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 };
908
909 return view;
910 }
911 };
912 #endif

mercurial