Bug 1522012 - Implement Touch Bar's native customization window and remove ui.touchbar.layout preference. r=spohl,mikedeboer,fluent-reviewers,Pike
☠☠ backed out by 2f763c73129d ☠ ☠
authorharry <htwyford@mozilla.com>
Thu, 04 Jul 2019 03:57:47 +0000
changeset 544089 3e2fe70b181ad24fac1251077b93e1e8040785cf
parent 544088 e41e92ec3f2f966939c30aa5024216676376c5dd
child 544090 91ec7cffedbaa59b3a1856da1401f307f94e0155
push id2131
push userffxbld-merge
push dateMon, 26 Aug 2019 18:30:20 +0000
treeherdermozilla-release@b19ffb3ca153 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspohl, mikedeboer, fluent-reviewers, Pike
bugs1522012
milestone69.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 1522012 - Implement Touch Bar's native customization window and remove ui.touchbar.layout preference. r=spohl,mikedeboer,fluent-reviewers,Pike This patch also fixes the Home and Sidebar Touch Bar buttons, since using them after customizing showed that they no longer worked. Differential Revision: https://phabricator.services.mozilla.com/D35085
browser/base/content/browser-menubar.inc
browser/components/customizableui/CustomizeMode.jsm
browser/components/customizableui/content/customizeMode.inc.xul
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_touchbar_customization.js
browser/components/touchbar/MacTouchBar.js
browser/components/touchbar/tests/browser/browser_touchbar_tests.js
browser/locales/en-US/browser/customizeMode.ftl
browser/locales/en-US/browser/touchbar/touchbar.ftl
browser/locales/en-US/chrome/browser/baseMenuOverlay.dtd
modules/libpref/init/all.js
widget/cocoa/nsCocoaWindow.mm
widget/cocoa/nsMenuBarX.mm
widget/cocoa/nsMenuBaseX.h
widget/cocoa/nsTouchBar.h
widget/cocoa/nsTouchBar.mm
widget/cocoa/nsTouchBarUpdater.mm
widget/nsITouchBarHelper.idl
widget/nsITouchBarUpdater.idl
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -527,16 +527,17 @@
 #endif
 #ifdef XP_MACOSX
 <!-- nsMenuBarX hides these and uses them to build the Application menu. -->
               <menuitem id="menu_preferences" label="&preferencesCmdMac.label;" key="key_preferencesCmdMac" oncommand="openPreferences(undefined);"/>
               <menuitem id="menu_mac_services" label="&servicesMenuMac.label;"/>
               <menuitem id="menu_mac_hide_app" label="&hideThisAppCmdMac2.label;" key="key_hideThisAppCmdMac"/>
               <menuitem id="menu_mac_hide_others" label="&hideOtherAppsCmdMac.label;" key="key_hideOtherAppsCmdMac"/>
               <menuitem id="menu_mac_show_all" label="&showAllAppsCmdMac.label;"/>
+              <menuitem id="menu_mac_touch_bar" label="&touchBarCmdMac.label;"/>
 #endif
               </menupopup>
             </menu>
 #ifdef XP_MACOSX
             <menu id="windowMenu"
                   label="&windowMenu.label;"
                   onpopupshowing="macWindowMenuDidShow();"
                   onpopuphidden="macWindowMenuDidHide();"
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -37,16 +37,19 @@ ChromeUtils.defineModuleGetter(this, "Dr
 ChromeUtils.defineModuleGetter(this, "BrowserUtils",
                                "resource://gre/modules/BrowserUtils.jsm");
 ChromeUtils.defineModuleGetter(this, "SessionStore",
                                "resource:///modules/sessionstore/SessionStore.jsm");
 XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
   const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
   return Services.strings.createBundle(kUrl);
 });
+XPCOMUtils.defineLazyServiceGetter(this, "gTouchBarUpdater",
+                                   "@mozilla.org/widget/touchbarupdater;1",
+                                   "nsITouchBarUpdater");
 XPCOMUtils.defineLazyPreferenceGetter(this, "gCosmeticAnimationsEnabled",
                                       "toolkit.cosmeticAnimations.enabled");
 
 let gDebug;
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let scope = {};
   ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
   gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
@@ -332,16 +335,17 @@ CustomizeMode.prototype = {
       this.populatePalette();
 
       this._setupPaletteDragging();
 
       window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
 
       this._updateResetButton();
       this._updateUndoResetButton();
+      this._updateTouchBarButton();
 
       this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
                                   Services.prefs.getBoolPref(kSkipSourceNodePref);
 
       CustomizableUI.addListener(this);
       this._customizing = true;
       this._transitioning = false;
 
@@ -1434,16 +1438,28 @@ CustomizeMode.prototype = {
     btn.disabled = CustomizableUI.inDefaultState;
   },
 
   _updateUndoResetButton() {
     let undoResetButton =  this.$("customization-undo-reset-button");
     undoResetButton.hidden = !CustomizableUI.canUndoReset;
   },
 
+  _updateTouchBarButton() {
+    if (AppConstants.platform != "macosx") {
+      return;
+    }
+    let touchBarButton = this.$("customization-touchbar-button");
+    let touchBarSpacer = this.$("customization-touchbar-spacer");
+
+    let isTouchBarInitialized = gTouchBarUpdater.isTouchBarInitialized();
+    touchBarButton.hidden = !isTouchBarInitialized;
+    touchBarSpacer.hidden = !isTouchBarInitialized;
+  },
+
   handleEvent(aEvent) {
     switch (aEvent.type) {
       case "toolbarvisibilitychange":
         this._onToolbarVisibilityChange(aEvent);
         break;
       case "dragstart":
         this._onDragStart(aEvent);
         break;
@@ -2377,16 +2393,22 @@ CustomizeMode.prototype = {
 
   onDownloadsAutoHideChange(event) {
     let checkbox = event.target.ownerDocument.getElementById(kDownloadAutohideCheckboxId);
     Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked);
     // Ensure we move the button (back) after the user leaves customize mode.
     event.view.gCustomizeMode._moveDownloadsButtonToNavBar = checkbox.checked;
   },
 
+  customizeTouchBar() {
+    let updater = Cc["@mozilla.org/widget/touchbarupdater;1"]
+                        .getService(Ci.nsITouchBarUpdater);
+    updater.enterCustomizeMode();
+  },
+
   togglePong(enabled) {
     // It's possible we're toggling for a reason other than hitting
     // the button (we might be exiting, for example), so make sure that
     // the state and checkbox are in sync.
     let whimsyButton = this.$("whimsy-button");
     whimsyButton.checked = enabled;
 
     if (enabled) {
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -106,16 +106,24 @@
 
 <button id="whimsy-button"
         type="checkbox"
         class="customizationmode-button"
         oncommand="gCustomizeMode.togglePong(this.checked);"
         hidden="true"/>
 
 <spacer id="customization-footer-spacer"/>
+#ifdef XP_MACOSX
+    <button id="customization-touchbar-button"
+        class="customizationmode-button"
+        hidden="true"
+        oncommand="gCustomizeMode.customizeTouchBar();"
+        data-l10n-id="customize-mode-touchbar-cmd"/>
+    <spacer hidden="true" id="customization-touchbar-spacer"/>
+#endif
 <button id="customization-undo-reset-button"
         class="customizationmode-button"
         hidden="true"
         oncommand="gCustomizeMode.undoReset();"
         data-l10n-id="customize-mode-undo-cmd"/>
 <button id="customization-reset-button"
         oncommand="gCustomizeMode.reset();"
         data-l10n-id="customize-mode-restore-defaults"
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -177,14 +177,16 @@ tags = clipboard
 [browser_PanelMultiView_focus.js]
 [browser_PanelMultiView_keyboard.js]
 [browser_reload_tab.js]
 [browser_sidebar_toggle.js]
 skip-if = verify
 [browser_tabbar_big_widgets.js]
 [browser_remote_tabs_button.js]
 skip-if = (verify && debug && (os == 'linux' || os == 'mac'))
+[browser_touchbar_customization.js]
+skip-if = (os == "linux" || os == "win")
 [browser_widget_animation.js]
 [browser_lwt_telemetry.js]
 
 # Unit tests for the PanelMultiView module. These are independent from
 # CustomizableUI, but are located here together with the module they're testing.
 [browser_PanelMultiView.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_touchbar_customization.js
@@ -0,0 +1,20 @@
+/* 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/. */
+
+"use strict";
+
+// Checks if the Customize Touch Bar button appears when a Touch Bar is
+// initialized.
+add_task(async function customizeTouchBarButtonAppears() {
+  let updater = Cc["@mozilla.org/widget/touchbarupdater;1"]
+                  .getService(Ci.nsITouchBarUpdater);
+  // This value will be reset to its default the next time a window is opened.
+  updater.setTouchBarInitialized(true);
+  await startCustomizing();
+  let touchbarButton = document.querySelector("#customization-touchbar-button");
+  ok(!touchbarButton.hidden, "Customize Touch Bar button is not hidden.");
+  let touchbarSpacer = document.querySelector("#customization-touchbar-spacer");
+  ok(!touchbarSpacer.hidden, "Customize Touch Bar spacer is not hidden.");
+  await endCustomizing();
+});
--- a/browser/components/touchbar/MacTouchBar.js
+++ b/browser/components/touchbar/MacTouchBar.js
@@ -68,17 +68,22 @@ const kBuiltInInputs = {
     image: "refresh.pdf",
     type: "button",
     callback: () => execCommand("Browser:Reload", "Reload"),
   },
   Home: {
     title: "home",
     image: "home.pdf",
     type: "button",
-    callback: () => execCommand("Browser:Home", "Home"),
+    callback: () => {
+      let win = BrowserWindowTracker.getTopWindow();
+      win.BrowserHome();
+      let telemetry = Services.telemetry.getHistogramById("TOUCHBAR_BUTTON_PRESSES");
+      telemetry.add("Home");
+    },
   },
   Fullscreen: {
     title: "fullscreen",
     image: "fullscreen.pdf",
     type: "button",
     callback: () => execCommand("View:FullScreen", "Fullscreen"),
   },
   Find: {
@@ -89,20 +94,25 @@ const kBuiltInInputs = {
   },
   NewTab: {
     title: "new-tab",
     image: "new.pdf",
     type: "button",
     callback: () => execCommand("cmd_newNavigatorTabNoEvent", "NewTab"),
   },
   Sidebar: {
-    title: "open-bookmarks-sidebar",
+    title: "open-sidebar",
     image: "sidebar-left.pdf",
     type: "button",
-    callback: () => execCommand("viewBookmarksSidebar", "Sidebar"),
+    callback: () => {
+      let win = BrowserWindowTracker.getTopWindow();
+      win.SidebarUI.toggle();
+      let telemetry = Services.telemetry.getHistogramById("TOUCHBAR_BUTTON_PRESSES");
+      telemetry.add("Sidebar");
+    },
   },
   AddBookmark: {
     title: "add-bookmark",
     image: "bookmark.pdf",
     type: "button",
     callback: () => execCommand("Browser:AddBookmarkAs", "AddBookmark"),
   },
   ReaderView: {
@@ -136,19 +146,16 @@ const kHelperObservers = new Set(["bookm
  * JS-implemented TouchBarHelper class.
  * Provides services to the Mac Touch Bar.
  */
 class TouchBarHelper {
   constructor() {
     for (let topic of kHelperObservers) {
       Services.obs.addObserver(this, topic);
     }
-
-    XPCOMUtils.defineLazyPreferenceGetter(this, "_touchBarLayout",
-      "ui.touchbar.layout", "Back,Forward,Reload,OpenLocation,NewTab,Share");
   }
 
   destructor() {
     for (let topic of kHelperObservers) {
       Services.obs.removeObserver(this, topic);
     }
   }
 
@@ -156,21 +163,19 @@ class TouchBarHelper {
     let tabbrowser = this.window.ownerGlobal.gBrowser;
     let activeTitle;
     if (tabbrowser) {
       activeTitle = tabbrowser.selectedBrowser.contentTitle;
     }
     return activeTitle;
   }
 
-  get layout() {
-    let prefArray = this.storedLayout;
+  get allItems() {
     let layoutItems = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
-
-    for (let inputName of prefArray) {
+    for (let inputName of Object.keys(kBuiltInInputs)) {
       if (typeof kBuiltInInputs[inputName].context == "function") {
         inputName = kBuiltInInputs[inputName].context();
       }
       let input = this.getTouchBarInput(inputName);
       if (!input) {
         continue;
       }
       layoutItems.appendElement(input);
@@ -182,33 +187,16 @@ class TouchBarHelper {
   get window() {
     return BrowserWindowTracker.getTopWindow();
   }
 
   get bookmarkingUI() {
     return this.window.BookmarkingUI;
   }
 
-  /**
-   * Returns a string array of the user's Touch Bar layout preference.
-   * Set by a pref ui.touchbar.layout and returned in an array.
-   */
-  get storedLayout() {
-    let prefArray = this._touchBarLayout.split(",");
-    prefArray = prefArray.map(str => str.trim());
-    // Remove duplicates.
-    prefArray = Array.from(new Set(prefArray));
-
-    // Remove unimplemented/mispelled inputs.
-    prefArray = prefArray.filter(input =>
-      Object.keys(kBuiltInInputs).includes(input));
-    this._storedLayout = prefArray;
-    return this._storedLayout;
-  }
-
   getTouchBarInput(inputName) {
     // inputName might be undefined if an input's context() returns undefined.
     if (!inputName || !kBuiltInInputs.hasOwnProperty(inputName)) {
       return null;
     }
 
     // context() may specify that one named input "point" to another.
     if (typeof kBuiltInInputs[inputName].context == "function") {
@@ -217,17 +205,16 @@ class TouchBarHelper {
 
     if (!inputName || !kBuiltInInputs.hasOwnProperty(inputName)) {
       return null;
     }
 
     let inputData = kBuiltInInputs[inputName];
 
     let item = new TouchBarInput(inputData);
-
     // Skip localization if there is already a cached localized title.
     if (kBuiltInInputs[inputName].hasOwnProperty("localTitle")) {
       return item;
     }
 
     // Async l10n fills in the localized input labels after the initial load.
     this._l10n.formatValue(item.key).then((result) => {
       item.title = result;
@@ -253,22 +240,16 @@ class TouchBarHelper {
    */
   _updateTouchBarInputs(...inputNames) {
     if (!this.window) {
       return;
     }
 
     let inputs = [];
     for (let inputName of inputNames) {
-      // Updating Touch Bar items isn't cheap in terms of performance, so
-      // only consider this input if it's actually part of the layout.
-      if (!this._storedLayout.includes(inputName)) {
-        continue;
-      }
-
       let input = this.getTouchBarInput(inputName);
       if (!input) {
         continue;
       }
       inputs.push(input);
     }
 
     let baseWindow = this.window.docShell.treeOwner.QueryInterface(Ci.nsIBaseWindow);
@@ -295,20 +276,20 @@ class TouchBarHelper {
         this._updateTouchBarInputs("AddBookmark");
         break;
       case "reader-mode-available":
         kBuiltInInputs.ReaderView.disabled = false;
         this._updateTouchBarInputs("ReaderView");
         break;
       case "intl:app-locales-changed":
         // On locale change, refresh all inputs after loading new localTitle.
-        for (let inputName of this._storedLayout) {
-          delete kBuiltInInputs[inputName].localTitle;
+        for (let input in kBuiltInInputs) {
+          delete input.localTitle;
         }
-        this._updateTouchBarInputs(...this._storedLayout);
+        this._updateTouchBarInputs(...kBuiltInInputs.keys());
         break;
       case "quit-application":
         this.destructor();
         break;
     }
   }
 }
 
--- a/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
+++ b/browser/components/touchbar/tests/browser/browser_touchbar_tests.js
@@ -1,14 +1,12 @@
 /* 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/. */
 
-const PREF_NAME = "ui.touchbar.layout";
-
 const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "TouchBarHelper",
                                    "@mozilla.org/widget/touchbarhelper;1",
                                    "nsITouchBarHelper");
 XPCOMUtils.defineLazyServiceGetter(this, "TouchBarInput",
                                    "@mozilla.org/widget/touchbarinput;1",
                                    "nsITouchBarInput");
@@ -19,41 +17,16 @@ function is_element_visible(aElement, aM
 }
 
 function is_element_hidden(aElement, aMsg) {
   isnot(aElement, null, "Element should not be null when checking visibility");
   ok(BrowserTestUtils.is_hidden(aElement), aMsg);
 }
 
 /**
- * Sets the pref to contain errors. .layout should contain only the
- * non-erroneous elements.
- */
-add_task(async function setWrongPref() {
-  registerCleanupFunction(function() {
-    Services.prefs.deleteBranch(PREF_NAME);
-  });
-
-  let wrongValue   = "Back, Back, Forwrd, NewTab, Unimplemented,";
-  let correctValue = ["back", "new-tab"];
-  let testValue = [];
-  Services.prefs.setStringPref(PREF_NAME, wrongValue);
-
-  let layoutItems = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
-  layoutItems = TouchBarHelper.layout;
-  for (let i = 0; i < layoutItems.length; i++) {
-    let input = layoutItems.queryElementAt(i, Ci.nsITouchBarInput);
-    testValue.push(input.key);
-  }
-  Assert.equal(testValue.toString(),
-               correctValue.toString(),
-               "Pref should filter out incorrect inputs.");
-});
-
-/**
  * Tests if our bookmark button updates with our event.
  */
 add_task(async function updateBookmarkButton() {
   Services.obs.notifyObservers(null, "bookmark-icon-updated", "starred");
   Assert.equal(TouchBarHelper.getTouchBarInput("AddBookmark").image,
     "bookmark-filled.pdf",
     "AddBookmark image should be filled bookmark after event.");
 
--- a/browser/locales/en-US/browser/customizeMode.ftl
+++ b/browser/locales/en-US/browser/customizeMode.ftl
@@ -38,8 +38,10 @@ customize-mode-uidensity-menu-compact =
     .tooltiptext = Compact
 customize-mode-lwthemes-menu-get-more =
     .label = Get More Themes
     .accesskey = G
 customize-mode-undo-cmd =
     .label = Undo
 customize-mode-lwthemes-my-themes =
     .value = My Themes
+customize-mode-touchbar-cmd =
+    .label = Customize Touch Bar…
--- a/browser/locales/en-US/browser/touchbar/touchbar.ftl
+++ b/browser/locales/en-US/browser/touchbar/touchbar.ftl
@@ -7,14 +7,14 @@
 back = Back
 forward = Forward
 reload = Reload
 home = Home
 fullscreen = Fullscreen
 find = Find
 new-tab = New tab
 add-bookmark = Add bookmark
-open-bookmarks-sidebar = Open Bookmarks Sidebar
 reader-view = Reader View
 # Meant to match the string displayed in an empty URL bar.
 open-location = Search or enter address
 share = Share
 close-window = Close Window
+open-sidebar = Sidebars
--- a/browser/locales/en-US/chrome/browser/baseMenuOverlay.dtd
+++ b/browser/locales/en-US/chrome/browser/baseMenuOverlay.dtd
@@ -44,8 +44,10 @@
 
 <!ENTITY hideThisAppCmdMac2.label       "Hide &brandShorterName;">
 <!ENTITY hideThisAppCmdMac2.commandkey  "H">
 
 <!ENTITY hideOtherAppsCmdMac.label      "Hide Others">
 <!ENTITY hideOtherAppsCmdMac.commandkey "H">
 
 <!ENTITY showAllAppsCmdMac.label        "Show All">
+
+<!ENTITY touchBarCmdMac.label           "Customize Touch Bar…">
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -203,20 +203,16 @@ pref("ui.android.mouse_as_touch", 1);
 // Pop up context menu on mouseup instead of mousedown, if that's the OS default.
 // Note: ignored on Windows (context menus always use mouseup)
 pref("ui.context_menus.after_mouseup", false);
 // Duration of timeout of incremental search in menus (ms).  0 means infinite.
 pref("ui.menu.incremental_search.timeout", 1000);
 // If true, all popups won't hide automatically on blur
 pref("ui.popup.disable_autohide", false);
 
-#ifdef XP_MACOSX
-pref("ui.touchbar.layout", "Back,Forward,Reload,OpenLocation,NewTab,Share");
-#endif
-
 // 0 = default: always, except in high contrast mode
 // 1 = always
 // 2 = never
 pref("browser.display.document_color_use", 0);
 pref("browser.display.use_system_colors",   false);
 pref("browser.display.foreground_color",    "#000000");
 pref("browser.display.background_color",    "#FFFFFF");
 pref("browser.display.force_inline_alttext", false); // true = force ALT text for missing images to be layed out inline
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -58,16 +58,18 @@ int32_t gXULModalLevel = 0;
 
 // In principle there should be only one app-modal window at any given time.
 // But sometimes, despite our best efforts, another window appears above the
 // current app-modal window.  So we need to keep a linked list of app-modal
 // windows.  (A non-sheet window that appears above an app-modal window is
 // also made app-modal.)  See nsCocoaWindow::SetModal().
 nsCocoaWindowList* gGeckoAppModalWindowList = NULL;
 
+BOOL sTouchBarIsInitialized = NO;
+
 // defined in nsMenuBarX.mm
 extern NSMenu* sApplicationMenu;  // Application menu shared by all menubars
 
 // defined in nsChildView.mm
 extern BOOL gSomeMenuBarPainted;
 
 #if !defined(MAC_OS_X_VERSION_10_9) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_9
 
@@ -2792,16 +2794,19 @@ static NSImage* GetMenuMaskImage() {
     [self swapOutChildViewWrapper:wrapper];
     [wrapper release];
   }
   mUseMenuStyle = aValue;
 }
 
 - (NSTouchBar*)makeTouchBar {
   mTouchBar = [[nsTouchBar alloc] init];
+  if (mTouchBar) {
+    sTouchBarIsInitialized = YES;
+  }
   return mTouchBar;
 }
 
 - (void)setBeingShown:(BOOL)aValue {
   mBeingShown = aValue;
 }
 
 - (BOOL)isBeingShown {
--- a/widget/cocoa/nsMenuBarX.mm
+++ b/widget/cocoa/nsMenuBarX.mm
@@ -15,31 +15,35 @@
 
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsGkAtoms.h"
 #include "nsObjCExceptions.h"
 #include "nsThreadUtils.h"
 
 #include "nsIContent.h"
+#include "nsITouchBarUpdater.h"
 #include "nsIWidget.h"
 #include "mozilla/dom/Document.h"
 #include "nsIAppStartup.h"
 #include "nsIStringBundle.h"
 #include "nsToolkitCompsCID.h"
 
 #include "mozilla/Components.h"
 #include "mozilla/dom/Element.h"
 
 NativeMenuItemTarget* nsMenuBarX::sNativeEventTarget = nil;
 nsMenuBarX* nsMenuBarX::sLastGeckoMenuBarPainted = nullptr;
 NSMenu* sApplicationMenu = nil;
 BOOL sApplicationMenuIsFallback = NO;
 BOOL gSomeMenuBarPainted = NO;
 
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
 // We keep references to the first quit and pref item content nodes we find, which
 // will be from the hidden window. We use these when the document for the current
 // window does not have a quit or pref item. We don't need strong refs here because
 // these items are always strong ref'd by their owning menu bar (instance variable).
 static nsIContent* sAboutItemContent = nullptr;
 static nsIContent* sPrefItemContent = nullptr;
 static nsIContent* sQuitItemContent = nullptr;
 
@@ -515,16 +519,17 @@ void nsMenuBarX::AquifyMenuBar() {
     HideItem(domDoc, NS_LITERAL_STRING("menu_preferences"), getter_AddRefs(mPrefItemContent));
     if (!sPrefItemContent) sPrefItemContent = mPrefItemContent;
 
     // hide items that we use for the Application menu
     HideItem(domDoc, NS_LITERAL_STRING("menu_mac_services"), nullptr);
     HideItem(domDoc, NS_LITERAL_STRING("menu_mac_hide_app"), nullptr);
     HideItem(domDoc, NS_LITERAL_STRING("menu_mac_hide_others"), nullptr);
     HideItem(domDoc, NS_LITERAL_STRING("menu_mac_show_all"), nullptr);
+    HideItem(domDoc, NS_LITERAL_STRING("menu_mac_touch_bar"), nullptr);
   }
 }
 
 // for creating menu items destined for the Application menu
 NSMenuItem* nsMenuBarX::CreateNativeAppMenuItem(nsMenuX* inMenu, const nsAString& nodeID,
                                                 SEL action, int tag, NativeMenuItemTarget* target) {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL;
 
@@ -620,16 +625,18 @@ nsresult nsMenuBarX::CreateApplicationMe
     = Preferences...       = <- menu_preferences
     ========================
     = Services     >       = <- menu_mac_services    <- (do not define key equivalent)
     ========================
     = Hide App             = <- menu_mac_hide_app
     = Hide Others          = <- menu_mac_hide_others
     = Show All             = <- menu_mac_show_all
     ========================
+    = Customize Touch Bar… = <- menu_mac_touch_bar
+    ========================
     = Quit                 = <- menu_FileQuitItem
     ========================
 
     If any of them are ommitted from the application's DOM, we just don't add
     them. We always add a "Quit" item, but if an app developer does not provide a
     DOM node with the right ID for the Quit item, we add it in English. App
     developers need only add each node with a label and a key equivalent (if they
     want one). Other attributes are optional. Like so:
@@ -735,16 +742,38 @@ nsresult nsMenuBarX::CreateApplicationMe
       itemBeingAdded = nil;
 
       addHideShowSeparator = TRUE;
     }
 
     // Add a separator after the hide/show menus if at least one exists
     if (addHideShowSeparator) [sApplicationMenu addItem:[NSMenuItem separatorItem]];
 
+    BOOL addTouchBarSeparator = NO;
+
+    // Add Touch Bar customization menu item.
+    itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_mac_touch_bar"),
+                                             @selector(menuItemHit:), eCommand_ID_TouchBar,
+                                             nsMenuBarX::sNativeEventTarget);
+
+    if (itemBeingAdded) {
+      [sApplicationMenu addItem:itemBeingAdded];
+      // We hide the menu item on Macs that don't have a Touch Bar.
+      if (!sTouchBarIsInitialized) {
+        [itemBeingAdded setHidden:YES];
+      } else {
+        addTouchBarSeparator = YES;
+      }
+      [itemBeingAdded release];
+      itemBeingAdded = nil;
+    }
+
+    // Add a separator after the Touch Bar menu item if it exists
+    if (addTouchBarSeparator) [sApplicationMenu addItem:[NSMenuItem separatorItem]];
+
     // Add quit menu item
     itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("menu_FileQuitItem"),
                                              @selector(menuItemHit:), eCommand_ID_Quit,
                                              nsMenuBarX::sNativeEventTarget);
     if (itemBeingAdded) {
       [sApplicationMenu addItem:itemBeingAdded];
       [itemBeingAdded release];
       itemBeingAdded = nil;
@@ -820,16 +849,22 @@ static BOOL gMenuItemsExecuteCommands = 
 }
 
 - (BOOL)performSuperKeyEquivalent:(NSEvent*)theEvent {
   return [super performKeyEquivalent:theEvent];
 }
 
 @end
 
+#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
+@interface NSApplication (TouchBarMenu)
+- (IBAction)toggleTouchBarCustomizationPalette:(id)sender;
+@end
+#endif
+
 //
 // Objective-C class used as action target for menu items
 //
 
 @implementation NativeMenuItemTarget
 
 // called when some menu item in this menu gets hit
 - (IBAction)menuItemHit:(id)sender {
@@ -870,16 +905,19 @@ static BOOL gMenuItemsExecuteCommands = 
     [NSApp hide:sender];
     return;
   } else if (tag == eCommand_ID_HideOthers) {
     [NSApp hideOtherApplications:sender];
     return;
   } else if (tag == eCommand_ID_ShowAll) {
     [NSApp unhideAllApplications:sender];
     return;
+  } else if (tag == eCommand_ID_TouchBar) {
+    [NSApp toggleTouchBarCustomizationPalette:sender];
+    return;
   } else if (tag == eCommand_ID_Quit) {
     nsIContent* mostSpecificContent = sQuitItemContent;
     if (menuBar && menuBar->mQuitItemContent) mostSpecificContent = menuBar->mQuitItemContent;
     // If we have some content for quit we execute it. Otherwise we send a native app terminate
     // message. If you want to stop a quit from happening, provide quit content and return
     // the event as unhandled.
     if (mostSpecificContent) {
       nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
--- a/widget/cocoa/nsMenuBaseX.h
+++ b/widget/cocoa/nsMenuBaseX.h
@@ -64,12 +64,13 @@ class nsMenuGroupOwnerX;
 enum {
   eCommand_ID_About = 1,
   eCommand_ID_Prefs = 2,
   eCommand_ID_Quit = 3,
   eCommand_ID_HideApp = 4,
   eCommand_ID_HideOthers = 5,
   eCommand_ID_ShowAll = 6,
   eCommand_ID_Update = 7,
-  eCommand_ID_Last = 8
+  eCommand_ID_TouchBar = 8,
+  eCommand_ID_Last = 9
 };
 
 #endif  // nsMenuBaseX_h_
--- a/widget/cocoa/nsTouchBar.h
+++ b/widget/cocoa/nsTouchBar.h
@@ -34,19 +34,22 @@ typedef NSString* NSTouchBarItemIdentifi
 __attribute__((weak_import)) @interface NSCustomTouchBarItem : NSTouchBarItem
 @property(strong) NSView* view;
 @property(strong) NSString* customizationLabel;
 @end
 
 @protocol NSTouchBarDelegate
 @end
 
+typedef NSString* NSTouchBarCustomizationIdentifier;
 __attribute__((weak_import)) @interface NSTouchBar : NSObject
 @property(strong) NSArray<NSTouchBarItemIdentifier>* defaultItemIdentifiers;
 @property(strong) id<NSTouchBarDelegate> delegate;
+@property(strong) NSTouchBarCustomizationIdentifier customizationIdentifier;
+@property(strong) NSArray<NSTouchBarItemIdentifier>* customizationAllowedItemIdentifiers;
 - (NSTouchBarItem*)itemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier;
 @end
 
 @protocol NSSharingServicePickerTouchBarItemDelegate
 @end
 
 __attribute__((weak_import)) @interface NSSharingServicePickerTouchBarItem : NSTouchBarItem
 @property(strong) id<NSSharingServicePickerTouchBarItemDelegate> delegate;
--- a/widget/cocoa/nsTouchBar.mm
+++ b/widget/cocoa/nsTouchBar.mm
@@ -33,56 +33,69 @@ static char sIdentifierAssociationKey;
 - (instancetype)init {
   if ((self = [super init])) {
     mTouchBarHelper = do_GetService(NS_TOUCHBARHELPER_CID);
     if (!mTouchBarHelper) {
       return nil;
     }
 
     self.delegate = self;
+    // This customization identifier is how users' custom layouts are saved by macOS.
+    // If this changes, all users' layouts would be reset to the default layout.
+    self.customizationIdentifier = @"com.mozilla.firefox.touchbar.defaultbar";
     self.mappedLayoutItems = [NSMutableDictionary dictionary];
-    nsCOMPtr<nsIArray> layoutItems;
+    nsCOMPtr<nsIArray> allItems;
 
-    nsresult rv = mTouchBarHelper->GetLayout(getter_AddRefs(layoutItems));
-    if (NS_FAILED(rv) || !layoutItems) {
+    nsresult rv = mTouchBarHelper->GetAllItems(getter_AddRefs(allItems));
+    if (NS_FAILED(rv) || !allItems) {
       return nil;
     }
 
     uint32_t itemCount = 0;
-    layoutItems->GetLength(&itemCount);
-    // This is copied to self.defaultItemIdentifiers. Required since
-    // [self.mappedLayoutItems allKeys] does not preserve order.
-    NSMutableArray* orderedLayoutIdentifiers = [NSMutableArray arrayWithCapacity:itemCount];
+    allItems->GetLength(&itemCount);
+    // This is copied to self.customizationAllowedItemIdentifiers. Required since
+    // [self.mappedItems allKeys] does not preserve order.
+    // One slot is added for the spacer item.
+    NSMutableArray* orderedIdentifiers = [NSMutableArray arrayWithCapacity:itemCount + 1];
     for (uint32_t i = 0; i < itemCount; ++i) {
-      nsCOMPtr<nsITouchBarInput> input = do_QueryElementAt(layoutItems, i);
+      nsCOMPtr<nsITouchBarInput> input = do_QueryElementAt(allItems, i);
       if (!input) {
         continue;
       }
 
       TouchBarInput* convertedInput = [[TouchBarInput alloc] initWithXPCOM:input];
 
       // Add new input to dictionary for lookup of properties in delegate.
       self.mappedLayoutItems[[convertedInput nativeIdentifier]] = convertedInput;
-      orderedLayoutIdentifiers[i] = [convertedInput nativeIdentifier];
+      orderedIdentifiers[i] = [convertedInput nativeIdentifier];
     }
+    [orderedIdentifiers addObject:@"NSTouchBarItemIdentifierFlexibleSpace"];
 
-    self.defaultItemIdentifiers = [orderedLayoutIdentifiers copy];
+    NSArray* defaultItemIdentifiers = @[
+      [CustomButtonIdentifier stringByAppendingPathExtension:@"back"],
+      [CustomButtonIdentifier stringByAppendingPathExtension:@"forward"],
+      [CustomButtonIdentifier stringByAppendingPathExtension:@"reload"],
+      [CustomMainButtonIdentifier stringByAppendingPathExtension:@"open-location"],
+      [CustomButtonIdentifier stringByAppendingPathExtension:@"new-tab"], ShareScrubberIdentifier
+    ];
+    self.defaultItemIdentifiers = [defaultItemIdentifiers copy];
+    self.customizationAllowedItemIdentifiers = [orderedIdentifiers copy];
   }
 
   return self;
 }
 
 - (void)dealloc {
   for (NSTouchBarItemIdentifier identifier in self.mappedLayoutItems) {
     NSTouchBarItem* item = [self itemForIdentifier:identifier];
     [item release];
   }
 
   [self.defaultItemIdentifiers release];
-
+  [self.customizationAllowedItemIdentifiers release];
   [self.mappedLayoutItems removeAllObjects];
   [self.mappedLayoutItems release];
   [super dealloc];
 }
 
 - (NSTouchBarItem*)touchBar:(NSTouchBar*)aTouchBar
       makeItemForIdentifier:(NSTouchBarItemIdentifier)aIdentifier {
   if ([aIdentifier hasPrefix:ScrubberIdentifier]) {
--- a/widget/cocoa/nsTouchBarUpdater.mm
+++ b/widget/cocoa/nsTouchBarUpdater.mm
@@ -8,20 +8,26 @@
 #include "nsITouchBarInput.h"
 #include "nsTouchBarUpdater.h"
 
 #include "nsCocoaWindow.h"
 #include "nsIArray.h"
 #include "nsIBaseWindow.h"
 #include "nsIWidget.h"
 
+// defined in nsCocoaWindow.mm.
+extern BOOL sTouchBarIsInitialized;
+
 #if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2
 @interface BaseWindow (NSTouchBarProvider)
 @property(strong) NSTouchBar* touchBar;
 @end
+@interface NSApplication (TouchBarMenu)
+- (IBAction)toggleTouchBarCustomizationPalette:(id)sender;
+@end
 #endif
 
 NS_IMPL_ISUPPORTS(nsTouchBarUpdater, nsITouchBarUpdater);
 
 NS_IMETHODIMP
 nsTouchBarUpdater::UpdateTouchBarInputs(nsIBaseWindow* aWindow,
                                         const nsTArray<RefPtr<nsITouchBarInput>>& aInputs) {
   nsCOMPtr<nsIWidget> widget = nullptr;
@@ -44,8 +50,27 @@ nsTouchBarUpdater::UpdateTouchBarInputs(
 
       TouchBarInput* convertedInput = [[TouchBarInput alloc] initWithXPCOM:input];
       [(nsTouchBar*)cocoaWin.touchBar updateItem:convertedInput];
     }
   }
 
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsTouchBarUpdater::EnterCustomizeMode() {
+  [NSApp toggleTouchBarCustomizationPalette:(id)this];
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTouchBarUpdater::IsTouchBarInitialized(bool* aResult) {
+  *aResult = sTouchBarIsInitialized;
+  return NS_OK;
+}
+
+// NOTE: This method is for internal unit tests only.
+NS_IMETHODIMP
+nsTouchBarUpdater::SetTouchBarInitialized(bool aIsInitialized) {
+  sTouchBarIsInitialized = aIsInitialized;
+  return NS_OK;
+}
--- a/widget/nsITouchBarHelper.idl
+++ b/widget/nsITouchBarHelper.idl
@@ -18,19 +18,19 @@ interface nsITouchBarHelper : nsISupport
   readonly attribute AString activeUrl;
 
   /**
    * Return the active browser's page title.
    */
   readonly attribute AString activeTitle;
 
   /**
-   * Returns the layout for the Touch Bar in an nsIArray
+   * Returns all available Touch Bar Inputs in an nsIArray
    * of nsITouchBarInput objects.
    */
-  attribute nsIArray layout;
+  attribute nsIArray allItems;
 
   /**
    * Returns the requested TouchBarInput.
    * Exposed for testing.
    */
   nsITouchBarInput getTouchBarInput(in string aInputName);
 };
--- a/widget/nsITouchBarUpdater.idl
+++ b/widget/nsITouchBarUpdater.idl
@@ -12,9 +12,26 @@
  */
 [scriptable, uuid(38f396e2-93c9-4a77-aaf7-2d50b9962186)]
 interface nsITouchBarUpdater : nsISupports
 {
   /**
    * Updates an array of nsITouchBarInputs in the specified window.
    */
   void updateTouchBarInputs(in nsIBaseWindow aWindow, in Array<nsITouchBarInput> aInputs);
+
+  /**
+   * Enter the native Touch Bar customization window.
+   */
+  void enterCustomizeMode();
+
+  /**
+   * Checks if the user is using a Touch Bar-compatible Mac.
+   */
+  boolean isTouchBarInitialized();
+
+  /**
+   * Sets whether the Touch Bar is initialized.
+   * NOTE: This method is for internal unit tests only! Normally, the system
+   * sets this value after a Touch Bar is initialized on compatible Macs.
+   */
+  void setTouchBarInitialized(in boolean aIsInitialized);
 };