Bug 1269954 - When a download is added, popup the detail of the download item as a notification. draft
authorRex Lee <rexboy@mozilla.com>
Mon, 26 Sep 2016 17:56:12 +0800
changeset 445850 7fd1df89c94e2035662ce14f33c81b07ab218f72
parent 440252 13f49da109ea460665ad27c8497cb1489548450c
child 538647 7682f28fb07c9af44b658bd4332227aacf12b577
push id37639
push userbmo:rexboy@mozilla.com
push dateWed, 30 Nov 2016 11:54:01 +0000
bugs1269954
milestone53.0a1
Bug 1269954 - When a download is added, popup the detail of the download item as a notification.
browser/components/downloads/DownloadsCommon.jsm
browser/components/downloads/content/downloads.css
browser/components/downloads/content/downloads.js
browser/components/downloads/content/downloadsOverlay.xul
browser/components/downloads/content/indicator.js
browser/themes/shared/downloads/downloads.inc.css
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -814,23 +814,23 @@ DownloadsDataCtor.prototype = {
           view.onDownloadStateChanged(download);
         } catch (ex) {
           Cu.reportError(ex);
         }
       }
 
       if (download.succeeded ||
           (download.error && download.error.becauseBlocked)) {
-        this._notifyDownloadEvent("finish");
+        this._notifyDownloadEvent("finish", download);
       }
     }
 
     if (!download.newDownloadNotified) {
       download.newDownloadNotified = true;
-      this._notifyDownloadEvent("start");
+      this._notifyDownloadEvent("start", download);
     }
 
     for (let view of this._views) {
       view.onDownloadChanged(download);
     }
   },
 
   onDownloadRemoved(download) {
@@ -889,57 +889,38 @@ DownloadsDataCtor.prototype = {
     // Notify the view that all data is available.
     aView.onDataLoadCompleted();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Notifications sent to the most recent browser window only
 
   /**
-   * Set to true after the first download causes the downloads panel to be
-   * displayed.
-   */
-  get panelHasShownBefore() {
-    try {
-      return Services.prefs.getBoolPref("browser.download.panel.shown");
-    } catch (ex) { }
-    return false;
-  },
-
-  set panelHasShownBefore(aValue) {
-    Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
-    return aValue;
-  },
-
-  /**
    * Displays a new or finished download notification in the most recent browser
    * window, if one is currently available with the required privacy type.
    *
    * @param aType
    *        Set to "start" for new downloads, "finish" for completed downloads.
+   * @param download
+   *        the download data object.
    */
-  _notifyDownloadEvent(aType) {
+  _notifyDownloadEvent(aType, download) {
     DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
 
     // Show the panel in the most recent browser window, if present.
     let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate });
     if (!browserWin) {
       return;
     }
+    DownloadsCommon.log("Showing new download notification.");
+    browserWin.DownloadsIndicatorView.showEventNotification(aType);
 
-    if (this.panelHasShownBefore) {
-      // For new downloads after the first one, don't show the panel
-      // automatically, but provide a visible notification in the topmost
-      // browser window, if the status indicator is already visible.
-      DownloadsCommon.log("Showing new download notification.");
-      browserWin.DownloadsIndicatorView.showEventNotification(aType);
-      return;
+    if (aType === 'start') {
+      browserWin.NewDownloadNotifyPanel.onDownloadAdded(download, true);
     }
-    this.panelHasShownBefore = true;
-    browserWin.DownloadsPanel.showPanel();
   }
 };
 
 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
   return new DownloadsDataCtor(true);
 });
 
 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -260,8 +260,25 @@ richlistitem.download button {
 #downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] > .panel-mainview #downloadsSummary {
   -moz-user-focus: ignore;
 }
 /* ... except for the downloadShowBlockedInfo button in the blocked download.
    Selecting it with the keyboard should show the main view again. */
 #downloadsPanel-multiView > .panel-viewcontainer > .panel-viewstack[viewtype="subview"] .download-state[showingsubview] .downloadShowBlockedInfo {
   -moz-user-focus: normal;
 }
+
+/*** New downloads notification ***/
+
+/* Hide all the usual buttons for blocked downoads. */
+#newDownloadPopupPanel .download-state[state="8"] .downloadCancel,
+#newDownloadPopupPanel .download-state[state="8"] .downloadConfirmBlock,
+#newDownloadPopupPanel .download-state[state="8"] .downloadChooseUnblock,
+#newDownloadPopupPanel .download-state[state="8"] .downloadChooseOpen,
+#newDownloadPopupPanel .download-state[state="8"] .downloadRetry,
+#newDownloadPopupPanel .download-state[state="8"] .downloadShow {
+  display: none;
+}
+
+/* And show the "show blocked info" button for blocked downloads. */
+#newDownloadPopupPanel .download-state[state="8"] .downloadShowBlockedInfo {
+  display: inline;
+}
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -220,16 +220,19 @@ const DownloadsPanel = {
     DownloadsCommon.log("Opening the downloads panel.");
 
     if (this.isPanelShowing) {
       DownloadsCommon.log("Panel is already showing - focusing instead.");
       this._focusPanel();
       return;
     }
 
+    // If the new download notification is showing, hide it first.
+    NewDownloadNotifyPanel.hidePanel();
+
     this.initialize(() => {
       let downloadsFooterButtons =
         document.getElementById("downloadsFooterButtons");
       if (DownloadsCommon.showPanelDropmarker) {
         downloadsFooterButtons.classList.remove("downloadsHideDropmarker");
       } else {
         downloadsFooterButtons.classList.add("downloadsHideDropmarker");
       }
@@ -899,16 +902,20 @@ const DownloadsView = {
    * DownloadsViewItem object.
    */
   _itemsForElements: new Map(),
 
   itemForElement(element) {
     return this._itemsForElements.get(element);
   },
 
+  itemForData(download) {
+    return this._visibleViewItems.get(download);
+  },
+
   /**
    * Creates a new view item associated with the specified data item, and adds
    * it to the top or the bottom of the list.
    */
   _addViewItem(download, aNewest)
   {
     DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
                         "aNewest =", aNewest);
@@ -953,17 +960,18 @@ const DownloadsView = {
    * @param aCommand
    *        The command to be performed.
    */
   onDownloadCommand(aEvent, aCommand) {
     let target = aEvent.target;
     while (target.nodeName != "richlistitem") {
       target = target.parentNode;
     }
-    DownloadsView.itemForElement(target).doCommand(aCommand);
+    (NewDownloadNotifyPanel.itemForElement(target) ||
+      DownloadsView.itemForElement(target)).doCommand(aCommand);
   },
 
   onDownloadClick(aEvent) {
     // Handle primary clicks only, and exclude the action button.
     if (aEvent.button == 0 &&
         !aEvent.originalTarget.hasAttribute("oncommand")) {
       let target = aEvent.target;
       while (target.nodeName != "richlistitem") {
@@ -1151,55 +1159,71 @@ DownloadsViewItem.prototype = {
   cmd_delete() {
     DownloadsCommon.removeAndFinalizeDownload(this.download);
     PlacesUtils.bhistory.removePage(
                            NetUtil.newURI(this.download.source.url));
   },
 
   downloadsCmd_unblock() {
     DownloadsPanel.hidePanel();
+    NewDownloadNotifyPanel.hidePanel();
     this.confirmUnblock(window, "unblock");
   },
 
   downloadsCmd_chooseUnblock() {
     DownloadsPanel.hidePanel();
+    NewDownloadNotifyPanel.hidePanel();
     this.confirmUnblock(window, "chooseUnblock");
   },
 
   downloadsCmd_unblockAndOpen() {
     DownloadsPanel.hidePanel();
+    NewDownloadNotifyPanel.hidePanel();
     this.unblockAndOpenDownload().catch(Cu.reportError);
   },
 
   downloadsCmd_open() {
     this.download.launch().catch(Cu.reportError);
 
     // We explicitly close the panel here to give the user the feedback that
     // their click has been received, and we're handling the action.
     // Otherwise, we'd have to wait for the file-type handler to execute
     // before the panel would close. This also helps to prevent the user from
     // accidentally opening a file several times.
     DownloadsPanel.hidePanel();
+    NewDownloadNotifyPanel.hidePanel();
   },
 
   downloadsCmd_show() {
     let file = new FileUtils.File(this.download.target.path);
     DownloadsCommon.showDownloadedFile(file);
 
     // We explicitly close the panel here to give the user the feedback that
     // their click has been received, and we're handling the action.
     // Otherwise, we'd have to wait for the operating system file manager
     // window to open before the panel closed. This also helps to prevent the
     // user from opening the containing folder several times.
     DownloadsPanel.hidePanel();
+    NewDownloadNotifyPanel.hidePanel();
   },
 
   downloadsCmd_showBlockedInfo() {
-    DownloadsBlockedSubview.toggle(this.element,
-                                   ...this.rawBlockedTitleAndDetails);
+    if (DownloadsPanel.isPanelShowing) {
+      DownloadsBlockedSubview.toggle(this.element,
+                                     ...this.rawBlockedTitleAndDetails);
+    } else {
+      // This is the case of popup notification.
+      DownloadsPanel.showPanel();
+      let dataitem = NewDownloadNotifyPanel.itemForElement(this.element).download;
+      var elementInDownloadPanel = DownloadsView.itemForData(dataitem).element;
+      DownloadsView.richListBox.selectedItem = elementInDownloadPanel;
+      DownloadsBlockedSubview.toggle(elementInDownloadPanel,
+                                     ...this.rawBlockedTitleAndDetails);
+    }
+
   },
 
   downloadsCmd_openReferrer() {
     openURL(this.download.source.referrer);
   },
 
   downloadsCmd_copyLocation() {
     let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
@@ -1686,8 +1710,156 @@ const DownloadsBlockedSubview = {
   confirmBlock() {
     goDoCommand("cmd_delete");
     DownloadsPanel.hidePanel();
   },
 };
 
 XPCOMUtils.defineConstant(this, "DownloadsBlockedSubview",
                           DownloadsBlockedSubview);
+
+////////////////////////////////////////////////////////////////////////////////
+//// NewDownloadNotifyPanel
+
+const NewDownloadNotifyPanel = {
+  _initialized: false,
+  _initializing: false,
+  _downloadItem: null,
+  _viewItem: null,
+  _hidingTimer: null,
+
+  initialize(aCallback) {
+    if (this._initialized) {
+      aCallback();
+      return;
+    }
+    if (this._initializing) {
+      setTimeout(aCallback);
+      return;
+    }
+
+    this._initializing = true;
+    DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, () => {
+      this._initializing = false;
+      this._initialized = true
+      DownloadsCommon.getData(window).addView(NewDownloadNotifyPanel);
+      this.panel = document.getElementById('newDownloadPopupPanel');
+      DownloadsCommon.log('initialize finished for download notify panel');
+      this.panel.onmouseover = this.onmouseover.bind(this);
+      this.panel.onmouseout = this.onmouseout.bind(this);
+      aCallback();
+    });
+  },
+
+  onmouseover(e) {
+    this.clearHidingTimer();
+  },
+
+  onmouseout(e) {
+    var rel = e.relatedTarget;
+
+    // find out if the node we are entering is our children
+    while (rel) {
+      if (rel == this.panel)
+        break;
+      rel = rel.parentNode;
+    }
+
+    // if the entered node is not a descendant of ours, the mouse is no longer
+    // over the panel and we setup the hiding timer again.
+    if (rel != this.panel) {
+      this.setHidingTimer();
+    }
+  },
+
+  onDownloadAdded(download, aNewest) {
+    if (!aNewest) {
+      return;
+    }
+    this.initialize(() => {
+      this._addViewItem(download);
+    });
+    // We show notification panel after download indicator animation is end.
+    setTimeout(this.showPanel.bind(this), 1000);
+  },
+
+  setHidingTimer() {
+    if(this._hidingTimer) {
+      clearTimeout(this._hidingTimer);
+    }
+    this._hidingTimer = setTimeout(this.hidePanel.bind(this), 5000);
+  },
+
+  clearHidingTimer() {
+    this._logcounter++;
+    if(this._hidingTimer) {
+      clearTimeout(this._hidingTimer);
+    }
+  },
+
+  showPanel() {
+    if(!this._initialized) {
+      this.initialize(this.showPanel.bind(this));
+    }
+
+    var anchor = DownloadsIndicatorView.indicatorAnchor;
+    this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null);
+    this.setHidingTimer();
+  },
+
+  itemForElement(element) {
+    if(this._viewItem.element === element) {
+      return this._viewItem;
+    }
+    return null;
+  },
+
+  hidePanel() {
+    if (!this.panel) {
+      return;
+    }
+    this._hidingTimer = null;
+    this.panel.hidePopup();
+  },
+
+  _addViewItem(download) {
+    this._downloadItem = download;
+    DownloadsCommon.log("Adding a new DownloadsViewItem to download notifier");
+    let element = document.createElement("richlistitem");
+    this._viewItem = new DownloadsViewItem(download, element);
+    if (this.panel.firstChild) {
+      this.panel.removeChild(this.panel.firstChild);
+    }
+    this.panel.appendChild(element);
+  },
+
+  onDownloadStateChanged(download) {
+    if (this._downloadItem === download) {
+      this._viewItem.onStateChanged();
+    }
+  },
+
+  onDownloadChanged(download) {
+    if (this._downloadItem === download) {
+      this._viewItem.onChanged();
+    }
+  },
+
+  onDownloadRemoved(download) {
+  },
+
+  onDataLoadStarting() {
+
+  },
+
+  onDataLoadCompleted() {
+
+  },
+  get initialized() {
+    return this._initialized;
+  },
+
+  get kDownloadsOverlay() {
+    return "chrome://browser/content/downloads/downloadsOverlay.xul";
+  },
+};
+
+XPCOMUtils.defineConstant(this, "NewDownloadNotifyPanel", NewDownloadNotifyPanel);
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -199,10 +199,11 @@
                     default="true"
                     flex="1"/>
           </hbox>
         </panelview>
 
       </panelmultiview>
 
     </panel>
+    <panel id="newDownloadPopupPanel" type="arrow" noautohide="true" />
   </popupset>
 </overlay>
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -498,16 +498,17 @@ const DownloadsIndicatorView = {
   },
 
   onCommand(aEvent) {
     // If the downloads button is in the menu panel, open the Library
     let widgetGroup = CustomizableUI.getWidget("downloads-button");
     if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
       DownloadsPanel.showDownloadsHistory();
     } else {
+      NewDownloadNotifyPanel.hidePanel();
       DownloadsPanel.showPanel();
     }
 
     aEvent.stopPropagation();
   },
 
   onDragOver(aEvent) {
     browserDragAndDrop.dragOver(aEvent);
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -388,8 +388,17 @@ richlistitem[type="download"] > .downloa
 }
 
 #downloadsPanel-blockedSubview-title,
 #downloadsPanel-blockedSubview-details1,
 #downloadsPanel-blockedSubview-details2 {
   -moz-margin-start: 64px;
   -moz-margin-end: 16px;
 }
+
+#newDownloadPopupPanel .panel-arrowcontent {
+  padding: 0;
+}
+
+#newDownloadPopupPanel > .panel-arrowcontainer > .panel-arrowcontent {
+  overflow: hidden;
+  display: block;
+}