michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Handles the Downloads panel user interface for each browser window. michael@0: * michael@0: * This file includes the following constructors and global objects: michael@0: * michael@0: * DownloadsPanel michael@0: * Main entry point for the downloads panel interface. michael@0: * michael@0: * DownloadsOverlayLoader michael@0: * Allows loading the downloads panel and the status indicator interfaces on michael@0: * demand, to improve startup performance. michael@0: * michael@0: * DownloadsView michael@0: * Builds and updates the downloads list widget, responding to changes in the michael@0: * download state and real-time data. In addition, handles part of the user michael@0: * interaction events raised by the downloads list widget. michael@0: * michael@0: * DownloadsViewItem michael@0: * Builds and updates a single item in the downloads list widget, responding to michael@0: * changes in the download state and real-time data. michael@0: * michael@0: * DownloadsViewController michael@0: * Handles part of the user interaction events raised by the downloads list michael@0: * widget, in particular the "commands" that apply to multiple items, and michael@0: * dispatches the commands that apply to individual items. michael@0: * michael@0: * DownloadsViewItemController michael@0: * Handles all the user interaction events, in particular the "commands", michael@0: * related to a single item in the downloads list widgets. michael@0: */ michael@0: michael@0: /** michael@0: * A few words on focus and focusrings michael@0: * michael@0: * We do quite a few hacks in the Downloads Panel for focusrings. In fact, we michael@0: * basically suppress most if not all XUL-level focusrings, and style/draw michael@0: * them ourselves (using :focus instead of -moz-focusring). There are a few michael@0: * reasons for this: michael@0: * michael@0: * 1) Richlists on OSX don't have focusrings; instead, they are shown as michael@0: * selected. This makes for some ambiguity when we have a focused/selected michael@0: * item in the list, and the mouse is hovering a completed download (which michael@0: * highlights). michael@0: * 2) Windows doesn't show focusrings until after the first time that tab is michael@0: * pressed (and by then you're focusing the second item in the panel). michael@0: * 3) Richlistbox sets -moz-focusring even when we select it with a mouse. michael@0: * michael@0: * In general, the desired behaviour is to focus the first item after pressing michael@0: * tab/down, and show that focus with a ring. Then, if the mouse moves over michael@0: * the panel, to hide that focus ring; essentially resetting us to the state michael@0: * before pressing the key. michael@0: * michael@0: * We end up capturing the tab/down key events, and preventing their default michael@0: * behaviour. We then set a "keyfocus" attribute on the panel, which allows michael@0: * us to draw a ring around the currently focused element. If the panel is michael@0: * closed or the mouse moves over the panel, we remove the attribute. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// Globals michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", michael@0: "resource://gre/modules/DownloadUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon", michael@0: "resource:///modules/DownloadsCommon.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", michael@0: "resource://gre/modules/osfile.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", michael@0: "resource://gre/modules/PlacesUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", michael@0: "resource://gre/modules/NetUtil.jsm"); michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsPanel michael@0: michael@0: /** michael@0: * Main entry point for the downloads panel interface. michael@0: */ michael@0: const DownloadsPanel = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Initialization and termination michael@0: michael@0: /** michael@0: * Internal state of the downloads panel, based on one of the kState michael@0: * constants. This is not the same state as the XUL panel element. michael@0: */ michael@0: _state: 0, michael@0: michael@0: /** The panel is not linked to downloads data yet. */ michael@0: get kStateUninitialized() 0, michael@0: /** This object is linked to data, but the panel is invisible. */ michael@0: get kStateHidden() 1, michael@0: /** The panel will be shown as soon as possible. */ michael@0: get kStateWaitingData() 2, michael@0: /** The panel is almost shown - we're just waiting to get a handle on the michael@0: anchor. */ michael@0: get kStateWaitingAnchor() 3, michael@0: /** The panel is open. */ michael@0: get kStateShown() 4, michael@0: michael@0: /** michael@0: * Location of the panel overlay. michael@0: */ michael@0: get kDownloadsOverlay() michael@0: "chrome://browser/content/downloads/downloadsOverlay.xul", michael@0: michael@0: /** michael@0: * Starts loading the download data in background, without opening the panel. michael@0: * Use showPanel instead to load the data and open the panel at the same time. michael@0: * michael@0: * @param aCallback michael@0: * Called when initialization is complete. michael@0: */ michael@0: initialize: function DP_initialize(aCallback) michael@0: { michael@0: DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window."); michael@0: if (this._state != this.kStateUninitialized) { michael@0: DownloadsCommon.log("DownloadsPanel is already initialized."); michael@0: DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, michael@0: aCallback); michael@0: return; michael@0: } michael@0: this._state = this.kStateHidden; michael@0: michael@0: window.addEventListener("unload", this.onWindowUnload, false); michael@0: michael@0: // Load and resume active downloads if required. If there are downloads to michael@0: // be shown in the panel, they will be loaded asynchronously. michael@0: DownloadsCommon.initializeAllDataLinks(); michael@0: michael@0: // Now that data loading has eventually started, load the required XUL michael@0: // elements and initialize our views. michael@0: DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded."); michael@0: DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, michael@0: function DP_I_callback() { michael@0: DownloadsViewController.initialize(); michael@0: DownloadsCommon.log("Attaching DownloadsView..."); michael@0: DownloadsCommon.getData(window).addView(DownloadsView); michael@0: DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) michael@0: .addView(DownloadsSummary); michael@0: DownloadsCommon.log("DownloadsView attached - the panel for this window", michael@0: "should now see download items come in."); michael@0: DownloadsPanel._attachEventListeners(); michael@0: DownloadsCommon.log("DownloadsPanel initialized."); michael@0: aCallback(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Closes the downloads panel and frees the internal resources related to the michael@0: * downloads. The downloads panel can be reopened later, even after this michael@0: * function has been called. michael@0: */ michael@0: terminate: function DP_terminate() michael@0: { michael@0: DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); michael@0: if (this._state == this.kStateUninitialized) { michael@0: DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do."); michael@0: return; michael@0: } michael@0: michael@0: window.removeEventListener("unload", this.onWindowUnload, false); michael@0: michael@0: // Ensure that the panel is closed before shutting down. michael@0: this.hidePanel(); michael@0: michael@0: DownloadsViewController.terminate(); michael@0: DownloadsCommon.getData(window).removeView(DownloadsView); michael@0: DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) michael@0: .removeView(DownloadsSummary); michael@0: this._unattachEventListeners(); michael@0: michael@0: this._state = this.kStateUninitialized; michael@0: michael@0: DownloadsSummary.active = false; michael@0: DownloadsCommon.log("DownloadsPanel terminated."); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Panel interface michael@0: michael@0: /** michael@0: * Main panel element in the browser window, or null if the panel overlay michael@0: * hasn't been loaded yet. michael@0: */ michael@0: get panel() michael@0: { michael@0: // If the downloads panel overlay hasn't loaded yet, just return null michael@0: // without reseting this.panel. michael@0: let downloadsPanel = document.getElementById("downloadsPanel"); michael@0: if (!downloadsPanel) michael@0: return null; michael@0: michael@0: delete this.panel; michael@0: return this.panel = downloadsPanel; michael@0: }, michael@0: michael@0: /** michael@0: * Starts opening the downloads panel interface, anchored to the downloads michael@0: * button of the browser window. The list of downloads to display is michael@0: * initialized the first time this method is called, and the panel is shown michael@0: * only when data is ready. michael@0: */ michael@0: showPanel: function DP_showPanel() michael@0: { michael@0: DownloadsCommon.log("Opening the downloads panel."); michael@0: michael@0: if (this.isPanelShowing) { michael@0: DownloadsCommon.log("Panel is already showing - focusing instead."); michael@0: this._focusPanel(); michael@0: return; michael@0: } michael@0: michael@0: this.initialize(function DP_SP_callback() { michael@0: // Delay displaying the panel because this function will sometimes be michael@0: // called while another window is closing (like the window for selecting michael@0: // whether to save or open the file), and that would cause the panel to michael@0: // close immediately. michael@0: setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0); michael@0: }.bind(this)); michael@0: michael@0: DownloadsCommon.log("Waiting for the downloads panel to appear."); michael@0: this._state = this.kStateWaitingData; michael@0: }, michael@0: michael@0: /** michael@0: * Hides the downloads panel, if visible, but keeps the internal state so that michael@0: * the panel can be reopened quickly if required. michael@0: */ michael@0: hidePanel: function DP_hidePanel() michael@0: { michael@0: DownloadsCommon.log("Closing the downloads panel."); michael@0: michael@0: if (!this.isPanelShowing) { michael@0: DownloadsCommon.log("Downloads panel is not showing - nothing to do."); michael@0: return; michael@0: } michael@0: michael@0: this.panel.hidePopup(); michael@0: michael@0: // Ensure that we allow the panel to be reopened. Note that, if the popup michael@0: // was open, then the onPopupHidden event handler has already updated the michael@0: // current state, otherwise we must update the state ourselves. michael@0: this._state = this.kStateHidden; michael@0: DownloadsCommon.log("Downloads panel is now closed."); michael@0: }, michael@0: michael@0: /** michael@0: * Indicates whether the panel is shown or will be shown. michael@0: */ michael@0: get isPanelShowing() michael@0: { michael@0: return this._state == this.kStateWaitingData || michael@0: this._state == this.kStateWaitingAnchor || michael@0: this._state == this.kStateShown; michael@0: }, michael@0: michael@0: /** michael@0: * Returns whether the user has started keyboard navigation. michael@0: */ michael@0: get keyFocusing() michael@0: { michael@0: return this.panel.hasAttribute("keyfocus"); michael@0: }, michael@0: michael@0: /** michael@0: * Set to true if the user has started keyboard navigation, and we should be michael@0: * showing focusrings in the panel. Also adds a mousemove event handler to michael@0: * the panel which disables keyFocusing. michael@0: */ michael@0: set keyFocusing(aValue) michael@0: { michael@0: if (aValue) { michael@0: this.panel.setAttribute("keyfocus", "true"); michael@0: this.panel.addEventListener("mousemove", this); michael@0: } else { michael@0: this.panel.removeAttribute("keyfocus"); michael@0: this.panel.removeEventListener("mousemove", this); michael@0: } michael@0: return aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Handles the mousemove event for the panel, which disables focusring michael@0: * visualization. michael@0: */ michael@0: handleEvent: function DP_handleEvent(aEvent) michael@0: { michael@0: if (aEvent.type == "mousemove") { michael@0: this.keyFocusing = false; michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsView michael@0: michael@0: /** michael@0: * Called after data loading finished. michael@0: */ michael@0: onViewLoadCompleted: function DP_onViewLoadCompleted() michael@0: { michael@0: this._openPopupIfDataReady(); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// User interface event functions michael@0: michael@0: onWindowUnload: function DP_onWindowUnload() michael@0: { michael@0: // This function is registered as an event listener, we can't use "this". michael@0: DownloadsPanel.terminate(); michael@0: }, michael@0: michael@0: onPopupShown: function DP_onPopupShown(aEvent) michael@0: { michael@0: // Ignore events raised by nested popups. michael@0: if (aEvent.target != aEvent.currentTarget) { michael@0: return; michael@0: } michael@0: michael@0: DownloadsCommon.log("Downloads panel has shown."); michael@0: this._state = this.kStateShown; michael@0: michael@0: // Since at most one popup is open at any given time, we can set globally. michael@0: DownloadsCommon.getIndicatorData(window).attentionSuppressed = true; michael@0: michael@0: // Ensure that the first item is selected when the panel is focused. michael@0: if (DownloadsView.richListBox.itemCount > 0 && michael@0: DownloadsView.richListBox.selectedIndex == -1) { michael@0: DownloadsView.richListBox.selectedIndex = 0; michael@0: } michael@0: michael@0: this._focusPanel(); michael@0: }, michael@0: michael@0: onPopupHidden: function DP_onPopupHidden(aEvent) michael@0: { michael@0: // Ignore events raised by nested popups. michael@0: if (aEvent.target != aEvent.currentTarget) { michael@0: return; michael@0: } michael@0: michael@0: DownloadsCommon.log("Downloads panel has hidden."); michael@0: michael@0: // Removes the keyfocus attribute so that we stop handling keyboard michael@0: // navigation. michael@0: this.keyFocusing = false; michael@0: michael@0: // Since at most one popup is open at any given time, we can set globally. michael@0: DownloadsCommon.getIndicatorData(window).attentionSuppressed = false; michael@0: michael@0: // Allow the anchor to be hidden. michael@0: DownloadsButton.releaseAnchor(); michael@0: michael@0: // Allow the panel to be reopened. michael@0: this._state = this.kStateHidden; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Related operations michael@0: michael@0: /** michael@0: * Shows or focuses the user interface dedicated to downloads history. michael@0: */ michael@0: showDownloadsHistory: function DP_showDownloadsHistory() michael@0: { michael@0: DownloadsCommon.log("Showing download history."); michael@0: // Hide the panel before showing another window, otherwise focus will return michael@0: // to the browser window when the panel closes automatically. michael@0: this.hidePanel(); michael@0: michael@0: BrowserDownloadsUI(); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Internal functions michael@0: michael@0: /** michael@0: * Attach event listeners to a panel element. These listeners should be michael@0: * removed in _unattachEventListeners. This is called automatically after the michael@0: * panel has successfully loaded. michael@0: */ michael@0: _attachEventListeners: function DP__attachEventListeners() michael@0: { michael@0: // Handle keydown to support accel-V. michael@0: this.panel.addEventListener("keydown", this._onKeyDown.bind(this), false); michael@0: // Handle keypress to be able to preventDefault() events before they reach michael@0: // the richlistbox, for keyboard navigation. michael@0: this.panel.addEventListener("keypress", this._onKeyPress.bind(this), false); michael@0: }, michael@0: michael@0: /** michael@0: * Unattach event listeners that were added in _attachEventListeners. This michael@0: * is called automatically on panel termination. michael@0: */ michael@0: _unattachEventListeners: function DP__unattachEventListeners() michael@0: { michael@0: this.panel.removeEventListener("keydown", this._onKeyDown.bind(this), michael@0: false); michael@0: this.panel.removeEventListener("keypress", this._onKeyPress.bind(this), michael@0: false); michael@0: }, michael@0: michael@0: _onKeyPress: function DP__onKeyPress(aEvent) michael@0: { michael@0: // Handle unmodified keys only. michael@0: if (aEvent.altKey || aEvent.ctrlKey || aEvent.shiftKey || aEvent.metaKey) { michael@0: return; michael@0: } michael@0: michael@0: let richListBox = DownloadsView.richListBox; michael@0: michael@0: // If the user has pressed the tab, up, or down cursor key, start keyboard michael@0: // navigation, thus enabling focusrings in the panel. Keyboard navigation michael@0: // is automatically disabled if the user moves the mouse on the panel, or michael@0: // if the panel is closed. michael@0: if ((aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_TAB || michael@0: aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP || michael@0: aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) && michael@0: !this.keyFocusing) { michael@0: this.keyFocusing = true; michael@0: // Ensure there's a selection, we will show the focus ring around it and michael@0: // prevent the richlistbox from changing the selection. michael@0: if (DownloadsView.richListBox.selectedIndex == -1) michael@0: DownloadsView.richListBox.selectedIndex = 0; michael@0: aEvent.preventDefault(); michael@0: return; michael@0: } michael@0: michael@0: if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_DOWN) { michael@0: // If the last element in the list is selected, or the footer is already michael@0: // focused, focus the footer. michael@0: if (richListBox.selectedItem === richListBox.lastChild || michael@0: document.activeElement.parentNode.id === "downloadsFooter") { michael@0: DownloadsFooter.focus(); michael@0: aEvent.preventDefault(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Pass keypress events to the richlistbox view when it's focused. michael@0: if (document.activeElement === richListBox) { michael@0: DownloadsView.onDownloadKeyPress(aEvent); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Keydown listener that listens for the keys to start key focusing, as well michael@0: * as the the accel-V "paste" event, which initiates a file download if the michael@0: * pasted item can be resolved to a URI. michael@0: */ michael@0: _onKeyDown: function DP__onKeyDown(aEvent) michael@0: { michael@0: // If the footer is focused and the downloads list has at least 1 element michael@0: // in it, focus the last element in the list when going up. michael@0: if (aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_UP && michael@0: document.activeElement.parentNode.id === "downloadsFooter" && michael@0: DownloadsView.richListBox.firstChild) { michael@0: DownloadsView.richListBox.focus(); michael@0: DownloadsView.richListBox.selectedItem = DownloadsView.richListBox.lastChild; michael@0: aEvent.preventDefault(); michael@0: return; michael@0: } michael@0: michael@0: let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V && michael@0: #ifdef XP_MACOSX michael@0: aEvent.metaKey; michael@0: #else michael@0: aEvent.ctrlKey; michael@0: #endif michael@0: michael@0: if (!pasting) { michael@0: return; michael@0: } michael@0: michael@0: DownloadsCommon.log("Received a paste event."); michael@0: michael@0: let trans = Cc["@mozilla.org/widget/transferable;1"] michael@0: .createInstance(Ci.nsITransferable); michael@0: trans.init(null); michael@0: let flavors = ["text/x-moz-url", "text/unicode"]; michael@0: flavors.forEach(trans.addDataFlavor); michael@0: Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); michael@0: // Getting the data or creating the nsIURI might fail michael@0: try { michael@0: let data = {}; michael@0: trans.getAnyTransferData({}, data, {}); michael@0: let [url, name] = data.value michael@0: .QueryInterface(Ci.nsISupportsString) michael@0: .data michael@0: .split("\n"); michael@0: if (!url) { michael@0: return; michael@0: } michael@0: michael@0: let uri = NetUtil.newURI(url); michael@0: DownloadsCommon.log("Pasted URL seems valid. Starting download."); michael@0: DownloadURL(uri.spec, name, document); michael@0: } catch (ex) {} michael@0: }, michael@0: michael@0: /** michael@0: * Move focus to the main element in the downloads panel, unless another michael@0: * element in the panel is already focused. michael@0: */ michael@0: _focusPanel: function DP_focusPanel() michael@0: { michael@0: // We may be invoked while the panel is still waiting to be shown. michael@0: if (this._state != this.kStateShown) { michael@0: return; michael@0: } michael@0: michael@0: let element = document.commandDispatcher.focusedElement; michael@0: while (element && element != this.panel) { michael@0: element = element.parentNode; michael@0: } michael@0: if (!element) { michael@0: if (DownloadsView.richListBox.itemCount > 0) { michael@0: DownloadsView.richListBox.focus(); michael@0: } else { michael@0: DownloadsFooter.focus(); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Opens the downloads panel when data is ready to be displayed. michael@0: */ michael@0: _openPopupIfDataReady: function DP_openPopupIfDataReady() michael@0: { michael@0: // We don't want to open the popup if we already displayed it, or if we are michael@0: // still loading data. michael@0: if (this._state != this.kStateWaitingData || DownloadsView.loading) { michael@0: return; michael@0: } michael@0: michael@0: this._state = this.kStateWaitingAnchor; michael@0: michael@0: // Ensure the anchor is visible. If that is not possible, show the panel michael@0: // anchored to the top area of the window, near the default anchor position. michael@0: DownloadsButton.getAnchor(function DP_OPIDR_callback(aAnchor) { michael@0: // If somehow we've switched states already (by getting a panel hiding michael@0: // event before an overlay is loaded, for example), bail out. michael@0: if (this._state != this.kStateWaitingAnchor) michael@0: return; michael@0: michael@0: // At this point, if the window is minimized, opening the panel could fail michael@0: // without any notification, and there would be no way to either open or michael@0: // close the panel anymore. To prevent this, check if the window is michael@0: // minimized and in that case force the panel to the closed state. michael@0: if (window.windowState == Ci.nsIDOMChromeWindow.STATE_MINIMIZED) { michael@0: DownloadsButton.releaseAnchor(); michael@0: this._state = this.kStateHidden; michael@0: return; michael@0: } michael@0: michael@0: // When the panel is opened, we check if the target files of visible items michael@0: // still exist, and update the allowed items interactions accordingly. We michael@0: // do these checks on a background thread, and don't prevent the panel to michael@0: // be displayed while these checks are being performed. michael@0: for each (let viewItem in DownloadsView._viewItems) { michael@0: viewItem.verifyTargetExists(); michael@0: } michael@0: michael@0: if (aAnchor) { michael@0: DownloadsCommon.log("Opening downloads panel popup."); michael@0: this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false, michael@0: null); michael@0: } else { michael@0: DownloadsCommon.error("We can't find the anchor! Failure case - opening", michael@0: "downloads panel on TabsToolbar. We should never", michael@0: "get here!"); michael@0: Components.utils.reportError( michael@0: "Downloads button cannot be found"); michael@0: } michael@0: }.bind(this)); michael@0: } michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsOverlayLoader michael@0: michael@0: /** michael@0: * Allows loading the downloads panel and the status indicator interfaces on michael@0: * demand, to improve startup performance. michael@0: */ michael@0: const DownloadsOverlayLoader = { michael@0: /** michael@0: * We cannot load two overlays at the same time, thus we use a queue of michael@0: * pending load requests. michael@0: */ michael@0: _loadRequests: [], michael@0: michael@0: /** michael@0: * True while we are waiting for an overlay to be loaded. michael@0: */ michael@0: _overlayLoading: false, michael@0: michael@0: /** michael@0: * This object has a key for each overlay URI that is already loaded. michael@0: */ michael@0: _loadedOverlays: {}, michael@0: michael@0: /** michael@0: * Loads the specified overlay and invokes the given callback when finished. michael@0: * michael@0: * @param aOverlay michael@0: * String containing the URI of the overlay to load in the current michael@0: * window. If this overlay has already been loaded using this michael@0: * function, then the overlay is not loaded again. michael@0: * @param aCallback michael@0: * Invoked when loading is completed. If the overlay is already michael@0: * loaded, the function is called immediately. michael@0: */ michael@0: ensureOverlayLoaded: function DOL_ensureOverlayLoaded(aOverlay, aCallback) michael@0: { michael@0: // The overlay is already loaded, invoke the callback immediately. michael@0: if (aOverlay in this._loadedOverlays) { michael@0: aCallback(); michael@0: return; michael@0: } michael@0: michael@0: // The callback will be invoked when loading is finished. michael@0: this._loadRequests.push({ overlay: aOverlay, callback: aCallback }); michael@0: if (this._overlayLoading) { michael@0: return; michael@0: } michael@0: michael@0: function DOL_EOL_loadCallback() { michael@0: this._overlayLoading = false; michael@0: this._loadedOverlays[aOverlay] = true; michael@0: michael@0: this.processPendingRequests(); michael@0: } michael@0: michael@0: this._overlayLoading = true; michael@0: DownloadsCommon.log("Loading overlay ", aOverlay); michael@0: document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Re-processes all the currently pending requests, invoking the callbacks michael@0: * and/or loading more overlays as needed. In most cases, there will be a michael@0: * single request for one overlay, that will be processed immediately. michael@0: */ michael@0: processPendingRequests: function DOL_processPendingRequests() michael@0: { michael@0: // Re-process all the currently pending requests, yet allow more requests michael@0: // to be appended at the end of the array if we're not ready for them. michael@0: let currentLength = this._loadRequests.length; michael@0: for (let i = 0; i < currentLength; i++) { michael@0: let request = this._loadRequests.shift(); michael@0: michael@0: // We must call ensureOverlayLoaded again for each request, to check if michael@0: // the associated callback can be invoked now, or if we must still wait michael@0: // for the associated overlay to load. michael@0: this.ensureOverlayLoaded(request.overlay, request.callback); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsView michael@0: michael@0: /** michael@0: * Builds and updates the downloads list widget, responding to changes in the michael@0: * download state and real-time data. In addition, handles part of the user michael@0: * interaction events raised by the downloads list widget. michael@0: */ michael@0: const DownloadsView = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Functions handling download items in the list michael@0: michael@0: /** michael@0: * Maximum number of items shown by the list at any given time. michael@0: */ michael@0: kItemCountLimit: 3, michael@0: michael@0: /** michael@0: * Indicates whether we are still loading downloads data asynchronously. michael@0: */ michael@0: loading: false, michael@0: michael@0: /** michael@0: * Ordered array of all DownloadsDataItem objects. We need to keep this array michael@0: * because only a limited number of items are shown at once, and if an item michael@0: * that is currently visible is removed from the list, we might need to take michael@0: * another item from the array and make it appear at the bottom. michael@0: */ michael@0: _dataItems: [], michael@0: michael@0: /** michael@0: * Object containing the available DownloadsViewItem objects, indexed by their michael@0: * numeric download identifier. There is a limited number of view items in michael@0: * the panel at any given time. michael@0: */ michael@0: _viewItems: {}, michael@0: michael@0: /** michael@0: * Called when the number of items in the list changes. michael@0: */ michael@0: _itemCountChanged: function DV_itemCountChanged() michael@0: { michael@0: DownloadsCommon.log("The downloads item count has changed - we are tracking", michael@0: this._dataItems.length, "downloads in total."); michael@0: let count = this._dataItems.length; michael@0: let hiddenCount = count - this.kItemCountLimit; michael@0: michael@0: if (count > 0) { michael@0: DownloadsCommon.log("Setting the panel's hasdownloads attribute to true."); michael@0: DownloadsPanel.panel.setAttribute("hasdownloads", "true"); michael@0: } else { michael@0: DownloadsCommon.log("Removing the panel's hasdownloads attribute."); michael@0: DownloadsPanel.panel.removeAttribute("hasdownloads"); michael@0: } michael@0: michael@0: // If we've got some hidden downloads, we should activate the michael@0: // DownloadsSummary. The DownloadsSummary will determine whether or not michael@0: // it's appropriate to actually display the summary. michael@0: DownloadsSummary.active = hiddenCount > 0; michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the list of downloads. michael@0: */ michael@0: get richListBox() michael@0: { michael@0: delete this.richListBox; michael@0: return this.richListBox = document.getElementById("downloadsListBox"); michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the button for showing more downloads. michael@0: */ michael@0: get downloadsHistory() michael@0: { michael@0: delete this.downloadsHistory; michael@0: return this.downloadsHistory = document.getElementById("downloadsHistory"); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsData michael@0: michael@0: /** michael@0: * Called before multiple downloads are about to be loaded. michael@0: */ michael@0: onDataLoadStarting: function DV_onDataLoadStarting() michael@0: { michael@0: DownloadsCommon.log("onDataLoadStarting called for DownloadsView."); michael@0: this.loading = true; michael@0: }, michael@0: michael@0: /** michael@0: * Called after data loading finished. michael@0: */ michael@0: onDataLoadCompleted: function DV_onDataLoadCompleted() michael@0: { michael@0: DownloadsCommon.log("onDataLoadCompleted called for DownloadsView."); michael@0: michael@0: this.loading = false; michael@0: michael@0: // We suppressed item count change notifications during the batch load, at michael@0: // this point we should just call the function once. michael@0: this._itemCountChanged(); michael@0: michael@0: // Notify the panel that all the initially available downloads have been michael@0: // loaded. This ensures that the interface is visible, if still required. michael@0: DownloadsPanel.onViewLoadCompleted(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a new download data item is available, either during the michael@0: * asynchronous data load or when a new download is started. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object that was just added. michael@0: * @param aNewest michael@0: * When true, indicates that this item is the most recent and should be michael@0: * added in the topmost position. This happens when a new download is michael@0: * started. When false, indicates that the item is the least recent michael@0: * and should be appended. The latter generally happens during the michael@0: * asynchronous data load. michael@0: */ michael@0: onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest) michael@0: { michael@0: DownloadsCommon.log("A new download data item was added - aNewest =", michael@0: aNewest); michael@0: michael@0: if (aNewest) { michael@0: this._dataItems.unshift(aDataItem); michael@0: } else { michael@0: this._dataItems.push(aDataItem); michael@0: } michael@0: michael@0: let itemsNowOverflow = this._dataItems.length > this.kItemCountLimit; michael@0: if (aNewest || !itemsNowOverflow) { michael@0: // The newly added item is visible in the panel and we must add the michael@0: // corresponding element. This is either because it is the first item, or michael@0: // because it was added at the bottom but the list still doesn't overflow. michael@0: this._addViewItem(aDataItem, aNewest); michael@0: } michael@0: if (aNewest && itemsNowOverflow) { michael@0: // If the list overflows, remove the last item from the panel to make room michael@0: // for the new one that we just added at the top. michael@0: this._removeViewItem(this._dataItems[this.kItemCountLimit]); michael@0: } michael@0: michael@0: // For better performance during batch loads, don't update the count for michael@0: // every item, because the interface won't be visible until load finishes. michael@0: if (!this.loading) { michael@0: this._itemCountChanged(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when a data item is removed. Ensures that the widget associated michael@0: * with the view item is removed from the user interface. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object that is being removed. michael@0: */ michael@0: onDataItemRemoved: function DV_onDataItemRemoved(aDataItem) michael@0: { michael@0: DownloadsCommon.log("A download data item was removed."); michael@0: michael@0: let itemIndex = this._dataItems.indexOf(aDataItem); michael@0: this._dataItems.splice(itemIndex, 1); michael@0: michael@0: if (itemIndex < this.kItemCountLimit) { michael@0: // The item to remove is visible in the panel. michael@0: this._removeViewItem(aDataItem); michael@0: if (this._dataItems.length >= this.kItemCountLimit) { michael@0: // Reinsert the next item into the panel. michael@0: this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false); michael@0: } michael@0: } michael@0: michael@0: this._itemCountChanged(); michael@0: }, michael@0: michael@0: /** michael@0: * Returns the view item associated with the provided data item for this view. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem object for which the view item is requested. michael@0: * michael@0: * @return Object that can be used to notify item status events. michael@0: */ michael@0: getViewItem: function DV_getViewItem(aDataItem) michael@0: { michael@0: // If the item is visible, just return it, otherwise return a mock object michael@0: // that doesn't react to notifications. michael@0: if (aDataItem.downloadGuid in this._viewItems) { michael@0: return this._viewItems[aDataItem.downloadGuid]; michael@0: } michael@0: return this._invisibleViewItem; michael@0: }, michael@0: michael@0: /** michael@0: * Mock DownloadsDataItem object that doesn't react to notifications. michael@0: */ michael@0: _invisibleViewItem: Object.freeze({ michael@0: onStateChange: function () { }, michael@0: onProgressChange: function () { } michael@0: }), michael@0: michael@0: /** michael@0: * Creates a new view item associated with the specified data item, and adds michael@0: * it to the top or the bottom of the list. michael@0: */ michael@0: _addViewItem: function DV_addViewItem(aDataItem, aNewest) michael@0: { michael@0: DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.", michael@0: "aNewest =", aNewest); michael@0: michael@0: let element = document.createElement("richlistitem"); michael@0: let viewItem = new DownloadsViewItem(aDataItem, element); michael@0: this._viewItems[aDataItem.downloadGuid] = viewItem; michael@0: if (aNewest) { michael@0: this.richListBox.insertBefore(element, this.richListBox.firstChild); michael@0: } else { michael@0: this.richListBox.appendChild(element); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Removes the view item associated with the specified data item. michael@0: */ michael@0: _removeViewItem: function DV_removeViewItem(aDataItem) michael@0: { michael@0: DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); michael@0: let element = this.getViewItem(aDataItem)._element; michael@0: let previousSelectedIndex = this.richListBox.selectedIndex; michael@0: this.richListBox.removeChild(element); michael@0: if (previousSelectedIndex != -1) { michael@0: this.richListBox.selectedIndex = Math.min(previousSelectedIndex, michael@0: this.richListBox.itemCount - 1); michael@0: } michael@0: delete this._viewItems[aDataItem.downloadGuid]; michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// User interface event functions michael@0: michael@0: /** michael@0: * Helper function to do commands on a specific download item. michael@0: * michael@0: * @param aEvent michael@0: * Event object for the event being handled. If the event target is michael@0: * not a richlistitem that represents a download, this function will michael@0: * walk up the parent nodes until it finds a DOM node that is. michael@0: * @param aCommand michael@0: * The command to be performed. michael@0: */ michael@0: onDownloadCommand: function DV_onDownloadCommand(aEvent, aCommand) michael@0: { michael@0: let target = aEvent.target; michael@0: while (target.nodeName != "richlistitem") { michael@0: target = target.parentNode; michael@0: } michael@0: new DownloadsViewItemController(target).doCommand(aCommand); michael@0: }, michael@0: michael@0: onDownloadClick: function DV_onDownloadClick(aEvent) michael@0: { michael@0: // Handle primary clicks only, and exclude the action button. michael@0: if (aEvent.button == 0 && michael@0: !aEvent.originalTarget.hasAttribute("oncommand")) { michael@0: goDoCommand("downloadsCmd_open"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handles keypress events on a download item. michael@0: */ michael@0: onDownloadKeyPress: function DV_onDownloadKeyPress(aEvent) michael@0: { michael@0: // Pressing the key on buttons should not invoke the action because the michael@0: // event has already been handled by the button itself. michael@0: if (aEvent.originalTarget.hasAttribute("command") || michael@0: aEvent.originalTarget.hasAttribute("oncommand")) { michael@0: return; michael@0: } michael@0: michael@0: if (aEvent.charCode == " ".charCodeAt(0)) { michael@0: goDoCommand("downloadsCmd_pauseResume"); michael@0: return; michael@0: } michael@0: michael@0: if (aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { michael@0: goDoCommand("downloadsCmd_doDefault"); michael@0: } michael@0: }, michael@0: michael@0: michael@0: /** michael@0: * Mouse listeners to handle selection on hover. michael@0: */ michael@0: onDownloadMouseOver: function DV_onDownloadMouseOver(aEvent) michael@0: { michael@0: if (aEvent.originalTarget.parentNode == this.richListBox) michael@0: this.richListBox.selectedItem = aEvent.originalTarget; michael@0: }, michael@0: onDownloadMouseOut: function DV_onDownloadMouseOut(aEvent) michael@0: { michael@0: if (aEvent.originalTarget.parentNode == this.richListBox) { michael@0: // If the destination element is outside of the richlistitem, clear the michael@0: // selection. michael@0: let element = aEvent.relatedTarget; michael@0: while (element && element != aEvent.originalTarget) { michael@0: element = element.parentNode; michael@0: } michael@0: if (!element) michael@0: this.richListBox.selectedIndex = -1; michael@0: } michael@0: }, michael@0: michael@0: onDownloadContextMenu: function DV_onDownloadContextMenu(aEvent) michael@0: { michael@0: let element = this.richListBox.selectedItem; michael@0: if (!element) { michael@0: return; michael@0: } michael@0: michael@0: DownloadsViewController.updateCommands(); michael@0: michael@0: // Set the state attribute so that only the appropriate items are displayed. michael@0: let contextMenu = document.getElementById("downloadsContextMenu"); michael@0: contextMenu.setAttribute("state", element.getAttribute("state")); michael@0: }, michael@0: michael@0: onDownloadDragStart: function DV_onDownloadDragStart(aEvent) michael@0: { michael@0: let element = this.richListBox.selectedItem; michael@0: if (!element) { michael@0: return; michael@0: } michael@0: michael@0: let controller = new DownloadsViewItemController(element); michael@0: let localFile = controller.dataItem.localFile; michael@0: if (!localFile.exists()) { michael@0: return; michael@0: } michael@0: michael@0: let dataTransfer = aEvent.dataTransfer; michael@0: dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0); michael@0: dataTransfer.effectAllowed = "copyMove"; michael@0: var url = Services.io.newFileURI(localFile).spec; michael@0: dataTransfer.setData("text/uri-list", url); michael@0: dataTransfer.setData("text/plain", url); michael@0: dataTransfer.addElement(element); michael@0: michael@0: aEvent.stopPropagation(); michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsViewItem michael@0: michael@0: /** michael@0: * Builds and updates a single item in the downloads list widget, responding to michael@0: * changes in the download state and real-time data. michael@0: * michael@0: * @param aDataItem michael@0: * DownloadsDataItem to be associated with the view item. michael@0: * @param aElement michael@0: * XUL element corresponding to the single download item in the view. michael@0: */ michael@0: function DownloadsViewItem(aDataItem, aElement) michael@0: { michael@0: this._element = aElement; michael@0: this.dataItem = aDataItem; michael@0: michael@0: this.lastEstimatedSecondsLeft = Infinity; michael@0: michael@0: // Set the URI that represents the correct icon for the target file. As soon michael@0: // as bug 239948 comment 12 is handled, the "file" property will be always a michael@0: // file URL rather than a file name. At that point we should remove the "//" michael@0: // (double slash) from the icon URI specification (see test_moz_icon_uri.js). michael@0: this.image = "moz-icon://" + this.dataItem.file + "?size=32"; michael@0: michael@0: let attributes = { michael@0: "type": "download", michael@0: "class": "download-state", michael@0: "id": "downloadsItem_" + this.dataItem.downloadGuid, michael@0: "downloadGuid": this.dataItem.downloadGuid, michael@0: "state": this.dataItem.state, michael@0: "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100, michael@0: "target": this.dataItem.target, michael@0: "image": this.image michael@0: }; michael@0: michael@0: for (let attributeName in attributes) { michael@0: this._element.setAttribute(attributeName, attributes[attributeName]); michael@0: } michael@0: michael@0: // Initialize more complex attributes. michael@0: this._updateProgress(); michael@0: this._updateStatusLine(); michael@0: this.verifyTargetExists(); michael@0: } michael@0: michael@0: DownloadsViewItem.prototype = { michael@0: /** michael@0: * The DownloadDataItem associated with this view item. michael@0: */ michael@0: dataItem: null, michael@0: michael@0: /** michael@0: * The XUL element corresponding to the associated richlistbox item. michael@0: */ michael@0: _element: null, michael@0: michael@0: /** michael@0: * The inner XUL element for the progress bar, or null if not available. michael@0: */ michael@0: _progressElement: null, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Callback functions from DownloadsData michael@0: michael@0: /** michael@0: * Called when the download state might have changed. Sometimes the state of michael@0: * the download might be the same as before, if the data layer received michael@0: * multiple events for the same download. michael@0: */ michael@0: onStateChange: function DVI_onStateChange(aOldState) michael@0: { michael@0: // If a download just finished successfully, it means that the target file michael@0: // now exists and we can extract its specific icon. To ensure that the icon michael@0: // is reloaded, we must change the URI used by the XUL image element, for michael@0: // example by adding a query parameter. Since this URI has a "moz-icon" michael@0: // scheme, this only works if we add one of the parameters explicitly michael@0: // supported by the nsIMozIconURI interface. michael@0: if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED && michael@0: aOldState != this.dataItem.state) { michael@0: this._element.setAttribute("image", this.image + "&state=normal"); michael@0: michael@0: // We assume the existence of the target of a download that just completed michael@0: // successfully, without checking the condition in the background. If the michael@0: // panel is already open, this will take effect immediately. If the panel michael@0: // is opened later, a new background existence check will be performed. michael@0: this._element.setAttribute("exists", "true"); michael@0: } michael@0: michael@0: // Update the user interface after switching states. michael@0: this._element.setAttribute("state", this.dataItem.state); michael@0: this._updateProgress(); michael@0: this._updateStatusLine(); michael@0: }, michael@0: michael@0: /** michael@0: * Called when the download progress has changed. michael@0: */ michael@0: onProgressChange: function DVI_onProgressChange() { michael@0: this._updateProgress(); michael@0: this._updateStatusLine(); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Functions for updating the user interface michael@0: michael@0: /** michael@0: * Updates the progress bar. michael@0: */ michael@0: _updateProgress: function DVI_updateProgress() { michael@0: if (this.dataItem.starting) { michael@0: // Before the download starts, the progress meter has its initial value. michael@0: this._element.setAttribute("progressmode", "normal"); michael@0: this._element.setAttribute("progress", "0"); michael@0: } else if (this.dataItem.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING || michael@0: this.dataItem.percentComplete == -1) { michael@0: // We might not know the progress of a running download, and we don't know michael@0: // the remaining time during the malware scanning phase. michael@0: this._element.setAttribute("progressmode", "undetermined"); michael@0: } else { michael@0: // This is a running download of which we know the progress. michael@0: this._element.setAttribute("progressmode", "normal"); michael@0: this._element.setAttribute("progress", this.dataItem.percentComplete); michael@0: } michael@0: michael@0: // Find the progress element as soon as the download binding is accessible. michael@0: if (!this._progressElement) { michael@0: this._progressElement = michael@0: document.getAnonymousElementByAttribute(this._element, "anonid", michael@0: "progressmeter"); michael@0: } michael@0: michael@0: // Dispatch the ValueChange event for accessibility, if possible. michael@0: if (this._progressElement) { michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("ValueChange", true, true); michael@0: this._progressElement.dispatchEvent(event); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Updates the main status line, including bytes transferred, bytes total, michael@0: * download rate, and time remaining. michael@0: */ michael@0: _updateStatusLine: function DVI_updateStatusLine() { michael@0: const nsIDM = Ci.nsIDownloadManager; michael@0: michael@0: let status = ""; michael@0: let statusTip = ""; michael@0: michael@0: if (this.dataItem.paused) { michael@0: let transfer = DownloadUtils.getTransferTotal(this.dataItem.currBytes, michael@0: this.dataItem.maxBytes); michael@0: michael@0: // We use the same XUL label to display both the state and the amount michael@0: // transferred, for example "Paused - 1.1 MB". michael@0: status = DownloadsCommon.strings.statusSeparatorBeforeNumber( michael@0: DownloadsCommon.strings.statePaused, michael@0: transfer); michael@0: } else if (this.dataItem.state == nsIDM.DOWNLOAD_DOWNLOADING) { michael@0: // We don't show the rate for each download in order to reduce clutter. michael@0: // The remaining time per download is likely enough information for the michael@0: // panel. michael@0: [status] = michael@0: DownloadUtils.getDownloadStatusNoRate(this.dataItem.currBytes, michael@0: this.dataItem.maxBytes, michael@0: this.dataItem.speed, michael@0: this.lastEstimatedSecondsLeft); michael@0: michael@0: // We are, however, OK with displaying the rate in the tooltip. michael@0: let newEstimatedSecondsLeft; michael@0: [statusTip, newEstimatedSecondsLeft] = michael@0: DownloadUtils.getDownloadStatus(this.dataItem.currBytes, michael@0: this.dataItem.maxBytes, michael@0: this.dataItem.speed, michael@0: this.lastEstimatedSecondsLeft); michael@0: this.lastEstimatedSecondsLeft = newEstimatedSecondsLeft; michael@0: } else if (this.dataItem.starting) { michael@0: status = DownloadsCommon.strings.stateStarting; michael@0: } else if (this.dataItem.state == nsIDM.DOWNLOAD_SCANNING) { michael@0: status = DownloadsCommon.strings.stateScanning; michael@0: } else if (!this.dataItem.inProgress) { michael@0: let stateLabel = function () { michael@0: let s = DownloadsCommon.strings; michael@0: switch (this.dataItem.state) { michael@0: case nsIDM.DOWNLOAD_FAILED: return s.stateFailed; michael@0: case nsIDM.DOWNLOAD_CANCELED: return s.stateCanceled; michael@0: case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return s.stateBlockedParentalControls; michael@0: case nsIDM.DOWNLOAD_BLOCKED_POLICY: return s.stateBlockedPolicy; michael@0: case nsIDM.DOWNLOAD_DIRTY: return s.stateDirty; michael@0: case nsIDM.DOWNLOAD_FINISHED: return this._fileSizeText; michael@0: } michael@0: return null; michael@0: }.apply(this); michael@0: michael@0: let [displayHost, fullHost] = michael@0: DownloadUtils.getURIHost(this.dataItem.referrer || this.dataItem.uri); michael@0: michael@0: let end = new Date(this.dataItem.endTime); michael@0: let [displayDate, fullDate] = DownloadUtils.getReadableDates(end); michael@0: michael@0: // We use the same XUL label to display the state, the host name, and the michael@0: // end time, for example "Canceled - 222.net - 11:15" or "1.1 MB - michael@0: // website2.com - Yesterday". We show the full host and the complete date michael@0: // in the tooltip. michael@0: let firstPart = DownloadsCommon.strings.statusSeparator(stateLabel, michael@0: displayHost); michael@0: status = DownloadsCommon.strings.statusSeparator(firstPart, displayDate); michael@0: statusTip = DownloadsCommon.strings.statusSeparator(fullHost, fullDate); michael@0: } michael@0: michael@0: this._element.setAttribute("status", status); michael@0: this._element.setAttribute("statusTip", statusTip || status); michael@0: }, michael@0: michael@0: /** michael@0: * Localized string representing the total size of completed downloads, for michael@0: * example "1.5 MB" or "Unknown size". michael@0: */ michael@0: get _fileSizeText() michael@0: { michael@0: // Display the file size, but show "Unknown" for negative sizes. michael@0: let fileSize = this.dataItem.maxBytes; michael@0: if (fileSize < 0) { michael@0: return DownloadsCommon.strings.sizeUnknown; michael@0: } michael@0: let [size, unit] = DownloadUtils.convertByteUnits(fileSize); michael@0: return DownloadsCommon.strings.sizeWithUnits(size, unit); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Functions called by the panel michael@0: michael@0: /** michael@0: * Starts checking whether the target file of a finished download is still michael@0: * available on disk, and sets an attribute that controls how the item is michael@0: * presented visually. michael@0: * michael@0: * The existence check is executed on a background thread. michael@0: */ michael@0: verifyTargetExists: function DVI_verifyTargetExists() { michael@0: // We don't need to check if the download is not finished successfully. michael@0: if (!this.dataItem.openable) { michael@0: return; michael@0: } michael@0: michael@0: OS.File.exists(this.dataItem.localFile.path).then( michael@0: function DVI_RTE_onSuccess(aExists) { michael@0: if (aExists) { michael@0: this._element.setAttribute("exists", "true"); michael@0: } else { michael@0: this._element.removeAttribute("exists"); michael@0: } michael@0: }.bind(this), Cu.reportError); michael@0: }, michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsViewController michael@0: michael@0: /** michael@0: * Handles part of the user interaction events raised by the downloads list michael@0: * widget, in particular the "commands" that apply to multiple items, and michael@0: * dispatches the commands that apply to individual items. michael@0: */ michael@0: const DownloadsViewController = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Initialization and termination michael@0: michael@0: initialize: function DVC_initialize() michael@0: { michael@0: window.controllers.insertControllerAt(0, this); michael@0: }, michael@0: michael@0: terminate: function DVC_terminate() michael@0: { michael@0: window.controllers.removeController(this); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// nsIController michael@0: michael@0: supportsCommand: function DVC_supportsCommand(aCommand) michael@0: { michael@0: // Firstly, determine if this is a command that we can handle. michael@0: if (!(aCommand in this.commands) && michael@0: !(aCommand in DownloadsViewItemController.prototype.commands)) { michael@0: return false; michael@0: } michael@0: // Secondly, determine if focus is on a control in the downloads list. michael@0: let element = document.commandDispatcher.focusedElement; michael@0: while (element && element != DownloadsView.richListBox) { michael@0: element = element.parentNode; michael@0: } michael@0: // We should handle the command only if the downloads list is among the michael@0: // ancestors of the focused element. michael@0: return !!element; michael@0: }, michael@0: michael@0: isCommandEnabled: function DVC_isCommandEnabled(aCommand) michael@0: { michael@0: // Handle commands that are not selection-specific. michael@0: if (aCommand == "downloadsCmd_clearList") { michael@0: return DownloadsCommon.getData(window).canRemoveFinished; michael@0: } michael@0: michael@0: // Other commands are selection-specific. michael@0: let element = DownloadsView.richListBox.selectedItem; michael@0: return element && michael@0: new DownloadsViewItemController(element).isCommandEnabled(aCommand); michael@0: }, michael@0: michael@0: doCommand: function DVC_doCommand(aCommand) michael@0: { michael@0: // If this command is not selection-specific, execute it. michael@0: if (aCommand in this.commands) { michael@0: this.commands[aCommand].apply(this); michael@0: return; michael@0: } michael@0: michael@0: // Other commands are selection-specific. michael@0: let element = DownloadsView.richListBox.selectedItem; michael@0: if (element) { michael@0: // The doCommand function also checks if the command is enabled. michael@0: new DownloadsViewItemController(element).doCommand(aCommand); michael@0: } michael@0: }, michael@0: michael@0: onEvent: function () { }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Other functions michael@0: michael@0: updateCommands: function DVC_updateCommands() michael@0: { michael@0: Object.keys(this.commands).forEach(goUpdateCommand); michael@0: Object.keys(DownloadsViewItemController.prototype.commands) michael@0: .forEach(goUpdateCommand); michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Selection-independent commands michael@0: michael@0: /** michael@0: * This object contains one key for each command that operates regardless of michael@0: * the currently selected item in the list. michael@0: */ michael@0: commands: { michael@0: downloadsCmd_clearList: function DVC_downloadsCmd_clearList() michael@0: { michael@0: DownloadsCommon.getData(window).removeFinished(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsViewItemController michael@0: michael@0: /** michael@0: * Handles all the user interaction events, in particular the "commands", michael@0: * related to a single item in the downloads list widgets. michael@0: */ michael@0: function DownloadsViewItemController(aElement) { michael@0: let downloadGuid = aElement.getAttribute("downloadGuid"); michael@0: this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid]; michael@0: } michael@0: michael@0: DownloadsViewItemController.prototype = { michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Command dispatching michael@0: michael@0: /** michael@0: * The DownloadDataItem controlled by this object. michael@0: */ michael@0: dataItem: null, michael@0: michael@0: isCommandEnabled: function DVIC_isCommandEnabled(aCommand) michael@0: { michael@0: switch (aCommand) { michael@0: case "downloadsCmd_open": { michael@0: return this.dataItem.openable && this.dataItem.localFile.exists(); michael@0: } michael@0: case "downloadsCmd_show": { michael@0: return this.dataItem.localFile.exists() || michael@0: this.dataItem.partFile.exists(); michael@0: } michael@0: case "downloadsCmd_pauseResume": michael@0: return this.dataItem.inProgress && this.dataItem.resumable; michael@0: case "downloadsCmd_retry": michael@0: return this.dataItem.canRetry; michael@0: case "downloadsCmd_openReferrer": michael@0: return !!this.dataItem.referrer; michael@0: case "cmd_delete": michael@0: case "downloadsCmd_cancel": michael@0: case "downloadsCmd_copyLocation": michael@0: case "downloadsCmd_doDefault": michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: doCommand: function DVIC_doCommand(aCommand) michael@0: { michael@0: if (this.isCommandEnabled(aCommand)) { michael@0: this.commands[aCommand].apply(this); michael@0: } michael@0: }, michael@0: michael@0: ////////////////////////////////////////////////////////////////////////////// michael@0: //// Item commands michael@0: michael@0: /** michael@0: * This object contains one key for each command that operates on this item. michael@0: * michael@0: * In commands, the "this" identifier points to the controller item. michael@0: */ michael@0: commands: { michael@0: cmd_delete: function DVIC_cmd_delete() michael@0: { michael@0: this.dataItem.remove(); michael@0: PlacesUtils.bhistory.removePage(NetUtil.newURI(this.dataItem.uri)); michael@0: }, michael@0: michael@0: downloadsCmd_cancel: function DVIC_downloadsCmd_cancel() michael@0: { michael@0: this.dataItem.cancel(); michael@0: }, michael@0: michael@0: downloadsCmd_open: function DVIC_downloadsCmd_open() michael@0: { michael@0: this.dataItem.openLocalFile(); michael@0: michael@0: // We explicitly close the panel here to give the user the feedback that michael@0: // their click has been received, and we're handling the action. michael@0: // Otherwise, we'd have to wait for the file-type handler to execute michael@0: // before the panel would close. This also helps to prevent the user from michael@0: // accidentally opening a file several times. michael@0: DownloadsPanel.hidePanel(); michael@0: }, michael@0: michael@0: downloadsCmd_show: function DVIC_downloadsCmd_show() michael@0: { michael@0: this.dataItem.showLocalFile(); michael@0: michael@0: // We explicitly close the panel here to give the user the feedback that michael@0: // their click has been received, and we're handling the action. michael@0: // Otherwise, we'd have to wait for the operating system file manager michael@0: // window to open before the panel closed. This also helps to prevent the michael@0: // user from opening the containing folder several times. michael@0: DownloadsPanel.hidePanel(); michael@0: }, michael@0: michael@0: downloadsCmd_pauseResume: function DVIC_downloadsCmd_pauseResume() michael@0: { michael@0: this.dataItem.togglePauseResume(); michael@0: }, michael@0: michael@0: downloadsCmd_retry: function DVIC_downloadsCmd_retry() michael@0: { michael@0: this.dataItem.retry(); michael@0: }, michael@0: michael@0: downloadsCmd_openReferrer: function DVIC_downloadsCmd_openReferrer() michael@0: { michael@0: openURL(this.dataItem.referrer); michael@0: }, michael@0: michael@0: downloadsCmd_copyLocation: function DVIC_downloadsCmd_copyLocation() michael@0: { michael@0: let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] michael@0: .getService(Ci.nsIClipboardHelper); michael@0: clipboard.copyString(this.dataItem.uri, document); michael@0: }, michael@0: michael@0: downloadsCmd_doDefault: function DVIC_downloadsCmd_doDefault() michael@0: { michael@0: const nsIDM = Ci.nsIDownloadManager; michael@0: michael@0: // Determine the default command for the current item. michael@0: let defaultCommand = function () { michael@0: switch (this.dataItem.state) { michael@0: case nsIDM.DOWNLOAD_NOTSTARTED: return "downloadsCmd_cancel"; michael@0: case nsIDM.DOWNLOAD_FINISHED: return "downloadsCmd_open"; michael@0: case nsIDM.DOWNLOAD_FAILED: return "downloadsCmd_retry"; michael@0: case nsIDM.DOWNLOAD_CANCELED: return "downloadsCmd_retry"; michael@0: case nsIDM.DOWNLOAD_PAUSED: return "downloadsCmd_pauseResume"; michael@0: case nsIDM.DOWNLOAD_QUEUED: return "downloadsCmd_cancel"; michael@0: case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: return "downloadsCmd_openReferrer"; michael@0: case nsIDM.DOWNLOAD_SCANNING: return "downloadsCmd_show"; michael@0: case nsIDM.DOWNLOAD_DIRTY: return "downloadsCmd_openReferrer"; michael@0: case nsIDM.DOWNLOAD_BLOCKED_POLICY: return "downloadsCmd_openReferrer"; michael@0: } michael@0: return ""; michael@0: }.apply(this); michael@0: if (defaultCommand && this.isCommandEnabled(defaultCommand)) michael@0: this.doCommand(defaultCommand); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsSummary michael@0: michael@0: /** michael@0: * Manages the summary at the bottom of the downloads panel list if the number michael@0: * of items in the list exceeds the panels limit. michael@0: */ michael@0: const DownloadsSummary = { michael@0: michael@0: /** michael@0: * Sets the active state of the summary. When active, the summary subscribes michael@0: * to the DownloadsCommon DownloadsSummaryData singleton. michael@0: * michael@0: * @param aActive michael@0: * Set to true to activate the summary. michael@0: */ michael@0: set active(aActive) michael@0: { michael@0: if (aActive == this._active || !this._summaryNode) { michael@0: return this._active; michael@0: } michael@0: if (aActive) { michael@0: DownloadsCommon.getSummary(window, DownloadsView.kItemCountLimit) michael@0: .refreshView(this); michael@0: } else { michael@0: DownloadsFooter.showingSummary = false; michael@0: } michael@0: michael@0: return this._active = aActive; michael@0: }, michael@0: michael@0: /** michael@0: * Returns the active state of the downloads summary. michael@0: */ michael@0: get active() this._active, michael@0: michael@0: _active: false, michael@0: michael@0: /** michael@0: * Sets whether or not we show the progress bar. michael@0: * michael@0: * @param aShowingProgress michael@0: * True if we should show the progress bar. michael@0: */ michael@0: set showingProgress(aShowingProgress) michael@0: { michael@0: if (aShowingProgress) { michael@0: this._summaryNode.setAttribute("inprogress", "true"); michael@0: } else { michael@0: this._summaryNode.removeAttribute("inprogress"); michael@0: } michael@0: // If progress isn't being shown, then we simply do not show the summary. michael@0: return DownloadsFooter.showingSummary = aShowingProgress; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the amount of progress that is visible in the progress bar. michael@0: * michael@0: * @param aValue michael@0: * A value between 0 and 100 to represent the progress of the michael@0: * summarized downloads. michael@0: */ michael@0: set percentComplete(aValue) michael@0: { michael@0: if (this._progressNode) { michael@0: this._progressNode.setAttribute("value", aValue); michael@0: } michael@0: return aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the description for the download summary. michael@0: * michael@0: * @param aValue michael@0: * A string representing the description of the summarized michael@0: * downloads. michael@0: */ michael@0: set description(aValue) michael@0: { michael@0: if (this._descriptionNode) { michael@0: this._descriptionNode.setAttribute("value", aValue); michael@0: this._descriptionNode.setAttribute("tooltiptext", aValue); michael@0: } michael@0: return aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Sets the details for the download summary, such as the time remaining, michael@0: * the amount of bytes transferred, etc. michael@0: * michael@0: * @param aValue michael@0: * A string representing the details of the summarized michael@0: * downloads. michael@0: */ michael@0: set details(aValue) michael@0: { michael@0: if (this._detailsNode) { michael@0: this._detailsNode.setAttribute("value", aValue); michael@0: this._detailsNode.setAttribute("tooltiptext", aValue); michael@0: } michael@0: return aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Focuses the root element of the summary. michael@0: */ michael@0: focus: function() michael@0: { michael@0: if (this._summaryNode) { michael@0: this._summaryNode.focus(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Respond to keydown events on the Downloads Summary node. michael@0: * michael@0: * @param aEvent michael@0: * The keydown event being handled. michael@0: */ michael@0: onKeyDown: function DS_onKeyDown(aEvent) michael@0: { michael@0: if (aEvent.charCode == " ".charCodeAt(0) || michael@0: aEvent.keyCode == KeyEvent.DOM_VK_RETURN) { michael@0: DownloadsPanel.showDownloadsHistory(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Respond to click events on the Downloads Summary node. michael@0: * michael@0: * @param aEvent michael@0: * The click event being handled. michael@0: */ michael@0: onClick: function DS_onClick(aEvent) michael@0: { michael@0: DownloadsPanel.showDownloadsHistory(); michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the root of the downloads summary. michael@0: */ michael@0: get _summaryNode() michael@0: { michael@0: let node = document.getElementById("downloadsSummary"); michael@0: if (!node) { michael@0: return null; michael@0: } michael@0: delete this._summaryNode; michael@0: return this._summaryNode = node; michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the progress bar in the downloads summary. michael@0: */ michael@0: get _progressNode() michael@0: { michael@0: let node = document.getElementById("downloadsSummaryProgress"); michael@0: if (!node) { michael@0: return null; michael@0: } michael@0: delete this._progressNode; michael@0: return this._progressNode = node; michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the main description of the downloads michael@0: * summary. michael@0: */ michael@0: get _descriptionNode() michael@0: { michael@0: let node = document.getElementById("downloadsSummaryDescription"); michael@0: if (!node) { michael@0: return null; michael@0: } michael@0: delete this._descriptionNode; michael@0: return this._descriptionNode = node; michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the secondary description of the downloads michael@0: * summary. michael@0: */ michael@0: get _detailsNode() michael@0: { michael@0: let node = document.getElementById("downloadsSummaryDetails"); michael@0: if (!node) { michael@0: return null; michael@0: } michael@0: delete this._detailsNode; michael@0: return this._detailsNode = node; michael@0: } michael@0: } michael@0: michael@0: //////////////////////////////////////////////////////////////////////////////// michael@0: //// DownloadsFooter michael@0: michael@0: /** michael@0: * Manages events sent to to the footer vbox, which contains both the michael@0: * DownloadsSummary as well as the "Show All Downloads" button. michael@0: */ michael@0: const DownloadsFooter = { michael@0: michael@0: /** michael@0: * Focuses the appropriate element within the footer. If the summary michael@0: * is visible, focus it. If not, focus the "Show All Downloads" michael@0: * button. michael@0: */ michael@0: focus: function DF_focus() michael@0: { michael@0: if (this._showingSummary) { michael@0: DownloadsSummary.focus(); michael@0: } else { michael@0: DownloadsView.downloadsHistory.focus(); michael@0: } michael@0: }, michael@0: michael@0: _showingSummary: false, michael@0: michael@0: /** michael@0: * Sets whether or not the Downloads Summary should be displayed in the michael@0: * footer. If not, the "Show All Downloads" button is shown instead. michael@0: */ michael@0: set showingSummary(aValue) michael@0: { michael@0: if (this._footerNode) { michael@0: if (aValue) { michael@0: this._footerNode.setAttribute("showingsummary", "true"); michael@0: } else { michael@0: this._footerNode.removeAttribute("showingsummary"); michael@0: } michael@0: this._showingSummary = aValue; michael@0: } michael@0: return aValue; michael@0: }, michael@0: michael@0: /** michael@0: * Element corresponding to the footer of the downloads panel. michael@0: */ michael@0: get _footerNode() michael@0: { michael@0: let node = document.getElementById("downloadsFooter"); michael@0: if (!node) { michael@0: return null; michael@0: } michael@0: delete this._footerNode; michael@0: return this._footerNode = node; michael@0: } michael@0: };