Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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 indicator that displays the progress of ongoing downloads, which
9 * is also used as the anchor for the downloads panel.
10 *
11 * This module includes the following constructors and global objects:
12 *
13 * DownloadsButton
14 * Main entry point for the downloads indicator. Depending on how the toolbars
15 * have been customized, this object determines if we should show a fully
16 * functional indicator, a placeholder used during customization and in the
17 * customization palette, or a neutral view as a temporary anchor for the
18 * downloads panel.
19 *
20 * DownloadsIndicatorView
21 * Builds and updates the actual downloads status widget, responding to changes
22 * in the global status data, or provides a neutral view if the indicator is
23 * removed from the toolbars and only used as a temporary anchor. In addition,
24 * handles the user interaction events raised by the widget.
25 */
27 "use strict";
29 ////////////////////////////////////////////////////////////////////////////////
30 //// DownloadsButton
32 /**
33 * Main entry point for the downloads indicator. Depending on how the toolbars
34 * have been customized, this object determines if we should show a fully
35 * functional indicator, a placeholder used during customization and in the
36 * customization palette, or a neutral view as a temporary anchor for the
37 * downloads panel.
38 */
39 const DownloadsButton = {
40 /**
41 * Location of the indicator overlay.
42 */
43 get kIndicatorOverlay()
44 "chrome://browser/content/downloads/indicatorOverlay.xul",
46 /**
47 * Returns a reference to the downloads button position placeholder, or null
48 * if not available because it has been removed from the toolbars.
49 */
50 get _placeholder()
51 {
52 return document.getElementById("downloads-button");
53 },
55 /**
56 * This function is called asynchronously just after window initialization.
57 *
58 * NOTE: This function should limit the input/output it performs to improve
59 * startup time.
60 */
61 initializeIndicator: function DB_initializeIndicator()
62 {
63 DownloadsIndicatorView.ensureInitialized();
64 },
66 /**
67 * Indicates whether toolbar customization is in progress.
68 */
69 _customizing: false,
71 /**
72 * This function is called when toolbar customization starts.
73 *
74 * During customization, we never show the actual download progress indication
75 * or the event notifications, but we show a neutral placeholder. The neutral
76 * placeholder is an ordinary button defined in the browser window that can be
77 * moved freely between the toolbars and the customization palette.
78 */
79 customizeStart: function DB_customizeStart()
80 {
81 // Prevent the indicator from being displayed as a temporary anchor
82 // during customization, even if requested using the getAnchor method.
83 this._customizing = true;
84 this._anchorRequested = false;
85 },
87 /**
88 * This function is called when toolbar customization ends.
89 */
90 customizeDone: function DB_customizeDone()
91 {
92 this._customizing = false;
93 DownloadsIndicatorView.afterCustomize();
94 },
96 /**
97 * Determines the position where the indicator should appear, and moves its
98 * associated element to the new position.
99 *
100 * @return Anchor element, or null if the indicator is not visible.
101 */
102 _getAnchorInternal: function DB_getAnchorInternal()
103 {
104 let indicator = DownloadsIndicatorView.indicator;
105 if (!indicator) {
106 // Exit now if the indicator overlay isn't loaded yet, or if the button
107 // is not in the document.
108 return null;
109 }
111 indicator.open = this._anchorRequested;
113 let widget = CustomizableUI.getWidget("downloads-button")
114 .forWindow(window);
115 // Determine if the indicator is located on an invisible toolbar.
116 if (!isElementVisible(indicator.parentNode) && !widget.overflowed) {
117 return null;
118 }
120 return DownloadsIndicatorView.indicatorAnchor;
121 },
123 /**
124 * Checks whether the indicator is, or will soon be visible in the browser
125 * window.
126 *
127 * @param aCallback
128 * Called once the indicator overlay has loaded. Gets a boolean
129 * argument representing the indicator visibility.
130 */
131 checkIsVisible: function DB_checkIsVisible(aCallback)
132 {
133 function DB_CEV_callback() {
134 if (!this._placeholder) {
135 aCallback(false);
136 } else {
137 let element = DownloadsIndicatorView.indicator || this._placeholder;
138 aCallback(isElementVisible(element.parentNode));
139 }
140 }
141 DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay,
142 DB_CEV_callback.bind(this));
143 },
145 /**
146 * Indicates whether we should try and show the indicator temporarily as an
147 * anchor for the panel, even if the indicator would be hidden by default.
148 */
149 _anchorRequested: false,
151 /**
152 * Ensures that there is an anchor available for the panel.
153 *
154 * @param aCallback
155 * Called when the anchor is available, passing the element where the
156 * panel should be anchored, or null if an anchor is not available (for
157 * example because both the tab bar and the navigation bar are hidden).
158 */
159 getAnchor: function DB_getAnchor(aCallback)
160 {
161 // Do not allow anchoring the panel to the element while customizing.
162 if (this._customizing) {
163 aCallback(null);
164 return;
165 }
167 function DB_GA_callback() {
168 this._anchorRequested = true;
169 aCallback(this._getAnchorInternal());
170 }
172 DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay,
173 DB_GA_callback.bind(this));
174 },
176 /**
177 * Allows the temporary anchor to be hidden.
178 */
179 releaseAnchor: function DB_releaseAnchor()
180 {
181 this._anchorRequested = false;
182 this._getAnchorInternal();
183 },
185 get _tabsToolbar()
186 {
187 delete this._tabsToolbar;
188 return this._tabsToolbar = document.getElementById("TabsToolbar");
189 },
191 get _navBar()
192 {
193 delete this._navBar;
194 return this._navBar = document.getElementById("nav-bar");
195 }
196 };
198 ////////////////////////////////////////////////////////////////////////////////
199 //// DownloadsIndicatorView
201 /**
202 * Builds and updates the actual downloads status widget, responding to changes
203 * in the global status data, or provides a neutral view if the indicator is
204 * removed from the toolbars and only used as a temporary anchor. In addition,
205 * handles the user interaction events raised by the widget.
206 */
207 const DownloadsIndicatorView = {
208 /**
209 * True when the view is connected with the underlying downloads data.
210 */
211 _initialized: false,
213 /**
214 * True when the user interface elements required to display the indicator
215 * have finished loading in the browser window, and can be referenced.
216 */
217 _operational: false,
219 /**
220 * Prepares the downloads indicator to be displayed.
221 */
222 ensureInitialized: function DIV_ensureInitialized()
223 {
224 if (this._initialized) {
225 return;
226 }
227 this._initialized = true;
229 window.addEventListener("unload", this.onWindowUnload, false);
230 DownloadsCommon.getIndicatorData(window).addView(this);
231 },
233 /**
234 * Frees the internal resources related to the indicator.
235 */
236 ensureTerminated: function DIV_ensureTerminated()
237 {
238 if (!this._initialized) {
239 return;
240 }
241 this._initialized = false;
243 window.removeEventListener("unload", this.onWindowUnload, false);
244 DownloadsCommon.getIndicatorData(window).removeView(this);
246 // Reset the view properties, so that a neutral indicator is displayed if we
247 // are visible only temporarily as an anchor.
248 this.counter = "";
249 this.percentComplete = 0;
250 this.paused = false;
251 this.attention = false;
252 },
254 /**
255 * Ensures that the user interface elements required to display the indicator
256 * are loaded, then invokes the given callback.
257 */
258 _ensureOperational: function DIV_ensureOperational(aCallback)
259 {
260 if (this._operational) {
261 if (aCallback) {
262 aCallback();
263 }
264 return;
265 }
267 // If we don't have a _placeholder, there's no chance that the overlay
268 // will load correctly: bail (and don't set _operational to true!)
269 if (!DownloadsButton._placeholder) {
270 return;
271 }
273 function DIV_EO_callback() {
274 this._operational = true;
276 // If the view is initialized, we need to update the elements now that
277 // they are finally available in the document.
278 if (this._initialized) {
279 DownloadsCommon.getIndicatorData(window).refreshView(this);
280 }
282 if (aCallback) {
283 aCallback();
284 }
285 }
287 DownloadsOverlayLoader.ensureOverlayLoaded(
288 DownloadsButton.kIndicatorOverlay,
289 DIV_EO_callback.bind(this));
290 },
292 //////////////////////////////////////////////////////////////////////////////
293 //// Direct control functions
295 /**
296 * Set while we are waiting for a notification to fade out.
297 */
298 _notificationTimeout: null,
300 /**
301 * Check if the panel containing aNode is open.
302 * @param aNode
303 * the node whose panel we're interested in.
304 */
305 _isAncestorPanelOpen: function DIV_isAncestorPanelOpen(aNode)
306 {
307 while (aNode && aNode.localName != "panel") {
308 aNode = aNode.parentNode;
309 }
310 return aNode && aNode.state == "open";
311 },
313 /**
314 * If the status indicator is visible in its assigned position, shows for a
315 * brief time a visual notification of a relevant event, like a new download.
316 *
317 * @param aType
318 * Set to "start" for new downloads, "finish" for completed downloads.
319 */
320 showEventNotification: function DIV_showEventNotification(aType)
321 {
322 if (!this._initialized) {
323 return;
324 }
326 if (!DownloadsCommon.animateNotifications) {
327 return;
328 }
330 // No need to show visual notification if the panel is visible.
331 if (DownloadsPanel.isPanelShowing) {
332 return;
333 }
335 let anchor = DownloadsButton._placeholder;
336 let widgetGroup = CustomizableUI.getWidget("downloads-button");
337 let widget = widgetGroup.forWindow(window);
338 if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
339 if (anchor && this._isAncestorPanelOpen(anchor)) {
340 // If the containing panel is open, don't do anything, because the
341 // notification would appear under the open panel. See
342 // https://bugzilla.mozilla.org/show_bug.cgi?id=984023
343 return;
344 }
346 // Otherwise, try to use the anchor of the panel:
347 anchor = widget.anchor;
348 }
349 if (!anchor || !isElementVisible(anchor.parentNode)) {
350 // Our container isn't visible, so can't show the animation:
351 return;
352 }
354 if (this._notificationTimeout) {
355 clearTimeout(this._notificationTimeout);
356 }
358 // The notification element is positioned to show in the same location as
359 // the downloads button. It's not in the downloads button itself in order to
360 // be able to anchor the notification elsewhere if required, and to ensure
361 // the notification isn't clipped by overflow properties of the anchor's
362 // container.
363 let notifier = this.notifier;
364 if (notifier.style.transform == '') {
365 let anchorRect = anchor.getBoundingClientRect();
366 let notifierRect = notifier.getBoundingClientRect();
367 let topDiff = anchorRect.top - notifierRect.top;
368 let leftDiff = anchorRect.left - notifierRect.left;
369 let heightDiff = anchorRect.height - notifierRect.height;
370 let widthDiff = anchorRect.width - notifierRect.width;
371 let translateX = (leftDiff + .5 * widthDiff) + "px";
372 let translateY = (topDiff + .5 * heightDiff) + "px";
373 notifier.style.transform = "translate(" + translateX + ", " + translateY + ")";
374 }
375 notifier.setAttribute("notification", aType);
376 this._notificationTimeout = setTimeout(function () {
377 notifier.removeAttribute("notification");
378 notifier.style.transform = '';
379 }, 1000);
380 },
382 //////////////////////////////////////////////////////////////////////////////
383 //// Callback functions from DownloadsIndicatorData
385 /**
386 * Indicates whether the indicator should be shown because there are some
387 * downloads to be displayed.
388 */
389 set hasDownloads(aValue)
390 {
391 if (this._hasDownloads != aValue || (!this._operational && aValue)) {
392 this._hasDownloads = aValue;
394 // If there is at least one download, ensure that the view elements are
395 if (aValue) {
396 this._ensureOperational();
397 }
398 }
399 return aValue;
400 },
401 get hasDownloads()
402 {
403 return this._hasDownloads;
404 },
405 _hasDownloads: false,
407 /**
408 * Status text displayed in the indicator. If this is set to an empty value,
409 * then the small downloads icon is displayed instead of the text.
410 */
411 set counter(aValue)
412 {
413 if (!this._operational) {
414 return this._counter;
415 }
417 if (this._counter !== aValue) {
418 this._counter = aValue;
419 if (this._counter)
420 this.indicator.setAttribute("counter", "true");
421 else
422 this.indicator.removeAttribute("counter");
423 // We have to set the attribute instead of using the property because the
424 // XBL binding isn't applied if the element is invisible for any reason.
425 this._indicatorCounter.setAttribute("value", aValue);
426 }
427 return aValue;
428 },
429 _counter: null,
431 /**
432 * Progress indication to display, from 0 to 100, or -1 if unknown. The
433 * progress bar is hidden if the current progress is unknown and no status
434 * text is set in the "counter" property.
435 */
436 set percentComplete(aValue)
437 {
438 if (!this._operational) {
439 return this._percentComplete;
440 }
442 if (this._percentComplete !== aValue) {
443 this._percentComplete = aValue;
444 if (this._percentComplete >= 0)
445 this.indicator.setAttribute("progress", "true");
446 else
447 this.indicator.removeAttribute("progress");
448 // We have to set the attribute instead of using the property because the
449 // XBL binding isn't applied if the element is invisible for any reason.
450 this._indicatorProgress.setAttribute("value", Math.max(aValue, 0));
451 }
452 return aValue;
453 },
454 _percentComplete: null,
456 /**
457 * Indicates whether the progress won't advance because of a paused state.
458 * Setting this property forces a paused progress bar to be displayed, even if
459 * the current progress information is unavailable.
460 */
461 set paused(aValue)
462 {
463 if (!this._operational) {
464 return this._paused;
465 }
467 if (this._paused != aValue) {
468 this._paused = aValue;
469 if (this._paused) {
470 this.indicator.setAttribute("paused", "true")
471 } else {
472 this.indicator.removeAttribute("paused");
473 }
474 }
475 return aValue;
476 },
477 _paused: false,
479 /**
480 * Set when the indicator should draw user attention to itself.
481 */
482 set attention(aValue)
483 {
484 if (!this._operational) {
485 return this._attention;
486 }
488 if (this._attention != aValue) {
489 this._attention = aValue;
490 if (aValue) {
491 this.indicator.setAttribute("attention", "true");
492 } else {
493 this.indicator.removeAttribute("attention");
494 }
495 }
496 return aValue;
497 },
498 _attention: false,
500 //////////////////////////////////////////////////////////////////////////////
501 //// User interface event functions
503 onWindowUnload: function DIV_onWindowUnload()
504 {
505 // This function is registered as an event listener, we can't use "this".
506 DownloadsIndicatorView.ensureTerminated();
507 },
509 onCommand: function DIV_onCommand(aEvent)
510 {
511 // If the downloads button is in the menu panel, open the Library
512 let widgetGroup = CustomizableUI.getWidget("downloads-button");
513 if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
514 DownloadsPanel.showDownloadsHistory();
515 } else {
516 DownloadsPanel.showPanel();
517 }
519 aEvent.stopPropagation();
520 },
522 onDragOver: function DIV_onDragOver(aEvent)
523 {
524 browserDragAndDrop.dragOver(aEvent);
525 },
527 onDrop: function DIV_onDrop(aEvent)
528 {
529 let dt = aEvent.dataTransfer;
530 // If dragged item is from our source, do not try to
531 // redownload already downloaded file.
532 if (dt.mozGetDataAt("application/x-moz-file", 0))
533 return;
535 let name = {};
536 let url = browserDragAndDrop.drop(aEvent, name);
537 if (url) {
538 if (url.startsWith("about:")) {
539 return;
540 }
542 let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
543 saveURL(url, name.value, null, true, true, null, sourceDoc);
544 aEvent.preventDefault();
545 }
546 },
548 _indicator: null,
549 __indicatorCounter: null,
550 __indicatorProgress: null,
552 /**
553 * Returns a reference to the main indicator element, or null if the element
554 * is not present in the browser window yet.
555 */
556 get indicator()
557 {
558 if (this._indicator) {
559 return this._indicator;
560 }
562 let indicator = document.getElementById("downloads-button");
563 if (!indicator || indicator.getAttribute("indicator") != "true") {
564 return null;
565 }
567 return this._indicator = indicator;
568 },
570 get indicatorAnchor()
571 {
572 let widget = CustomizableUI.getWidget("downloads-button")
573 .forWindow(window);
574 if (widget.overflowed) {
575 return widget.anchor;
576 }
577 return document.getElementById("downloads-indicator-anchor");
578 },
580 get _indicatorCounter()
581 {
582 return this.__indicatorCounter ||
583 (this.__indicatorCounter = document.getElementById("downloads-indicator-counter"));
584 },
586 get _indicatorProgress()
587 {
588 return this.__indicatorProgress ||
589 (this.__indicatorProgress = document.getElementById("downloads-indicator-progress"));
590 },
592 get notifier()
593 {
594 return this._notifier ||
595 (this._notifier = document.getElementById("downloads-notification-anchor"));
596 },
598 _onCustomizedAway: function() {
599 this._indicator = null;
600 this.__indicatorCounter = null;
601 this.__indicatorProgress = null;
602 },
604 afterCustomize: function() {
605 // If the cached indicator is not the one currently in the document,
606 // invalidate our references
607 if (this._indicator != document.getElementById("downloads-button")) {
608 this._onCustomizedAway();
609 this._operational = false;
610 this.ensureTerminated();
611 this.ensureInitialized();
612 }
613 }
614 };