|
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 |