|
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /** |
|
8 * Handles the 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 */ |
|
26 |
|
27 "use strict"; |
|
28 |
|
29 //////////////////////////////////////////////////////////////////////////////// |
|
30 //// DownloadsButton |
|
31 |
|
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", |
|
45 |
|
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 }, |
|
54 |
|
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 }, |
|
65 |
|
66 /** |
|
67 * Indicates whether toolbar customization is in progress. |
|
68 */ |
|
69 _customizing: false, |
|
70 |
|
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 }, |
|
86 |
|
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 }, |
|
95 |
|
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 } |
|
110 |
|
111 indicator.open = this._anchorRequested; |
|
112 |
|
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 } |
|
119 |
|
120 return DownloadsIndicatorView.indicatorAnchor; |
|
121 }, |
|
122 |
|
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 }, |
|
144 |
|
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, |
|
150 |
|
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 } |
|
166 |
|
167 function DB_GA_callback() { |
|
168 this._anchorRequested = true; |
|
169 aCallback(this._getAnchorInternal()); |
|
170 } |
|
171 |
|
172 DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, |
|
173 DB_GA_callback.bind(this)); |
|
174 }, |
|
175 |
|
176 /** |
|
177 * Allows the temporary anchor to be hidden. |
|
178 */ |
|
179 releaseAnchor: function DB_releaseAnchor() |
|
180 { |
|
181 this._anchorRequested = false; |
|
182 this._getAnchorInternal(); |
|
183 }, |
|
184 |
|
185 get _tabsToolbar() |
|
186 { |
|
187 delete this._tabsToolbar; |
|
188 return this._tabsToolbar = document.getElementById("TabsToolbar"); |
|
189 }, |
|
190 |
|
191 get _navBar() |
|
192 { |
|
193 delete this._navBar; |
|
194 return this._navBar = document.getElementById("nav-bar"); |
|
195 } |
|
196 }; |
|
197 |
|
198 //////////////////////////////////////////////////////////////////////////////// |
|
199 //// DownloadsIndicatorView |
|
200 |
|
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, |
|
212 |
|
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, |
|
218 |
|
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; |
|
228 |
|
229 window.addEventListener("unload", this.onWindowUnload, false); |
|
230 DownloadsCommon.getIndicatorData(window).addView(this); |
|
231 }, |
|
232 |
|
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; |
|
242 |
|
243 window.removeEventListener("unload", this.onWindowUnload, false); |
|
244 DownloadsCommon.getIndicatorData(window).removeView(this); |
|
245 |
|
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 }, |
|
253 |
|
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 } |
|
266 |
|
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 } |
|
272 |
|
273 function DIV_EO_callback() { |
|
274 this._operational = true; |
|
275 |
|
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 } |
|
281 |
|
282 if (aCallback) { |
|
283 aCallback(); |
|
284 } |
|
285 } |
|
286 |
|
287 DownloadsOverlayLoader.ensureOverlayLoaded( |
|
288 DownloadsButton.kIndicatorOverlay, |
|
289 DIV_EO_callback.bind(this)); |
|
290 }, |
|
291 |
|
292 ////////////////////////////////////////////////////////////////////////////// |
|
293 //// Direct control functions |
|
294 |
|
295 /** |
|
296 * Set while we are waiting for a notification to fade out. |
|
297 */ |
|
298 _notificationTimeout: null, |
|
299 |
|
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 }, |
|
312 |
|
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 } |
|
325 |
|
326 if (!DownloadsCommon.animateNotifications) { |
|
327 return; |
|
328 } |
|
329 |
|
330 // No need to show visual notification if the panel is visible. |
|
331 if (DownloadsPanel.isPanelShowing) { |
|
332 return; |
|
333 } |
|
334 |
|
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 } |
|
345 |
|
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 } |
|
353 |
|
354 if (this._notificationTimeout) { |
|
355 clearTimeout(this._notificationTimeout); |
|
356 } |
|
357 |
|
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 }, |
|
381 |
|
382 ////////////////////////////////////////////////////////////////////////////// |
|
383 //// Callback functions from DownloadsIndicatorData |
|
384 |
|
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; |
|
393 |
|
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, |
|
406 |
|
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 } |
|
416 |
|
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, |
|
430 |
|
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 } |
|
441 |
|
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, |
|
455 |
|
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 } |
|
466 |
|
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, |
|
478 |
|
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 } |
|
487 |
|
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, |
|
499 |
|
500 ////////////////////////////////////////////////////////////////////////////// |
|
501 //// User interface event functions |
|
502 |
|
503 onWindowUnload: function DIV_onWindowUnload() |
|
504 { |
|
505 // This function is registered as an event listener, we can't use "this". |
|
506 DownloadsIndicatorView.ensureTerminated(); |
|
507 }, |
|
508 |
|
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 } |
|
518 |
|
519 aEvent.stopPropagation(); |
|
520 }, |
|
521 |
|
522 onDragOver: function DIV_onDragOver(aEvent) |
|
523 { |
|
524 browserDragAndDrop.dragOver(aEvent); |
|
525 }, |
|
526 |
|
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; |
|
534 |
|
535 let name = {}; |
|
536 let url = browserDragAndDrop.drop(aEvent, name); |
|
537 if (url) { |
|
538 if (url.startsWith("about:")) { |
|
539 return; |
|
540 } |
|
541 |
|
542 let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; |
|
543 saveURL(url, name.value, null, true, true, null, sourceDoc); |
|
544 aEvent.preventDefault(); |
|
545 } |
|
546 }, |
|
547 |
|
548 _indicator: null, |
|
549 __indicatorCounter: null, |
|
550 __indicatorProgress: null, |
|
551 |
|
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 } |
|
561 |
|
562 let indicator = document.getElementById("downloads-button"); |
|
563 if (!indicator || indicator.getAttribute("indicator") != "true") { |
|
564 return null; |
|
565 } |
|
566 |
|
567 return this._indicator = indicator; |
|
568 }, |
|
569 |
|
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 }, |
|
579 |
|
580 get _indicatorCounter() |
|
581 { |
|
582 return this.__indicatorCounter || |
|
583 (this.__indicatorCounter = document.getElementById("downloads-indicator-counter")); |
|
584 }, |
|
585 |
|
586 get _indicatorProgress() |
|
587 { |
|
588 return this.__indicatorProgress || |
|
589 (this.__indicatorProgress = document.getElementById("downloads-indicator-progress")); |
|
590 }, |
|
591 |
|
592 get notifier() |
|
593 { |
|
594 return this._notifier || |
|
595 (this._notifier = document.getElementById("downloads-notification-anchor")); |
|
596 }, |
|
597 |
|
598 _onCustomizedAway: function() { |
|
599 this._indicator = null; |
|
600 this.__indicatorCounter = null; |
|
601 this.__indicatorProgress = null; |
|
602 }, |
|
603 |
|
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 }; |
|
615 |