Bug 1269962 - Implement a popup menu for showing a submenu in Downloads Panel Footer. draft
authorSean Lee <selee@mozilla.com>
Wed, 13 Jul 2016 13:39:00 +0800
changeset 398681 51b2fad8aa8a01c80b592ac537769e937b5f0163
parent 397688 763fe887c37cee5fcfe0f00e94fdffc84a41ea1c
child 527718 fc69d110b0599301a08c0dafd8564adcb1db9c70
push id25596
push userbmo:selee@mozilla.com
push dateTue, 09 Aug 2016 16:12:00 +0000
bugs1269962
milestone51.0a1
Bug 1269962 - Implement a popup menu for showing a submenu in Downloads Panel Footer. MozReview-Commit-ID: 7K1W15039W8
browser/components/downloads/content/downloads.css
browser/components/downloads/content/downloads.js
browser/components/downloads/content/downloadsOverlay.xul
browser/components/downloads/test/browser/browser.ini
browser/components/downloads/test/browser/browser_downloads_panel_footer.js
browser/locales/en-US/chrome/browser/downloads/downloads.dtd
browser/themes/linux/downloads/downloads.css
browser/themes/shared/downloads/button_more.svg
browser/themes/shared/downloads/downloads.inc.css
browser/themes/shared/jar.inc.mn
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -19,16 +19,48 @@ richlistitem[type="download"].download-s
 
 #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress,
 #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails,
 #downloadsFooter[showingsummary] > #downloadsHistory,
 #downloadsFooter:not([showingsummary]) > #downloadsSummary {
   display: none;
 }
 
+#downloadsPanel[hasdownloads] #downloadsHistory {
+  padding-left: 58px !important;
+}
+
+.rollOverSplitter {
+  margin: 4px 0;
+  width: 0 !important;
+  border-left: 1px solid rgb(212, 212, 212);
+}
+
+#downloadsFooter:hover .rollOverSplitter {
+  padding: 0;
+  margin: 0;
+}
+
+#downloadsDropmarker {
+  margin: 0;
+  list-style-image: url("chrome://browser/skin/downloads/button_more.svg");
+  filter: url("chrome://browser/skin/filters.svg#fill");
+  fill: rgb(133, 133, 133);
+  -moz-image-region: rect(0px, 16px, 16px, 0px);
+  -moz-appearance: none;
+  min-width: 0;
+  max-width: 58px;
+  height: 16px;
+}
+
+#downloadsDropmarker:hover {
+  outline: 1px solid hsla(210,4%,10%,.07);
+  background-color: hsla(210,4%,10%,.07);
+}
+
 /*** Downloads View ***/
 
 /**
  * The downloads richlistbox may list thousands of items, and it turns out
  * XBL binding attachment, and even more so detachment, is a performance hog.
  * This hack makes sure we don't apply any binding to inactive items (inactive
  * items are history downloads that haven't been in the visible area).
  * We can do this because the richlistbox implementation does not interact
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -73,16 +73,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
+const nsIDM = Ci.nsIDownloadManager;
+
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsPanel
 
 /**
  * Main entry point for the downloads panel interface.
  */
 const DownloadsPanel = {
   //////////////////////////////////////////////////////////////////////////////
@@ -518,16 +520,30 @@ const DownloadsPanel = {
       if (DownloadsView.richListBox.itemCount > 0) {
         DownloadsView.richListBox.focus();
       } else {
         DownloadsFooter.focus();
       }
     }
   },
 
+  clearPreviewPanel() {
+    DownloadsCommon.getData(window).removeFinished();
+    this.hidePanel();
+  },
+
+  openDownloadsFolder() {
+    Downloads.getPreferredDownloadsDirectory().then(downloadsPath => {
+      let file = new FileUtils.File(downloadsPath);
+      DownloadsCommon.showDownloadedFile(file);
+      this.hidePanel();
+      return;
+    });
+  },
+
   /**
    * Opens the downloads panel when data is ready to be displayed.
    */
   _openPopupIfDataReady() {
     // We don't want to open the popup if we already displayed it, or if we are
     // still loading data.
     if (this._state != this.kStateWaitingData || DownloadsView.loading) {
       return;
@@ -710,16 +726,18 @@ const DownloadsView = {
     }
 
     DownloadsBlockedSubview.view.setHeightToFit();
 
     // If we've got some hidden downloads, we should activate the
     // DownloadsSummary. The DownloadsSummary will determine whether or not
     // it's appropriate to actually display the summary.
     DownloadsSummary.active = hiddenCount > 0;
+    DownloadsSummary.showingClearPreviewPanelItem =
+      DownloadsCommon.getData(window).canRemoveFinished;
   },
 
   /**
    * Element corresponding to the list of downloads.
    */
   get richListBox() {
     delete this.richListBox;
     return this.richListBox = document.getElementById("downloadsListBox");
@@ -804,16 +822,18 @@ const DownloadsView = {
     }
   },
 
   onDownloadStateChanged(download) {
     let viewItem = this._visibleViewItems.get(download);
     if (viewItem) {
       viewItem.onStateChanged();
     }
+    DownloadsSummary.showingClearPreviewPanelItem =
+      DownloadsCommon.getData(window).canRemoveFinished;
   },
 
   onDownloadChanged(download) {
     let viewItem = this._visibleViewItems.get(download);
     if (viewItem) {
       viewItem.onChanged();
     }
   },
@@ -1304,16 +1324,20 @@ const DownloadsSummary = {
    * Returns the active state of the downloads summary.
    */
   get active() {
     return this._active;
   },
 
   _active: false,
 
+  set showingClearPreviewPanelItem(aShowingItem) {
+    document.getElementById("clearPreviewPanelButton").setAttribute("hidden", !aShowingItem);
+  },
+
   /**
    * Sets whether or not we show the progress bar.
    *
    * @param aShowingProgress
    *        True if we should show the progress bar.
    */
   set showingProgress(aShowingProgress) {
     if (aShowingProgress) {
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -143,21 +143,39 @@
                                min="0"
                                max="100"
                                mode="normal" />
                 <description id="downloadsSummaryDetails"
                              style="width: &downloadDetails.width;"
                              crop="end"/>
               </vbox>
             </hbox>
-            <button id="downloadsHistory"
-                    class="plain downloadsPanelFooterButton"
-                    label="&downloadsHistory.label;"
-                    accesskey="&downloadsHistory.accesskey;"
-                    oncommand="DownloadsPanel.showDownloadsHistory();"/>
+            <hbox flex="1">
+              <button id="downloadsHistory"
+                      class="plain downloadsPanelFooterButton"
+                      label="&downloadsHistory.label;"
+                      accesskey="&downloadsHistory.accesskey;"
+                      flex="1"
+                      oncommand="DownloadsPanel.showDownloadsHistory();"/>
+              <toolbarseparator class="rollOverSplitter"/>
+              <button id="downloadsDropmarker"
+                      type="menu"
+                      flex="1"
+                      popup="downloadSubPanel">
+                <menupopup id="downloadSubPanel" position="after_end">
+                  <menuitem id="clearPreviewPanelButton"
+                            oncommand="DownloadsPanel.clearPreviewPanel();event.stopPropagation();"
+                            hidden="true"
+                            label="&clearPreviewPanelButton.label;"/>
+                  <menuitem id="openDownloadsFolderButton"
+                            oncommand="DownloadsPanel.openDownloadsFolder();event.stopPropagation();"
+                            label="&openDownloadsFolderButton.label;"/>
+                </menupopup>
+              </button>
+            </hbox>
           </vbox>
         </panelview>
 
         <panelview id="downloadsPanel-blockedSubview"
                    orient="vertical"
                    flex="1">
           <description id="downloadsPanel-blockedSubview-title"/>
           <description id="downloadsPanel-blockedSubview-details1"/>
--- a/browser/components/downloads/test/browser/browser.ini
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -5,8 +5,9 @@ support-files = head.js
 skip-if = buildapp == "mulet"
 [browser_first_download_panel.js]
 skip-if = os == "linux" # Bug 949434
 [browser_overflow_anchor.js]
 skip-if = os == "linux" # Bug 952422
 [browser_confirm_unblock_download.js]
 [browser_iframe_gone_mid_download.js]
 [browser_downloads_panel_block.js]
+[browser_downloads_panel_footer.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_footer.js
@@ -0,0 +1,81 @@
+"use strict";
+
+add_task(function* mainTest() {
+  yield openPanel();
+  let downloadsDropmarker = document.getElementById('downloadsDropmarker');
+  EventUtils.sendMouseEvent({ type: "click" }, downloadsDropmarker);
+  Assert.ok(true);
+  yield promiseSubPanelShow(true);
+});
+
+function promiseSubPanelShow(shown) {
+  // More terribleness, but I'm tired of fighting intermittent timeouts on try.
+  // Just poll for the subview and wait a second before resolving the promise.
+  return new Promise(resolve => {
+    let interval = setInterval(() => {
+      let downloadsDropmarker = document.getElementById('downloadsDropmarker');
+      if (shown == downloadsDropmarker.getAttribute('_moz-menuactive')) {
+        clearInterval(interval);
+        setTimeout(resolve, 1000);
+        return;
+      }
+    }, 0);
+  });
+}
+
+function* openPanel() {
+  // This function is insane but something intermittently causes the panel to be
+  // closed as soon as it's opening on Linux ASAN.  Maybe it would also happen
+  // on other build machines if the test ran often enough.  Not only is the
+  // panel closed, it's closed while it's opening, leaving DownloadsPanel._state
+  // such that when you try to open the panel again, it thinks it's already
+  // open, but it's not.  The result is that the test times out.
+  //
+  // What this does is call DownloadsPanel.showPanel over and over again until
+  // the panel is really open.  There are a few wrinkles:
+  //
+  // (1) When panel.state is "open", check four more times (for a total of five)
+  // before returning to make the panel stays open.
+  // (2) If the panel is not open, check the _state.  It should be either
+  // kStateUninitialized or kStateHidden.  If it's not, then the panel is in the
+  // process of opening -- or maybe it's stuck in that process -- so reset the
+  // _state to kStateHidden.
+  // (3) If the _state is not kStateUninitialized or kStateHidden, then it may
+  // actually be properly opening and not stuck at all.  To avoid always closing
+  // the panel while it's properly opening, use an exponential backoff mechanism
+  // for retries.
+  //
+  // If all that fails, then the test will time out, but it would have timed out
+  // anyway.
+
+  yield promiseFocus();
+  yield new Promise(resolve => {
+    let verifyCount = 5;
+    let backoff = 0;
+    let iBackoff = 0;
+    let interval = setInterval(() => {
+      if (DownloadsPanel.panel && DownloadsPanel.panel.state == "open") {
+        if (verifyCount > 0) {
+          verifyCount--;
+        } else {
+          clearInterval(interval);
+          resolve();
+        }
+      } else {
+        if (iBackoff < backoff) {
+          // Keep backing off before trying again.
+          iBackoff++;
+        } else {
+          // Try (or retry) opening the panel.
+          verifyCount = 5;
+          backoff = Math.max(1, 2 * backoff);
+          iBackoff = 0;
+          if (DownloadsPanel._state != DownloadsPanel.kStateUninitialized) {
+            DownloadsPanel._state = DownloadsPanel.kStateHidden;
+          }
+          DownloadsPanel.showPanel();
+        }
+      }
+    }, 100);
+  });
+}
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -57,17 +57,17 @@
 <!ENTITY cmd.showMac.accesskey            "F">
 <!ENTITY cmd.retry.label                  "Retry">
 <!ENTITY cmd.goToDownloadPage.label       "Go To Download Page">
 <!ENTITY cmd.goToDownloadPage.accesskey   "G">
 <!ENTITY cmd.copyDownloadLink.label       "Copy Download Link">
 <!ENTITY cmd.copyDownloadLink.accesskey   "L">
 <!ENTITY cmd.removeFromHistory.label      "Remove From History">
 <!ENTITY cmd.removeFromHistory.accesskey  "e">
-<!ENTITY cmd.clearList.label              "Clear List">
+<!ENTITY cmd.clearList.label              "Clear Preview Panel">
 <!ENTITY cmd.clearList.accesskey          "a">
 <!ENTITY cmd.clearDownloads.label         "Clear Downloads">
 <!ENTITY cmd.clearDownloads.accesskey     "D">
 <!-- LOCALIZATION NOTE (cmd.unblock2.label):
      This command is shown in the context menu when downloads are blocked.
      -->
 <!ENTITY cmd.unblock2.label               "Allow Download">
 <!ENTITY cmd.unblock2.accesskey           "o">
@@ -104,16 +104,22 @@
 <!-- LOCALIZATION NOTE (downloadsHistory.label, downloadsHistory.accesskey):
      This string is shown at the bottom of the Downloads Panel when all the
      downloads fit in the available space, or when there are no downloads in
      the panel at all.
      -->
 <!ENTITY downloadsHistory.label           "Show All Downloads">
 <!ENTITY downloadsHistory.accesskey       "S">
 
+<!ENTITY downloadFooterSubPanelButton.accesskey       "P">
+
+<!ENTITY clearPreviewPanelButton.label       "Clear Preview Panel">
+
+<!ENTITY openDownloadsFolderButton.label       "Open Downloads Folder">
+
 <!ENTITY clearDownloadsButton.label       "Clear Downloads">
 <!ENTITY clearDownloadsButton.tooltip     "Clears completed, canceled and failed downloads">
 
 <!-- LOCALIZATION NOTE (downloadsListEmpty.label):
      This string is shown when there are no items in the Downloads view, when it
      is displayed inside a browser tab.
      -->
 <!ENTITY downloadsListEmpty.label         "There are no downloads.">
--- a/browser/themes/linux/downloads/downloads.css
+++ b/browser/themes/linux/downloads/downloads.css
@@ -1,16 +1,21 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 %include ../../shared/downloads/downloads.inc.css
 
 /*** Panel and outer controls ***/
 
+#downloadsHistory .button-menubutton-dropmarker {
+  /* Force the dropmarker image to align center */
+  padding-left: 21px;
+}
+
 @keyfocus@ #downloadsSummary:focus,
 @keyfocus@ .downloadsPanelFooterButton:focus {
   outline: 1px -moz-dialogtext dotted;
   outline-offset: -5px;
 }
 
 /*** List items and similar elements in the summary ***/
 
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/downloads/button_more.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill-rule:evenodd;clip-rule:evenodd;}
+</style>
+<path class="st0" d="M2.9,10.3l2.6-2.6l10.6,10.6L26.7,7.7l2.6,2.6L16.1,23.6L2.9,10.3z"/>
+</svg>
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -25,17 +25,18 @@
 
 #downloadsListBox {
   background: transparent;
   padding: 4px;
   color: inherit;
 }
 
 #emptyDownloads {
-  padding: 10px 20px;
+  height: 40px;
+  padding: 16px 20px;
   /* The panel can be wider than this description after the blocked subview is
      shown, so center the text. */
   text-align: center;
 }
 
 .downloadsPanelFooter {
   background-color: hsla(210,4%,10%,.07);
   border-top: 1px solid hsla(210,4%,10%,.14);
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -48,16 +48,17 @@
   skin/classic/browser/customizableui/subView-arrow-back-inverted.png  (../shared/customizableui/subView-arrow-back-inverted.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted@2x.png  (../shared/customizableui/subView-arrow-back-inverted@2x.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted-rtl.png  (../shared/customizableui/subView-arrow-back-inverted-rtl.png)
   skin/classic/browser/customizableui/subView-arrow-back-inverted-rtl@2x.png  (../shared/customizableui/subView-arrow-back-inverted-rtl@2x.png)
   skin/classic/browser/customizableui/whimsy.png               (../shared/customizableui/whimsy.png)
   skin/classic/browser/customizableui/whimsy@2x.png            (../shared/customizableui/whimsy@2x.png)
   skin/classic/browser/downloads/contentAreaDownloadsView.css  (../shared/downloads/contentAreaDownloadsView.css)
   skin/classic/browser/downloads/download-blocked.svg          (../shared/downloads/download-blocked.svg)
+  skin/classic/browser/downloads/button_more.svg               (../shared/downloads/button_more.svg)
   skin/classic/browser/drm-icon.svg                            (../shared/drm-icon.svg)
   skin/classic/browser/filters.svg                             (../shared/filters.svg)
   skin/classic/browser/fullscreen/insecure.svg                 (../shared/fullscreen/insecure.svg)
   skin/classic/browser/fullscreen/secure.svg                   (../shared/fullscreen/secure.svg)
   skin/classic/browser/heartbeat-icon.svg                      (../shared/heartbeat-icon.svg)
   skin/classic/browser/heartbeat-star-lit.svg                  (../shared/heartbeat-star-lit.svg)
   skin/classic/browser/heartbeat-star-off.svg                  (../shared/heartbeat-star-off.svg)
   skin/classic/browser/identity-icon.svg                       (../shared/identity-block/identity-icon.svg)