|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /** |
|
8 * Handles the Downloads panel user interface for each browser window. |
|
9 * |
|
10 * This file includes the following constructors and global objects: |
|
11 * |
|
12 * DownloadsPanel |
|
13 * Main entry point for the downloads panel interface. |
|
14 * |
|
15 * DownloadsOverlayLoader |
|
16 * Allows loading the downloads panel and the status indicator interfaces on |
|
17 * demand, to improve startup performance. |
|
18 * |
|
19 * DownloadsView |
|
20 * Builds and updates the downloads list widget, responding to changes in the |
|
21 * download state and real-time data. In addition, handles part of the user |
|
22 * interaction events raised by the downloads list widget. |
|
23 * |
|
24 * DownloadsViewItem |
|
25 * Builds and updates a single item in the downloads list widget, responding to |
|
26 * changes in the download state and real-time data. |
|
27 * |
|
28 * DownloadsViewController |
|
29 * Handles part of the user interaction events raised by the downloads list |
|
30 * widget, in particular the "commands" that apply to multiple items, and |
|
31 * dispatches the commands that apply to individual items. |
|
32 * |
|
33 * DownloadsViewItemController |
|
34 * Handles all the user interaction events, in particular the "commands", |
|
35 * related to a single item in the downloads list widgets. |
|
36 */ |
|
37 |
|
38 /** |
|
39 * A few words on focus and focusrings |
|
40 * |
|
41 * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we |
|
42 * basically suppress most if not all XUL-level focusrings, and style/draw |
|
43 * them ourselves (using :focus instead of -moz-focusring). There are a few |
|
44 * reasons for this: |
|
45 * |
|
46 * 1) Richlists on OSX don't have focusrings; instead, they are shown as |
|
47 * selected. This makes for some ambiguity when we have a focused/selected |
|
48 * item in the list, and the mouse is hovering a completed download (which |
|
49 * highlights). |
|
50 * 2) Windows doesn't show focusrings until after the first time that tab is |
|
51 * pressed (and by then you're focusing the second item in the panel). |
|
52 * 3) Richlistbox sets -moz-focusring even when we select it with a mouse. |
|
53 * |
|
54 * In general, the desired behaviour is to focus the first item after pressing |
|
55 * tab/down, and show that focus with a ring. Then, if the mouse moves over |
|
56 * the panel, to hide that focus ring; essentially resetting us to the state |
|
57 * before pressing the key. |
|
58 * |
|
59 * We end up capturing the tab/down key events, and preventing their default |
|
60 * behaviour. We then set a "keyfocus" attribute on the panel, which allows |
|
61 * us to draw a ring around the currently focused element. If the panel is |
|
62 * closed or the mouse moves over the panel, we remove the attribute. |
|
63 */ |
|
64 |
|
65 "use strict"; |
|
66 |
|
67 //////////////////////////////////////////////////////////////////////////////// |
|
68 //// Globals |
|
69 |
|
70 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", |
|
71 "resource://gre/modules/DownloadUtils.jsm"); |
|
72 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", |
|
73 "resource:///modules/DownloadsCommon.jsm"); |
|
74 XPCOMUtils.defineLazyModuleGetter(this, "OS", |
|
75 "resource://gre/modules/osfile.jsm"); |
|
76 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", |
|
77 "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
|
78 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", |
|
79 "resource://gre/modules/PlacesUtils.jsm"); |
|
80 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", |
|
81 "resource://gre/modules/NetUtil.jsm"); |
|
82 |
|
83 //////////////////////////////////////////////////////////////////////////////// |
|
84 //// DownloadsPanel |
|
85 |
|
86 /** |
|
87 * Main entry point for the downloads panel interface. |
|
88 */ |
|
89 const DownloadsPanel = { |
|
90 ////////////////////////////////////////////////////////////////////////////// |
|
91 //// Initialization and termination |
|
92 |
|
93 /** |
|
94 * Internal state of the downloads panel, based on one of the kState |
|
95 * constants. This is not the same state as the XUL panel element. |
|
96 */ |
|
97 _state: 0, |
|
98 |
|
99 /** The panel is not linked to downloads data yet. */ |
|
100 get kStateUninitialized() 0, |
|
101 /** This object is linked to data, but the panel is invisible. */ |
|
102 get kStateHidden() 1, |
|
103 /** The panel will be shown as soon as possible. */ |
|
104 get kStateWaitingData() 2, |
|
105 /** The panel is almost shown - we're just waiting to get a handle on the |
|
106 anchor. */ |
|
107 get kStateWaitingAnchor() 3, |
|
108 /** The panel is open. */ |
|
109 get kStateShown() 4, |
|
110 |
|
111 /** |
|
112 * Location of the panel overlay. |
|
113 */ |
|
114 get kDownloadsOverlay() |
|
115 "chrome://browser/content/downloads/downloadsOverlay.xul", |
|
116 |
|
117 /** |
|
118 * Starts loading the download data in background, without opening the panel. |
|
119 * Use showPanel instead to load the data and open the panel at the same time. |
|
120 * |
|
121 * @param aCallback |
|
122 * Called when initialization is complete. |
|
123 */ |
|
124 initialize: function DP_initialize(aCallback) |
|
125 { |
|
126 DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window."); |
|
127 if (this._state != this.kStateUninitialized) { |
|
128 DownloadsCommon.log("DownloadsPanel is already initialized."); |
|
129 DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, |
|
130 aCallback); |
|
131 return; |
|
132 } |
|
133 this._state = this.kStateHidden; |
|
134 |
|
135 window.addEventListener("unload", this.onWindowUnload, false); |
|
136 |
|
137 // Load and resume active downloads if required. If there are downloads to |
|
138 // be shown in the panel, they will be loaded asynchronously. |
|
139 DownloadsCommon.initializeAllDataLinks(); |
|
140 |
|
141 // Now that data loading has eventually started, load the required XUL |
|
142 // elements and initialize our views. |
|
143 DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded."); |
|
144 DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, |
|
145 function DP_I_callback() { |
|
146 DownloadsViewController.initialize(); |
|
147 DownloadsCommon.log("Attaching DownloadsView..."); |
|
148 DownloadsCommon.getData(window).addView(DownloadsView); |
|
149 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) |
|
150 .addView(DownloadsSummary); |
|
151 DownloadsCommon.log("DownloadsView attached - the panel for this window", |
|
152 "should now see download items come in."); |
|
153 DownloadsPanel._attachEventListeners(); |
|
154 DownloadsCommon.log("DownloadsPanel initialized."); |
|
155 aCallback(); |
|
156 }); |
|
157 }, |
|
158 |
|
159 /** |
|
160 * Closes the downloads panel and frees the internal resources related to the |
|
161 * downloads. The downloads panel can be reopened later, even after this |
|
162 * function has been called. |
|
163 */ |
|
164 terminate: function DP_terminate() |
|
165 { |
|
166 DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); |
|
167 if (this._state == this.kStateUninitialized) { |
|
168 DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do."); |
|
169 return; |
|
170 } |
|
171 |
|
172 window.removeEventListener("unload", this.onWindowUnload, false); |
|
173 |
|
174 // Ensure that the panel is closed before shutting down. |
|
175 this.hidePanel(); |
|
176 |
|
177 DownloadsViewController.terminate(); |
|
178 DownloadsCommon.getData(window).removeView(DownloadsView); |
|
179 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) |
|
180 .removeView(DownloadsSummary); |
|
181 this._unattachEventListeners(); |
|
182 |
|
183 this._state = this.kStateUninitialized; |
|
184 |
|
185 DownloadsSummary.active = false; |
|
186 DownloadsCommon.log("DownloadsPanel terminated."); |
|
187 }, |
|
188 |
|
189 ////////////////////////////////////////////////////////////////////////////// |
|
190 //// Panel interface |
|
191 |
|
192 /** |
|
193 * Main panel element in the browser window, or null if the panel overlay |
|
194 * hasn't been loaded yet. |
|
195 */ |
|
196 get panel() |
|
197 { |
|
198 // If the downloads panel overlay hasn't loaded yet, just return null |
|
199 // without reseting this.panel. |
|
200 let downloadsPanel = document.getElementById("downloadsPanel"); |
|
201 if (!downloadsPanel) |
|
202 return null; |
|
203 |
|
204 delete this.panel; |
|
205 return this.panel = downloadsPanel; |
|
206 }, |
|
207 |
|
208 /** |
|
209 * Starts opening the downloads panel interface, anchored to the downloads |
|
210 * button of the browser window. The list of downloads to display is |
|
211 * initialized the first time this method is called, and the panel is shown |
|
212 * only when data is ready. |
|
213 */ |
|
214 showPanel: function DP_showPanel() |
|
215 { |
|
216 DownloadsCommon.log("Opening the downloads panel."); |
|
217 |
|
218 if (this.isPanelShowing) { |
|
219 DownloadsCommon.log("Panel is already showing - focusing instead."); |
|
220 this._focusPanel(); |
|
221 return; |
|
222 } |
|
223 |
|
224 this.initialize(function DP_SP_callback() { |
|
225 // Delay displaying the panel because this function will sometimes be |
|
226 // called while another window is closing (like the window for selecting |
|
227 // whether to save or open the file), and that would cause the panel to |
|
228 // close immediately. |
|
229 setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0); |
|
230 }.bind(this)); |
|
231 |
|
232 DownloadsCommon.log("Waiting for the downloads panel to appear."); |
|
233 this._state = this.kStateWaitingData; |
|
234 }, |
|
235 |
|
236 /** |
|
237 * Hides the downloads panel, if visible, but keeps the internal state so that |
|
238 * the panel can be reopened quickly if required. |
|
239 */ |
|
240 hidePanel: function DP_hidePanel() |
|
241 { |
|
242 DownloadsCommon.log("Closing the downloads panel."); |
|
243 |
|
244 if (!this.isPanelShowing) { |
|
245 DownloadsCommon.log("Downloads panel is not showing - nothing to do."); |
|
246 return; |
|
247 } |
|
248 |
|
249 this.panel.hidePopup(); |
|
250 |
|
251 // Ensure that we allow the panel to be reopened. Note that, if the popup |
|
252 // was open, then the onPopupHidden event handler has already updated the |
|
253 // current state, otherwise we must update the state ourselves. |
|
254 this._state = this.kStateHidden; |
|
255 DownloadsCommon.log("Downloads panel is now closed."); |
|
256 }, |
|
257 |
|
258 /** |
|
259 * Indicates whether the panel is shown or will be shown. |
|
260 */ |
|
261 get isPanelShowing() |
|
262 { |
|
263 return this._state == this.kStateWaitingData || |
|
264 this._state == this.kStateWaitingAnchor || |
|
265 this._state == this.kStateShown; |
|
266 }, |
|
267 |
|
268 /** |
|
269 * Returns whether the user has started keyboard navigation. |
|
270 */ |
|
271 get keyFocusing() |
|
272 { |
|
273 return this.panel.hasAttribute("keyfocus"); |
|
274 }, |
|
275 |
|
276 /** |
|
277 * Set to true if the user has started keyboard navigation, and we should be |
|
278 * showing focusrings in the panel. Also adds a mousemove event handler to |
|
279 * the panel which disables keyFocusing. |
|
280 */ |
|
281 set keyFocusing(aValue) |
|
282 { |
|
283 if (aValue) { |
|
284 this.panel.setAttribute("keyfocus", "true"); |
|
285 this.panel.addEventListener("mousemove", this); |
|
286 } else { |
|
287 this.panel.removeAttribute("keyfocus"); |
|
288 this.panel.removeEventListener("mousemove", this); |
|
289 } |
|
290 return aValue; |
|
291 }, |
|
292 |
|
293 /** |
|
294 * Handles the mousemove event for the panel, which disables focusring |
|
295 * visualization. |
|
296 */ |
|
297 handleEvent: function DP_handleEvent(aEvent) |
|
298 { |
|
299 if (aEvent.type == "mousemove") { |
|
300 this.keyFocusing = false; |
|
301 } |
|
302 }, |
|
303 |
|
304 ////////////////////////////////////////////////////////////////////////////// |
|
305 //// Callback functions from DownloadsView |
|
306 |
|
307 /** |
|
308 * Called after data loading finished. |
|
309 */ |
|
310 onViewLoadCompleted: function DP_onViewLoadCompleted() |
|
311 { |
|
312 this._openPopupIfDataReady(); |
|
313 }, |
|
314 |
|
315 ////////////////////////////////////////////////////////////////////////////// |
|
316 //// User interface event functions |
|
317 |
|
318 onWindowUnload: function DP_onWindowUnload() |
|
319 { |
|
320 // This function is registered as an event listener, we can't use "this". |
|
321 DownloadsPanel.terminate(); |
|
322 }, |
|
323 |
|
324 onPopupShown: function DP_onPopupShown(aEvent) |
|
325 { |
|
326 // Ignore events raised by nested popups. |
|
327 if (aEvent.target != aEvent.currentTarget) { |
|
328 return; |
|
329 } |
|
330 |
|
331 DownloadsCommon.log("Downloads panel has shown."); |
|
332 this._state = this.kStateShown; |
|
333 |
|
334 // Since at most one popup is open at any given time, we can set globally. |
|
335 DownloadsCommon.getIndicatorData(window).attentionSuppressed = true; |
|
336 |
|
337 // Ensure that the first item is selected when the panel is focused. |
|
338 if (DownloadsView.richListBox.itemCount > 0 && |
|
339 DownloadsView.richListBox.selectedIndex == -1) { |
|
340 DownloadsView.richListBox.selectedIndex = 0; |
|
341 } |
|
342 |
|
343 this._focusPanel(); |
|
344 }, |
|
345 |
|
346 onPopupHidden: function DP_onPopupHidden(aEvent) |
|
347 { |
|
348 // Ignore events raised by nested popups. |
|
349 if (aEvent.target != aEvent.currentTarget) { |
|
350 return; |
|
351 } |
|
352 |
|
353 DownloadsCommon.log("Downloads panel has hidden."); |
|
354 |
|
355 // Removes the keyfocus attribute so that we stop handling keyboard |
|
356 // navigation. |
|
357 this.keyFocusing = false; |
|
358 |
|
359 // Since at most one popup is open at any given time, we can set globally. |
|
360 DownloadsCommon.getIndicatorData(window).attentionSuppressed = false; |
|
361 |
|
362 // Allow the anchor to be hidden. |
|
363 DownloadsButton.releaseAnchor(); |
|
364 |
|
365 // Allow the panel to be reopened. |
|
366 this._state = this.kStateHidden; |
|
367 }, |
|
368 |
|
369 ////////////////////////////////////////////////////////////////////////////// |
|
370 //// Related operations |
|
371 |
|
372 /** |
|
373 * Shows or focuses the user interface dedicated to downloads history. |
|
374 */ |
|
375 showDownloadsHistory: function DP_showDownloadsHistory() |
|
376 { |
|
377 DownloadsCommon.log("Showing download history."); |
|
378 // Hide the panel before showing another window, otherwise focus will return |
|
379 // to the browser window when the panel closes automatically. |
|
380 this.hidePanel(); |
|
381 |
|
382 BrowserDownloadsUI(); |
|
383 }, |
|
384 |
|
385 ////////////////////////////////////////////////////////////////////////////// |
|
386 //// Internal functions |
|
387 |
|
388 /** |
|
389 * Attach event listeners to a panel element. These listeners should be |
|
390 * removed in _unattachEventListeners. This is called automatically after the |
|
391 * panel has successfully loaded. |
|
392 */ |
|
393 _attachEventListeners: function DP__attachEventListeners() |
|
394 { |
|
395 // Handle keydown to support accel-V. |
|
396 this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false); |
|
397 // Handle keypress to be able to preventDefault() events before they reach |
|
398 // the richlistbox, for keyboard navigation. |
|
399 this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false); |
|
400 }, |
|
401 |
|
402 /** |
|
403 * Unattach event listeners that were added in _attachEventListeners. This |
|
404 * is called automatically on panel termination. |
|
405 */ |
|
406 _unattachEventListeners: function DP__unattachEventListeners() |
|
407 { |
|
408 this.panel.removeEventListener("keydown", this._onKeyDown.bind(this), |
|
409 false); |
|
410 this.panel.removeEventListener("keypress", this._onKeyPress.bind(this), |
|
411 false); |
|
412 }, |
|
413 |
|
414 _onKeyPress: function DP__onKeyPress(aEvent) |
|
415 { |
|
416 // Handle unmodified keys only. |
|
417 if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) { |
|
418 return; |
|
419 } |
|
420 |
|
421 let richListBox = DownloadsView.richListBox; |
|
422 |
|
423 // If the user has pressed the tab, up, or down cursor key, start keyboard |
|
424 // navigation, thus enabling focusrings in the panel. Keyboard navigation |
|
425 // is automatically disabled if the user moves the mouse on the panel, or |
|
426 // if the panel is closed. |
|
427 if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB || |
|
428 aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP || |
|
429 aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) && |
|
430 !this.keyFocusing) { |
|
431 this.keyFocusing = true; |
|
432 // Ensure there's a selection, we will show the focus ring around it and |
|
433 // prevent the richlistbox from changing the selection. |
|
434 if (DownloadsView.richListBox.selectedIndex == -1) |
|
435 DownloadsView.richListBox.selectedIndex = 0; |
|
436 aEvent.preventDefault(); |
|
437 return; |
|
438 } |
|
439 |
|
440 if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) { |
|
441 // If the last element in the list is selected, or the footer is already |
|
442 // focused, focus the footer. |
|
443 if (richListBox.selectedItem === richListBox.lastChild || |
|
444 document.activeElement.parentNode.id === "downloadsFooter") { |
|
445 DownloadsFooter.focus(); |
|
446 aEvent.preventDefault(); |
|
447 return; |
|
448 } |
|
449 } |
|
450 |
|
451 // Pass keypress events to the richlistbox view when it's focused. |
|
452 if (document.activeElement === richListBox) { |
|
453 DownloadsView.onDownloadKeyPress(aEvent); |
|
454 } |
|
455 }, |
|
456 |
|
457 /** |
|
458 * Keydown listener that listens for the keys to start key focusing, as well |
|
459 * as the the accel-V "paste" event, which initiates a file download if the |
|
460 * pasted item can be resolved to a URI. |
|
461 */ |
|
462 _onKeyDown: function DP__onKeyDown(aEvent) |
|
463 { |
|
464 // If the footer is focused and the downloads list has at least 1 element |
|
465 // in it, focus the last element in the list when going up. |
|
466 if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && |
|
467 document.activeElement.parentNode.id === "downloadsFooter" && |
|
468 DownloadsView.richListBox.firstChild) { |
|
469 DownloadsView.richListBox.focus(); |
|
470 DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild; |
|
471 aEvent.preventDefault(); |
|
472 return; |
|
473 } |
|
474 |
|
475 let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V && |
|
476 #ifdef XP_MACOSX |
|
477 aEvent.metaKey; |
|
478 #else |
|
479 aEvent.ctrlKey; |
|
480 #endif |
|
481 |
|
482 if (!pasting) { |
|
483 return; |
|
484 } |
|
485 |
|
486 DownloadsCommon.log("Received a paste event."); |
|
487 |
|
488 let trans = Cc["@mozilla.org/widget/transferable;1"] |
|
489 .createInstance(Ci.nsITransferable); |
|
490 trans.init(null); |
|
491 let flavors = ["text/x-moz-url", "text/unicode"]; |
|
492 flavors.forEach(trans.addDataFlavor); |
|
493 Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); |
|
494 // Getting the data or creating the nsIURI might fail |
|
495 try { |
|
496 let data = {}; |
|
497 trans.getAnyTransferData({}, data, {}); |
|
498 let [url, name] = data.value |
|
499 .QueryInterface(Ci.nsISupportsString) |
|
500 .data |
|
501 .split("\n"); |
|
502 if (!url) { |
|
503 return; |
|
504 } |
|
505 |
|
506 let uri = NetUtil.newURI(url); |
|
507 DownloadsCommon.log("Pasted URL seems valid. Starting download."); |
|
508 DownloadURL(uri.spec, name, document); |
|
509 } catch (ex) {} |
|
510 }, |
|
511 |
|
512 /** |
|
513 * Move focus to the main element in the downloads panel, unless another |
|
514 * element in the panel is already focused. |
|
515 */ |
|
516 _focusPanel: function DP_focusPanel() |
|
517 { |
|
518 // We may be invoked while the panel is still waiting to be shown. |
|
519 if (this._state != this.kStateShown) { |
|
520 return; |
|
521 } |
|
522 |
|
523 let element = document.commandDispatcher.focusedElement; |
|
524 while (element && element != this.panel) { |
|
525 element = element.parentNode; |
|
526 } |
|
527 if (!element) { |
|
528 if (DownloadsView.richListBox.itemCount > 0) { |
|
529 DownloadsView.richListBox.focus(); |
|
530 } else { |
|
531 DownloadsFooter.focus(); |
|
532 } |
|
533 } |
|
534 }, |
|
535 |
|
536 /** |
|
537 * Opens the downloads panel when data is ready to be displayed. |
|
538 */ |
|
539 _openPopupIfDataReady: function DP_openPopupIfDataReady() |
|
540 { |
|
541 // We don't want to open the popup if we already displayed it, or if we are |
|
542 // still loading data. |
|
543 if (this._state != this.kStateWaitingData || DownloadsView.loading) { |
|
544 return; |
|
545 } |
|
546 |
|
547 this._state = this.kStateWaitingAnchor; |
|
548 |
|
549 // Ensure the anchor is visible. If that is not possible, show the panel |
|
550 // anchored to the top area of the window, near the default anchor position. |
|
551 DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) { |
|
552 // If somehow we've switched states already (by getting a panel hiding |
|
553 // event before an overlay is loaded, for example), bail out. |
|
554 if (this._state != this.kStateWaitingAnchor) |
|
555 return; |
|
556 |
|
557 // At this point, if the window is minimized, opening the panel could fail |
|
558 // without any notification, and there would be no way to either open or |
|
559 // close the panel anymore. To prevent this, check if the window is |
|
560 // minimized and in that case force the panel to the closed state. |
|
561 if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) { |
|
562 DownloadsButton.releaseAnchor(); |
|
563 this._state = this.kStateHidden; |
|
564 return; |
|
565 } |
|
566 |
|
567 // When the panel is opened, we check if the target files of visible items |
|
568 // still exist, and update the allowed items interactions accordingly. We |
|
569 // do these checks on a background thread, and don't prevent the panel to |
|
570 // be displayed while these checks are being performed. |
|
571 for each (let viewItem in DownloadsView._viewItems) { |
|
572 viewItem.verifyTargetExists(); |
|
573 } |
|
574 |
|
575 if (aAnchor) { |
|
576 DownloadsCommon.log("Opening downloads panel popup."); |
|
577 this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false, |
|
578 null); |
|
579 } else { |
|
580 DownloadsCommon.error("We can't find the anchor! Failure case - opening", |
|
581 "downloads panel on TabsToolbar. We should never", |
|
582 "get here!"); |
|
583 Components.utils.reportError( |
|
584 "Downloads button cannot be found"); |
|
585 } |
|
586 }.bind(this)); |
|
587 } |
|
588 }; |
|
589 |
|
590 //////////////////////////////////////////////////////////////////////////////// |
|
591 //// DownloadsOverlayLoader |
|
592 |
|
593 /** |
|
594 * Allows loading the downloads panel and the status indicator interfaces on |
|
595 * demand, to improve startup performance. |
|
596 */ |
|
597 const DownloadsOverlayLoader = { |
|
598 /** |
|
599 * We cannot load two overlays at the same time, thus we use a queue of |
|
600 * pending load requests. |
|
601 */ |
|
602 _loadRequests: [], |
|
603 |
|
604 /** |
|
605 * True while we are waiting for an overlay to be loaded. |
|
606 */ |
|
607 _overlayLoading: false, |
|
608 |
|
609 /** |
|
610 * This object has a key for each overlay URI that is already loaded. |
|
611 */ |
|
612 _loadedOverlays: {}, |
|
613 |
|
614 /** |
|
615 * Loads the specified overlay and invokes the given callback when finished. |
|
616 * |
|
617 * @param aOverlay |
|
618 * String containing the URI of the overlay to load in the current |
|
619 * window. If this overlay has already been loaded using this |
|
620 * function, then the overlay is not loaded again. |
|
621 * @param aCallback |
|
622 * Invoked when loading is completed. If the overlay is already |
|
623 * loaded, the function is called immediately. |
|
624 */ |
|
625 ensureOverlayLoaded: function DOL_ensureOverlayLoaded(aOverlay, aCallback) |
|
626 { |
|
627 // The overlay is already loaded, invoke the callback immediately. |
|
628 if (aOverlay in this._loadedOverlays) { |
|
629 aCallback(); |
|
630 return; |
|
631 } |
|
632 |
|
633 // The callback will be invoked when loading is finished. |
|
634 this._loadRequests.push({ overlay: aOverlay, callback: aCallback }); |
|
635 if (this._overlayLoading) { |
|
636 return; |
|
637 } |
|
638 |
|
639 function DOL_EOL_loadCallback() { |
|
640 this._overlayLoading = false; |
|
641 this._loadedOverlays[aOverlay] = true; |
|
642 |
|
643 this.processPendingRequests(); |
|
644 } |
|
645 |
|
646 this._overlayLoading = true; |
|
647 DownloadsCommon.log("Loading overlay ", aOverlay); |
|
648 document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this)); |
|
649 }, |
|
650 |
|
651 /** |
|
652 * Re-processes all the currently pending requests, invoking the callbacks |
|
653 * and/or loading more overlays as needed. In most cases, there will be a |
|
654 * single request for one overlay, that will be processed immediately. |
|
655 */ |
|
656 processPendingRequests: function DOL_processPendingRequests() |
|
657 { |
|
658 // Re-process all the currently pending requests, yet allow more requests |
|
659 // to be appended at the end of the array if we're not ready for them. |
|
660 let currentLength = this._loadRequests.length; |
|
661 for (let i = 0; i < currentLength; i++) { |
|
662 let request = this._loadRequests.shift(); |
|
663 |
|
664 // We must call ensureOverlayLoaded again for each request, to check if |
|
665 // the associated callback can be invoked now, or if we must still wait |
|
666 // for the associated overlay to load. |
|
667 this.ensureOverlayLoaded(request.overlay, request.callback); |
|
668 } |
|
669 } |
|
670 }; |
|
671 |
|
672 //////////////////////////////////////////////////////////////////////////////// |
|
673 //// DownloadsView |
|
674 |
|
675 /** |
|
676 * Builds and updates the downloads list widget, responding to changes in the |
|
677 * download state and real-time data. In addition, handles part of the user |
|
678 * interaction events raised by the downloads list widget. |
|
679 */ |
|
680 const DownloadsView = { |
|
681 ////////////////////////////////////////////////////////////////////////////// |
|
682 //// Functions handling download items in the list |
|
683 |
|
684 /** |
|
685 * Maximum number of items shown by the list at any given time. |
|
686 */ |
|
687 kItemCountLimit: 3, |
|
688 |
|
689 /** |
|
690 * Indicates whether we are still loading downloads data asynchronously. |
|
691 */ |
|
692 loading: false, |
|
693 |
|
694 /** |
|
695 * Ordered array of all DownloadsDataItem objects. We need to keep this array |
|
696 * because only a limited number of items are shown at once, and if an item |
|
697 * that is currently visible is removed from the list, we might need to take |
|
698 * another item from the array and make it appear at the bottom. |
|
699 */ |
|
700 _dataItems: [], |
|
701 |
|
702 /** |
|
703 * Object containing the available DownloadsViewItem objects, indexed by their |
|
704 * numeric download identifier. There is a limited number of view items in |
|
705 * the panel at any given time. |
|
706 */ |
|
707 _viewItems: {}, |
|
708 |
|
709 /** |
|
710 * Called when the number of items in the list changes. |
|
711 */ |
|
712 _itemCountChanged: function DV_itemCountChanged() |
|
713 { |
|
714 DownloadsCommon.log("The downloads item count has changed - we are tracking", |
|
715 this._dataItems.length, "downloads in total."); |
|
716 let count = this._dataItems.length; |
|
717 let hiddenCount = count - this.kItemCountLimit; |
|
718 |
|
719 if (count > 0) { |
|
720 DownloadsCommon.log("Setting the panel's hasdownloads attribute to true."); |
|
721 DownloadsPanel.panel.setAttribute("hasdownloads", "true"); |
|
722 } else { |
|
723 DownloadsCommon.log("Removing the panel's hasdownloads attribute."); |
|
724 DownloadsPanel.panel.removeAttribute("hasdownloads"); |
|
725 } |
|
726 |
|
727 // If we've got some hidden downloads, we should activate the |
|
728 // DownloadsSummary. The DownloadsSummary will determine whether or not |
|
729 // it's appropriate to actually display the summary. |
|
730 DownloadsSummary.active = hiddenCount > 0; |
|
731 }, |
|
732 |
|
733 /** |
|
734 * Element corresponding to the list of downloads. |
|
735 */ |
|
736 get richListBox() |
|
737 { |
|
738 delete this.richListBox; |
|
739 return this.richListBox = document.getElementById("downloadsListBox"); |
|
740 }, |
|
741 |
|
742 /** |
|
743 * Element corresponding to the button for showing more downloads. |
|
744 */ |
|
745 get downloadsHistory() |
|
746 { |
|
747 delete this.downloadsHistory; |
|
748 return this.downloadsHistory = document.getElementById("downloadsHistory"); |
|
749 }, |
|
750 |
|
751 ////////////////////////////////////////////////////////////////////////////// |
|
752 //// Callback functions from DownloadsData |
|
753 |
|
754 /** |
|
755 * Called before multiple downloads are about to be loaded. |
|
756 */ |
|
757 onDataLoadStarting: function DV_onDataLoadStarting() |
|
758 { |
|
759 DownloadsCommon.log("onDataLoadStarting called for DownloadsView."); |
|
760 this.loading = true; |
|
761 }, |
|
762 |
|
763 /** |
|
764 * Called after data loading finished. |
|
765 */ |
|
766 onDataLoadCompleted: function DV_onDataLoadCompleted() |
|
767 { |
|
768 DownloadsCommon.log("onDataLoadCompleted called for DownloadsView."); |
|
769 |
|
770 this.loading = false; |
|
771 |
|
772 // We suppressed item count change notifications during the batch load, at |
|
773 // this point we should just call the function once. |
|
774 this._itemCountChanged(); |
|
775 |
|
776 // Notify the panel that all the initially available downloads have been |
|
777 // loaded. This ensures that the interface is visible, if still required. |
|
778 DownloadsPanel.onViewLoadCompleted(); |
|
779 }, |
|
780 |
|
781 /** |
|
782 * Called when a new download data item is available, either during the |
|
783 * asynchronous data load or when a new download is started. |
|
784 * |
|
785 * @param aDataItem |
|
786 * DownloadsDataItem object that was just added. |
|
787 * @param aNewest |
|
788 * When true, indicates that this item is the most recent and should be |
|
789 * added in the topmost position. This happens when a new download is |
|
790 * started. When false, indicates that the item is the least recent |
|
791 * and should be appended. The latter generally happens during the |
|
792 * asynchronous data load. |
|
793 */ |
|
794 onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest) |
|
795 { |
|
796 DownloadsCommon.log("A new download data item was added - aNewest =", |
|
797 aNewest); |
|
798 |
|
799 if (aNewest) { |
|
800 this._dataItems.unshift(aDataItem); |
|
801 } else { |
|
802 this._dataItems.push(aDataItem); |
|
803 } |
|
804 |
|
805 let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit; |
|
806 if (aNewest || !itemsNowOverflow) { |
|
807 // The newly added item is visible in the panel and we must add the |
|
808 // corresponding element. This is either because it is the first item, or |
|
809 // because it was added at the bottom but the list still doesn't overflow. |
|
810 this._addViewItem(aDataItem, aNewest); |
|
811 } |
|
812 if (aNewest && itemsNowOverflow) { |
|
813 // If the list overflows, remove the last item from the panel to make room |
|
814 // for the new one that we just added at the top. |
|
815 this._removeViewItem(this._dataItems[this.kItemCountLimit]); |
|
816 } |
|
817 |
|
818 // For better performance during batch loads, don't update the count for |
|
819 // every item, because the interface won't be visible until load finishes. |
|
820 if (!this.loading) { |
|
821 this._itemCountChanged(); |
|
822 } |
|
823 }, |
|
824 |
|
825 /** |
|
826 * Called when a data item is removed. Ensures that the widget associated |
|
827 * with the view item is removed from the user interface. |
|
828 * |
|
829 * @param aDataItem |
|
830 * DownloadsDataItem object that is being removed. |
|
831 */ |
|
832 onDataItemRemoved: function DV_onDataItemRemoved(aDataItem) |
|
833 { |
|
834 DownloadsCommon.log("A download data item was removed."); |
|
835 |
|
836 let itemIndex = this._dataItems.indexOf(aDataItem); |
|
837 this._dataItems.splice(itemIndex, 1); |
|
838 |
|
839 if (itemIndex < this.kItemCountLimit) { |
|
840 // The item to remove is visible in the panel. |
|
841 this._removeViewItem(aDataItem); |
|
842 if (this._dataItems.length >= this.kItemCountLimit) { |
|
843 // Reinsert the next item into the panel. |
|
844 this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false); |
|
845 } |
|
846 } |
|
847 |
|
848 this._itemCountChanged(); |
|
849 }, |
|
850 |
|
851 /** |
|
852 * Returns the view item associated with the provided data item for this view. |
|
853 * |
|
854 * @param aDataItem |
|
855 * DownloadsDataItem object for which the view item is requested. |
|
856 * |
|
857 * @return Object that can be used to notify item status events. |
|
858 */ |
|
859 getViewItem: function DV_getViewItem(aDataItem) |
|
860 { |
|
861 // If the item is visible, just return it, otherwise return a mock object |
|
862 // that doesn't react to notifications. |
|
863 if (aDataItem.downloadGuid in this._viewItems) { |
|
864 return this._viewItems[aDataItem.downloadGuid]; |
|
865 } |
|
866 return this._invisibleViewItem; |
|
867 }, |
|
868 |
|
869 /** |
|
870 * Mock DownloadsDataItem object that doesn't react to notifications. |
|
871 */ |
|
872 _invisibleViewItem: Object.freeze({ |
|
873 onStateChange: function () { }, |
|
874 onProgressChange: function () { } |
|
875 }), |
|
876 |
|
877 /** |
|
878 * Creates a new view item associated with the specified data item, and adds |
|
879 * it to the top or the bottom of the list. |
|
880 */ |
|
881 _addViewItem: function DV_addViewItem(aDataItem, aNewest) |
|
882 { |
|
883 DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.", |
|
884 "aNewest =", aNewest); |
|
885 |
|
886 let element = document.createElement("richlistitem"); |
|
887 let viewItem = new DownloadsViewItem(aDataItem, element); |
|
888 this._viewItems[aDataItem.downloadGuid] = viewItem; |
|
889 if (aNewest) { |
|
890 this.richListBox.insertBefore(element, this.richListBox.firstChild); |
|
891 } else { |
|
892 this.richListBox.appendChild(element); |
|
893 } |
|
894 }, |
|
895 |
|
896 /** |
|
897 * Removes the view item associated with the specified data item. |
|
898 */ |
|
899 _removeViewItem: function DV_removeViewItem(aDataItem) |
|
900 { |
|
901 DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); |
|
902 let element = this.getViewItem(aDataItem)._element; |
|
903 let previousSelectedIndex = this.richListBox.selectedIndex; |
|
904 this.richListBox.removeChild(element); |
|
905 if (previousSelectedIndex != -1) { |
|
906 this.richListBox.selectedIndex = Math.min(previousSelectedIndex, |
|
907 this.richListBox.itemCount - 1); |
|
908 } |
|
909 delete this._viewItems[aDataItem.downloadGuid]; |
|
910 }, |
|
911 |
|
912 ////////////////////////////////////////////////////////////////////////////// |
|
913 //// User interface event functions |
|
914 |
|
915 /** |
|
916 * Helper function to do commands on a specific download item. |
|
917 * |
|
918 * @param aEvent |
|
919 * Event object for the event being handled. If the event target is |
|
920 * not a richlistitem that represents a download, this function will |
|
921 * walk up the parent nodes until it finds a DOM node that is. |
|
922 * @param aCommand |
|
923 * The command to be performed. |
|
924 */ |
|
925 onDownloadCommand: function DV_onDownloadCommand(aEvent, aCommand) |
|
926 { |
|
927 let target = aEvent.target; |
|
928 while (target.nodeName != "richlistitem") { |
|
929 target = target.parentNode; |
|
930 } |
|
931 new DownloadsViewItemController(target).doCommand(aCommand); |
|
932 }, |
|
933 |
|
934 onDownloadClick: function DV_onDownloadClick(aEvent) |
|
935 { |
|
936 // Handle primary clicks only, and exclude the action button. |
|
937 if (aEvent.button == 0 && |
|
938 !aEvent.originalTarget.hasAttribute("oncommand")) { |
|
939 goDoCommand("downloadsCmd_open"); |
|
940 } |
|
941 }, |
|
942 |
|
943 /** |
|
944 * Handles keypress events on a download item. |
|
945 */ |
|
946 onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent) |
|
947 { |
|
948 // Pressing the key on buttons should not invoke the action because the |
|
949 // event has already been handled by the button itself. |
|
950 if (aEvent.originalTarget.hasAttribute("command") || |
|
951 aEvent.originalTarget.hasAttribute("oncommand")) { |
|
952 return; |
|
953 } |
|
954 |
|
955 if (aEvent.charCode == " ".charCodeAt(0)) { |
|
956 goDoCommand("downloadsCmd_pauseResume"); |
|
957 return; |
|
958 } |
|
959 |
|
960 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { |
|
961 goDoCommand("downloadsCmd_doDefault"); |
|
962 } |
|
963 }, |
|
964 |
|
965 |
|
966 /** |
|
967 * Mouse listeners to handle selection on hover. |
|
968 */ |
|
969 onDownloadMouseOver: function DV_onDownloadMouseOver(aEvent) |
|
970 { |
|
971 if (aEvent.originalTarget.parentNode == this.richListBox) |
|
972 this.richListBox.selectedItem = aEvent.originalTarget; |
|
973 }, |
|
974 onDownloadMouseOut: function DV_onDownloadMouseOut(aEvent) |
|
975 { |
|
976 if (aEvent.originalTarget.parentNode == this.richListBox) { |
|
977 // If the destination element is outside of the richlistitem, clear the |
|
978 // selection. |
|
979 let element = aEvent.relatedTarget; |
|
980 while (element && element != aEvent.originalTarget) { |
|
981 element = element.parentNode; |
|
982 } |
|
983 if (!element) |
|
984 this.richListBox.selectedIndex = -1; |
|
985 } |
|
986 }, |
|
987 |
|
988 onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent) |
|
989 { |
|
990 let element = this.richListBox.selectedItem; |
|
991 if (!element) { |
|
992 return; |
|
993 } |
|
994 |
|
995 DownloadsViewController.updateCommands(); |
|
996 |
|
997 // Set the state attribute so that only the appropriate items are displayed. |
|
998 let contextMenu = document.getElementById("downloadsContextMenu"); |
|
999 contextMenu.setAttribute("state", element.getAttribute("state")); |
|
1000 }, |
|
1001 |
|
1002 onDownloadDragStart: function DV_onDownloadDragStart(aEvent) |
|
1003 { |
|
1004 let element = this.richListBox.selectedItem; |
|
1005 if (!element) { |
|
1006 return; |
|
1007 } |
|
1008 |
|
1009 let controller = new DownloadsViewItemController(element); |
|
1010 let localFile = controller.dataItem.localFile; |
|
1011 if (!localFile.exists()) { |
|
1012 return; |
|
1013 } |
|
1014 |
|
1015 let dataTransfer = aEvent.dataTransfer; |
|
1016 dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0); |
|
1017 dataTransfer.effectAllowed = "copyMove"; |
|
1018 var url = Services.io.newFileURI(localFile).spec; |
|
1019 dataTransfer.setData("text/uri-list", url); |
|
1020 dataTransfer.setData("text/plain", url); |
|
1021 dataTransfer.addElement(element); |
|
1022 |
|
1023 aEvent.stopPropagation(); |
|
1024 } |
|
1025 } |
|
1026 |
|
1027 //////////////////////////////////////////////////////////////////////////////// |
|
1028 //// DownloadsViewItem |
|
1029 |
|
1030 /** |
|
1031 * Builds and updates a single item in the downloads list widget, responding to |
|
1032 * changes in the download state and real-time data. |
|
1033 * |
|
1034 * @param aDataItem |
|
1035 * DownloadsDataItem to be associated with the view item. |
|
1036 * @param aElement |
|
1037 * XUL element corresponding to the single download item in the view. |
|
1038 */ |
|
1039 function DownloadsViewItem(aDataItem, aElement) |
|
1040 { |
|
1041 this._element = aElement; |
|
1042 this.dataItem = aDataItem; |
|
1043 |
|
1044 this.lastEstimatedSecondsLeft = Infinity; |
|
1045 |
|
1046 // Set the URI that represents the correct icon for the target file. As soon |
|
1047 // as bug 239948 comment 12 is handled, the "file" property will be always a |
|
1048 // file URL rather than a file name. At that point we should remove the "//" |
|
1049 // (double slash) from the icon URI specification (see test_moz_icon_uri.js). |
|
1050 this.image = "moz-icon://" + this.dataItem.file + "?size=32"; |
|
1051 |
|
1052 let attributes = { |
|
1053 "type": "download", |
|
1054 "class": "download-state", |
|
1055 "id": "downloadsItem_" + this.dataItem.downloadGuid, |
|
1056 "downloadGuid": this.dataItem.downloadGuid, |
|
1057 "state": this.dataItem.state, |
|
1058 "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100, |
|
1059 "target": this.dataItem.target, |
|
1060 "image": this.image |
|
1061 }; |
|
1062 |
|
1063 for (let attributeName in attributes) { |
|
1064 this._element.setAttribute(attributeName, attributes[attributeName]); |
|
1065 } |
|
1066 |
|
1067 // Initialize more complex attributes. |
|
1068 this._updateProgress(); |
|
1069 this._updateStatusLine(); |
|
1070 this.verifyTargetExists(); |
|
1071 } |
|
1072 |
|
1073 DownloadsViewItem.prototype = { |
|
1074 /** |
|
1075 * The DownloadDataItem associated with this view item. |
|
1076 */ |
|
1077 dataItem: null, |
|
1078 |
|
1079 /** |
|
1080 * The XUL element corresponding to the associated richlistbox item. |
|
1081 */ |
|
1082 _element: null, |
|
1083 |
|
1084 /** |
|
1085 * The inner XUL element for the progress bar, or null if not available. |
|
1086 */ |
|
1087 _progressElement: null, |
|
1088 |
|
1089 ////////////////////////////////////////////////////////////////////////////// |
|
1090 //// Callback functions from DownloadsData |
|
1091 |
|
1092 /** |
|
1093 * Called when the download state might have changed. Sometimes the state of |
|
1094 * the download might be the same as before, if the data layer received |
|
1095 * multiple events for the same download. |
|
1096 */ |
|
1097 onStateChange: function DVI_onStateChange(aOldState) |
|
1098 { |
|
1099 // If a download just finished successfully, it means that the target file |
|
1100 // now exists and we can extract its specific icon. To ensure that the icon |
|
1101 // is reloaded, we must change the URI used by the XUL image element, for |
|
1102 // example by adding a query parameter. Since this URI has a "moz-icon" |
|
1103 // scheme, this only works if we add one of the parameters explicitly |
|
1104 // supported by the nsIMozIconURI interface. |
|
1105 if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED && |
|
1106 aOldState != this.dataItem.state) { |
|
1107 this._element.setAttribute("image", this.image + "&state=normal"); |
|
1108 |
|
1109 // We assume the existence of the target of a download that just completed |
|
1110 // successfully, without checking the condition in the background. If the |
|
1111 // panel is already open, this will take effect immediately. If the panel |
|
1112 // is opened later, a new background existence check will be performed. |
|
1113 this._element.setAttribute("exists", "true"); |
|
1114 } |
|
1115 |
|
1116 // Update the user interface after switching states. |
|
1117 this._element.setAttribute("state", this.dataItem.state); |
|
1118 this._updateProgress(); |
|
1119 this._updateStatusLine(); |
|
1120 }, |
|
1121 |
|
1122 /** |
|
1123 * Called when the download progress has changed. |
|
1124 */ |
|
1125 onProgressChange: function DVI_onProgressChange() { |
|
1126 this._updateProgress(); |
|
1127 this._updateStatusLine(); |
|
1128 }, |
|
1129 |
|
1130 ////////////////////////////////////////////////////////////////////////////// |
|
1131 //// Functions for updating the user interface |
|
1132 |
|
1133 /** |
|
1134 * Updates the progress bar. |
|
1135 */ |
|
1136 _updateProgress: function DVI_updateProgress() { |
|
1137 if (this.dataItem.starting) { |
|
1138 // Before the download starts, the progress meter has its initial value. |
|
1139 this._element.setAttribute("progressmode", "normal"); |
|
1140 this._element.setAttribute("progress", "0"); |
|
1141 } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING || |
|
1142 this.dataItem.percentComplete == -1) { |
|
1143 // We might not know the progress of a running download, and we don't know |
|
1144 // the remaining time during the malware scanning phase. |
|
1145 this._element.setAttribute("progressmode", "undetermined"); |
|
1146 } else { |
|
1147 // This is a running download of which we know the progress. |
|
1148 this._element.setAttribute("progressmode", "normal"); |
|
1149 this._element.setAttribute("progress", this.dataItem.percentComplete); |
|
1150 } |
|
1151 |
|
1152 // Find the progress element as soon as the download binding is accessible. |
|
1153 if (!this._progressElement) { |
|
1154 this._progressElement = |
|
1155 document.getAnonymousElementByAttribute(this._element, "anonid", |
|
1156 "progressmeter"); |
|
1157 } |
|
1158 |
|
1159 // Dispatch the ValueChange event for accessibility, if possible. |
|
1160 if (this._progressElement) { |
|
1161 let event = document.createEvent("Events"); |
|
1162 event.initEvent("ValueChange", true, true); |
|
1163 this._progressElement.dispatchEvent(event); |
|
1164 } |
|
1165 }, |
|
1166 |
|
1167 /** |
|
1168 * Updates the main status line, including bytes transferred, bytes total, |
|
1169 * download rate, and time remaining. |
|
1170 */ |
|
1171 _updateStatusLine: function DVI_updateStatusLine() { |
|
1172 const nsIDM = Ci.nsIDownloadManager; |
|
1173 |
|
1174 let status = ""; |
|
1175 let statusTip = ""; |
|
1176 |
|
1177 if (this.dataItem.paused) { |
|
1178 let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes, |
|
1179 this.dataItem.maxBytes); |
|
1180 |
|
1181 // We use the same XUL label to display both the state and the amount |
|
1182 // transferred, for example "Paused - 1.1 MB". |
|
1183 status = DownloadsCommon.strings.statusSeparatorBeforeNumber( |
|
1184 DownloadsCommon.strings.statePaused, |
|
1185 transfer); |
|
1186 } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) { |
|
1187 // We don't show the rate for each download in order to reduce clutter. |
|
1188 // The remaining time per download is likely enough information for the |
|
1189 // panel. |
|
1190 [status] = |
|
1191 DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes, |
|
1192 this.dataItem.maxBytes, |
|
1193 this.dataItem.speed, |
|
1194 this.lastEstimatedSecondsLeft); |
|
1195 |
|
1196 // We are, however, OK with displaying the rate in the tooltip. |
|
1197 let newEstimatedSecondsLeft; |
|
1198 [statusTip, newEstimatedSecondsLeft] = |
|
1199 DownloadUtils.getDownloadStatus(this.dataItem.currBytes, |
|
1200 this.dataItem.maxBytes, |
|
1201 this.dataItem.speed, |
|
1202 this.lastEstimatedSecondsLeft); |
|
1203 this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; |
|
1204 } else if (this.dataItem.starting) { |
|
1205 status = DownloadsCommon.strings.stateStarting; |
|
1206 } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) { |
|
1207 status = DownloadsCommon.strings.stateScanning; |
|
1208 } else if (!this.dataItem.inProgress) { |
|
1209 let stateLabel = function () { |
|
1210 let s = DownloadsCommon.strings; |
|
1211 switch (this.dataItem.state) { |
|
1212 case nsIDM.DOWNLOAD_FAILED: return s.stateFailed; |
|
1213 case nsIDM.DOWNLOAD_CANCELED: return s.stateCanceled; |
|
1214 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls; |
|
1215 case nsIDM.DOWNLOAD_BLOCKED_POLICY: return s.stateBlockedPolicy; |
|
1216 case nsIDM.DOWNLOAD_DIRTY: return s.stateDirty; |
|
1217 case nsIDM.DOWNLOAD_FINISHED: return this._fileSizeText; |
|
1218 } |
|
1219 return null; |
|
1220 }.apply(this); |
|
1221 |
|
1222 let [displayHost, fullHost] = |
|
1223 DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri); |
|
1224 |
|
1225 let end = new Date(this.dataItem.endTime); |
|
1226 let [displayDate, fullDate] = DownloadUtils.getReadableDates(end); |
|
1227 |
|
1228 // We use the same XUL label to display the state, the host name, and the |
|
1229 // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB - |
|
1230 // website2.com - Yesterday". We show the full host and the complete date |
|
1231 // in the tooltip. |
|
1232 let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel, |
|
1233 displayHost); |
|
1234 status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate); |
|
1235 statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate); |
|
1236 } |
|
1237 |
|
1238 this._element.setAttribute("status", status); |
|
1239 this._element.setAttribute("statusTip", statusTip || status); |
|
1240 }, |
|
1241 |
|
1242 /** |
|
1243 * Localized string representing the total size of completed downloads, for |
|
1244 * example "1.5 MB" or "Unknown size". |
|
1245 */ |
|
1246 get _fileSizeText() |
|
1247 { |
|
1248 // Display the file size, but show "Unknown" for negative sizes. |
|
1249 let fileSize = this.dataItem.maxBytes; |
|
1250 if (fileSize < 0) { |
|
1251 return DownloadsCommon.strings.sizeUnknown; |
|
1252 } |
|
1253 let [size, unit] = DownloadUtils.convertByteUnits(fileSize); |
|
1254 return DownloadsCommon.strings.sizeWithUnits(size, unit); |
|
1255 }, |
|
1256 |
|
1257 ////////////////////////////////////////////////////////////////////////////// |
|
1258 //// Functions called by the panel |
|
1259 |
|
1260 /** |
|
1261 * Starts checking whether the target file of a finished download is still |
|
1262 * available on disk, and sets an attribute that controls how the item is |
|
1263 * presented visually. |
|
1264 * |
|
1265 * The existence check is executed on a background thread. |
|
1266 */ |
|
1267 verifyTargetExists: function DVI_verifyTargetExists() { |
|
1268 // We don't need to check if the download is not finished successfully. |
|
1269 if (!this.dataItem.openable) { |
|
1270 return; |
|
1271 } |
|
1272 |
|
1273 OS.File.exists(this.dataItem.localFile.path).then( |
|
1274 function DVI_RTE_onSuccess(aExists) { |
|
1275 if (aExists) { |
|
1276 this._element.setAttribute("exists", "true"); |
|
1277 } else { |
|
1278 this._element.removeAttribute("exists"); |
|
1279 } |
|
1280 }.bind(this), Cu.reportError); |
|
1281 }, |
|
1282 }; |
|
1283 |
|
1284 //////////////////////////////////////////////////////////////////////////////// |
|
1285 //// DownloadsViewController |
|
1286 |
|
1287 /** |
|
1288 * Handles part of the user interaction events raised by the downloads list |
|
1289 * widget, in particular the "commands" that apply to multiple items, and |
|
1290 * dispatches the commands that apply to individual items. |
|
1291 */ |
|
1292 const DownloadsViewController = { |
|
1293 ////////////////////////////////////////////////////////////////////////////// |
|
1294 //// Initialization and termination |
|
1295 |
|
1296 initialize: function DVC_initialize() |
|
1297 { |
|
1298 window.controllers.insertControllerAt(0, this); |
|
1299 }, |
|
1300 |
|
1301 terminate: function DVC_terminate() |
|
1302 { |
|
1303 window.controllers.removeController(this); |
|
1304 }, |
|
1305 |
|
1306 ////////////////////////////////////////////////////////////////////////////// |
|
1307 //// nsIController |
|
1308 |
|
1309 supportsCommand: function DVC_supportsCommand(aCommand) |
|
1310 { |
|
1311 // Firstly, determine if this is a command that we can handle. |
|
1312 if (!(aCommand in this.commands) && |
|
1313 !(aCommand in DownloadsViewItemController.prototype.commands)) { |
|
1314 return false; |
|
1315 } |
|
1316 // Secondly, determine if focus is on a control in the downloads list. |
|
1317 let element = document.commandDispatcher.focusedElement; |
|
1318 while (element && element != DownloadsView.richListBox) { |
|
1319 element = element.parentNode; |
|
1320 } |
|
1321 // We should handle the command only if the downloads list is among the |
|
1322 // ancestors of the focused element. |
|
1323 return !!element; |
|
1324 }, |
|
1325 |
|
1326 isCommandEnabled: function DVC_isCommandEnabled(aCommand) |
|
1327 { |
|
1328 // Handle commands that are not selection-specific. |
|
1329 if (aCommand == "downloadsCmd_clearList") { |
|
1330 return DownloadsCommon.getData(window).canRemoveFinished; |
|
1331 } |
|
1332 |
|
1333 // Other commands are selection-specific. |
|
1334 let element = DownloadsView.richListBox.selectedItem; |
|
1335 return element && |
|
1336 new DownloadsViewItemController(element).isCommandEnabled(aCommand); |
|
1337 }, |
|
1338 |
|
1339 doCommand: function DVC_doCommand(aCommand) |
|
1340 { |
|
1341 // If this command is not selection-specific, execute it. |
|
1342 if (aCommand in this.commands) { |
|
1343 this.commands[aCommand].apply(this); |
|
1344 return; |
|
1345 } |
|
1346 |
|
1347 // Other commands are selection-specific. |
|
1348 let element = DownloadsView.richListBox.selectedItem; |
|
1349 if (element) { |
|
1350 // The doCommand function also checks if the command is enabled. |
|
1351 new DownloadsViewItemController(element).doCommand(aCommand); |
|
1352 } |
|
1353 }, |
|
1354 |
|
1355 onEvent: function () { }, |
|
1356 |
|
1357 ////////////////////////////////////////////////////////////////////////////// |
|
1358 //// Other functions |
|
1359 |
|
1360 updateCommands: function DVC_updateCommands() |
|
1361 { |
|
1362 Object.keys(this.commands).forEach(goUpdateCommand); |
|
1363 Object.keys(DownloadsViewItemController.prototype.commands) |
|
1364 .forEach(goUpdateCommand); |
|
1365 }, |
|
1366 |
|
1367 ////////////////////////////////////////////////////////////////////////////// |
|
1368 //// Selection-independent commands |
|
1369 |
|
1370 /** |
|
1371 * This object contains one key for each command that operates regardless of |
|
1372 * the currently selected item in the list. |
|
1373 */ |
|
1374 commands: { |
|
1375 downloadsCmd_clearList: function DVC_downloadsCmd_clearList() |
|
1376 { |
|
1377 DownloadsCommon.getData(window).removeFinished(); |
|
1378 } |
|
1379 } |
|
1380 }; |
|
1381 |
|
1382 //////////////////////////////////////////////////////////////////////////////// |
|
1383 //// DownloadsViewItemController |
|
1384 |
|
1385 /** |
|
1386 * Handles all the user interaction events, in particular the "commands", |
|
1387 * related to a single item in the downloads list widgets. |
|
1388 */ |
|
1389 function DownloadsViewItemController(aElement) { |
|
1390 let downloadGuid = aElement.getAttribute("downloadGuid"); |
|
1391 this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid]; |
|
1392 } |
|
1393 |
|
1394 DownloadsViewItemController.prototype = { |
|
1395 ////////////////////////////////////////////////////////////////////////////// |
|
1396 //// Command dispatching |
|
1397 |
|
1398 /** |
|
1399 * The DownloadDataItem controlled by this object. |
|
1400 */ |
|
1401 dataItem: null, |
|
1402 |
|
1403 isCommandEnabled: function DVIC_isCommandEnabled(aCommand) |
|
1404 { |
|
1405 switch (aCommand) { |
|
1406 case "downloadsCmd_open": { |
|
1407 return this.dataItem.openable && this.dataItem.localFile.exists(); |
|
1408 } |
|
1409 case "downloadsCmd_show": { |
|
1410 return this.dataItem.localFile.exists() || |
|
1411 this.dataItem.partFile.exists(); |
|
1412 } |
|
1413 case "downloadsCmd_pauseResume": |
|
1414 return this.dataItem.inProgress && this.dataItem.resumable; |
|
1415 case "downloadsCmd_retry": |
|
1416 return this.dataItem.canRetry; |
|
1417 case "downloadsCmd_openReferrer": |
|
1418 return !!this.dataItem.referrer; |
|
1419 case "cmd_delete": |
|
1420 case "downloadsCmd_cancel": |
|
1421 case "downloadsCmd_copyLocation": |
|
1422 case "downloadsCmd_doDefault": |
|
1423 return true; |
|
1424 } |
|
1425 return false; |
|
1426 }, |
|
1427 |
|
1428 doCommand: function DVIC_doCommand(aCommand) |
|
1429 { |
|
1430 if (this.isCommandEnabled(aCommand)) { |
|
1431 this.commands[aCommand].apply(this); |
|
1432 } |
|
1433 }, |
|
1434 |
|
1435 ////////////////////////////////////////////////////////////////////////////// |
|
1436 //// Item commands |
|
1437 |
|
1438 /** |
|
1439 * This object contains one key for each command that operates on this item. |
|
1440 * |
|
1441 * In commands, the "this" identifier points to the controller item. |
|
1442 */ |
|
1443 commands: { |
|
1444 cmd_delete: function DVIC_cmd_delete() |
|
1445 { |
|
1446 this.dataItem.remove(); |
|
1447 PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri)); |
|
1448 }, |
|
1449 |
|
1450 downloadsCmd_cancel: function DVIC_downloadsCmd_cancel() |
|
1451 { |
|
1452 this.dataItem.cancel(); |
|
1453 }, |
|
1454 |
|
1455 downloadsCmd_open: function DVIC_downloadsCmd_open() |
|
1456 { |
|
1457 this.dataItem.openLocalFile(); |
|
1458 |
|
1459 // We explicitly close the panel here to give the user the feedback that |
|
1460 // their click has been received, and we're handling the action. |
|
1461 // Otherwise, we'd have to wait for the file-type handler to execute |
|
1462 // before the panel would close. This also helps to prevent the user from |
|
1463 // accidentally opening a file several times. |
|
1464 DownloadsPanel.hidePanel(); |
|
1465 }, |
|
1466 |
|
1467 downloadsCmd_show: function DVIC_downloadsCmd_show() |
|
1468 { |
|
1469 this.dataItem.showLocalFile(); |
|
1470 |
|
1471 // We explicitly close the panel here to give the user the feedback that |
|
1472 // their click has been received, and we're handling the action. |
|
1473 // Otherwise, we'd have to wait for the operating system file manager |
|
1474 // window to open before the panel closed. This also helps to prevent the |
|
1475 // user from opening the containing folder several times. |
|
1476 DownloadsPanel.hidePanel(); |
|
1477 }, |
|
1478 |
|
1479 downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume() |
|
1480 { |
|
1481 this.dataItem.togglePauseResume(); |
|
1482 }, |
|
1483 |
|
1484 downloadsCmd_retry: function DVIC_downloadsCmd_retry() |
|
1485 { |
|
1486 this.dataItem.retry(); |
|
1487 }, |
|
1488 |
|
1489 downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer() |
|
1490 { |
|
1491 openURL(this.dataItem.referrer); |
|
1492 }, |
|
1493 |
|
1494 downloadsCmd_copyLocation: function DVIC_downloadsCmd_copyLocation() |
|
1495 { |
|
1496 let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] |
|
1497 .getService(Ci.nsIClipboardHelper); |
|
1498 clipboard.copyString(this.dataItem.uri, document); |
|
1499 }, |
|
1500 |
|
1501 downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault() |
|
1502 { |
|
1503 const nsIDM = Ci.nsIDownloadManager; |
|
1504 |
|
1505 // Determine the default command for the current item. |
|
1506 let defaultCommand = function () { |
|
1507 switch (this.dataItem.state) { |
|
1508 case nsIDM.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel"; |
|
1509 case nsIDM.DOWNLOAD_FINISHED: return "downloadsCmd_open"; |
|
1510 case nsIDM.DOWNLOAD_FAILED: return "downloadsCmd_retry"; |
|
1511 case nsIDM.DOWNLOAD_CANCELED: return "downloadsCmd_retry"; |
|
1512 case nsIDM.DOWNLOAD_PAUSED: return "downloadsCmd_pauseResume"; |
|
1513 case nsIDM.DOWNLOAD_QUEUED: return "downloadsCmd_cancel"; |
|
1514 case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer"; |
|
1515 case nsIDM.DOWNLOAD_SCANNING: return "downloadsCmd_show"; |
|
1516 case nsIDM.DOWNLOAD_DIRTY: return "downloadsCmd_openReferrer"; |
|
1517 case nsIDM.DOWNLOAD_BLOCKED_POLICY: return "downloadsCmd_openReferrer"; |
|
1518 } |
|
1519 return ""; |
|
1520 }.apply(this); |
|
1521 if (defaultCommand && this.isCommandEnabled(defaultCommand)) |
|
1522 this.doCommand(defaultCommand); |
|
1523 } |
|
1524 } |
|
1525 }; |
|
1526 |
|
1527 |
|
1528 //////////////////////////////////////////////////////////////////////////////// |
|
1529 //// DownloadsSummary |
|
1530 |
|
1531 /** |
|
1532 * Manages the summary at the bottom of the downloads panel list if the number |
|
1533 * of items in the list exceeds the panels limit. |
|
1534 */ |
|
1535 const DownloadsSummary = { |
|
1536 |
|
1537 /** |
|
1538 * Sets the active state of the summary. When active, the summary subscribes |
|
1539 * to the DownloadsCommon DownloadsSummaryData singleton. |
|
1540 * |
|
1541 * @param aActive |
|
1542 * Set to true to activate the summary. |
|
1543 */ |
|
1544 set active(aActive) |
|
1545 { |
|
1546 if (aActive == this._active || !this._summaryNode) { |
|
1547 return this._active; |
|
1548 } |
|
1549 if (aActive) { |
|
1550 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) |
|
1551 .refreshView(this); |
|
1552 } else { |
|
1553 DownloadsFooter.showingSummary = false; |
|
1554 } |
|
1555 |
|
1556 return this._active = aActive; |
|
1557 }, |
|
1558 |
|
1559 /** |
|
1560 * Returns the active state of the downloads summary. |
|
1561 */ |
|
1562 get active() this._active, |
|
1563 |
|
1564 _active: false, |
|
1565 |
|
1566 /** |
|
1567 * Sets whether or not we show the progress bar. |
|
1568 * |
|
1569 * @param aShowingProgress |
|
1570 * True if we should show the progress bar. |
|
1571 */ |
|
1572 set showingProgress(aShowingProgress) |
|
1573 { |
|
1574 if (aShowingProgress) { |
|
1575 this._summaryNode.setAttribute("inprogress", "true"); |
|
1576 } else { |
|
1577 this._summaryNode.removeAttribute("inprogress"); |
|
1578 } |
|
1579 // If progress isn't being shown, then we simply do not show the summary. |
|
1580 return DownloadsFooter.showingSummary = aShowingProgress; |
|
1581 }, |
|
1582 |
|
1583 /** |
|
1584 * Sets the amount of progress that is visible in the progress bar. |
|
1585 * |
|
1586 * @param aValue |
|
1587 * A value between 0 and 100 to represent the progress of the |
|
1588 * summarized downloads. |
|
1589 */ |
|
1590 set percentComplete(aValue) |
|
1591 { |
|
1592 if (this._progressNode) { |
|
1593 this._progressNode.setAttribute("value", aValue); |
|
1594 } |
|
1595 return aValue; |
|
1596 }, |
|
1597 |
|
1598 /** |
|
1599 * Sets the description for the download summary. |
|
1600 * |
|
1601 * @param aValue |
|
1602 * A string representing the description of the summarized |
|
1603 * downloads. |
|
1604 */ |
|
1605 set description(aValue) |
|
1606 { |
|
1607 if (this._descriptionNode) { |
|
1608 this._descriptionNode.setAttribute("value", aValue); |
|
1609 this._descriptionNode.setAttribute("tooltiptext", aValue); |
|
1610 } |
|
1611 return aValue; |
|
1612 }, |
|
1613 |
|
1614 /** |
|
1615 * Sets the details for the download summary, such as the time remaining, |
|
1616 * the amount of bytes transferred, etc. |
|
1617 * |
|
1618 * @param aValue |
|
1619 * A string representing the details of the summarized |
|
1620 * downloads. |
|
1621 */ |
|
1622 set details(aValue) |
|
1623 { |
|
1624 if (this._detailsNode) { |
|
1625 this._detailsNode.setAttribute("value", aValue); |
|
1626 this._detailsNode.setAttribute("tooltiptext", aValue); |
|
1627 } |
|
1628 return aValue; |
|
1629 }, |
|
1630 |
|
1631 /** |
|
1632 * Focuses the root element of the summary. |
|
1633 */ |
|
1634 focus: function() |
|
1635 { |
|
1636 if (this._summaryNode) { |
|
1637 this._summaryNode.focus(); |
|
1638 } |
|
1639 }, |
|
1640 |
|
1641 /** |
|
1642 * Respond to keydown events on the Downloads Summary node. |
|
1643 * |
|
1644 * @param aEvent |
|
1645 * The keydown event being handled. |
|
1646 */ |
|
1647 onKeyDown: function DS_onKeyDown(aEvent) |
|
1648 { |
|
1649 if (aEvent.charCode == " ".charCodeAt(0) || |
|
1650 aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { |
|
1651 DownloadsPanel.showDownloadsHistory(); |
|
1652 } |
|
1653 }, |
|
1654 |
|
1655 /** |
|
1656 * Respond to click events on the Downloads Summary node. |
|
1657 * |
|
1658 * @param aEvent |
|
1659 * The click event being handled. |
|
1660 */ |
|
1661 onClick: function DS_onClick(aEvent) |
|
1662 { |
|
1663 DownloadsPanel.showDownloadsHistory(); |
|
1664 }, |
|
1665 |
|
1666 /** |
|
1667 * Element corresponding to the root of the downloads summary. |
|
1668 */ |
|
1669 get _summaryNode() |
|
1670 { |
|
1671 let node = document.getElementById("downloadsSummary"); |
|
1672 if (!node) { |
|
1673 return null; |
|
1674 } |
|
1675 delete this._summaryNode; |
|
1676 return this._summaryNode = node; |
|
1677 }, |
|
1678 |
|
1679 /** |
|
1680 * Element corresponding to the progress bar in the downloads summary. |
|
1681 */ |
|
1682 get _progressNode() |
|
1683 { |
|
1684 let node = document.getElementById("downloadsSummaryProgress"); |
|
1685 if (!node) { |
|
1686 return null; |
|
1687 } |
|
1688 delete this._progressNode; |
|
1689 return this._progressNode = node; |
|
1690 }, |
|
1691 |
|
1692 /** |
|
1693 * Element corresponding to the main description of the downloads |
|
1694 * summary. |
|
1695 */ |
|
1696 get _descriptionNode() |
|
1697 { |
|
1698 let node = document.getElementById("downloadsSummaryDescription"); |
|
1699 if (!node) { |
|
1700 return null; |
|
1701 } |
|
1702 delete this._descriptionNode; |
|
1703 return this._descriptionNode = node; |
|
1704 }, |
|
1705 |
|
1706 /** |
|
1707 * Element corresponding to the secondary description of the downloads |
|
1708 * summary. |
|
1709 */ |
|
1710 get _detailsNode() |
|
1711 { |
|
1712 let node = document.getElementById("downloadsSummaryDetails"); |
|
1713 if (!node) { |
|
1714 return null; |
|
1715 } |
|
1716 delete this._detailsNode; |
|
1717 return this._detailsNode = node; |
|
1718 } |
|
1719 } |
|
1720 |
|
1721 //////////////////////////////////////////////////////////////////////////////// |
|
1722 //// DownloadsFooter |
|
1723 |
|
1724 /** |
|
1725 * Manages events sent to to the footer vbox, which contains both the |
|
1726 * DownloadsSummary as well as the "Show All Downloads" button. |
|
1727 */ |
|
1728 const DownloadsFooter = { |
|
1729 |
|
1730 /** |
|
1731 * Focuses the appropriate element within the footer. If the summary |
|
1732 * is visible, focus it. If not, focus the "Show All Downloads" |
|
1733 * button. |
|
1734 */ |
|
1735 focus: function DF_focus() |
|
1736 { |
|
1737 if (this._showingSummary) { |
|
1738 DownloadsSummary.focus(); |
|
1739 } else { |
|
1740 DownloadsView.downloadsHistory.focus(); |
|
1741 } |
|
1742 }, |
|
1743 |
|
1744 _showingSummary: false, |
|
1745 |
|
1746 /** |
|
1747 * Sets whether or not the Downloads Summary should be displayed in the |
|
1748 * footer. If not, the "Show All Downloads" button is shown instead. |
|
1749 */ |
|
1750 set showingSummary(aValue) |
|
1751 { |
|
1752 if (this._footerNode) { |
|
1753 if (aValue) { |
|
1754 this._footerNode.setAttribute("showingsummary", "true"); |
|
1755 } else { |
|
1756 this._footerNode.removeAttribute("showingsummary"); |
|
1757 } |
|
1758 this._showingSummary = aValue; |
|
1759 } |
|
1760 return aValue; |
|
1761 }, |
|
1762 |
|
1763 /** |
|
1764 * Element corresponding to the footer of the downloads panel. |
|
1765 */ |
|
1766 get _footerNode() |
|
1767 { |
|
1768 let node = document.getElementById("downloadsFooter"); |
|
1769 if (!node) { |
|
1770 return null; |
|
1771 } |
|
1772 delete this._footerNode; |
|
1773 return this._footerNode = node; |
|
1774 } |
|
1775 }; |