Bug 1269962 - Implement a popup menu for showing a submenu in Downloads Panel Footer., r=Paolo
authorSean Lee <selee@mozilla.com>
Wed, 13 Jul 2016 13:39:00 +0800
changeset 310699 d17dd12093e3f80471436ff5b4a80805a5df7b60
parent 310698 5aea87afc510f2b64c9ca3617fd27b78af95f4af
child 310700 b1cc4e06979ab207eefff38298c7cf7b574dad7d
push id80949
push userryanvm@gmail.com
push dateTue, 23 Aug 2016 14:07:01 +0000
treeherdermozilla-inbound@12c339c60630 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersPaolo
bugs1269962
milestone51.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1269962 - Implement a popup menu for showing a submenu in Downloads Panel Footer., r=Paolo MozReview-Commit-ID: 7K1W15039W8
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/test/browser/browser.ini
browser/components/downloads/test/browser/browser_downloads_panel_footer.js
browser/components/downloads/test/browser/head.js
browser/locales/en-US/chrome/browser/downloads/downloads.dtd
browser/themes/shared/downloads/downloads.inc.css
browser/themes/shared/downloads/menubutton-dropmarker.svg
browser/themes/shared/jar.inc.mn
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -503,31 +503,43 @@ this.DownloadsCommon = {
     try {
       // Show the directory containing the file and select the file.
       aFile.reveal();
     } catch (ex) {
       // If reveal fails for some reason (e.g., it's not implemented on unix
       // or the file doesn't exist), try using the parent if we have it.
       let parent = aFile.parent;
       if (parent) {
-        try {
-          // Open the parent directory to show where the file should be.
-          parent.launch();
-        } catch (ex) {
-          // If launch also fails (probably because it's not implemented), let
-          // the OS handler try to open the parent.
-          Cc["@mozilla.org/uriloader/external-protocol-service;1"]
-            .getService(Ci.nsIExternalProtocolService)
-            .loadUrl(NetUtil.newURI(parent));
-        }
+        this.showDirectory(parent);
       }
     }
   },
 
   /**
+   * Show the specified folder in the system file manager.
+   *
+   * @param aDirectory
+   *        a directory to be opened with system file manager.
+   */
+  showDirectory(aDirectory) {
+    if (!(aDirectory instanceof Ci.nsIFile)) {
+      throw new Error("aDirectory must be a nsIFile object");
+    }
+    try {
+      aDirectory.launch();
+    } catch (ex) {
+      // If launch fails (probably because it's not implemented), let
+      // the OS handler try to open the directory.
+      Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+        .getService(Ci.nsIExternalProtocolService)
+        .loadUrl(NetUtil.newURI(aDirectory));
+    }
+  },
+
+  /**
    * Displays an alert message box which asks the user if they want to
    * unblock the downloaded file or not.
    *
    * @param options
    *        An object with the following properties:
    *        {
    *          verdict:
    *            The detailed reason why the download was blocked, according to
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -14,17 +14,17 @@ richlistitem[type="download"]:not([selec
 }
 
 richlistitem[type="download"].download-state[state="1"]:not([exists]) .downloadShow {
   display: none;
 }
 
 #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress,
 #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails,
-#downloadsFooter[showingsummary] > #downloadsHistory,
+#downloadsFooter[showingsummary] > #downloadsFooterButtons,
 #downloadsFooter:not([showingsummary]) > #downloadsSummary {
   display: none;
 }
 
 /*** Downloads View ***/
 
 /**
  * The downloads richlistbox may list thousands of items, and it turns out
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -362,31 +362,55 @@ const DownloadsPanel = {
 
     // Allow the anchor to be hidden.
     DownloadsButton.releaseAnchor();
 
     // Allow the panel to be reopened.
     this._state = this.kStateHidden;
   },
 
+  onFooterPopupShowing(aEvent) {
+    let itemClearList = document.getElementById("downloadsDropdownItemClearList");
+    if (DownloadsCommon.getData(window).canRemoveFinished) {
+      itemClearList.removeAttribute("hidden");
+    } else {
+      itemClearList.setAttribute("hidden", "true");
+    }
+
+    document.getElementById("downloadsFooterButtonsSplitter").classList
+      .add("downloadsDropmarkerSplitterExtend");
+  },
+
+  onFooterPopupHidden(aEvent) {
+    document.getElementById("downloadsFooterButtonsSplitter").classList
+      .remove("downloadsDropmarkerSplitterExtend");
+  },
+
   //////////////////////////////////////////////////////////////////////////////
   //// Related operations
 
   /**
    * Shows or focuses the user interface dedicated to downloads history.
    */
   showDownloadsHistory() {
     DownloadsCommon.log("Showing download history.");
     // Hide the panel before showing another window, otherwise focus will return
     // to the browser window when the panel closes automatically.
     this.hidePanel();
 
     BrowserDownloadsUI();
   },
 
+  openDownloadsFolder() {
+    Downloads.getPreferredDownloadsDirectory().then(downloadsPath => {
+      DownloadsCommon.showDirectory(new FileUtils.File(downloadsPath));
+    }).catch(Cu.reportError);
+    this.hidePanel();
+  },
+
   //////////////////////////////////////////////////////////////////////////////
   //// Internal functions
 
   /**
    * Attach event listeners to a panel element. These listeners should be
    * removed in _unattachEventListeners. This is called automatically after the
    * panel has successfully loaded.
    */
@@ -1183,16 +1207,19 @@ const DownloadsViewController = {
   terminate() {
     window.controllers.removeController(this);
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIController
 
   supportsCommand(aCommand) {
+    if (aCommand === "downloadsCmd_clearList") {
+      return true;
+    }
     // Firstly, determine if this is a command that we can handle.
     if (!DownloadsViewUI.isCommandName(aCommand)) {
       return false;
     }
     if (!(aCommand in this) &&
         !(aCommand in DownloadsViewItem.prototype)) {
       return false;
     }
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -99,18 +99,18 @@
                   accesskey="&cmd.goToDownloadPage.accesskey;"/>
         <menuitem command="downloadsCmd_copyLocation"
                   label="&cmd.copyDownloadLink.label;"
                   accesskey="&cmd.copyDownloadLink.accesskey;"/>
 
         <menuseparator/>
 
         <menuitem command="downloadsCmd_clearList"
-                  label="&cmd.clearList.label;"
-                  accesskey="&cmd.clearList.accesskey;"/>
+                  label="&cmd.clearList2.label;"
+                  accesskey="&cmd.clearList2.accesskey;"/>
       </menupopup>
 
       <panelmultiview id="downloadsPanel-multiView"
                       mainViewId="downloadsPanel-mainView"
                       align="stretch">
 
         <panelview id="downloadsPanel-mainView"
                    flex="1"
@@ -143,21 +143,41 @@
                                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 id="downloadsFooterButtons">
+              <button id="downloadsHistory"
+                      class="plain downloadsPanelFooterButton"
+                      label="&downloadsHistory.label;"
+                      accesskey="&downloadsHistory.accesskey;"
+                      flex="1"
+                      oncommand="DownloadsPanel.showDownloadsHistory();"/>
+              <toolbarseparator id="downloadsFooterButtonsSplitter"
+                      class="downloadsDropmarkerSplitter"/>
+              <button id="downloadsFooterDropmarker"
+                      class="plain downloadsPanelFooterButton downloadsDropmarker"
+                      type="menu">
+                <menupopup id="downloadSubPanel"
+                           onpopupshowing="DownloadsPanel.onFooterPopupShowing(event);"
+                           onpopuphidden="DownloadsPanel.onFooterPopupHidden(event);"
+                           position="after_end">
+                  <menuitem id="downloadsDropdownItemClearList"
+                            command="downloadsCmd_clearList"
+                            label="&cmd.clearList2.label;"/>
+                  <menuitem id="downloadsDropdownItemOpenDownloadsFolder"
+                            oncommand="DownloadsPanel.openDownloadsFolder();"
+                            label="&openDownloadsFolder.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,94 @@
+"use strict";
+
+function *task_openDownloadsSubPanel() {
+  let downloadSubPanel = document.getElementById("downloadSubPanel");
+  let popupShownPromise = BrowserTestUtils.waitForEvent(downloadSubPanel, "popupshown");
+
+  let downloadsDropmarker = document.getElementById("downloadsFooterDropmarker");
+  EventUtils.synthesizeMouseAtCenter(downloadsDropmarker, {}, window);
+
+  yield popupShownPromise;
+}
+
+add_task(function* test_openDownloadsFolder() {
+  yield task_openPanel();
+
+  yield task_openDownloadsSubPanel();
+
+  yield new Promise(resolve => {
+    sinon.stub(DownloadsCommon, "showDirectory", file => {
+      resolve(Downloads.getPreferredDownloadsDirectory().then(downloadsPath => {
+        is(file.path, downloadsPath, "Check the download folder path.");
+      }));
+    });
+
+    let itemOpenDownloadsFolder =
+      document.getElementById("downloadsDropdownItemOpenDownloadsFolder");
+    EventUtils.synthesizeMouseAtCenter(itemOpenDownloadsFolder, {}, window);
+  });
+
+  yield task_resetState();
+});
+
+add_task(function* test_clearList() {
+  const kTestCases = [{
+    downloads: [
+      { state: nsIDM.DOWNLOAD_NOTSTARTED },
+      { state: nsIDM.DOWNLOAD_FINISHED },
+      { state: nsIDM.DOWNLOAD_FAILED },
+      { state: nsIDM.DOWNLOAD_CANCELED },
+    ],
+    expectClearListShown: true,
+    expectedItemNumber: 0,
+  },{
+    downloads: [
+      { state: nsIDM.DOWNLOAD_NOTSTARTED },
+      { state: nsIDM.DOWNLOAD_FINISHED },
+      { state: nsIDM.DOWNLOAD_FAILED },
+      { state: nsIDM.DOWNLOAD_PAUSED },
+      { state: nsIDM.DOWNLOAD_CANCELED },
+    ],
+    expectClearListShown: true,
+    expectedItemNumber: 1,
+  },{
+    downloads: [
+      { state: nsIDM.DOWNLOAD_PAUSED },
+    ],
+    expectClearListShown: false,
+    expectedItemNumber: 1,
+  }];
+
+  for (let testCase of kTestCases) {
+    yield verify_clearList(testCase);
+  }
+});
+
+function *verify_clearList(testCase) {
+  let downloads = testCase.downloads;
+  yield task_addDownloads(downloads);
+
+  yield task_openPanel();
+  is(DownloadsView._downloads.length, downloads.length,
+    "Expect the number of download items");
+
+  yield task_openDownloadsSubPanel();
+
+  let itemClearList = document.getElementById("downloadsDropdownItemClearList");
+  let itemNumberPromise = BrowserTestUtils.waitForCondition(() => {
+    return DownloadsView._downloads.length === testCase.expectedItemNumber;
+  });
+  if (testCase.expectClearListShown) {
+    isnot("true", itemClearList.getAttribute("hidden"),
+      "Should show Clear Preview Panel button");
+    EventUtils.synthesizeMouseAtCenter(itemClearList, {}, window);
+  } else {
+    is("true", itemClearList.getAttribute("hidden"),
+      "Should not show Clear Preview Panel button");
+  }
+
+  yield itemNumberPromise;
+  is(DownloadsView._downloads.length, testCase.expectedItemNumber,
+    "Download items remained.");
+
+  yield task_resetState();
+}
--- a/browser/components/downloads/test/browser/head.js
+++ b/browser/components/downloads/test/browser/head.js
@@ -19,18 +19,27 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 const nsIDM = Ci.nsIDownloadManager;
 
 var gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]);
 gTestTargetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+// Load mocking/stubbing library, sinon
+// docs: http://sinonjs.org/docs/
+Services.scriptloader.loadSubScript("resource://testing-common/sinon-1.16.1.js");
+
 registerCleanupFunction(function () {
   gTestTargetFile.remove(false);
+
+  delete window.sinon;
+  delete window.setImmediate;
+  delete window.clearImmediate;
 });
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Asynchronous support subroutines
 
 function promiseOpenAndLoadWindow(aOptions)
 {
   return new Promise((resolve, reject) => {
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -57,18 +57,18 @@
 <!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.accesskey          "a">
+<!ENTITY cmd.clearList2.label             "Clear Preview Panel">
+<!ENTITY cmd.clearList2.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">
 <!-- LOCALIZATION NOTE (cmd.removeFile.label):
@@ -104,16 +104,18 @@
 <!-- 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 openDownloadsFolder.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/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -25,50 +25,53 @@
 
 #downloadsListBox {
   background: transparent;
   padding: 4px;
   color: inherit;
 }
 
 #emptyDownloads {
-  padding: 10px 20px;
+  padding: 16px 25px;
+  margin: 0;
   /* 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 var(--panel-separator-color);
 }
 
-.downloadsPanelFooter > toolbarseparator {
+.downloadsPanelFooter toolbarseparator {
   margin: 0;
   border: 0;
   min-width: 0;
   border-left: 1px solid var(--panel-separator-color);
   -moz-appearance: none;
 }
 
 .downloadsPanelFooterButton {
   -moz-appearance: none;
   background-color: transparent;
   color: inherit;
   margin: 0;
   padding: 0;
+  min-width: 0;
   min-height: 40px;
 }
 
 .downloadsPanelFooterButton:hover {
   outline: 1px solid hsla(210,4%,10%,.07);
   background-color: hsla(210,4%,10%,.07);
 }
 
-.downloadsPanelFooterButton:hover:active {
+.downloadsPanelFooterButton:hover:active,
+.downloadsPanelFooterButton[open="true"] {
   outline: 1px solid hsla(210,4%,10%,.12);
   background-color: hsla(210,4%,10%,.12);
   box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
 .downloadsPanelFooterButton[default] {
   background-color: #0996f8;
   color: white;
@@ -77,16 +80,57 @@
 .downloadsPanelFooterButton[default]:hover {
   background-color: #0675d3;
 }
 
 .downloadsPanelFooterButton[default]:hover:active {
   background-color: #0568ba;
 }
 
+#downloadsPanel[hasdownloads] #downloadsHistory {
+  padding-left: 58px !important;
+}
+
+toolbarseparator.downloadsDropmarkerSplitter {
+  margin: 7px 0;
+}
+
+#downloadsFooter:hover toolbarseparator.downloadsDropmarkerSplitter,
+#downloadsFooter toolbarseparator.downloadsDropmarkerSplitterExtend {
+  margin: 0;
+}
+
+.downloadsDropmarker {
+  padding: 0 19px !important;
+}
+
+.downloadsDropmarker > .button-box > hbox {
+  display: none;
+}
+
+.downloadsDropmarker > .button-box > .button-menu-dropmarker {
+  /* This is to override the linux !important */
+  -moz-appearance: none !important;
+  display: -moz-box;
+}
+
+.downloadsDropmarker > .button-box > .button-menu-dropmarker > .dropmarker-icon {
+  width: 16px;
+  height: 16px;
+  list-style-image: url("chrome://browser/skin/downloads/menubutton-dropmarker.svg");
+  filter: url("chrome://browser/skin/filters.svg#fill");
+  fill: currentColor;
+}
+
+/* Override default icon size which is too small for this dropdown */
+.downloadsDropmarker > .button-box > .button-menu-dropmarker {
+  width: 16px;
+  height: 16px;
+}
+
 #downloadsSummary {
   --summary-padding-end: 38px;
   --summary-padding-start: 12px;
   padding: 8px var(--summary-padding-end) 8px var(--summary-padding-start);
   cursor: pointer;
   -moz-user-focus: normal;
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/downloads/menubutton-dropmarker.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="16" height="16" viewBox="0 0 16 16">
+  <path d="m 2,6 6,6 6,-6 -1.5,-1.5 -4.5,4.5 -4.5,-4.5 z" />
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -49,16 +49,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/menubutton-dropmarker.svg     (../shared/downloads/menubutton-dropmarker.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)