Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
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 */
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 */
65 "use strict";
67 ////////////////////////////////////////////////////////////////////////////////
68 //// Globals
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");
83 ////////////////////////////////////////////////////////////////////////////////
84 //// DownloadsPanel
86 /**
87 * Main entry point for the downloads panel interface.
88 */
89 const DownloadsPanel = {
90 //////////////////////////////////////////////////////////////////////////////
91 //// Initialization and termination
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,
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,
111 /**
112 * Location of the panel overlay.
113 */
114 get kDownloadsOverlay()
115 "chrome://browser/content/downloads/downloadsOverlay.xul",
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;
135 window.addEventListener("unload", this.onWindowUnload, false);
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();
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 },
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 }
172 window.removeEventListener("unload", this.onWindowUnload, false);
174 // Ensure that the panel is closed before shutting down.
175 this.hidePanel();
177 DownloadsViewController.terminate();
178 DownloadsCommon.getData(window).removeView(DownloadsView);
179 DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit)
180 .removeView(DownloadsSummary);
181 this._unattachEventListeners();
183 this._state = this.kStateUninitialized;
185 DownloadsSummary.active = false;
186 DownloadsCommon.log("DownloadsPanel terminated.");
187 },
189 //////////////////////////////////////////////////////////////////////////////
190 //// Panel interface
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;
204 delete this.panel;
205 return this.panel = downloadsPanel;
206 },
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.");
218 if (this.isPanelShowing) {
219 DownloadsCommon.log("Panel is already showing - focusing instead.");
220 this._focusPanel();
221 return;
222 }
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));
232 DownloadsCommon.log("Waiting for the downloads panel to appear.");
233 this._state = this.kStateWaitingData;
234 },
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.");
244 if (!this.isPanelShowing) {
245 DownloadsCommon.log("Downloads panel is not showing - nothing to do.");
246 return;
247 }
249 this.panel.hidePopup();
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 },
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 },
268 /**
269 * Returns whether the user has started keyboard navigation.
270 */
271 get keyFocusing()
272 {
273 return this.panel.hasAttribute("keyfocus");
274 },
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 },
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 },
304 //////////////////////////////////////////////////////////////////////////////
305 //// Callback functions from DownloadsView
307 /**
308 * Called after data loading finished.
309 */
310 onViewLoadCompleted: function DP_onViewLoadCompleted()
311 {
312 this._openPopupIfDataReady();
313 },
315 //////////////////////////////////////////////////////////////////////////////
316 //// User interface event functions
318 onWindowUnload: function DP_onWindowUnload()
319 {
320 // This function is registered as an event listener, we can't use "this".
321 DownloadsPanel.terminate();
322 },
324 onPopupShown: function DP_onPopupShown(aEvent)
325 {
326 // Ignore events raised by nested popups.
327 if (aEvent.target != aEvent.currentTarget) {
328 return;
329 }
331 DownloadsCommon.log("Downloads panel has shown.");
332 this._state = this.kStateShown;
334 // Since at most one popup is open at any given time, we can set globally.
335 DownloadsCommon.getIndicatorData(window).attentionSuppressed = true;
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 }
343 this._focusPanel();
344 },
346 onPopupHidden: function DP_onPopupHidden(aEvent)
347 {
348 // Ignore events raised by nested popups.
349 if (aEvent.target != aEvent.currentTarget) {
350 return;
351 }
353 DownloadsCommon.log("Downloads panel has hidden.");
355 // Removes the keyfocus attribute so that we stop handling keyboard
356 // navigation.
357 this.keyFocusing = false;
359 // Since at most one popup is open at any given time, we can set globally.
360 DownloadsCommon.getIndicatorData(window).attentionSuppressed = false;
362 // Allow the anchor to be hidden.
363 DownloadsButton.releaseAnchor();
365 // Allow the panel to be reopened.
366 this._state = this.kStateHidden;
367 },
369 //////////////////////////////////////////////////////////////////////////////
370 //// Related operations
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();
382 BrowserDownloadsUI();
383 },
385 //////////////////////////////////////////////////////////////////////////////
386 //// Internal functions
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 },
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 },
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 }
421 let richListBox = DownloadsView.richListBox;
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 }
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 }
451 // Pass keypress events to the richlistbox view when it's focused.
452 if (document.activeElement === richListBox) {
453 DownloadsView.onDownloadKeyPress(aEvent);
454 }
455 },
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 }
475 let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V &&
476 #ifdef XP_MACOSX
477 aEvent.metaKey;
478 #else
479 aEvent.ctrlKey;
480 #endif
482 if (!pasting) {
483 return;
484 }
486 DownloadsCommon.log("Received a paste event.");
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 }
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 },
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 }
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 },
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 }
547 this._state = this.kStateWaitingAnchor;
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;
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 }
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 }
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 };
590 ////////////////////////////////////////////////////////////////////////////////
591 //// DownloadsOverlayLoader
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: [],
604 /**
605 * True while we are waiting for an overlay to be loaded.
606 */
607 _overlayLoading: false,
609 /**
610 * This object has a key for each overlay URI that is already loaded.
611 */
612 _loadedOverlays: {},
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 }
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 }
639 function DOL_EOL_loadCallback() {
640 this._overlayLoading = false;
641 this._loadedOverlays[aOverlay] = true;
643 this.processPendingRequests();
644 }
646 this._overlayLoading = true;
647 DownloadsCommon.log("Loading overlay ", aOverlay);
648 document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this));
649 },
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();
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 };
672 ////////////////////////////////////////////////////////////////////////////////
673 //// DownloadsView
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
684 /**
685 * Maximum number of items shown by the list at any given time.
686 */
687 kItemCountLimit: 3,
689 /**
690 * Indicates whether we are still loading downloads data asynchronously.
691 */
692 loading: false,
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: [],
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: {},
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;
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 }
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 },
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 },
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 },
751 //////////////////////////////////////////////////////////////////////////////
752 //// Callback functions from DownloadsData
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 },
763 /**
764 * Called after data loading finished.
765 */
766 onDataLoadCompleted: function DV_onDataLoadCompleted()
767 {
768 DownloadsCommon.log("onDataLoadCompleted called for DownloadsView.");
770 this.loading = false;
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();
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 },
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);
799 if (aNewest) {
800 this._dataItems.unshift(aDataItem);
801 } else {
802 this._dataItems.push(aDataItem);
803 }
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 }
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 },
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.");
836 let itemIndex = this._dataItems.indexOf(aDataItem);
837 this._dataItems.splice(itemIndex, 1);
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 }
848 this._itemCountChanged();
849 },
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 },
869 /**
870 * Mock DownloadsDataItem object that doesn't react to notifications.
871 */
872 _invisibleViewItem: Object.freeze({
873 onStateChange: function () { },
874 onProgressChange: function () { }
875 }),
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);
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 },
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 },
912 //////////////////////////////////////////////////////////////////////////////
913 //// User interface event functions
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 },
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 },
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 }
955 if (aEvent.charCode == " ".charCodeAt(0)) {
956 goDoCommand("downloadsCmd_pauseResume");
957 return;
958 }
960 if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) {
961 goDoCommand("downloadsCmd_doDefault");
962 }
963 },
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 },
988 onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent)
989 {
990 let element = this.richListBox.selectedItem;
991 if (!element) {
992 return;
993 }
995 DownloadsViewController.updateCommands();
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 },
1002 onDownloadDragStart: function DV_onDownloadDragStart(aEvent)
1003 {
1004 let element = this.richListBox.selectedItem;
1005 if (!element) {
1006 return;
1007 }
1009 let controller = new DownloadsViewItemController(element);
1010 let localFile = controller.dataItem.localFile;
1011 if (!localFile.exists()) {
1012 return;
1013 }
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);
1023 aEvent.stopPropagation();
1024 }
1025 }
1027 ////////////////////////////////////////////////////////////////////////////////
1028 //// DownloadsViewItem
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;
1044 this.lastEstimatedSecondsLeft = Infinity;
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";
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 };
1063 for (let attributeName in attributes) {
1064 this._element.setAttribute(attributeName, attributes[attributeName]);
1065 }
1067 // Initialize more complex attributes.
1068 this._updateProgress();
1069 this._updateStatusLine();
1070 this.verifyTargetExists();
1071 }
1073 DownloadsViewItem.prototype = {
1074 /**
1075 * The DownloadDataItem associated with this view item.
1076 */
1077 dataItem: null,
1079 /**
1080 * The XUL element corresponding to the associated richlistbox item.
1081 */
1082 _element: null,
1084 /**
1085 * The inner XUL element for the progress bar, or null if not available.
1086 */
1087 _progressElement: null,
1089 //////////////////////////////////////////////////////////////////////////////
1090 //// Callback functions from DownloadsData
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");
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 }
1116 // Update the user interface after switching states.
1117 this._element.setAttribute("state", this.dataItem.state);
1118 this._updateProgress();
1119 this._updateStatusLine();
1120 },
1122 /**
1123 * Called when the download progress has changed.
1124 */
1125 onProgressChange: function DVI_onProgressChange() {
1126 this._updateProgress();
1127 this._updateStatusLine();
1128 },
1130 //////////////////////////////////////////////////////////////////////////////
1131 //// Functions for updating the user interface
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 }
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 }
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 },
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;
1174 let status = "";
1175 let statusTip = "";
1177 if (this.dataItem.paused) {
1178 let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes,
1179 this.dataItem.maxBytes);
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);
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);
1222 let [displayHost, fullHost] =
1223 DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri);
1225 let end = new Date(this.dataItem.endTime);
1226 let [displayDate, fullDate] = DownloadUtils.getReadableDates(end);
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 }
1238 this._element.setAttribute("status", status);
1239 this._element.setAttribute("statusTip", statusTip || status);
1240 },
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 },
1257 //////////////////////////////////////////////////////////////////////////////
1258 //// Functions called by the panel
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 }
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 };
1284 ////////////////////////////////////////////////////////////////////////////////
1285 //// DownloadsViewController
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
1296 initialize: function DVC_initialize()
1297 {
1298 window.controllers.insertControllerAt(0, this);
1299 },
1301 terminate: function DVC_terminate()
1302 {
1303 window.controllers.removeController(this);
1304 },
1306 //////////////////////////////////////////////////////////////////////////////
1307 //// nsIController
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 },
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 }
1333 // Other commands are selection-specific.
1334 let element = DownloadsView.richListBox.selectedItem;
1335 return element &&
1336 new DownloadsViewItemController(element).isCommandEnabled(aCommand);
1337 },
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 }
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 },
1355 onEvent: function () { },
1357 //////////////////////////////////////////////////////////////////////////////
1358 //// Other functions
1360 updateCommands: function DVC_updateCommands()
1361 {
1362 Object.keys(this.commands).forEach(goUpdateCommand);
1363 Object.keys(DownloadsViewItemController.prototype.commands)
1364 .forEach(goUpdateCommand);
1365 },
1367 //////////////////////////////////////////////////////////////////////////////
1368 //// Selection-independent commands
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 };
1382 ////////////////////////////////////////////////////////////////////////////////
1383 //// DownloadsViewItemController
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 }
1394 DownloadsViewItemController.prototype = {
1395 //////////////////////////////////////////////////////////////////////////////
1396 //// Command dispatching
1398 /**
1399 * The DownloadDataItem controlled by this object.
1400 */
1401 dataItem: null,
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 },
1428 doCommand: function DVIC_doCommand(aCommand)
1429 {
1430 if (this.isCommandEnabled(aCommand)) {
1431 this.commands[aCommand].apply(this);
1432 }
1433 },
1435 //////////////////////////////////////////////////////////////////////////////
1436 //// Item commands
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 },
1450 downloadsCmd_cancel: function DVIC_downloadsCmd_cancel()
1451 {
1452 this.dataItem.cancel();
1453 },
1455 downloadsCmd_open: function DVIC_downloadsCmd_open()
1456 {
1457 this.dataItem.openLocalFile();
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 },
1467 downloadsCmd_show: function DVIC_downloadsCmd_show()
1468 {
1469 this.dataItem.showLocalFile();
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 },
1479 downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume()
1480 {
1481 this.dataItem.togglePauseResume();
1482 },
1484 downloadsCmd_retry: function DVIC_downloadsCmd_retry()
1485 {
1486 this.dataItem.retry();
1487 },
1489 downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer()
1490 {
1491 openURL(this.dataItem.referrer);
1492 },
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 },
1501 downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault()
1502 {
1503 const nsIDM = Ci.nsIDownloadManager;
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 };
1528 ////////////////////////////////////////////////////////////////////////////////
1529 //// DownloadsSummary
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 = {
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 }
1556 return this._active = aActive;
1557 },
1559 /**
1560 * Returns the active state of the downloads summary.
1561 */
1562 get active() this._active,
1564 _active: false,
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 },
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 },
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 },
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 },
1631 /**
1632 * Focuses the root element of the summary.
1633 */
1634 focus: function()
1635 {
1636 if (this._summaryNode) {
1637 this._summaryNode.focus();
1638 }
1639 },
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 },
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 },
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 },
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 },
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 },
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 }
1721 ////////////////////////////////////////////////////////////////////////////////
1722 //// DownloadsFooter
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 = {
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 },
1744 _showingSummary: false,
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 },
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 };