Bug 1269962 - Implement a popup menu for showing a submenu in Downloads Panel Footer., r=paolo draft
authorSean Lee <selee@mozilla.com>
Wed, 13 Jul 2016 13:39:00 +0800
changeset 403732 0c8c9d06919f45fbd65ab0aa76ba59db5754bbae
parent 403386 8ef9629d8f90d6507b1bad01146b14101de79174
child 403845 270b1019d4fe89d6ebdec3d4bcf2ef53cb1f48e2
child 404003 efd15d0ff0e71d1e753ccb15769131900ef7b4c9
child 404474 e4ac3e8f28548b7fb88a54e013fe2e5c85660fe4
push id26994
push userbmo:selee@mozilla.com
push dateMon, 22 Aug 2016 02:17:29 +0000
reviewerspaolo
bugs1269962
milestone51.0a1
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)