Bug 1397447 - make downloads button autohide by default, r?mak draft
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 08 Sep 2017 14:16:36 +0100
changeset 663080 cd5ac2d2413a3ff6fd1c0990a350f658efbfdfe1
parent 662980 b0e945eed81db8bf076daf64e381c514f70144f0
child 663081 10f163485600bb7c8e002b34cbfda16fd75635db
push id79307
push userbmo:gijskruitbosch+bugs@gmail.com
push dateTue, 12 Sep 2017 15:18:02 +0000
reviewersmak
bugs1397447
milestone57.0a1
Bug 1397447 - make downloads button autohide by default, r?mak MozReview-Commit-ID: E9izQpa4fFZ
browser/app/profile/firefox.js
browser/base/content/browser-customization.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/browser.xul
browser/base/content/test/general/browser_documentnavigation.js
browser/base/content/test/urlbar/browser_dragdropURL.js
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/test/browser_1042100_default_placements_update.js
browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js
browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js
browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js
browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js
browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js
browser/components/customizableui/test/browser_overflow_use_subviews.js
browser/components/downloads/content/downloads.js
browser/components/downloads/content/indicator.js
browser/components/downloads/test/browser/browser.ini
browser/components/downloads/test/browser/browser_downloads_autohide.js
browser/components/downloads/test/browser/browser_downloads_panel_height.js
browser/components/downloads/test/browser/browser_first_download_panel.js
browser/components/downloads/test/browser/browser_indicatorDrop.js
browser/components/downloads/test/browser/browser_overflow_anchor.js
browser/components/downloads/test/browser/head.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -363,16 +363,20 @@ pref("browser.download.manager.resumeOnW
 
 // This allows disabling the animated notifications shown by
 // the Downloads Indicator when a download starts or completes.
 pref("browser.download.animateNotifications", true);
 
 // This records whether or not the panel has been shown at least once.
 pref("browser.download.panel.shown", false);
 
+// This controls whether the button is automatically shown/hidden depending
+// on whether there are downloads to show.
+pref("browser.download.autohideButton", true);
+
 #ifndef XP_MACOSX
 pref("browser.helperApps.deleteTempFileOnExit", true);
 #endif
 
 // search engines URL
 pref("browser.search.searchEnginesURL",      "https://addons.mozilla.org/%LOCALE%/firefox/search-engines/");
 
 // pointer to the default engine name
--- a/browser/base/content/browser-customization.js
+++ b/browser/base/content/browser-customization.js
@@ -34,17 +34,16 @@ var CustomizationHandler = {
       childNode.setAttribute("disabled", true);
 
     let cmd = document.getElementById("cmd_CustomizeToolbars");
     cmd.setAttribute("disabled", "true");
 
     UpdateUrlbarSearchSplitterState();
 
     PlacesToolbarHelper.customizeStart();
-    DownloadsButton.customizeStart();
   },
 
   _customizationEnding(aDetails) {
     // Update global UI elements that may have been added or removed
     if (aDetails.changed) {
       gURLBar = document.getElementById("urlbar");
 
       gHomeButton.updateTooltip();
@@ -58,17 +57,16 @@ var CustomizationHandler = {
       if (!window.__lookupGetter__("PopupNotifications")) {
         PopupNotifications.iconBox =
           document.getElementById("notification-popup-box");
       }
 
     }
 
     PlacesToolbarHelper.customizeDone();
-    DownloadsButton.customizeDone();
 
     UpdateUrlbarSearchSplitterState();
 
     // Update the urlbar
     URLBarSetURI();
     XULBrowserWindow.asyncUpdateUI();
 
     // Re-enable parts of the UI we disabled during the dialog
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -513,20 +513,19 @@ toolbar:not(#TabsToolbar) > #personal-bo
 
 /* Hide menu elements intended for keyboard access support */
 #main-menubar[openedwithkey=false] .show-only-for-keyboard {
   display: none;
 }
 
 /* ::::: location bar & search bar ::::: */
 
-#urlbar-container {
-  min-width: 50ch;
-}
-
+/* url bar min-width is defined further down, together with the maximum size
+ * of the identity icon block, for different window sizes.
+ */
 #search-container {
   min-width: 25ch;
 }
 
 #urlbar,
 .searchbar-textbox {
   /* Setting a width and min-width to let the location & search bars maintain
      a constant width in case they haven't be resized manually. (bug 965772) */
@@ -696,51 +695,79 @@ html|input.urlbar-input[textoverflow]:no
   -moz-user-focus: normal;
 }
 
 #urlbar[pageproxystate="invalid"] > #identity-box {
   pointer-events: none;
   -moz-user-focus: ignore;
 }
 
+
+/* We leave 49ch plus whatever space the download button will need when it
+ * appears. Normally this should be 16px for the icon, plus 2 * 2px padding
+ * plus the toolbarbutton-inner-padding. We're adding 4px to ensure things
+ * like rounding on hidpi don't accidentally result in the button going
+ * into overflow.
+ */
+#urlbar-container {
+  min-width: calc(49ch + 24px + 2 * var(--toolbarbutton-inner-padding));
+}
+
+#nav-bar[downloadsbuttonshown] #urlbar-container {
+  min-width: 49ch;
+}
+
 #identity-icon-labels {
-  max-width: 18em;
+  max-width: 17em;
 }
 @media (max-width: 700px) {
   #urlbar-container {
-    min-width: 45ch;
+    min-width: calc(44ch + 24px + 2 * var(--toolbarbutton-inner-padding));
   }
+  #nav-bar[downloadsbuttonshown] #urlbar-container {
+    min-width: 44ch;
+  }
+
   #identity-icon-labels {
-    max-width: 70px;
+    max-width: 60px;
   }
 }
 @media (max-width: 600px) {
   #urlbar-container {
-    min-width: 40ch;
-  }
-  #identity-icon-labels {
-    max-width: 60px;
+    min-width: calc(39ch + 24px + 2 * var(--toolbarbutton-inner-padding));
   }
-}
-@media (max-width: 500px) {
-  #urlbar-container {
-    min-width: 35ch;
+  #nav-bar[downloadsbuttonshown] #urlbar-container {
+    min-width: 39ch;
   }
   #identity-icon-labels {
     max-width: 50px;
   }
 }
-@media (max-width: 400px) {
+@media (max-width: 500px) {
   #urlbar-container {
-    min-width: 28ch;
+    min-width: calc(34ch + 24px + 2 * var(--toolbarbutton-inner-padding));
+  }
+  #nav-bar[downloadsbuttonshown] #urlbar-container {
+    min-width: 34ch;
   }
   #identity-icon-labels {
     max-width: 40px;
   }
 }
+@media (max-width: 400px) {
+  #urlbar-container {
+    min-width: calc(27ch + 24px + 2 * var(--toolbarbutton-inner-padding));
+  }
+  #nav-bar[downloadsbuttonshown] #urlbar-container {
+    min-width: 27ch;
+  }
+  #identity-icon-labels {
+    max-width: 30px;
+  }
+}
 
 #identity-icon-country-label {
   direction: ltr;
 }
 
 #identity-box.verifiedIdentity > #identity-icon-labels > #identity-icon-label {
   margin-inline-end: 0.25em !important;
 }
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1316,16 +1316,20 @@ var gBrowserInit = {
     }
 
     // hook up UI through progress listener
     gBrowser.addProgressListener(window.XULBrowserWindow);
     gBrowser.addTabsProgressListener(window.TabsProgressListener);
 
     SidebarUI.init();
 
+    // We do this in onload because we want to ensure the button's state
+    // doesn't flicker as the window is being shown.
+    DownloadsButton.init();
+
     // Certain kinds of automigration rely on this notification to complete
     // their tasks BEFORE the browser window is shown. SessionStore uses it to
     // restore tabs into windows AFTER important parts like gMultiProcessBrowser
     // have been initialized.
     Services.obs.notifyObservers(window, "browser-window-before-show");
 
     gUIDensity.init();
 
@@ -1842,16 +1846,18 @@ var gBrowserInit = {
     CompactTheme.uninit();
 
     TrackingProtection.uninit();
 
     CaptivePortalWatcher.uninit();
 
     SidebarUI.uninit();
 
+    DownloadsButton.uninit();
+
     // Now either cancel delayedStartup, or clean up the services initialized from
     // it.
     if (this._boundDelayedStartup) {
       this._cancelDelayedStartup();
     } else {
       if (Win7Features)
         Win7Features.onCloseWindow();
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -934,17 +934,19 @@
                        class="toolbarbutton-1 chromeclass-toolbar-additional badged-button"
                        key="key_openDownloads"
                        onmousedown="DownloadsIndicatorView.onCommand(event);"
                        ondrop="DownloadsIndicatorView.onDrop(event);"
                        ondragover="DownloadsIndicatorView.onDragOver(event);"
                        ondragenter="DownloadsIndicatorView.onDragOver(event);"
                        label="&downloads.label;"
                        removable="true"
+                       overflows="false"
                        cui-areatype="toolbar"
+                       hidden="true"
                        tooltip="dynamic-shortcut-tooltip"/>
 
         <toolbarbutton id="library-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                        removable="true"
                        onmousedown="PanelUI.showSubView('appMenu-libraryView', this, null, event);"
                        closemenu="none"
                        cui-areatype="toolbar"
                        tooltiptext="&libraryButton.tooltip;"
--- a/browser/base/content/test/general/browser_documentnavigation.js
+++ b/browser/base/content/test/general/browser_documentnavigation.js
@@ -69,17 +69,17 @@ async function expectFocusOnF6(backward,
   }
 
   if (gMultiProcessBrowser && onContent) {
     expectedDocument = "main-window";
     expectedElement = gBrowser.selectedBrowser;
   }
 
   is(fm.focusedWindow.document.documentElement.id, expectedDocument, desc + " document matches");
-  is(fm.focusedElement, expectedElement, desc + " element matches");
+  is(fm.focusedElement, expectedElement, desc + " element matches (wanted: " + expectedElement.id + " got: " + fm.focusedElement.id + ")");
 
   if (onContent) {
     window.messageManager.removeMessageListener("BrowserTest:FocusChanged", focusChangedListener);
   }
 }
 
 // Load a page and navigate between it and the chrome window.
 add_task(async function() {
@@ -166,18 +166,19 @@ add_task(async function() {
                                false, "back focus with sidebar open sidebar");
   await expectFocusOnF6(true, "main-window", gURLBar.inputField,
                                false, "back focus with sidebar urlbar");
 
   SidebarUI.toggle("viewBookmarksSidebar");
 });
 
 // Navigate when the downloads panel is open
-add_task(async function() {
-  await pushPrefs(["accessibility.tabfocus", 7]);
+add_task(async function test_download_focus() {
+  await pushPrefs(["accessibility.tabfocus", 7], ["browser.download.autohideButton", false]);
+  await promiseButtonShown("downloads-button");
 
   let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown", true);
   EventUtils.synthesizeMouseAtCenter(document.getElementById("downloads-button"), { });
   await popupShownPromise;
 
   gURLBar.focus();
   await expectFocusOnF6(false, "main-window", document.getElementById("downloadsHistory"),
                                 false, "focus with downloads panel open panel");
@@ -248,8 +249,17 @@ add_task(async function() {
                                true, "back focus on frameset frame 0");
   await expectFocusOnF6(true, "main-window", gURLBar.inputField,
                                false, "back focus on frameset frame urlbar");
 
   await BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 // XXXndeakin add tests for browsers inside of panels
+
+function promiseButtonShown(id) {
+  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+  return BrowserTestUtils.waitForCondition(() => {
+    let target = document.getElementById(id);
+    let bounds = dwu.getBoundsWithoutFlushing(target);
+    return bounds.width > 0 && bounds.height > 0;
+  }, `Waiting for button ${id} to have non-0 size`);
+}
--- a/browser/base/content/test/urlbar/browser_dragdropURL.js
+++ b/browser/base/content/test/urlbar/browser_dragdropURL.js
@@ -4,35 +4,35 @@ const TEST_URL = "data:text/html,a test 
 const DRAG_URL = "http://www.example.com/";
 const DRAG_FORBIDDEN_URL = "chrome://browser/content/aboutDialog.xul";
 const DRAG_TEXT = "Firefox is awesome";
 const DRAG_WORD = "Firefox";
 
 add_task(async function checkDragURL() {
   await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
     // Have to use something other than the URL bar as a source, so picking the
-    // downloads button somewhat arbitrarily:
-    EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
+    // home button somewhat arbitrarily:
+    EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
                               [[{type: "text/plain", data: DRAG_URL}]], "copy", window);
     is(gURLBar.value, TEST_URL, "URL bar value should not have changed");
     is(gBrowser.selectedBrowser.userTypedValue, null, "Stored URL bar value should not have changed");
   });
 });
 
 add_task(async function checkDragForbiddenURL() {
   await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
-    EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
+    EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
                               [[{type: "text/plain", data: DRAG_FORBIDDEN_URL}]], "copy", window);
     isnot(gURLBar.value, DRAG_FORBIDDEN_URL, "Shouldn't be allowed to drop forbidden URL on URL bar");
   });
 });
 
 add_task(async function checkDragText() {
   await BrowserTestUtils.withNewTab(TEST_URL, function(browser) {
-    EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
+    EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
                               [[{type: "text/plain", data: DRAG_TEXT}]], "copy", window);
     is(gURLBar.value, DRAG_TEXT, "Dragging normal text should replace the URL bar value");
 
-    EventUtils.synthesizeDrop(document.getElementById("downloads-button"), gURLBar,
+    EventUtils.synthesizeDrop(document.getElementById("home-button"), gURLBar,
                               [[{type: "text/plain", data: DRAG_WORD}]], "copy", window);
     is(gURLBar.value, DRAG_WORD, "Dragging a single word should replace the URL bar value");
   });
 });
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -35,16 +35,17 @@ const kNSXUL = "http://www.mozilla.org/k
 const kSpecialWidgetPfx = "customizableui-special-";
 
 const kPrefCustomizationState        = "browser.uiCustomization.state";
 const kPrefCustomizationAutoAdd      = "browser.uiCustomization.autoAdd";
 const kPrefCustomizationDebug        = "browser.uiCustomization.debug";
 const kPrefDrawInTitlebar            = "browser.tabs.drawInTitlebar";
 const kPrefUIDensity                 = "browser.uidensity";
 const kPrefAutoTouchMode             = "browser.touchmode.auto";
+const kPrefAutoHideDownloadsButton   = "browser.download.autohideButton";
 
 const kExpectedWindowURL = "chrome://browser/content/browser.xul";
 
 /**
  * The keys are the handlers that are fired when the event type (the value)
  * is fired on the subview. A widget that provides a subview has the option
  * of providing onViewShowing and onViewHiding event handlers.
  */
@@ -172,17 +173,17 @@ XPCOMUtils.defineLazyGetter(this, "log",
 
 var CustomizableUIInternal = {
   initialize() {
     log.debug("Initializing");
 
     this.addListener(this);
     this._defineBuiltInWidgets();
     this.loadSavedState();
-    this._introduceNewBuiltinWidgets();
+    this._updateForNewVersion();
     this._markObsoleteBuiltinButtonsSeen();
 
     this.registerArea(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, {
       type: CustomizableUI.TYPE_MENU_PANEL,
       defaultPlacements: [],
       anchor: "nav-bar-overflow-button",
     }, true);
 
@@ -257,17 +258,17 @@ var CustomizableUIInternal = {
   },
 
   _defineBuiltInWidgets() {
     for (let widgetDefinition of CustomizableWidgets) {
       this.createBuiltinWidget(widgetDefinition);
     }
   },
 
-  _introduceNewBuiltinWidgets() {
+  _updateForNewVersion() {
     // We should still enter even if gSavedState.currentVersion >= kVersion
     // because the per-widget pref facility is independent of versioning.
     if (!gSavedState) {
       // Flip all the prefs so we don't try to re-introduce later:
       for (let [, widget] of gPalette) {
         if (widget.defaultArea && widget._introducedInVersion === "pref") {
           let prefId = "browser.toolbarbuttons.introduced." + widget.id;
           Services.prefs.setBoolPref(prefId, true);
@@ -2592,25 +2593,27 @@ var CustomizableUIInternal = {
 
   _resetUIState() {
     try {
       gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
       gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
       gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity);
       gUIStateBeforeReset.autoTouchMode = Services.prefs.getBoolPref(kPrefAutoTouchMode);
       gUIStateBeforeReset.currentTheme = LightweightThemeManager.currentTheme;
+      gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref(kPrefAutoHideDownloadsButton);
       gUIStateBeforeReset.newElementCount = gNewElementCount;
     } catch (e) { }
 
     this._resetExtraToolbars();
 
     Services.prefs.clearUserPref(kPrefCustomizationState);
     Services.prefs.clearUserPref(kPrefDrawInTitlebar);
     Services.prefs.clearUserPref(kPrefUIDensity);
     Services.prefs.clearUserPref(kPrefAutoTouchMode);
+    Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton);
     LightweightThemeManager.currentTheme = null;
     gNewElementCount = 0;
     log.debug("State reset");
 
     // Reset placements to make restoring default placements possible.
     gPlacements = new Map();
     gDirtyAreaCache = new Set();
     gSeenWidgets = new Set();
@@ -2669,31 +2672,31 @@ var CustomizableUIInternal = {
    */
   undoReset() {
     if (gUIStateBeforeReset.uiCustomizationState == null ||
         gUIStateBeforeReset.drawInTitlebar == null) {
       return;
     }
     gUndoResetting = true;
 
-    let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
-    let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
-    let currentTheme = gUIStateBeforeReset.currentTheme;
-    let uiDensity = gUIStateBeforeReset.uiDensity;
-    let autoTouchMode = gUIStateBeforeReset.autoTouchMode;
+    const {
+      uiCustomizationState, drawInTitlebar, currentTheme, uiDensity,
+      autoTouchMode, autoHideDownloadsButton,
+    } = gUIStateBeforeReset;
     gNewElementCount = gUIStateBeforeReset.newElementCount;
 
     // Need to clear the previous state before setting the prefs
     // because pref observers may check if there is a previous UI state.
     this._clearPreviousUIState();
 
     Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
     Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
     Services.prefs.setIntPref(kPrefUIDensity, uiDensity);
     Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode);
+    Services.prefs.setBoolPref(kPrefAutoHideDownloadsButton, autoHideDownloadsButton);
     LightweightThemeManager.currentTheme = currentTheme;
     this.loadSavedState();
     // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
     // and we don't need to do anything else here:
     if (gSavedState) {
       for (let areaId of Object.keys(gSavedState.placements)) {
         let placements = gSavedState.placements[areaId];
         gPlacements.set(areaId, placements);
--- a/browser/components/customizableui/test/browser_1042100_default_placements_update.js
+++ b/browser/components/customizableui/test/browser_1042100_default_placements_update.js
@@ -14,17 +14,17 @@ function test() {
 
   let oldState = CustomizableUIBSPass.gSavedState;
   registerCleanupFunction(() => CustomizableUIBSPass.gSavedState = oldState );
 
   is(CustomizableUIBSPass.gFuturePlacements.size, 0,
      "All future placements should be dealt with by now.");
 
   let {CustomizableUIInternal, gFuturePlacements, gPalette} = CustomizableUIBSPass;
-  CustomizableUIInternal._introduceNewBuiltinWidgets();
+  CustomizableUIInternal._updateForNewVersion();
   is(gFuturePlacements.size, 0,
      "No change to future placements initially.");
 
   let currentVersion = CustomizableUIBSPass.kVersion;
 
 
   // Add our widget to the defaults:
   let testWidgetNew = {
@@ -62,17 +62,17 @@ function test() {
   CustomizableUIBSPass.kVersion++;
 
   let hadSavedState = !!CustomizableUIBSPass.gSavedState
   if (!hadSavedState) {
     CustomizableUIBSPass.gSavedState = {currentVersion: CustomizableUIBSPass.kVersion - 1};
   }
 
   // Then call the re-init routine so we re-add the builtin widgets
-  CustomizableUIInternal._introduceNewBuiltinWidgets();
+  CustomizableUIInternal._updateForNewVersion();
   is(gFuturePlacements.size, 1,
      "Should have 1 more future placement");
   let haveNavbarPlacements = gFuturePlacements.has(CustomizableUI.AREA_NAVBAR);
   ok(haveNavbarPlacements, "Should have placements for nav-bar");
   if (haveNavbarPlacements) {
     let placements = [...gFuturePlacements.get(CustomizableUI.AREA_NAVBAR)];
 
     // Ignore widgets that are placed using the pref facility and not the
@@ -101,17 +101,17 @@ function test() {
 
   CustomizableUIBSPass.gSavedState = {
     currentVersion: 6,
     placements: {
       "nav-bar": ["urlbar-container", "bookmarks-menu-button"],
       "PanelUI-contents": ["panic-button", "edit-controls"],
     },
   };
-  CustomizableUIInternal._introduceNewBuiltinWidgets();
+  CustomizableUIInternal._updateForNewVersion();
   let navbarPlacements = CustomizableUIBSPass.gSavedState.placements["nav-bar"];
   let springs = navbarPlacements.filter(id => id.includes("spring"));
   is(springs.length, 2, "Should have 2 toolbarsprings in placements now");
   navbarPlacements = navbarPlacements.filter(id => !id.includes("spring"));
   is(navbarPlacements[0], "back-button", "Back button is in the right place.");
   is(navbarPlacements[1], "forward-button", "Fwd button is in the right place.");
   is(navbarPlacements[2], "stop-reload-button", "Stop/reload button is in the right place.");
   is(navbarPlacements[3], "home-button", "Home button is in the right place.");
--- a/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js
+++ b/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js
@@ -12,17 +12,17 @@ function test() {
      "All future placements should be dealt with by now.");
 
   let {CustomizableUIInternal, gFuturePlacements, gPalette} = CustomizableUIBSPass;
 
   // Force us to have a saved state:
   CustomizableUIInternal.saveState();
   CustomizableUIInternal.loadSavedState();
 
-  CustomizableUIInternal._introduceNewBuiltinWidgets();
+  CustomizableUIInternal._updateForNewVersion();
   is(gFuturePlacements.size, 0,
      "No change to future placements initially.");
 
   // Add our widget to the defaults:
   let testWidgetNew = {
     id: "test-messing-with-default-placements-new-pref",
     label: "Test messing with default placements - pref-based",
     defaultArea: CustomizableUI.AREA_NAVBAR,
@@ -39,17 +39,17 @@ function test() {
 
   // Now adjust default placements for area:
   let navbarArea = CustomizableUIBSPass.gAreas.get(CustomizableUI.AREA_NAVBAR);
   let navbarPlacements = navbarArea.get("defaultPlacements");
   navbarPlacements.splice(navbarPlacements.indexOf("bookmarks-menu-button") + 1, 0, testWidgetNew.id);
 
   let savedPlacements = CustomizableUIBSPass.gSavedState.placements[CustomizableUI.AREA_NAVBAR];
   // Then call the re-init routine so we re-add the builtin widgets
-  CustomizableUIInternal._introduceNewBuiltinWidgets();
+  CustomizableUIInternal._updateForNewVersion();
   is(gFuturePlacements.size, 1,
      "Should have 1 more future placement");
   let futureNavbarPlacements = gFuturePlacements.get(CustomizableUI.AREA_NAVBAR);
   ok(futureNavbarPlacements, "Should have placements for nav-bar");
   if (futureNavbarPlacements) {
     ok(futureNavbarPlacements.has(testWidgetNew.id), "widget should be in future placements");
   }
   CustomizableUIInternal._placeNewDefaultWidgetsInArea(CustomizableUI.AREA_NAVBAR);
--- a/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js
+++ b/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js
@@ -11,27 +11,27 @@ var skippedItem;
 add_task(async function() {
   navbar = document.getElementById("nav-bar");
   skippedItem = document.createElement("toolbarbutton");
   skippedItem.id = "test-skipintoolbarset-item";
   skippedItem.setAttribute("label", "Test");
   skippedItem.setAttribute("skipintoolbarset", "true");
   skippedItem.setAttribute("removable", "true");
   navbar.customizationTarget.appendChild(skippedItem);
-  let downloadsButton = document.getElementById("downloads-button");
+  let libraryButton = document.getElementById("library-button");
   await startCustomizing();
   ok(CustomizableUI.inDefaultState, "Should still be in default state");
-  simulateItemDrag(skippedItem, downloadsButton);
+  simulateItemDrag(skippedItem, libraryButton);
   ok(CustomizableUI.inDefaultState, "Should still be in default state");
   let skippedItemWrapper = skippedItem.parentNode;
   is(skippedItemWrapper.nextSibling && skippedItemWrapper.nextSibling.id,
-     downloadsButton.parentNode.id, "Should be next to downloads button");
-  simulateItemDrag(downloadsButton, skippedItem);
-  let downloadWrapper = downloadsButton.parentNode;
-  is(downloadWrapper.nextSibling && downloadWrapper.nextSibling.id,
+     libraryButton.parentNode.id, "Should be next to library button");
+  simulateItemDrag(libraryButton, skippedItem);
+  let libraryWrapper = libraryButton.parentNode;
+  is(libraryWrapper.nextSibling && libraryWrapper.nextSibling.id,
      skippedItem.parentNode.id, "Should be next to skipintoolbarset item");
   ok(CustomizableUI.inDefaultState, "Should still be in default state");
 });
 
 add_task(async function asyncCleanup() {
   await endCustomizing();
   skippedItem.remove();
   await resetCustomization();
--- a/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js
+++ b/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js
@@ -3,21 +3,21 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // Customize mode reset button should revert correctly
 add_task(async function() {
   await startCustomizing();
   let devButton = document.getElementById("developer-button");
-  let downloadsButton = document.getElementById("downloads-button");
+  let libraryButton = document.getElementById("library-button");
   let homeButton = document.getElementById("home-button");
   let palette = document.getElementById("customization-palette");
-  ok(devButton && downloadsButton && homeButton && palette, "Stuff should exist");
-  simulateItemDrag(devButton, downloadsButton);
+  ok(devButton && libraryButton && homeButton && palette, "Stuff should exist");
+  simulateItemDrag(devButton, libraryButton);
   simulateItemDrag(homeButton, palette);
   await gCustomizeMode.reset();
   ok(CustomizableUI.inDefaultState, "Should be back in default state");
   await endCustomizing();
 });
 
 add_task(async function asyncCleanup() {
   await resetCustomization();
--- a/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js
+++ b/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js
@@ -5,21 +5,21 @@
 "use strict";
 
 const kTestToolbarId = "test-empty-drag";
 
 // Attempting to drag an item to an empty container should work.
 add_task(async function() {
   await createToolbarWithPlacements(kTestToolbarId, []);
   await startCustomizing();
-  let downloadButton = document.getElementById("downloads-button");
+  let libraryButton = document.getElementById("library-button");
   let customToolbar = document.getElementById(kTestToolbarId);
-  simulateItemDrag(downloadButton, customToolbar);
-  assertAreaPlacements(kTestToolbarId, ["downloads-button"]);
-  ok(downloadButton.parentNode && downloadButton.parentNode.parentNode == customToolbar,
+  simulateItemDrag(libraryButton, customToolbar);
+  assertAreaPlacements(kTestToolbarId, ["library-button"]);
+  ok(libraryButton.parentNode && libraryButton.parentNode.parentNode == customToolbar,
      "Button should really be in toolbar");
   await endCustomizing();
   removeCustomToolbars();
 });
 
 add_task(async function asyncCleanup() {
   await endCustomizing();
   await resetCustomization();
--- a/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js
+++ b/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js
@@ -39,18 +39,18 @@ add_task(async function() {
 
   // Make sure we have some hidden items at the end of the nav-bar.
   navbar.insertItem(hidden1.id);
   navbar.insertItem(hidden2.id);
 
   // Drag an item and drop it onto the nav-bar customization target, but
   // not over a particular item.
   await startCustomizing();
-  let downloadsButton = document.getElementById("downloads-button");
-  simulateItemDrag(downloadsButton, navbar.customizationTarget);
+  let homeButton = document.getElementById("home-button");
+  simulateItemDrag(homeButton, navbar.customizationTarget);
 
   await endCustomizing();
 
-  is(downloadsButton.previousSibling.id, lastVisible.id,
+  is(homeButton.previousSibling.id, lastVisible.id,
      "The downloads button should be placed after the last visible item.");
 
   await resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_overflow_use_subviews.js
+++ b/browser/components/customizableui/test/browser_overflow_use_subviews.js
@@ -1,19 +1,21 @@
 "use strict";
 
 const kOverflowPanel = document.getElementById("widget-overflow");
 
 var gOriginalWidth;
-registerCleanupFunction(async function() {
+async function stopOverflowing() {
   kOverflowPanel.removeAttribute("animate");
   window.resizeTo(gOriginalWidth, window.outerHeight);
   await waitForCondition(() => !document.getElementById("nav-bar").hasAttribute("overflowing"));
   CustomizableUI.reset();
-});
+}
+
+registerCleanupFunction(stopOverflowing);
 
 /**
  * This checks that subview-compatible items show up as subviews rather than
  * re-anchored panels. If we ever remove the developer widget, please
  * replace this test with another subview - don't remove it.
  */
 add_task(async function check_developer_subview_in_overflow() {
   kOverflowPanel.setAttribute("animate", "false");
@@ -37,33 +39,35 @@ add_task(async function check_developer_
   let subviewShownPromise = subviewShown(developerView);
   button.click();
   await subviewShownPromise;
   let hasSubviews = !!kOverflowPanel.querySelector("photonpanelmultiview,panelmultiview");
   let expectedPanel = hasSubviews ? kOverflowPanel : document.getElementById("customizationui-widget-panel");
   is(developerView.closest("panel"), expectedPanel, "Should be inside the panel");
   expectedPanel.hidePopup();
   await Promise.resolve(); // wait for popup to hide fully.
+  await stopOverflowing();
 });
 
 /**
  * This checks that non-subview-compatible items still work correctly.
  * Ideally we should make the downloads panel and bookmarks/library item
  * proper subview items, then this test can go away, and potentially we can
  * simplify some of the subview anchoring code.
  */
 add_task(async function check_downloads_panel_in_overflow() {
-  let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
-  ok(navbar.hasAttribute("overflowing"), "Should still be overflowing");
+  let button = document.getElementById("downloads-button");
+  gCustomizeMode.addToPanel(button);
+  await waitForOverflowButtonShown();
+
   let chevron = document.getElementById("nav-bar-overflow-button");
   let shownPanelPromise = promisePanelElementShown(window, kOverflowPanel);
   chevron.click();
   await shownPanelPromise;
 
-  let button = document.getElementById("downloads-button");
   button.click();
   await waitForCondition(() => {
     let panel = document.getElementById("downloadsPanel");
     return panel && panel.state != "closed";
   });
   let downloadsPanel = document.getElementById("downloadsPanel");
   isnot(downloadsPanel.state, "closed", "Should be attempting to show the downloads panel.");
   downloadsPanel.hidePopup();
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -218,16 +218,19 @@ var DownloadsPanel = {
     DownloadsCommon.log("Opening the downloads panel.");
 
     if (this.isPanelShowing) {
       DownloadsCommon.log("Panel is already showing - focusing instead.");
       this._focusPanel();
       return;
     }
 
+    // As a belt-and-suspenders check, ensure the button is not hidden.
+    DownloadsButton.unhide();
+
     this.initialize(() => {
       // Delay displaying the panel because this function will sometimes be
       // called while another window is closing (like the window for selecting
       // whether to save or open the file), and that would cause the panel to
       // close immediately.
       setTimeout(() => this._openPopupIfDataReady(), 0);
     });
 
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -50,99 +50,57 @@ const DownloadsButton = {
    * Returns a reference to the downloads button position placeholder, or null
    * if not available because it has been removed from the toolbars.
    */
   get _placeholder() {
     return document.getElementById("downloads-button");
   },
 
   /**
+   * Indicates whether toolbar customization is in progress.
+   */
+  _customizing: false,
+
+  /**
    * This function is called asynchronously just after window initialization.
    *
    * NOTE: This function should limit the input/output it performs to improve
    *       startup time.
    */
   initializeIndicator() {
     DownloadsIndicatorView.ensureInitialized();
   },
 
   /**
-   * Indicates whether toolbar customization is in progress.
-   */
-  _customizing: false,
-
-  /**
-   * This function is called when toolbar customization starts.
-   *
-   * During customization, we never show the actual download progress indication
-   * or the event notifications, but we show a neutral placeholder.  The neutral
-   * placeholder is an ordinary button defined in the browser window that can be
-   * moved freely between the toolbars and the customization palette.
-   */
-  customizeStart() {
-    // Prevent the indicator from being displayed as a temporary anchor
-    // during customization, even if requested using the getAnchor method.
-    this._customizing = true;
-    this._anchorRequested = false;
-  },
-
-  /**
-   * This function is called when toolbar customization ends.
-   */
-  customizeDone() {
-    this._customizing = false;
-    DownloadsIndicatorView.afterCustomize();
-  },
-
-  /**
    * Determines the position where the indicator should appear, and moves its
    * associated element to the new position.
    *
    * @return Anchor element, or null if the indicator is not visible.
    */
   _getAnchorInternal() {
     let indicator = DownloadsIndicatorView.indicator;
     if (!indicator) {
       // Exit now if the indicator overlay isn't loaded yet, or if the button
       // is not in the document.
       return null;
     }
 
     indicator.open = this._anchorRequested;
 
-    let widget = CustomizableUI.getWidget("downloads-button")
-                               .forWindow(window);
+    let widget = CustomizableUI.getWidget("downloads-button");
      // Determine if the indicator is located on an invisible toolbar.
-     if (!isElementVisible(indicator.parentNode) && !widget.overflowed) {
+     if (!isElementVisible(indicator.parentNode) &&
+         widget.areaType == CustomizableUI.TYPE_TOOLBAR) {
        return null;
      }
 
     return DownloadsIndicatorView.indicatorAnchor;
   },
 
   /**
-   * Checks whether the indicator is, or will soon be visible in the browser
-   * window.
-   *
-   * @param aCallback
-   *        Called once the indicator overlay has loaded. Gets a boolean
-   *        argument representing the indicator visibility.
-   */
-  checkIsVisible(aCallback) {
-    DownloadsOverlayLoader.ensureOverlayLoaded(this.kIndicatorOverlay, () => {
-      if (!this._placeholder) {
-        aCallback(false);
-      } else {
-        let element = DownloadsIndicatorView.indicator || this._placeholder;
-        aCallback(isElementVisible(element.parentNode));
-      }
-    });
-  },
-
-  /**
    * Indicates whether we should try and show the indicator temporarily as an
    * anchor for the panel, even if the indicator would be hidden by default.
    */
   _anchorRequested: false,
 
   /**
    * Ensures that there is an anchor available for the panel.
    *
@@ -167,16 +125,101 @@ const DownloadsButton = {
   /**
    * Allows the temporary anchor to be hidden.
    */
   releaseAnchor() {
     this._anchorRequested = false;
     this._getAnchorInternal();
   },
 
+  unhide() {
+    let button = this._placeholder;
+    if (button && button.hasAttribute("hidden")) {
+      button.removeAttribute("hidden");
+      if (this._navBar.contains(button)) {
+        this._navBar.setAttribute("downloadsbuttonshown", "true");
+      }
+    }
+  },
+
+  hide() {
+    let button = this._placeholder;
+    if (this.autoHideDownloadsButton && button && button.closest("toolbar")) {
+      DownloadsPanel.hidePanel();
+      button.setAttribute("hidden", "true");
+      this._navBar.removeAttribute("downloadsbuttonshown");
+    }
+  },
+
+  startAutoHide() {
+    if (DownloadsIndicatorView.hasDownloads) {
+      this.unhide();
+    } else {
+      this.hide();
+    }
+  },
+
+  checkForAutoHide() {
+    let button = this._placeholder;
+    if (!this._customizing && this.autoHideDownloadsButton &&
+        button && button.closest("toolbar")) {
+      this.startAutoHide();
+    } else {
+      this.unhide();
+    }
+  },
+
+  // Callback from CustomizableUI when nodes get moved around.
+  // We use this to track whether our node has moved somewhere
+  // where we should (not) autohide it.
+  onWidgetAfterDOMChange(node) {
+    if (node == this._placeholder) {
+      this.checkForAutoHide();
+    }
+  },
+
+  /**
+   * This function is called when toolbar customization starts.
+   *
+   * During customization, we never show the actual download progress indication
+   * or the event notifications, but we show a neutral placeholder.  The neutral
+   * placeholder is an ordinary button defined in the browser window that can be
+   * moved freely between the toolbars and the customization palette.
+   */
+  onCustomizeStart(win) {
+    if (win == window) {
+      // Prevent the indicator from being displayed as a temporary anchor
+      // during customization, even if requested using the getAnchor method.
+      this._customizing = true;
+      this._anchorRequested = false;
+      this.unhide();
+    }
+  },
+
+  onCustomizeEnd(win) {
+    if (win == window) {
+      this._customizing = false;
+      this.checkForAutoHide();
+      DownloadsIndicatorView.afterCustomize();
+    }
+  },
+
+  init() {
+    XPCOMUtils.defineLazyPreferenceGetter(
+      this, "autoHideDownloadsButton", "browser.download.autohideButton",
+      true, this.checkForAutoHide.bind(this));
+
+    CustomizableUI.addListener(this);
+    this.checkForAutoHide();
+  },
+
+  uninit() {
+    CustomizableUI.removeListener(this);
+  },
+
   get _tabsToolbar() {
     delete this._tabsToolbar;
     return this._tabsToolbar = document.getElementById("TabsToolbar");
   },
 
   get _navBar() {
     delete this._navBar;
     return this._navBar = document.getElementById("nav-bar");
@@ -337,17 +380,17 @@ const DownloadsIndicatorView = {
     // No need to show visual notification if the panel is visible.
     if (DownloadsPanel.isPanelShowing) {
       return;
     }
 
     let anchor = DownloadsButton._placeholder;
     let widgetGroup = CustomizableUI.getWidget("downloads-button");
     let widget = widgetGroup.forWindow(window);
-    if (widget.overflowed || widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+    if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
       if (anchor && this._isAncestorPanelOpen(anchor)) {
         // If the containing panel is open, don't do anything, because the
         // notification would appear under the open panel. See
         // https://bugzilla.mozilla.org/show_bug.cgi?id=984023
         return;
       }
 
       // Otherwise, try to use the anchor of the panel:
@@ -418,18 +461,22 @@ const DownloadsIndicatorView = {
    * Indicates whether the indicator should be shown because there are some
    * downloads to be displayed.
    */
   set hasDownloads(aValue) {
     if (this._hasDownloads != aValue || (!this._operational && aValue)) {
       this._hasDownloads = aValue;
 
       // If there is at least one download, ensure that the view elements are
+      // operational
       if (aValue) {
+        DownloadsButton.unhide();
         this._ensureOperational();
+      } else {
+        DownloadsButton.checkForAutoHide();
       }
     }
     return aValue;
   },
   get hasDownloads() {
     return this._hasDownloads;
   },
   _hasDownloads: false,
@@ -503,24 +550,17 @@ const DownloadsIndicatorView = {
     DownloadsIndicatorView.ensureTerminated();
   },
 
   onCommand(aEvent) {
     if (aEvent.type == "mousedown" && aEvent.button != 0) {
       return;
     }
 
-    // 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 {
-      DownloadsPanel.showPanel();
-    }
-
+    DownloadsPanel.showPanel();
     aEvent.stopPropagation();
   },
 
   onDragOver(aEvent) {
     browserDragAndDrop.dragOver(aEvent);
   },
 
   onDrop(aEvent) {
@@ -562,20 +602,19 @@ const DownloadsIndicatorView = {
     if (!indicator || indicator.getAttribute("indicator") != "true") {
       return null;
     }
 
     return this._indicator = indicator;
   },
 
   get indicatorAnchor() {
-    let widget = CustomizableUI.getWidget("downloads-button")
-                               .forWindow(window);
-    if (widget.overflowed) {
-      return widget.anchor;
+    let widgetGroup = CustomizableUI.getWidget("downloads-button");
+    if (widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL) {
+      return widgetGroup.forWindow(window).anchor;
     }
     return document.getElementById("downloads-indicator-anchor");
   },
 
   get _progressIcon() {
     return this.__progressIcon ||
       (this.__progressIcon = document.getElementById("downloads-indicator-progress-inner"));
   },
--- a/browser/components/downloads/test/browser/browser.ini
+++ b/browser/components/downloads/test/browser/browser.ini
@@ -8,8 +8,9 @@ skip-if = os == "linux" # Bug 949434
 skip-if = os == "linux" # Bug 952422
 [browser_confirm_unblock_download.js]
 [browser_iframe_gone_mid_download.js]
 [browser_indicatorDrop.js]
 [browser_libraryDrop.js]
 [browser_downloads_panel_block.js]
 skip-if = true # Bug 1352792
 [browser_downloads_panel_height.js]
+[browser_downloads_autohide.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/downloads/test/browser/browser_downloads_autohide.js
@@ -0,0 +1,252 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kDownloadAutoHidePref = "browser.download.autohideButton";
+
+registerCleanupFunction(async function() {
+  Services.prefs.clearUserPref(kDownloadAutoHidePref);
+  if (document.documentElement.hasAttribute("customizing")) {
+    await gCustomizeMode.reset();
+    await promiseCustomizeEnd();
+  } else {
+    CustomizableUI.reset();
+  }
+});
+
+add_task(async function checkStateDuringPrefFlips() {
+  ok(Services.prefs.getBoolPref(kDownloadAutoHidePref),
+     "Should be autohiding the button by default");
+  ok(!DownloadsIndicatorView.hasDownloads,
+     "Should be no downloads when starting the test");
+  let downloadsButton = document.getElementById("downloads-button");
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden in the toolbar");
+  gCustomizeMode.addToPanel(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button shouldn't be hidden in the panel");
+  gCustomizeMode.addToToolbar(downloadsButton);
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden again in the toolbar");
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button shouldn't be hidden with autohide turned off");
+  gCustomizeMode.addToPanel(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button shouldn't be hidden with autohide turned off " +
+     "after moving it to the panel");
+  gCustomizeMode.addToToolbar(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button shouldn't be hidden with autohide turned off " +
+     "after moving it back to the toolbar");
+  gCustomizeMode.addToPanel(downloadsButton);
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still not be hidden with autohide turned back on " +
+     "because it's in the panel");
+  gCustomizeMode.addToToolbar(downloadsButton);
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden again in the toolbar");
+  gCustomizeMode.removeFromArea(downloadsButton);
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+  // Can't use gCustomizeMode.addToToolbar here because it doesn't work for
+  // palette items if the window isn't in customize mode:
+  CustomizableUI.addWidgetToArea(downloadsButton.id, CustomizableUI.AREA_NAVBAR);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be unhidden again in the toolbar " +
+     "even if the pref was flipped while the button was in the palette");
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+});
+
+add_task(async function checkStateInCustomizeMode() {
+  ok(Services.prefs.getBoolPref("browser.download.autohideButton"),
+     "Should be autohiding the button");
+  let downloadsButton = document.getElementById("downloads-button");
+  await promiseCustomizeStart();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode.");
+  gCustomizeMode.addToPanel(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode when moved to the panel");
+  gCustomizeMode.addToToolbar(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode when moved back to the toolbar");
+  gCustomizeMode.removeFromArea(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode when in the palette");
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode " +
+     "even when flipping the autohide pref");
+  gCustomizeMode.addToPanel(downloadsButton);
+  await promiseCustomizeEnd();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown after customize mode when moved to the panel");
+  await promiseCustomizeStart();
+  gCustomizeMode.addToToolbar(downloadsButton);
+  await promiseCustomizeEnd();
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden if it's in the toolbar after customize mode.");
+  await promiseCustomizeStart();
+  await gCustomizeMode.reset();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the toolbar in customize mode after a reset.");
+  await gCustomizeMode.undoReset();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the toolbar in customize mode " +
+     "when undoing the reset.");
+  gCustomizeMode.addToPanel(downloadsButton);
+  await gCustomizeMode.reset();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the toolbar in customize mode " +
+     "after a reset moved it.");
+  await gCustomizeMode.undoReset();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the panel in customize mode " +
+     "when undoing the reset.");
+  await gCustomizeMode.reset();
+  await promiseCustomizeEnd();
+});
+
+add_task(async function checkStateInCustomizeModeMultipleWindows() {
+  ok(Services.prefs.getBoolPref("browser.download.autohideButton"),
+     "Should be autohiding the button");
+  let downloadsButton = document.getElementById("downloads-button");
+  await promiseCustomizeStart();
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode.");
+  let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+  let otherDownloadsButton = otherWin.document.getElementById("downloads-button");
+  ok(otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be hidden in the other window.");
+
+  gCustomizeMode.addToPanel(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be shown in customize mode.");
+  ok(!otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the other window too because it's in a panel.");
+
+  gCustomizeMode.addToToolbar(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be shown in customize mode.");
+  ok(otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be hidden again in the other window.");
+
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode");
+  ok(!otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the other window with the pref flipped");
+
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be shown in customize mode " +
+     "even when flipping the autohide pref");
+  ok(otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be hidden in the other window with the pref flipped again");
+
+  gCustomizeMode.addToPanel(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be shown in customize mode.");
+  ok(!otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be shown in the other window too because it's in a panel.");
+
+  gCustomizeMode.removeFromArea(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be shown in customize mode.");
+  // Don't need to assert in the other window - button is gone there.
+
+  await gCustomizeMode.reset();
+  ok(Services.prefs.getBoolPref(kDownloadAutoHidePref),
+     "Autohide pref reset by reset()");
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be shown in customize mode.");
+  ok(otherDownloadsButton.hasAttribute("hidden"),
+     "Button should be hidden in the other window.");
+  ok(otherDownloadsButton.closest("#nav-bar"),
+     "Button should be back in the nav bar in the other window.");
+
+  await promiseCustomizeEnd();
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden again outside of customize mode");
+  await BrowserTestUtils.closeWindow(otherWin);
+});
+
+add_task(async function checkStateForDownloads() {
+  ok(Services.prefs.getBoolPref("browser.download.autohideButton"),
+     "Should be autohiding the button");
+  let downloadsButton = document.getElementById("downloads-button");
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden when there are no downloads.");
+
+  await task_addDownloads([
+    { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
+  ]);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be unhidden when there are downloads.");
+  let publicList = await Downloads.getList(Downloads.PUBLIC);
+  let downloads = await publicList.getAll();
+  for (let download of downloads) {
+    publicList.remove(download);
+  }
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden when the download is removed");
+  await task_addDownloads([
+    { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
+  ]);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should be unhidden when there are downloads.");
+
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be unhidden.");
+
+  downloads = await publicList.getAll();
+  for (let download of downloads) {
+    publicList.remove(download);
+  }
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still be unhidden because the pref was flipped.");
+  Services.prefs.setBoolPref(kDownloadAutoHidePref, true);
+  ok(downloadsButton.hasAttribute("hidden"),
+     "Button should be hidden now that the pref flipped back " +
+     "because there were already no downloads.");
+
+  gCustomizeMode.addToPanel(downloadsButton);
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should not be hidden in the panel.");
+
+  await task_addDownloads([
+    { state: DownloadsCommon.DOWNLOAD_DOWNLOADING },
+  ]);
+
+  downloads = await publicList.getAll();
+  for (let download of downloads) {
+    publicList.remove(download);
+  }
+
+  ok(!downloadsButton.hasAttribute("hidden"),
+     "Button should still not be hidden in the panel " +
+     "when downloads count reaches 0 after being non-0.");
+});
+
+function promiseCustomizeStart(aWindow = window) {
+  return new Promise(resolve => {
+    aWindow.gNavToolbox.addEventListener("customizationready", resolve,
+                                         {once: true});
+    aWindow.gCustomizeMode.enter();
+  });
+}
+
+function promiseCustomizeEnd(aWindow = window) {
+  return new Promise(resolve => {
+    aWindow.gNavToolbox.addEventListener("aftercustomization", resolve,
+                                         {once: true});
+    aWindow.gCustomizeMode.exit();
+  });
+}
+
--- a/browser/components/downloads/test/browser/browser_downloads_panel_height.js
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_height.js
@@ -4,16 +4,18 @@
 "use strict";
 
 /**
  * This test exists because we use a <panelmultiview> element and it handles
  * some of the height changes for us. We need to verify that the height is
  * updated correctly if downloads are removed while the panel is hidden.
  */
 add_task(async function test_height_reduced_after_removal() {
+  await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
+  await promiseButtonShown("downloads-button");
   await task_addDownloads([
     { state: DownloadsCommon.DOWNLOAD_FINISHED },
   ]);
 
   await task_openPanel();
   let panel = document.getElementById("downloadsPanel");
   let heightBeforeRemoval = panel.getBoundingClientRect().height;
 
--- a/browser/components/downloads/test/browser/browser_first_download_panel.js
+++ b/browser/components/downloads/test/browser/browser_first_download_panel.js
@@ -5,16 +5,18 @@
 /* eslint-disable mozilla/no-arbitrary-setTimeout */
 
 /**
  * Make sure the downloads panel only opens automatically on the first
  * download it notices. All subsequent downloads, even across sessions, should
  * not open the panel automatically.
  */
 add_task(async function test_first_download_panel() {
+  await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
+  await promiseButtonShown("downloads-button");
   // Clear the download panel has shown preference first as this test is used to
   // verify this preference's behaviour.
   let oldPrefValue = Services.prefs.getBoolPref("browser.download.panel.shown");
   Services.prefs.setBoolPref("browser.download.panel.shown", false);
 
   registerCleanupFunction(async function() {
     // Clean up when the test finishes.
     await task_resetState();
@@ -27,16 +29,17 @@ add_task(async function test_first_downl
 
   // Ensure that state is reset in case previous tests didn't finish.
   await task_resetState();
 
   // With this set to false, we should automatically open the panel the first
   // time a download is started.
   DownloadsCommon.getData(window).panelHasShownBefore = false;
 
+  info("waiting for panel open");
   let promise = promisePanelOpened();
   DownloadsCommon.getData(window)._notifyDownloadEvent("start");
   await promise;
 
   // If we got here, that means the panel opened.
   DownloadsPanel.hidePanel();
 
   ok(DownloadsCommon.getData(window).panelHasShownBefore,
--- a/browser/components/downloads/test/browser/browser_indicatorDrop.js
+++ b/browser/components/downloads/test/browser/browser_indicatorDrop.js
@@ -7,18 +7,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
   "resource://testing-common/httpd.js");
 
 registerCleanupFunction(async function() {
   await task_resetState();
   await PlacesUtils.history.clear();
 });
 
 add_task(async function test_indicatorDrop() {
+  await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
   let downloadButton = document.getElementById("downloads-button");
   ok(downloadButton, "download button present");
+  await promiseButtonShown(downloadButton.id);
 
   let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
       getService(Ci.mozIJSSubScriptLoader);
   let EventUtils = {};
   scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
 
   async function task_drop(urls) {
     let dragData = [[{type: "text/plain", data: urls.join("\n")}]];
--- a/browser/components/downloads/test/browser/browser_overflow_anchor.js
+++ b/browser/components/downloads/test/browser/browser_overflow_anchor.js
@@ -10,106 +10,43 @@ registerCleanupFunction(async function()
 });
 
 /**
  * Make sure the downloads button and indicator overflows into the nav-bar
  * chevron properly, and then when those buttons are clicked in the overflow
  * panel that the downloads panel anchors to the chevron.
  */
 add_task(async function test_overflow_anchor() {
+  await SpecialPowers.pushPrefEnv({set: [["browser.download.autohideButton", false]]});
   // Ensure that state is reset in case previous tests didn't finish.
   await task_resetState();
 
-  // Record the original width of the window so we can put it back when
-  // this test finishes.
-  let oldWidth = window.outerWidth;
-
   // The downloads button should not be overflowed to begin with.
   let button = CustomizableUI.getWidget("downloads-button")
                              .forWindow(window);
   ok(!button.overflowed, "Downloads button should not be overflowed.");
+  is(button.node.getAttribute("cui-areatype"), "toolbar", "Button should know it's in the toolbar");
 
-  // Hack - we lock the size of the default flex-y items in the nav-bar, namely,
-  // the URL input. That way we can resize the window without worrying about it
-  // flexing.
-  const kFlexyItems = ["urlbar-container"];
-  registerCleanupFunction(() => unlockWidth(kFlexyItems));
-  lockWidth(kFlexyItems);
-
-  window.resizeTo(kForceOverflowWidthPx, window.outerHeight);
-  await waitForOverflowed(button, true);
+  gCustomizeMode.addToPanel(button.node);
 
   let promise = promisePanelOpened();
   EventUtils.sendMouseEvent({ type: "mousedown", button: 0 }, button.node);
+  info("waiting for panel to open");
   await promise;
 
   let panel = DownloadsPanel.panel;
   let chevron = document.getElementById("nav-bar-overflow-button");
   is(panel.anchorNode, chevron, "Panel should be anchored to the chevron.");
 
   DownloadsPanel.hidePanel();
 
-  // Unlock the widths on the flex-y items.
-  unlockWidth(kFlexyItems);
-
-  // Put the window back to its original dimensions.
-  window.resizeTo(oldWidth, window.outerHeight);
-
-  // The downloads button should eventually be un-overflowed.
-  await waitForOverflowed(button, false);
+  gCustomizeMode.addToToolbar(button.node);
 
   // Now try opening the panel again.
   promise = promisePanelOpened();
   EventUtils.sendMouseEvent({ type: "mousedown", button: 0 }, button.node);
   await promise;
 
   is(panel.anchorNode.id, "downloads-indicator-anchor");
 
   DownloadsPanel.hidePanel();
 });
 
-/**
- * For some node IDs, finds the nodes and sets their min-width's to their
- * current width, preventing them from flex-shrinking.
- *
- * @param aItemIDs an array of item IDs to set min-width on.
- */
-function lockWidth(aItemIDs) {
-  for (let itemID of aItemIDs) {
-    let item = document.getElementById(itemID);
-    let curWidth = item.getBoundingClientRect().width + "px";
-    item.style.minWidth = curWidth;
-  }
-}
-
-/**
- * Clears the min-width's set on a set of IDs by lockWidth.
- *
- * @param aItemIDs an array of ItemIDs to remove min-width on.
- */
-function unlockWidth(aItemIDs) {
-  for (let itemID of aItemIDs) {
-    let item = document.getElementById(itemID);
-    item.style.minWidth = "";
-  }
-}
-
-/**
- * Waits for a node to enter or exit the overflowed state.
- *
- * @param aItem the node to wait for.
- * @param aIsOverflowed if we're waiting for the item to be overflowed.
- */
-function waitForOverflowed(aItem, aIsOverflowed) {
-  if (aItem.overflowed == aIsOverflowed) {
-    return Promise.resolve();
-  }
-
-  return new Promise(resolve => {
-    let observer = new MutationObserver(function(aMutations) {
-      if (aItem.overflowed == aIsOverflowed) {
-        observer.disconnect();
-        resolve();
-      }
-    });
-    observer.observe(aItem.node, {attributes: true});
-  });
-}
--- a/browser/components/downloads/test/browser/head.js
+++ b/browser/components/downloads/test/browser/head.js
@@ -193,8 +193,20 @@ function promiseAlertDialogOpen(buttonAc
             doc.getButton(buttonAction).click();
             resolve();
           }
         }, {once: true});
       }
     });
   });
 }
+
+/**
+ * Waits for a given button to become visible.
+ */
+function promiseButtonShown(id) {
+  let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+  return BrowserTestUtils.waitForCondition(() => {
+    let target = document.getElementById(id);
+    let bounds = dwu.getBoundsWithoutFlushing(target);
+    return bounds.width > 0 && bounds.height > 0;
+  }, `Waiting for button ${id} to have non-0 size`);
+}