Bug 1546309 - Add customizable UI files. rs=jorgk
authorPaul Morris <paul@paulwmorris.com>
Thu, 06 Jun 2019 12:03:47 -0400
changeset 35849 fe4c6acd6880bdd9360662e2742dc96c24035c95
parent 35848 534295374182843b26aece5515c042d8cf1a6dd0
child 35850 bc8ef4b819df75e637f7f191f53de0c5088520e6
push id392
push userclokep@gmail.com
push dateMon, 02 Sep 2019 20:17:19 +0000
reviewersjorgk
bugs1546309
Bug 1546309 - Add customizable UI files. rs=jorgk A straight copy of these files from Firefox over to Thunderbird.
mail/components/customizableui/CustomizableUI.jsm
mail/components/customizableui/CustomizableWidgets.jsm
mail/components/customizableui/CustomizeMode.jsm
mail/components/customizableui/DragPositionManager.jsm
mail/components/customizableui/PanelMultiView.jsm
mail/components/customizableui/SearchWidgetTracker.jsm
mail/components/customizableui/content/customizeMode.inc.xul
mail/components/customizableui/content/jar.mn
mail/components/customizableui/content/moz.build
mail/components/customizableui/content/panelUI.inc.xul
mail/components/customizableui/content/panelUI.js
mail/components/customizableui/moz.build
mail/themes/linux/customizableui/panelUI.css
mail/themes/osx/customizableui/panelUI.css
mail/themes/shared/customizableui/customizeMode.inc.css
mail/themes/shared/customizableui/density-compact.svg
mail/themes/shared/customizableui/density-normal.svg
mail/themes/shared/customizableui/density-touch.svg
mail/themes/shared/customizableui/empty-overflow-panel.png
mail/themes/shared/customizableui/empty-overflow-panel@2x.png
mail/themes/shared/customizableui/menu-arrow.svg
mail/themes/shared/customizableui/panelUI.inc.css
mail/themes/shared/customizableui/whimsy.png
mail/themes/windows/customizableui/panelUI.css
new file mode 100644
--- /dev/null
+++ b/mail/components/customizableui/CustomizableUI.jsm
@@ -0,0 +1,4820 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["CustomizableUI"];
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AddonManager: "resource://gre/modules/AddonManager.jsm",
+  AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
+  SearchWidgetTracker: "resource:///modules/SearchWidgetTracker.jsm",
+  CustomizableWidgets: "resource:///modules/CustomizableWidgets.jsm",
+  DeferredTask: "resource://gre/modules/DeferredTask.jsm",
+  PanelMultiView: "resource:///modules/PanelMultiView.jsm",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+  ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
+  const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
+  return Services.strings.createBundle(kUrl);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gELS",
+  "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
+
+const kDefaultThemeID = "default-theme@mozilla.org";
+
+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 kPrefExtraDragSpace            = "browser.tabs.extraDragSpace";
+const kPrefUIDensity                 = "browser.uidensity";
+const kPrefAutoTouchMode             = "browser.touchmode.auto";
+const kPrefAutoHideDownloadsButton   = "browser.download.autohideButton";
+
+const kExpectedWindowURL = AppConstants.BROWSER_CHROME_URL;
+
+var gDefaultTheme;
+var gSelectedTheme;
+
+/**
+ * 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.
+ */
+const kSubviewEvents = [
+  "ViewShowing",
+  "ViewHiding",
+];
+
+/**
+ * The current version. We can use this to auto-add new default widgets as necessary.
+ * (would be const but isn't because of testing purposes)
+ */
+var kVersion = 16;
+
+/**
+ * Buttons removed from built-ins by version they were removed. kVersion must be
+ * bumped any time a new id is added to this. Use the button id as key, and
+ * version the button is removed in as the value.  e.g. "pocket-button": 5
+ */
+var ObsoleteBuiltinButtons = {
+  "feed-button": 15,
+};
+
+/**
+ * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
+ * on their IDs.
+ */
+var gPalette = new Map();
+
+/**
+ * gAreas maps area IDs to Sets of properties about those areas. An area is a
+ * place where a widget can be put.
+ */
+var gAreas = new Map();
+
+/**
+ * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
+ * are placed within that area (either directly in the area node, or in the
+ * customizationTarget of the node).
+ */
+var gPlacements = new Map();
+
+/**
+ * gFuturePlacements represent placements that will happen for areas that have
+ * not yet loaded (due to lazy-loading). This can occur when add-ons register
+ * widgets.
+ */
+var gFuturePlacements = new Map();
+
+// XXXunf Temporary. Need a nice way to abstract functions to build widgets
+//       of these types.
+var gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
+
+/**
+ * gPanelsForWindow is a list of known panels in a window which we may need to close
+ * should command events fire which target them.
+ */
+var gPanelsForWindow = new WeakMap();
+
+/**
+ * gSeenWidgets remembers which widgets the user has seen for the first time
+ * before. This way, if a new widget is created, and the user has not seen it
+ * before, it can be put in its default location. Otherwise, it remains in the
+ * palette.
+ */
+var gSeenWidgets = new Set();
+
+/**
+ * gDirtyAreaCache is a set of area IDs for areas where items have been added,
+ * moved or removed at least once. This set is persisted, and is used to
+ * optimize building of toolbars in the default case where no toolbars should
+ * be "dirty".
+ */
+var gDirtyAreaCache = new Set();
+
+/**
+ * gPendingBuildAreas is a map from area IDs to map from build nodes to their
+ * existing children at the time of node registration, that are waiting
+ * for the area to be registered
+ */
+var gPendingBuildAreas = new Map();
+
+var gSavedState = null;
+var gRestoring = false;
+var gDirty = false;
+var gInBatchStack = 0;
+var gResetting = false;
+var gUndoResetting = false;
+
+/**
+ * gBuildAreas maps area IDs to actual area nodes within browser windows.
+ */
+var gBuildAreas = new Map();
+
+/**
+ * gBuildWindows is a map of windows that have registered build areas, mapped
+ * to a Set of known toolboxes in that window.
+ */
+var gBuildWindows = new Map();
+
+var gNewElementCount = 0;
+var gGroupWrapperCache = new Map();
+var gSingleWrapperCache = new WeakMap();
+var gListeners = new Set();
+
+var gUIStateBeforeReset = {
+  uiCustomizationState: null,
+  drawInTitlebar: null,
+  extraDragSpace: null,
+  currentTheme: null,
+  uiDensity: null,
+  autoTouchMode: null,
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "gDebuggingEnabled", kPrefCustomizationDebug, false,
+  (pref, oldVal, newVal) => {
+    if (typeof log != "undefined") {
+      log.maxLogLevel = newVal ? "all" : "log";
+    }
+  }
+);
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let scope = {};
+  ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
+  let consoleOptions = {
+    maxLogLevel: gDebuggingEnabled ? "all" : "log",
+    prefix: "CustomizableUI",
+  };
+  return new scope.ConsoleAPI(consoleOptions);
+});
+
+var CustomizableUIInternal = {
+  initialize() {
+    log.debug("Initializing");
+
+    AddonManagerPrivate.databaseReady.then(async () => {
+      AddonManager.addAddonListener(this);
+
+      let addons = await AddonManager.getAddonsByTypes(["theme"]);
+      gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID);
+      gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme;
+    });
+
+    this.addListener(this);
+    this._defineBuiltInWidgets();
+    this.loadSavedState();
+    this._updateForNewVersion();
+    this._markObsoleteBuiltinButtonsSeen();
+
+    this.registerArea(CustomizableUI.AREA_FIXED_OVERFLOW_PANEL, {
+      type: CustomizableUI.TYPE_MENU_PANEL,
+      defaultPlacements: [],
+      anchor: "nav-bar-overflow-button",
+    }, true);
+
+    let navbarPlacements = [
+      "back-button",
+      "forward-button",
+      "stop-reload-button",
+      "home-button",
+      "spring",
+      "urlbar-container",
+      "spring",
+      "downloads-button",
+      "library-button",
+      "sidebar-button",
+      "fxa-toolbar-menu-button",
+    ];
+
+    if (AppConstants.MOZ_DEV_EDITION) {
+      navbarPlacements.splice(2, 0, "developer-button");
+    }
+
+    this.registerArea(CustomizableUI.AREA_NAVBAR, {
+      type: CustomizableUI.TYPE_TOOLBAR,
+      overflowable: true,
+      defaultPlacements: navbarPlacements,
+      defaultCollapsed: false,
+    }, true);
+
+    if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
+      this.registerArea(CustomizableUI.AREA_MENUBAR, {
+        type: CustomizableUI.TYPE_TOOLBAR,
+        defaultPlacements: [
+          "menubar-items",
+        ],
+        defaultCollapsed: true,
+      }, true);
+    }
+
+    this.registerArea(CustomizableUI.AREA_TABSTRIP, {
+      type: CustomizableUI.TYPE_TOOLBAR,
+      defaultPlacements: [
+        "tabbrowser-tabs",
+        "new-tab-button",
+        "alltabs-button",
+      ],
+      defaultCollapsed: null,
+    }, true);
+    this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
+      type: CustomizableUI.TYPE_TOOLBAR,
+      defaultPlacements: [
+        "personal-bookmarks",
+      ],
+      defaultCollapsed: true,
+    }, true);
+
+    SearchWidgetTracker.init();
+  },
+
+  onEnabled(addon) {
+    if (addon.type == "theme") {
+      gSelectedTheme = addon;
+    }
+  },
+
+  get _builtinAreas() {
+    return new Set([
+      ...this._builtinToolbars,
+      CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
+    ]);
+  },
+
+  get _builtinToolbars() {
+    let toolbars = new Set([
+      CustomizableUI.AREA_NAVBAR,
+      CustomizableUI.AREA_BOOKMARKS,
+      CustomizableUI.AREA_TABSTRIP,
+    ]);
+    if (AppConstants.platform != "macosx") {
+      toolbars.add(CustomizableUI.AREA_MENUBAR);
+    }
+    return toolbars;
+  },
+
+  _defineBuiltInWidgets() {
+    for (let widgetDefinition of CustomizableWidgets) {
+      this.createBuiltinWidget(widgetDefinition);
+    }
+  },
+
+  // eslint-disable-next-line complexity
+  _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);
+        }
+      }
+      return;
+    }
+
+    let currentVersion = gSavedState.currentVersion;
+    for (let [id, widget] of gPalette) {
+      if (widget.defaultArea) {
+        let shouldAdd = false;
+        let shouldSetPref = false;
+        let prefId = "browser.toolbarbuttons.introduced." + widget.id;
+        if (widget._introducedInVersion === "pref") {
+          try {
+            shouldAdd = !Services.prefs.getBoolPref(prefId);
+          } catch (ex) {
+            // Pref doesn't exist:
+            shouldAdd = true;
+          }
+          shouldSetPref = shouldAdd;
+        } else if (widget._introducedInVersion > currentVersion) {
+          shouldAdd = true;
+        }
+
+        if (shouldAdd) {
+          let futurePlacements = gFuturePlacements.get(widget.defaultArea);
+          if (futurePlacements) {
+            futurePlacements.add(id);
+          } else {
+            gFuturePlacements.set(widget.defaultArea, new Set([id]));
+          }
+          if (shouldSetPref) {
+            Services.prefs.setBoolPref(prefId, true);
+          }
+        }
+      }
+    }
+
+    if (currentVersion < 7 && gSavedState.placements &&
+        gSavedState.placements[CustomizableUI.AREA_NAVBAR]) {
+      let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
+      let newPlacements = ["back-button", "forward-button", "stop-reload-button", "home-button"];
+      for (let button of placements) {
+        if (!newPlacements.includes(button)) {
+          newPlacements.push(button);
+        }
+      }
+
+      if (!newPlacements.includes("sidebar-button")) {
+        newPlacements.push("sidebar-button");
+      }
+
+      gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements;
+    }
+
+    if (currentVersion < 8 && gSavedState.placements &&
+        gSavedState.placements["PanelUI-contents"]) {
+      let savedPanelPlacements = gSavedState.placements["PanelUI-contents"];
+      delete gSavedState.placements["PanelUI-contents"];
+      let defaultPlacements = [
+        "edit-controls",
+        "zoom-controls",
+        "new-window-button",
+        "privatebrowsing-button",
+        "save-page-button",
+        "print-button",
+        "history-panelmenu",
+        "fullscreen-button",
+        "find-button",
+        "preferences-button",
+        "add-ons-button",
+        "sync-button",
+      ];
+
+      if (!AppConstants.MOZ_DEV_EDITION) {
+        defaultPlacements.splice(-1, 0, "developer-button");
+      }
+
+      let showCharacterEncoding = Services.prefs.getComplexValue(
+        "browser.menu.showCharacterEncoding",
+        Ci.nsIPrefLocalizedString
+      ).data;
+      if (showCharacterEncoding == "true") {
+        defaultPlacements.push("characterencoding-button");
+      }
+
+      savedPanelPlacements = savedPanelPlacements.filter(id => !defaultPlacements.includes(id));
+
+      if (savedPanelPlacements.length) {
+        gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] = savedPanelPlacements;
+      }
+    }
+
+    if (currentVersion < 9 && gSavedState.placements && gSavedState.placements["nav-bar"]) {
+      let placements = gSavedState.placements["nav-bar"];
+      if (placements.includes("urlbar-container")) {
+        let urlbarIndex = placements.indexOf("urlbar-container");
+        let secondSpringIndex = urlbarIndex + 1;
+        // Insert if there isn't already a spring before the urlbar
+        if (urlbarIndex == 0 || !placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring")) {
+          placements.splice(urlbarIndex, 0, "spring");
+          // The url bar is now 1 index later, so increment the insertion point for
+          // the second spring.
+          secondSpringIndex++;
+        }
+        // If the search container is present, insert after the search container
+        // instead of after the url bar
+        let searchContainerIndex = placements.indexOf("search-container");
+        if (searchContainerIndex != -1) {
+          secondSpringIndex = searchContainerIndex + 1;
+        }
+        if (secondSpringIndex == placements.length ||
+            !placements[secondSpringIndex].startsWith(kSpecialWidgetPfx + "spring")) {
+          placements.splice(secondSpringIndex, 0, "spring");
+        }
+      }
+
+      // Finally, replace the bookmarks menu button with the library one if present
+      if (placements.includes("bookmarks-menu-button")) {
+        let bmbIndex = placements.indexOf("bookmarks-menu-button");
+        placements.splice(bmbIndex, 1);
+        let downloadButtonIndex = placements.indexOf("downloads-button");
+        let libraryIndex = downloadButtonIndex == -1 ? bmbIndex : (downloadButtonIndex + 1);
+        placements.splice(libraryIndex, 0, "library-button");
+      }
+    }
+
+    if (currentVersion < 10 && gSavedState.placements) {
+      for (let placements of Object.values(gSavedState.placements)) {
+        if (placements.includes("webcompat-reporter-button")) {
+          placements.splice(placements.indexOf("webcompat-reporter-button"), 1);
+          break;
+        }
+      }
+    }
+
+    // Move the downloads button to the default position in the navbar if it's
+    // not there already.
+    if (currentVersion < 11 && gSavedState.placements) {
+      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
+      // First remove from wherever it currently lives, if anywhere:
+      for (let placements of Object.values(gSavedState.placements)) {
+        let existingIndex = placements.indexOf("downloads-button");
+        if (existingIndex != -1) {
+          placements.splice(existingIndex, 1);
+          break; // It can only be in 1 place, so no point looking elsewhere.
+        }
+      }
+
+      // Now put the button in the navbar in the correct spot:
+      if (navbarPlacements) {
+        let insertionPoint = navbarPlacements.indexOf("urlbar-container");
+        // Deliberately iterate to 1 past the end of the array to insert at the
+        // end if need be.
+        while (++insertionPoint < navbarPlacements.length) {
+          let widget = navbarPlacements[insertionPoint];
+          // If we find a non-searchbar, non-spacer node, break out of the loop:
+          if (widget != "search-container" && !this.matchingSpecials(widget, "spring")) {
+            break;
+          }
+        }
+        // We either found the right spot, or reached the end of the
+        // placements, so insert here:
+        navbarPlacements.splice(insertionPoint, 0, "downloads-button");
+      }
+    }
+
+    if (currentVersion < 12 && gSavedState.placements) {
+      const removedButtons = ["loop-call-button", "loop-button-throttled", "pocket-button"];
+      for (let placements of Object.values(gSavedState.placements)) {
+        for (let button of removedButtons) {
+          let buttonIndex = placements.indexOf(button);
+          if (buttonIndex != -1) {
+            placements.splice(buttonIndex, 1);
+          }
+        }
+      }
+    }
+
+    // Remove the old placements from the now-gone Nightly-only
+    // "New non-e10s window" button.
+    if (currentVersion < 13 && gSavedState.placements) {
+      for (let placements of Object.values(gSavedState.placements)) {
+        let buttonIndex = placements.indexOf("e10s-button");
+        if (buttonIndex != -1) {
+          placements.splice(buttonIndex, 1);
+        }
+      }
+    }
+
+    // Remove unsupported custom toolbar saved placements
+    if (currentVersion < 14 && gSavedState.placements) {
+      for (let area in gSavedState.placements) {
+        if (!this._builtinAreas.has(area)) {
+          delete gSavedState.placements[area];
+        }
+      }
+    }
+
+    // Add the FxA toolbar menu as the right most button item
+    if (currentVersion < 16 && gSavedState.placements) {
+      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
+      // Place the menu item as the first item to the left of the hamburger menu
+      if (navbarPlacements) {
+        navbarPlacements.push("fxa-toolbar-menu-button");
+      }
+    }
+  },
+
+  /**
+   * _markObsoleteBuiltinButtonsSeen
+   * when upgrading, ensure obsoleted buttons are in seen state.
+   */
+  _markObsoleteBuiltinButtonsSeen() {
+    if (!gSavedState)
+      return;
+    let currentVersion = gSavedState.currentVersion;
+    if (currentVersion >= kVersion)
+      return;
+    // we're upgrading, update state if necessary
+    for (let id in ObsoleteBuiltinButtons) {
+      let version = ObsoleteBuiltinButtons[id];
+      if (version == kVersion) {
+        gSeenWidgets.add(id);
+        gDirty = true;
+      }
+    }
+  },
+
+  _placeNewDefaultWidgetsInArea(aArea) {
+    let futurePlacedWidgets = gFuturePlacements.get(aArea);
+    let savedPlacements = gSavedState && gSavedState.placements && gSavedState.placements[aArea];
+    let defaultPlacements = gAreas.get(aArea).get("defaultPlacements");
+    if (!savedPlacements || !savedPlacements.length || !futurePlacedWidgets || !defaultPlacements ||
+        !defaultPlacements.length) {
+      return;
+    }
+    let defaultWidgetIndex = -1;
+
+    for (let widgetId of futurePlacedWidgets) {
+      let widget = gPalette.get(widgetId);
+      if (!widget || widget.source !== CustomizableUI.SOURCE_BUILTIN ||
+          !widget.defaultArea || !widget._introducedInVersion ||
+          savedPlacements.includes(widget.id)) {
+        continue;
+      }
+      defaultWidgetIndex = defaultPlacements.indexOf(widget.id);
+      if (defaultWidgetIndex === -1) {
+        continue;
+      }
+      // Now we know that this widget should be here by default, was newly introduced,
+      // and we have a saved state to insert into, and a default state to work off of.
+      // Try introducing after widgets that come before it in the default placements:
+      for (let i = defaultWidgetIndex; i >= 0; i--) {
+        // Special case: if the defaults list this widget as coming first, insert at the beginning:
+        if (i === 0 && i === defaultWidgetIndex) {
+          savedPlacements.splice(0, 0, widget.id);
+          // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
+          // safe, and we won't skip any items.
+          futurePlacedWidgets.delete(widget.id);
+          gDirty = true;
+          break;
+        }
+        // Otherwise, if we're somewhere other than the beginning, check if the previous
+        // widget is in the saved placements.
+        if (i) {
+          let previousWidget = defaultPlacements[i - 1];
+          let previousWidgetIndex = savedPlacements.indexOf(previousWidget);
+          if (previousWidgetIndex != -1) {
+            savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
+            futurePlacedWidgets.delete(widget.id);
+            gDirty = true;
+            break;
+          }
+        }
+      }
+      // The loop above either inserts the item or doesn't - either way, we can get away
+      // with doing nothing else now; if the item remains in gFuturePlacements, we'll
+      // add it at the end in restoreStateForArea.
+    }
+    this.saveState();
+  },
+
+  getCustomizationTarget(aElement) {
+    if (!aElement) {
+      return null;
+    }
+
+    if (!aElement._customizationTarget && aElement.hasAttribute("customizable")) {
+      let id = aElement.getAttribute("customizationtarget");
+      if (id) {
+        aElement._customizationTarget = aElement.ownerDocument.getElementById(id);
+      }
+
+      if (!aElement._customizationTarget) {
+        aElement._customizationTarget = aElement;
+      }
+    }
+
+    return aElement._customizationTarget;
+  },
+
+  wrapWidget(aWidgetId) {
+    if (gGroupWrapperCache.has(aWidgetId)) {
+      return gGroupWrapperCache.get(aWidgetId);
+    }
+
+    let provider = this.getWidgetProvider(aWidgetId);
+    if (!provider) {
+      return null;
+    }
+
+    if (provider == CustomizableUI.PROVIDER_API) {
+      let widget = gPalette.get(aWidgetId);
+      if (!widget.wrapper) {
+        widget.wrapper = new WidgetGroupWrapper(widget);
+        gGroupWrapperCache.set(aWidgetId, widget.wrapper);
+      }
+      return widget.wrapper;
+    }
+
+    // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
+    // XXXgijs: this causes bugs in code that depends on widgetWrapper.provider
+    // giving an accurate answer... filed as bug 1379821
+    let wrapper = new XULWidgetGroupWrapper(aWidgetId);
+    gGroupWrapperCache.set(aWidgetId, wrapper);
+    return wrapper;
+  },
+
+  registerArea(aName, aProperties, aInternalCaller) {
+    if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
+      throw new Error("Invalid area name");
+    }
+
+    let areaIsKnown = gAreas.has(aName);
+    let props = areaIsKnown ? gAreas.get(aName) : new Map();
+    const kImmutableProperties = new Set(["type", "overflowable"]);
+    for (let key in aProperties) {
+      if (areaIsKnown && kImmutableProperties.has(key) &&
+          props.get(key) != aProperties[key]) {
+        throw new Error("An area cannot change the property for '" + key + "'");
+      }
+      props.set(key, aProperties[key]);
+    }
+    // Default to a toolbar:
+    if (!props.has("type")) {
+      props.set("type", CustomizableUI.TYPE_TOOLBAR);
+    }
+    if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
+      // Check aProperties instead of props because this check is only interested
+      // in the passed arguments, not the state of a potentially pre-existing area.
+      if (!aInternalCaller && aProperties.defaultCollapsed) {
+        throw new Error("defaultCollapsed is only allowed for default toolbars.");
+      }
+      if (!props.has("defaultCollapsed")) {
+        props.set("defaultCollapsed", true);
+      }
+    } else if (props.has("defaultCollapsed")) {
+      throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
+    }
+    // Sanity check type:
+    let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL];
+    if (!allTypes.includes(props.get("type"))) {
+      throw new Error("Invalid area type " + props.get("type"));
+    }
+
+    // And to no placements:
+    if (!props.has("defaultPlacements")) {
+      props.set("defaultPlacements", []);
+    }
+    // Sanity check default placements array:
+    if (!Array.isArray(props.get("defaultPlacements"))) {
+      throw new Error("Should provide an array of default placements");
+    }
+
+    if (!areaIsKnown) {
+      gAreas.set(aName, props);
+
+      // Reconcile new default widgets. Have to do this before we start restoring things.
+      this._placeNewDefaultWidgetsInArea(aName);
+
+      if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && !gPlacements.has(aName)) {
+        // Guarantee this area exists in gFuturePlacements, to avoid checking it in
+        // various places elsewhere.
+        if (!gFuturePlacements.has(aName)) {
+          gFuturePlacements.set(aName, new Set());
+        }
+      } else {
+        this.restoreStateForArea(aName);
+      }
+
+      // If we have pending build area nodes, register all of them
+      if (gPendingBuildAreas.has(aName)) {
+        let pendingNodes = gPendingBuildAreas.get(aName);
+        for (let pendingNode of pendingNodes) {
+          this.registerToolbarNode(pendingNode);
+        }
+        gPendingBuildAreas.delete(aName);
+      }
+    }
+  },
+
+  unregisterArea(aName, aDestroyPlacements) {
+    if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
+      throw new Error("Invalid area name");
+    }
+    if (!gAreas.has(aName) && !gPlacements.has(aName)) {
+      throw new Error("Area not registered");
+    }
+
+    // Move all the widgets out
+    this.beginBatchUpdate();
+    try {
+      let placements = gPlacements.get(aName);
+      if (placements) {
+        // Need to clone this array so removeWidgetFromArea doesn't modify it
+        placements = [...placements];
+        placements.forEach(this.removeWidgetFromArea, this);
+      }
+
+      // Delete all remaining traces.
+      gAreas.delete(aName);
+      // Only destroy placements when necessary:
+      if (aDestroyPlacements) {
+        gPlacements.delete(aName);
+      } else {
+        // Otherwise we need to re-set them, as removeFromArea will have emptied
+        // them out:
+        gPlacements.set(aName, placements);
+      }
+      gFuturePlacements.delete(aName);
+      let existingAreaNodes = gBuildAreas.get(aName);
+      if (existingAreaNodes) {
+        for (let areaNode of existingAreaNodes) {
+          this.notifyListeners("onAreaNodeUnregistered", aName,
+                               this.getCustomizationTarget(areaNode),
+                               CustomizableUI.REASON_AREA_UNREGISTERED);
+        }
+      }
+      gBuildAreas.delete(aName);
+    } finally {
+      this.endBatchUpdate(true);
+    }
+  },
+
+  registerToolbarNode(aToolbar) {
+    let area = aToolbar.id;
+    if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
+      return;
+    }
+    let areaProperties = gAreas.get(area);
+
+    // If this area is not registered, try to do it automatically:
+    if (!areaProperties) {
+      if (!gPendingBuildAreas.has(area)) {
+        gPendingBuildAreas.set(area, []);
+      }
+      gPendingBuildAreas.get(area).push(aToolbar);
+      return;
+    }
+
+    this.beginBatchUpdate();
+    try {
+      let placements = gPlacements.get(area);
+      if (!placements && areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR) {
+        this.restoreStateForArea(area);
+        placements = gPlacements.get(area);
+      }
+
+      // For toolbars that need it, mark as dirty.
+      let defaultPlacements = areaProperties.get("defaultPlacements");
+      if (!this._builtinToolbars.has(area) ||
+          placements.length != defaultPlacements.length ||
+          !placements.every((id, i) => id == defaultPlacements[i])) {
+        gDirtyAreaCache.add(area);
+      }
+
+      if (areaProperties.has("overflowable")) {
+        aToolbar.overflowable = new OverflowableToolbar(aToolbar);
+      }
+
+      this.registerBuildArea(area, aToolbar);
+
+      // We only build the toolbar if it's been marked as "dirty". Dirty means
+      // one of the following things:
+      // 1) Items have been added, moved or removed from this toolbar before.
+      // 2) The number of children of the toolbar does not match the length of
+      //    the placements array for that area.
+      //
+      // This notion of being "dirty" is stored in a cache which is persisted
+      // in the saved state.
+      if (gDirtyAreaCache.has(area)) {
+        this.buildArea(area, placements, aToolbar);
+      } else {
+        // We must have a builtin toolbar that's in the default state. We need
+        // to only make sure that all the special nodes are correct.
+        let specials = placements.filter(p => this.isSpecialWidget(p));
+        if (specials.length) {
+          this.updateSpecialsForBuiltinToolbar(aToolbar, specials);
+        }
+      }
+      this.notifyListeners("onAreaNodeRegistered", area,
+                           this.getCustomizationTarget(aToolbar));
+    } finally {
+      this.endBatchUpdate();
+    }
+  },
+
+  updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) {
+    // Nodes are going to be in the correct order, so we can do this straightforwardly:
+    let {children} = this.getCustomizationTarget(aToolbar);
+    for (let kid of children) {
+      if (this.matchingSpecials(aSpecialIDs[0], kid) &&
+          kid.getAttribute("skipintoolbarset") != "true") {
+        kid.id = aSpecialIDs.shift();
+      }
+      if (!aSpecialIDs.length) {
+        return;
+      }
+    }
+  },
+
+  buildArea(aArea, aPlacements, aAreaNode) {
+    let document = aAreaNode.ownerDocument;
+    let window = document.defaultView;
+    let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
+    let container = this.getCustomizationTarget(aAreaNode);
+    let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
+
+    if (!container) {
+      throw new Error("Expected area " + aArea
+                      + " to have a customizationTarget attribute.");
+    }
+
+    // Restore nav-bar visibility since it may have been hidden
+    // through a migration path (bug 938980) or an add-on.
+    if (aArea == CustomizableUI.AREA_NAVBAR) {
+      aAreaNode.collapsed = false;
+    }
+
+    this.beginBatchUpdate();
+
+    try {
+      let currentNode = container.firstElementChild;
+      let placementsToRemove = new Set();
+      for (let id of aPlacements) {
+        while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") {
+          currentNode = currentNode.nextElementSibling;
+        }
+
+        // Fix ids for specials and continue, for correctly placed specials.
+        if (currentNode && (!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) &&
+            this.matchingSpecials(id, currentNode)) {
+          currentNode.id = id;
+        }
+        if (currentNode && currentNode.id == id) {
+          currentNode = currentNode.nextElementSibling;
+          continue;
+        }
+
+        if (this.isSpecialWidget(id) && areaIsPanel) {
+          placementsToRemove.add(id);
+          continue;
+        }
+
+        let [provider, node] = this.getWidgetNode(id, window);
+        if (!node) {
+          log.debug("Unknown widget: " + id);
+          continue;
+        }
+
+        let widget = null;
+        // If the placements have items in them which are (now) no longer removable,
+        // we shouldn't be moving them:
+        if (provider == CustomizableUI.PROVIDER_API) {
+          widget = gPalette.get(id);
+          if (!widget.removable && aArea != widget.defaultArea) {
+            placementsToRemove.add(id);
+            continue;
+          }
+        } else if (provider == CustomizableUI.PROVIDER_XUL &&
+                   node.parentNode != container && !this.isWidgetRemovable(node)) {
+          placementsToRemove.add(id);
+          continue;
+        } // Special widgets are always removable, so no need to check them
+
+        if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) {
+          continue;
+        }
+
+        this.ensureButtonContextMenu(node, aAreaNode);
+
+        // This needs updating in case we're resetting / undoing a reset.
+        if (widget) {
+          widget.currentArea = aArea;
+        }
+        this.insertWidgetBefore(node, currentNode, container, aArea);
+        if (gResetting) {
+          this.notifyListeners("onWidgetReset", node, container);
+        } else if (gUndoResetting) {
+          this.notifyListeners("onWidgetUndoMove", node, container);
+        }
+      }
+
+      if (currentNode) {
+        let palette = window.gNavToolbox ? window.gNavToolbox.palette : null;
+        let limit = currentNode.previousElementSibling;
+        let node = container.lastElementChild;
+        while (node && node != limit) {
+          let previousSibling = node.previousElementSibling;
+          // Nodes opt-in to removability. If they're removable, and we haven't
+          // seen them in the placements array, then we toss them into the palette
+          // if one exists. If no palette exists, we just remove the node. If the
+          // node is not removable, we leave it where it is. However, we can only
+          // safely touch elements that have an ID - both because we depend on
+          // IDs (or are specials), and because such elements are not intended to
+          // be widgets (eg, titlebar-spacer elements).
+          if ((node.id || this.isSpecialWidget(node)) &&
+              node.getAttribute("skipintoolbarset") != "true") {
+            if (this.isWidgetRemovable(node)) {
+              if (node.id && (gResetting || gUndoResetting)) {
+                let widget = gPalette.get(node.id);
+                if (widget) {
+                  widget.currentArea = null;
+                }
+              }
+              if (palette && !this.isSpecialWidget(node.id)) {
+                palette.appendChild(node);
+                this.removeLocationAttributes(node);
+              } else {
+                container.removeChild(node);
+              }
+            } else {
+              node.setAttribute("removable", false);
+              log.debug("Adding non-removable widget to placements of " + aArea + ": " +
+                        node.id);
+              gPlacements.get(aArea).push(node.id);
+              gDirty = true;
+            }
+          }
+          node = previousSibling;
+        }
+      }
+
+      // If there are placements in here which aren't removable from their original area,
+      // we remove them from this area's placement array. They will (have) be(en) added
+      // to their original area's placements array in the block above this one.
+      if (placementsToRemove.size) {
+        let placementAry = gPlacements.get(aArea);
+        for (let id of placementsToRemove) {
+          let index = placementAry.indexOf(id);
+          placementAry.splice(index, 1);
+        }
+      }
+
+      if (gResetting) {
+        this.notifyListeners("onAreaReset", aArea, container);
+      }
+    } finally {
+      this.endBatchUpdate();
+    }
+  },
+
+  addPanelCloseListeners(aPanel) {
+    gELS.addSystemEventListener(aPanel, "click", this, false);
+    gELS.addSystemEventListener(aPanel, "keypress", this, false);
+    let win = aPanel.ownerGlobal;
+    if (!gPanelsForWindow.has(win)) {
+      gPanelsForWindow.set(win, new Set());
+    }
+    gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
+  },
+
+  removePanelCloseListeners(aPanel) {
+    gELS.removeSystemEventListener(aPanel, "click", this, false);
+    gELS.removeSystemEventListener(aPanel, "keypress", this, false);
+    let win = aPanel.ownerGlobal;
+    let panels = gPanelsForWindow.get(win);
+    if (panels) {
+      panels.delete(this._getPanelForNode(aPanel));
+    }
+  },
+
+  ensureButtonContextMenu(aNode, aAreaNode, forcePanel) {
+    const kPanelItemContextMenu = "customizationPanelItemContextMenu";
+
+    let currentContextMenu = aNode.getAttribute("context") ||
+                             aNode.getAttribute("contextmenu");
+    let contextMenuForPlace =
+      (forcePanel || "menu-panel" == CustomizableUI.getPlaceForItem(aAreaNode)) ?
+      kPanelItemContextMenu :
+      null;
+    if (contextMenuForPlace && !currentContextMenu) {
+      aNode.setAttribute("context", contextMenuForPlace);
+    } else if (currentContextMenu == kPanelItemContextMenu &&
+               contextMenuForPlace != kPanelItemContextMenu) {
+      aNode.removeAttribute("context");
+      aNode.removeAttribute("contextmenu");
+    }
+  },
+
+  getWidgetProvider(aWidgetId) {
+    if (this.isSpecialWidget(aWidgetId)) {
+      return CustomizableUI.PROVIDER_SPECIAL;
+    }
+    if (gPalette.has(aWidgetId)) {
+      return CustomizableUI.PROVIDER_API;
+    }
+    // If this was an API widget that was destroyed, return null:
+    if (gSeenWidgets.has(aWidgetId)) {
+      return null;
+    }
+
+    // We fall back to the XUL provider, but we don't know for sure (at this
+    // point) whether it exists there either. So the API is technically lying.
+    // Ideally, it would be able to return an error value (or throw an
+    // exception) if it really didn't exist. Our code calling this function
+    // handles that fine, but this is a public API.
+    return CustomizableUI.PROVIDER_XUL;
+  },
+
+  getWidgetNode(aWidgetId, aWindow) {
+    let document = aWindow.document;
+
+    if (this.isSpecialWidget(aWidgetId)) {
+      let widgetNode = document.getElementById(aWidgetId) ||
+                       this.createSpecialWidget(aWidgetId, document);
+      return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode];
+    }
+
+    let widget = gPalette.get(aWidgetId);
+    if (widget) {
+      // If we have an instance of this widget already, just use that.
+      if (widget.instances.has(document)) {
+        log.debug("An instance of widget " + aWidgetId + " already exists in this "
+                  + "document. Reusing.");
+        return [ CustomizableUI.PROVIDER_API,
+                 widget.instances.get(document) ];
+      }
+
+      return [ CustomizableUI.PROVIDER_API,
+               this.buildWidget(document, widget) ];
+    }
+
+    log.debug("Searching for " + aWidgetId + " in toolbox.");
+    let node = this.findWidgetInWindow(aWidgetId, aWindow);
+    if (node) {
+      return [ CustomizableUI.PROVIDER_XUL, node ];
+    }
+
+    log.debug("No node for " + aWidgetId + " found.");
+    return [null, null];
+  },
+
+  registerMenuPanel(aPanelContents, aArea) {
+    if (gBuildAreas.has(aArea) && gBuildAreas.get(aArea).has(aPanelContents)) {
+      return;
+    }
+
+    aPanelContents._customizationTarget = aPanelContents;
+    this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
+
+    let placements = gPlacements.get(aArea);
+    this.buildArea(aArea, placements, aPanelContents);
+    this.notifyListeners("onAreaNodeRegistered", aArea, aPanelContents);
+
+    for (let child of aPanelContents.children) {
+      if (child.localName != "toolbarbutton") {
+        if (child.localName == "toolbaritem") {
+          this.ensureButtonContextMenu(child, aPanelContents, true);
+        }
+        continue;
+      }
+      this.ensureButtonContextMenu(child, aPanelContents, true);
+    }
+
+    this.registerBuildArea(aArea, aPanelContents);
+  },
+
+  onWidgetAdded(aWidgetId, aArea, aPosition) {
+    this.insertNode(aWidgetId, aArea, aPosition, true);
+
+    if (!gResetting) {
+      this._clearPreviousUIState();
+    }
+  },
+
+  onWidgetRemoved(aWidgetId, aArea) {
+    let areaNodes = gBuildAreas.get(aArea);
+    if (!areaNodes) {
+      return;
+    }
+
+    let area = gAreas.get(aArea);
+    let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
+    let isOverflowable = isToolbar && area.get("overflowable");
+    let showInPrivateBrowsing = gPalette.has(aWidgetId)
+                              ? gPalette.get(aWidgetId).showInPrivateBrowsing
+                              : true;
+
+    for (let areaNode of areaNodes) {
+      let window = areaNode.ownerGlobal;
+      if (!showInPrivateBrowsing &&
+          PrivateBrowsingUtils.isWindowPrivate(window)) {
+        continue;
+      }
+
+      let container = this.getCustomizationTarget(areaNode);
+      let widgetNode = window.document.getElementById(aWidgetId);
+      if (widgetNode && isOverflowable) {
+        container = areaNode.overflowable.getContainerFor(widgetNode);
+      }
+
+      if (!widgetNode || !container.contains(widgetNode)) {
+        log.info("Widget " + aWidgetId + " not found, unable to remove from " + aArea);
+        continue;
+      }
+
+      this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
+
+      // We remove location attributes here to make sure they're gone too when a
+      // widget is removed from a toolbar to the palette. See bug 930950.
+      this.removeLocationAttributes(widgetNode);
+      // We also need to remove the panel context menu if it's there:
+      this.ensureButtonContextMenu(widgetNode);
+      if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
+        container.removeChild(widgetNode);
+      } else {
+        window.gNavToolbox.palette.appendChild(widgetNode);
+      }
+      this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
+
+      let windowCache = gSingleWrapperCache.get(window);
+      if (windowCache) {
+        windowCache.delete(aWidgetId);
+      }
+    }
+    if (!gResetting) {
+      this._clearPreviousUIState();
+    }
+  },
+
+  onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
+    this.insertNode(aWidgetId, aArea, aNewPosition);
+    if (!gResetting) {
+      this._clearPreviousUIState();
+    }
+  },
+
+  onCustomizeEnd(aWindow) {
+    this._clearPreviousUIState();
+  },
+
+  registerBuildArea(aArea, aNode) {
+    // We ensure that the window is registered to have its customization data
+    // cleaned up when unloading.
+    let window = aNode.ownerGlobal;
+    if (window.closed) {
+      return;
+    }
+    this.registerBuildWindow(window);
+
+    // Also register this build area's toolbox.
+    if (window.gNavToolbox) {
+      gBuildWindows.get(window).add(window.gNavToolbox);
+    }
+
+    if (!gBuildAreas.has(aArea)) {
+      gBuildAreas.set(aArea, new Set());
+    }
+
+    gBuildAreas.get(aArea).add(aNode);
+
+    // Give a class to all customize targets to be used for styling in Customize Mode
+    let customizableNode = this.getCustomizeTargetForArea(aArea, window);
+    customizableNode.classList.add("customization-target");
+  },
+
+  registerBuildWindow(aWindow) {
+    if (!gBuildWindows.has(aWindow)) {
+      gBuildWindows.set(aWindow, new Set());
+
+      aWindow.addEventListener("unload", this);
+      aWindow.addEventListener("command", this, true);
+
+      this.notifyListeners("onWindowOpened", aWindow);
+    }
+  },
+
+  unregisterBuildWindow(aWindow) {
+    aWindow.removeEventListener("unload", this);
+    aWindow.removeEventListener("command", this, true);
+    gPanelsForWindow.delete(aWindow);
+    gBuildWindows.delete(aWindow);
+    gSingleWrapperCache.delete(aWindow);
+    let document = aWindow.document;
+
+    for (let [areaId, areaNodes] of gBuildAreas) {
+      let areaProperties = gAreas.get(areaId);
+      for (let node of areaNodes) {
+        if (node.ownerDocument == document) {
+          this.notifyListeners("onAreaNodeUnregistered", areaId,
+                               this.getCustomizationTarget(node),
+                               CustomizableUI.REASON_WINDOW_CLOSED);
+          if (areaProperties.has("overflowable")) {
+            node.overflowable.uninit();
+            node.overflowable = null;
+          }
+          areaNodes.delete(node);
+        }
+      }
+    }
+
+    for (let [, widget] of gPalette) {
+      widget.instances.delete(document);
+      this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
+    }
+
+    for (let [, pendingNodes] of gPendingBuildAreas) {
+      for (let i = pendingNodes.length - 1; i >= 0; i--) {
+        if (pendingNodes[i].ownerDocument == document) {
+          pendingNodes.splice(i, 1);
+        }
+      }
+    }
+
+    this.notifyListeners("onWindowClosed", aWindow);
+  },
+
+  setLocationAttributes(aNode, aArea) {
+    let props = gAreas.get(aArea);
+    if (!props) {
+      throw new Error("Expected area " + aArea + " to have a properties Map " +
+                      "associated with it.");
+    }
+
+    aNode.setAttribute("cui-areatype", props.get("type") || "");
+    let anchor = props.get("anchor");
+    if (anchor) {
+      aNode.setAttribute("cui-anchorid", anchor);
+    } else {
+      aNode.removeAttribute("cui-anchorid");
+    }
+  },
+
+  removeLocationAttributes(aNode) {
+    aNode.removeAttribute("cui-areatype");
+    aNode.removeAttribute("cui-anchorid");
+  },
+
+  insertNode(aWidgetId, aArea, aPosition, isNew) {
+    let areaNodes = gBuildAreas.get(aArea);
+    if (!areaNodes) {
+      return;
+    }
+
+    let placements = gPlacements.get(aArea);
+    if (!placements) {
+      log.error("Could not find any placements for " + aArea +
+                " when moving a widget.");
+      return;
+    }
+
+    // Go through each of the nodes associated with this area and move the
+    // widget to the requested location.
+    for (let areaNode of areaNodes) {
+      this.insertNodeInWindow(aWidgetId, areaNode, isNew);
+    }
+  },
+
+  insertNodeInWindow(aWidgetId, aAreaNode, isNew) {
+    let window = aAreaNode.ownerGlobal;
+    let showInPrivateBrowsing = gPalette.has(aWidgetId)
+                              ? gPalette.get(aWidgetId).showInPrivateBrowsing
+                              : true;
+
+    if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) {
+      return;
+    }
+
+    let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
+    if (!widgetNode) {
+      log.error("Widget '" + aWidgetId + "' not found, unable to move");
+      return;
+    }
+
+    let areaId = aAreaNode.id;
+    if (isNew) {
+      this.ensureButtonContextMenu(widgetNode, aAreaNode);
+    }
+
+    let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode);
+    this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
+  },
+
+  findInsertionPoints(aNode, aAreaNode) {
+    let areaId = aAreaNode.id;
+    let props = gAreas.get(areaId);
+
+    // For overflowable toolbars, rely on them (because the work is more complicated):
+    if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) {
+      return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
+    }
+
+    let container = this.getCustomizationTarget(aAreaNode);
+    let placements = gPlacements.get(areaId);
+    let nodeIndex = placements.indexOf(aNode.id);
+
+    while (++nodeIndex < placements.length) {
+      let nextNodeId = placements[nodeIndex];
+      let nextNode = aNode.ownerDocument.getElementById(nextNodeId);
+      // If the next placed widget exists, and is a direct child of the
+      // container, or wrapped in a customize mode wrapper (toolbarpaletteitem)
+      // inside the container, insert beside it.
+      // We have to check the parent to avoid errors when the placement ids
+      // are for nodes that are no longer customizable.
+      if (nextNode && (nextNode.parentNode == container ||
+                       (nextNode.parentNode.localName == "toolbarpaletteitem" &&
+                        nextNode.parentNode.parentNode == container))) {
+        return [container, nextNode];
+      }
+    }
+
+    return [container, null];
+  },
+
+  insertWidgetBefore(aNode, aNextNode, aContainer, aArea) {
+    this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer);
+    this.setLocationAttributes(aNode, aArea);
+    aContainer.insertBefore(aNode, aNextNode);
+    this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer);
+  },
+
+  handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "command":
+        if (!this._originalEventInPanel(aEvent)) {
+          break;
+        }
+        aEvent = aEvent.sourceEvent;
+        // Fall through
+      case "click":
+      case "keypress":
+        this.maybeAutoHidePanel(aEvent);
+        break;
+      case "unload":
+        this.unregisterBuildWindow(aEvent.currentTarget);
+        break;
+    }
+  },
+
+  _originalEventInPanel(aEvent) {
+    let e = aEvent.sourceEvent;
+    if (!e) {
+      return false;
+    }
+    let node = this._getPanelForNode(e.target);
+    if (!node) {
+      return false;
+    }
+    let win = e.view;
+    let panels = gPanelsForWindow.get(win);
+    return !!panels && panels.has(node);
+  },
+
+  _getSpecialIdForNode(aNode) {
+    if (typeof aNode == "object" && aNode.localName) {
+      if (aNode.id) {
+        return aNode.id;
+      }
+      if (aNode.localName.startsWith("toolbar")) {
+        return aNode.localName.substring(7);
+      }
+      return "";
+    }
+    return aNode;
+  },
+
+  isSpecialWidget(aId) {
+    aId = this._getSpecialIdForNode(aId);
+    return (aId.startsWith(kSpecialWidgetPfx) ||
+            aId.startsWith("separator") ||
+            aId.startsWith("spring") ||
+            aId.startsWith("spacer"));
+  },
+
+  matchingSpecials(aId1, aId2) {
+    aId1 = this._getSpecialIdForNode(aId1);
+    aId2 = this._getSpecialIdForNode(aId2);
+
+    return this.isSpecialWidget(aId1) &&
+           this.isSpecialWidget(aId2) &&
+           aId1.match(/spring|spacer|separator/)[0] == aId2.match(/spring|spacer|separator/)[0];
+  },
+
+  ensureSpecialWidgetId(aId) {
+    let nodeType = aId.match(/spring|spacer|separator/)[0];
+    // If the ID we were passed isn't a generated one, generate one now:
+    if (nodeType == aId) {
+      // Ids are differentiated through a unique count suffix.
+      return kSpecialWidgetPfx + aId + (++gNewElementCount);
+    }
+    return aId;
+  },
+
+  createSpecialWidget(aId, aDocument) {
+    let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
+    let node = aDocument.createXULElement(nodeName);
+    node.className = "chromeclass-toolbar-additional";
+    node.id = this.ensureSpecialWidgetId(aId);
+    return node;
+  },
+
+  /* Find a XUL-provided widget in a window. Don't try to use this
+   * for an API-provided widget or a special widget.
+   */
+  findWidgetInWindow(aId, aWindow) {
+    if (!gBuildWindows.has(aWindow)) {
+      throw new Error("Build window not registered");
+    }
+
+    if (!aId) {
+      log.error("findWidgetInWindow was passed an empty string.");
+      return null;
+    }
+
+    let document = aWindow.document;
+
+    // look for a node with the same id, as the node may be
+    // in a different toolbar.
+    let node = document.getElementById(aId);
+    if (node) {
+      let parent = node.parentNode;
+      while (parent && !(this.getCustomizationTarget(parent) ||
+                         parent == aWindow.gNavToolbox.palette)) {
+        parent = parent.parentNode;
+      }
+
+      if (parent) {
+        let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ?
+                         node.parentNode : node;
+        // Check if we're in a customization target, or in the palette:
+        if ((this.getCustomizationTarget(parent) == nodeInArea.parentNode &&
+             gBuildWindows.get(aWindow).has(aWindow.gNavToolbox)) ||
+            aWindow.gNavToolbox.palette == nodeInArea.parentNode) {
+          // Normalize the removable attribute. For backwards compat, if
+          // the widget is not located in a toolbox palette then absence
+          // of the "removable" attribute means it is not removable.
+          if (!node.hasAttribute("removable")) {
+            // If we first see this in customization mode, it may be in the
+            // customization palette instead of the toolbox palette.
+            node.setAttribute("removable", !this.getCustomizationTarget(parent));
+          }
+          return node;
+        }
+      }
+    }
+
+    let toolboxes = gBuildWindows.get(aWindow);
+    for (let toolbox of toolboxes) {
+      if (toolbox.palette) {
+        // Attempt to locate an element with a matching ID within
+        // the palette.
+        let element = toolbox.palette.getElementsByAttribute("id", aId)[0];
+        if (element) {
+          // Normalize the removable attribute. For backwards compat, this
+          // is optional if the widget is located in the toolbox palette,
+          // and defaults to *true*, unlike if it was located elsewhere.
+          if (!element.hasAttribute("removable")) {
+            element.setAttribute("removable", true);
+          }
+          return element;
+        }
+      }
+    }
+    return null;
+  },
+
+  buildWidget(aDocument, aWidget) {
+    if (aDocument.documentURI != kExpectedWindowURL) {
+      throw new Error("buildWidget was called for a non-browser window!");
+    }
+    if (typeof aWidget == "string") {
+      aWidget = gPalette.get(aWidget);
+    }
+    if (!aWidget) {
+      throw new Error("buildWidget was passed a non-widget to build.");
+    }
+    if (!aWidget.showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)) {
+      return null;
+    }
+
+    log.debug("Building " + aWidget.id + " of type " + aWidget.type);
+
+    let node;
+    if (aWidget.type == "custom") {
+      if (aWidget.onBuild) {
+        node = aWidget.onBuild(aDocument);
+      }
+      if (!node || !(node instanceof aDocument.defaultView.XULElement))
+        log.error("Custom widget with id " + aWidget.id + " does not return a valid node");
+    } else {
+      if (aWidget.onBeforeCreated) {
+        aWidget.onBeforeCreated(aDocument);
+      }
+      node = aDocument.createXULElement("toolbarbutton");
+
+      node.setAttribute("id", aWidget.id);
+      node.setAttribute("widget-id", aWidget.id);
+      node.setAttribute("widget-type", aWidget.type);
+      if (aWidget.disabled) {
+        node.setAttribute("disabled", true);
+      }
+      node.setAttribute("removable", aWidget.removable);
+      node.setAttribute("overflows", aWidget.overflows);
+      if (aWidget.tabSpecific) {
+        node.setAttribute("tabspecific", aWidget.tabSpecific);
+      }
+      node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
+      let additionalTooltipArguments = [];
+      if (aWidget.shortcutId) {
+        let keyEl = aDocument.getElementById(aWidget.shortcutId);
+        if (keyEl) {
+          additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
+        } else {
+          log.error("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
+                    "' not found!");
+        }
+      }
+
+      let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments);
+      if (tooltip) {
+        node.setAttribute("tooltiptext", tooltip);
+      }
+
+      let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
+      node.addEventListener("command", commandHandler);
+      let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
+      node.addEventListener("click", clickHandler);
+
+      let nodeClasses = ["toolbarbutton-1", "chromeclass-toolbar-additional"];
+
+      // If the widget has a view, and has view showing / hiding listeners,
+      // hook those up to this widget.
+      if (aWidget.type == "view") {
+        log.debug("Widget " + aWidget.id + " has a view. Auto-registering event handlers.");
+        let viewNode = aDocument.getElementById(aWidget.viewId);
+
+        if (viewNode) {
+          // PanelUI relies on the .PanelUI-subView class to be able to show only
+          // one sub-view at a time.
+          viewNode.classList.add("PanelUI-subView");
+          if (aWidget.source == CustomizableUI.SOURCE_BUILTIN) {
+            nodeClasses.push("subviewbutton-nav");
+          }
+          this.ensureSubviewListeners(viewNode);
+        } else {
+          log.error("Could not find the view node with id: " + aWidget.viewId +
+                    ", for widget: " + aWidget.id + ".");
+        }
+
+        let keyPressHandler = this.handleWidgetKeyPress.bind(this, aWidget, node);
+        node.addEventListener("keypress", keyPressHandler);
+      }
+      node.setAttribute("class", nodeClasses.join(" "));
+
+      if (aWidget.onCreated) {
+        aWidget.onCreated(node);
+      }
+    }
+
+    aWidget.instances.set(aDocument, node);
+    return node;
+  },
+
+  ensureSubviewListeners(viewNode) {
+    if (viewNode._addedEventListeners) {
+      return;
+    }
+    let viewId = viewNode.id;
+    let widget = [...gPalette.values()].find(w => w.viewId == viewId);
+    if (!widget) {
+      return;
+    }
+    for (let eventName of kSubviewEvents) {
+      let handler = "on" + eventName;
+      if (typeof widget[handler] == "function") {
+        viewNode.addEventListener(eventName, widget[handler]);
+      }
+    }
+    viewNode._addedEventListeners = true;
+    log.debug("Widget " + widget.id + " showing and hiding event handlers set.");
+  },
+
+  getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
+    const kReqStringProps = ["label"];
+
+    if (typeof aWidget == "string") {
+      aWidget = gPalette.get(aWidget);
+    }
+    if (!aWidget) {
+      throw new Error("getLocalizedProperty was passed a non-widget to work with.");
+    }
+    let def, name;
+    // Let widgets pass their own string identifiers or strings, so that
+    // we can use strings which aren't the default (in case string ids change)
+    // and so that non-builtin-widgets can also provide labels, tooltips, etc.
+    if (aWidget[aProp] != null) {
+      name = aWidget[aProp];
+      // By using this as the default, if a widget provides a full string rather
+      // than a string ID for localization, we will fall back to that string
+      // and return that.
+      def = aDef || name;
+    } else {
+      name = aWidget.id + "." + aProp;
+      def = aDef || "";
+    }
+    if (aWidget.localized === false) {
+      return def;
+    }
+    try {
+      if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
+        return gWidgetsBundle.formatStringFromName(name, aFormatArgs,
+          aFormatArgs.length) || def;
+      }
+      return gWidgetsBundle.GetStringFromName(name) || def;
+    } catch (ex) {
+      // If an empty string was explicitly passed, treat it as an actual
+      // value rather than a missing property.
+      if (!def && (name != "" || kReqStringProps.includes(aProp))) {
+        log.error("Could not localize property '" + name + "'.");
+      }
+    }
+    return def;
+  },
+
+  addShortcut(aShortcutNode, aTargetNode = aShortcutNode) {
+    // Detect if we've already been here before.
+    if (aTargetNode.hasAttribute("shortcut"))
+      return;
+
+    let document = aShortcutNode.ownerDocument;
+    let shortcutId = aShortcutNode.getAttribute("key");
+    let shortcut;
+    if (shortcutId) {
+      shortcut = document.getElementById(shortcutId);
+    } else {
+      let commandId = aShortcutNode.getAttribute("command");
+      if (commandId)
+        shortcut = ShortcutUtils.findShortcut(document.getElementById(commandId));
+    }
+    if (!shortcut) {
+      return;
+    }
+
+    aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
+  },
+
+  handleWidgetCommand(aWidget, aNode, aEvent) {
+    // Note that aEvent can be a keypress event for widgets of type "view".
+    log.debug("handleWidgetCommand");
+
+    if (aWidget.onBeforeCommand) {
+      try {
+        aWidget.onBeforeCommand.call(null, aEvent);
+      } catch (e) {
+        log.error(e);
+      }
+    }
+
+    if (aWidget.type == "button") {
+      if (aWidget.onCommand) {
+        try {
+          aWidget.onCommand.call(null, aEvent);
+        } catch (e) {
+          log.error(e);
+        }
+      } else {
+        // XXXunf Need to think this through more, and formalize.
+        Services.obs.notifyObservers(aNode,
+                                     "customizedui-widget-command",
+                                     aWidget.id);
+      }
+    } else if (aWidget.type == "view") {
+      let ownerWindow = aNode.ownerGlobal;
+      let area = this.getPlacementOfWidget(aNode.id).area;
+      let areaType = CustomizableUI.getAreaType(area);
+      let anchor = aNode;
+      if (areaType != CustomizableUI.TYPE_MENU_PANEL) {
+        let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
+
+        let hasMultiView = !!aNode.closest("panelmultiview");
+        if (wrapper && !hasMultiView && wrapper.anchor) {
+          this.hidePanelForNode(aNode);
+          anchor = wrapper.anchor;
+        }
+      }
+
+      ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, aEvent);
+    }
+  },
+
+  handleWidgetClick(aWidget, aNode, aEvent) {
+    log.debug("handleWidgetClick");
+    if (aWidget.onClick) {
+      try {
+        aWidget.onClick.call(null, aEvent);
+      } catch (e) {
+        Cu.reportError(e);
+      }
+    } else {
+      // XXXunf Need to think this through more, and formalize.
+      Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id);
+    }
+  },
+
+  handleWidgetKeyPress(aWidget, aNode, aEvent) {
+    if (aEvent.key != " " && aEvent.key != "Enter") {
+      return;
+    }
+    aEvent.stopPropagation();
+    aEvent.preventDefault();
+    this.handleWidgetCommand(aWidget, aNode, aEvent);
+  },
+
+  _getPanelForNode(aNode) {
+    return aNode.closest("panel");
+  },
+
+  /*
+   * If people put things in the panel which need more than single-click interaction,
+   * we don't want to close it. Right now we check for text inputs and menu buttons.
+   * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
+   * part of the menu.
+   */
+  _isOnInteractiveElement(aEvent) {
+    function getMenuPopupForDescendant(aNode) {
+      let lastPopup = null;
+      while (aNode && aNode.parentNode &&
+             aNode.parentNode.localName.startsWith("menu")) {
+        lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
+        aNode = aNode.parentNode;
+      }
+      return lastPopup;
+    }
+
+    let target = aEvent.originalTarget;
+    let panel = this._getPanelForNode(aEvent.currentTarget);
+    // This can happen in e.g. customize mode. If there's no panel,
+    // there's clearly nothing for us to close; pretend we're interactive.
+    if (!panel) {
+      return true;
+    }
+    // We keep track of:
+    // whether we're in an input container (text field)
+    let inInput = false;
+    // whether we're in a popup/context menu
+    let inMenu = false;
+    // whether we're in a toolbarbutton/toolbaritem
+    let inItem = false;
+    // whether the current menuitem has a valid closemenu attribute
+    let menuitemCloseMenu = "auto";
+
+    // While keeping track of that, we go from the original target back up,
+    // to the panel if we have to. We bail as soon as we find an input,
+    // a toolbarbutton/item, or the panel:
+    while (true && target) {
+      // Skip out of iframes etc:
+      if (target.nodeType == target.DOCUMENT_NODE) {
+        if (!target.defaultView) {
+          // Err, we're done.
+          break;
+        }
+        // Find containing browser or iframe element in the parent doc.
+        target = target.defaultView.docShell.chromeEventHandler;
+        if (!target) {
+          break;
+        }
+      }
+      let tagName = target.localName;
+      inInput = tagName == "input" || tagName == "textbox";
+      inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
+      let isMenuItem = tagName == "menuitem";
+      inMenu = inMenu || isMenuItem;
+
+      if (isMenuItem && target.hasAttribute("closemenu")) {
+        let closemenuVal = target.getAttribute("closemenu");
+        menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ?
+                            closemenuVal : "auto";
+      }
+      // Break out of the loop immediately for disabled items, as we need to
+      // keep the menu open in that case.
+      if (target.getAttribute("disabled") == "true") {
+        return true;
+      }
+
+      // This isn't in the loop condition because we want to break before
+      // changing |target| if any of these conditions are true
+      if (inInput || inItem || target == panel) {
+        break;
+      }
+      // We need specific code for popups: the item on which they were invoked
+      // isn't necessarily in their parentNode chain:
+      if (isMenuItem) {
+        let topmostMenuPopup = getMenuPopupForDescendant(target);
+        target = (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
+                 target.parentNode;
+      } else {
+        target = target.parentNode;
+      }
+    }
+
+    // If the user clicked a menu item...
+    if (inMenu) {
+      // We care if we're in an input also,
+      // or if the user specified closemenu!="auto":
+      if (inInput || menuitemCloseMenu != "auto") {
+        return true;
+      }
+      // Otherwise, we're probably fine to close the panel
+      return false;
+    }
+    // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
+    // we'll now interact with the menu
+    if (inItem && target.getAttribute("type") == "menu") {
+      return true;
+    }
+    return inInput || !inItem;
+  },
+
+  hidePanelForNode(aNode) {
+    let panel = this._getPanelForNode(aNode);
+    if (panel) {
+      PanelMultiView.hidePopup(panel);
+    }
+  },
+
+  maybeAutoHidePanel(aEvent) {
+    if (aEvent.type == "keypress") {
+      if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
+        return;
+      }
+      // If the user hit enter/return, we don't check preventDefault - it makes sense
+      // that this was prevented, but we probably still want to close the panel.
+      // If consumers don't want this to happen, they should specify the closemenu
+      // attribute.
+    } else if (aEvent.type != "command") { // mouse events:
+      if (aEvent.defaultPrevented || aEvent.button != 0) {
+        return;
+      }
+      let isInteractive = this._isOnInteractiveElement(aEvent);
+      log.debug("maybeAutoHidePanel: interactive ? " + isInteractive);
+      if (isInteractive) {
+        return;
+      }
+    }
+
+    // We can't use event.target because we might have passed an anonymous
+    // content boundary as well, and so target points to the outer element in
+    // that case. Unfortunately, this means we get anonymous child nodes instead
+    // of the real ones, so looking for the 'stoooop, don't close me' attributes
+    // is more involved.
+    let target = aEvent.originalTarget;
+    while (target.parentNode && target.localName != "panel") {
+      if (target.getAttribute("closemenu") == "none" ||
+          target.getAttribute("widget-type") == "view") {
+        return;
+      }
+      target = target.parentNode;
+    }
+
+    // If we get here, we can actually hide the popup:
+    this.hidePanelForNode(aEvent.target);
+  },
+
+  getUnusedWidgets(aWindowPalette) {
+    let window = aWindowPalette.ownerGlobal;
+    let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
+    // We use a Set because there can be overlap between the widgets in
+    // gPalette and the items in the palette, especially after the first
+    // customization, since programmatically generated widgets will remain
+    // in the toolbox palette.
+    let widgets = new Set();
+
+    // It's possible that some widgets have been defined programmatically and
+    // have not been overlayed into the palette. We can find those inside
+    // gPalette.
+    for (let [id, widget] of gPalette) {
+      if (!widget.currentArea) {
+        if (widget.showInPrivateBrowsing || !isWindowPrivate) {
+          widgets.add(id);
+        }
+      }
+    }
+
+    log.debug("Iterating the actual nodes of the window palette");
+    for (let node of aWindowPalette.children) {
+      log.debug("In palette children: " + node.id);
+      if (node.id && !this.getPlacementOfWidget(node.id)) {
+        widgets.add(node.id);
+      }
+    }
+
+    return [...widgets];
+  },
+
+  getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas) {
+    if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
+      return null;
+    }
+
+    for (let [area, placements] of gPlacements) {
+      if (!gAreas.has(area) && !aDeadAreas) {
+        continue;
+      }
+      let index = placements.indexOf(aWidgetId);
+      if (index != -1) {
+        return { area, position: index };
+      }
+    }
+
+    return null;
+  },
+
+  widgetExists(aWidgetId) {
+    if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
+      return true;
+    }
+
+    // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
+    if (gSeenWidgets.has(aWidgetId)) {
+      return false;
+    }
+
+    // We're assuming XUL widgets always exist, as it's much harder to check,
+    // and checking would be much more error prone.
+    return true;
+  },
+
+  addWidgetToArea(aWidgetId, aArea, aPosition, aInitialAdd) {
+    if (!gAreas.has(aArea)) {
+      throw new Error("Unknown customization area: " + aArea);
+    }
+
+    // Hack: don't want special widgets in the panel (need to check here as well
+    // as in canWidgetMoveToArea because the menu panel is lazy):
+    if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL &&
+        this.isSpecialWidget(aWidgetId)) {
+      return;
+    }
+
+    // If this is a lazy area that hasn't been restored yet, we can't yet modify
+    // it - would would at least like to add to it. So we keep track of it in
+    // gFuturePlacements,  and use that to add it when restoring the area. We
+    // throw away aPosition though, as that can only be bogus if the area hasn't
+    // yet been restorted (caller can't possibly know where its putting the
+    // widget in relation to other widgets).
+    if (this.isAreaLazy(aArea)) {
+      gFuturePlacements.get(aArea).add(aWidgetId);
+      return;
+    }
+
+    if (this.isSpecialWidget(aWidgetId)) {
+      aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
+    }
+
+    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
+    if (oldPlacement && oldPlacement.area == aArea) {
+      this.moveWidgetWithinArea(aWidgetId, aPosition);
+      return;
+    }
+
+    // Do nothing if the widget is not allowed to move to the target area.
+    if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
+      return;
+    }
+
+    if (oldPlacement) {
+      this.removeWidgetFromArea(aWidgetId);
+    }
+
+    if (!gPlacements.has(aArea)) {
+      gPlacements.set(aArea, [aWidgetId]);
+      aPosition = 0;
+    } else {
+      let placements = gPlacements.get(aArea);
+      if (typeof aPosition != "number") {
+        aPosition = placements.length;
+      }
+      if (aPosition < 0) {
+        aPosition = 0;
+      }
+      placements.splice(aPosition, 0, aWidgetId);
+    }
+
+    let widget = gPalette.get(aWidgetId);
+    if (widget) {
+      widget.currentArea = aArea;
+      widget.currentPosition = aPosition;
+    }
+
+    // We initially set placements with addWidgetToArea, so in that case
+    // we don't consider the area "dirtied".
+    if (!aInitialAdd) {
+      gDirtyAreaCache.add(aArea);
+    }
+
+    gDirty = true;
+    this.saveState();
+
+    this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
+  },
+
+  removeWidgetFromArea(aWidgetId) {
+    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
+    if (!oldPlacement) {
+      return;
+    }
+
+    if (!this.isWidgetRemovable(aWidgetId)) {
+      return;
+    }
+
+    let placements = gPlacements.get(oldPlacement.area);
+    let position = placements.indexOf(aWidgetId);
+    if (position != -1) {
+      placements.splice(position, 1);
+    }
+
+    let widget = gPalette.get(aWidgetId);
+    if (widget) {
+      widget.currentArea = null;
+      widget.currentPosition = null;
+    }
+
+    gDirty = true;
+    this.saveState();
+    gDirtyAreaCache.add(oldPlacement.area);
+
+    this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
+  },
+
+  moveWidgetWithinArea(aWidgetId, aPosition) {
+    let oldPlacement = this.getPlacementOfWidget(aWidgetId);
+    if (!oldPlacement) {
+      return;
+    }
+
+    let placements = gPlacements.get(oldPlacement.area);
+    if (typeof aPosition != "number") {
+      aPosition = placements.length;
+    } else if (aPosition < 0) {
+      aPosition = 0;
+    } else if (aPosition > placements.length) {
+      aPosition = placements.length;
+    }
+
+    let widget = gPalette.get(aWidgetId);
+    if (widget) {
+      widget.currentPosition = aPosition;
+      widget.currentArea = oldPlacement.area;
+    }
+
+    if (aPosition == oldPlacement.position) {
+      return;
+    }
+
+    placements.splice(oldPlacement.position, 1);
+    // If we just removed the item from *before* where it is now added,
+    // we need to compensate the position offset for that:
+    if (oldPlacement.position < aPosition) {
+      aPosition--;
+    }
+    placements.splice(aPosition, 0, aWidgetId);
+
+    gDirty = true;
+    gDirtyAreaCache.add(oldPlacement.area);
+
+    this.saveState();
+
+    this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area,
+                         oldPlacement.position, aPosition);
+  },
+
+  // Note that this does not populate gPlacements, which is done lazily.
+  // The panel area is an exception here.
+  loadSavedState() {
+    let state = Services.prefs.getCharPref(kPrefCustomizationState, "");
+    if (!state) {
+      log.debug("No saved state found");
+      // Nothing has been customized, so silently fall back to the defaults.
+      return;
+    }
+    try {
+      gSavedState = JSON.parse(state);
+      if (typeof gSavedState != "object" || gSavedState === null) {
+        throw new Error("Invalid saved state");
+      }
+    } catch (e) {
+      Services.prefs.clearUserPref(kPrefCustomizationState);
+      gSavedState = {};
+      log.debug("Error loading saved UI customization state, falling back to defaults.");
+    }
+
+    if (!("placements" in gSavedState)) {
+      gSavedState.placements = {};
+    }
+
+    if (!("currentVersion" in gSavedState)) {
+      gSavedState.currentVersion = 0;
+    }
+
+    gSeenWidgets = new Set(gSavedState.seen || []);
+    gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
+    gNewElementCount = gSavedState.newElementCount || 0;
+  },
+
+  restoreStateForArea(aArea) {
+    let placementsPreexisted = gPlacements.has(aArea);
+
+    this.beginBatchUpdate();
+    try {
+      gRestoring = true;
+
+      let restored = false;
+      if (placementsPreexisted) {
+        log.debug("Restoring " + aArea + " from pre-existing placements");
+        for (let [position, id] of gPlacements.get(aArea).entries()) {
+          this.moveWidgetWithinArea(id, position);
+        }
+        gDirty = false;
+        restored = true;
+      } else {
+        gPlacements.set(aArea, []);
+      }
+
+      if (!restored && gSavedState && aArea in gSavedState.placements) {
+        log.debug("Restoring " + aArea + " from saved state");
+        let placements = gSavedState.placements[aArea];
+        for (let id of placements)
+          this.addWidgetToArea(id, aArea);
+        gDirty = false;
+        restored = true;
+      }
+
+      if (!restored) {
+        log.debug("Restoring " + aArea + " from default state");
+        let defaults = gAreas.get(aArea).get("defaultPlacements");
+        if (defaults) {
+          for (let id of defaults)
+            this.addWidgetToArea(id, aArea, null, true);
+        }
+        gDirty = false;
+      }
+
+      // Finally, add widgets to the area that were added before the it was able
+      // to be restored. This can occur when add-ons register widgets for a
+      // lazily-restored area before it's been restored.
+      if (gFuturePlacements.has(aArea)) {
+        for (let id of gFuturePlacements.get(aArea))
+          this.addWidgetToArea(id, aArea);
+        gFuturePlacements.delete(aArea);
+      }
+
+      log.debug("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
+
+      gRestoring = false;
+    } finally {
+      this.endBatchUpdate();
+    }
+  },
+
+  saveState() {
+    if (gInBatchStack || !gDirty) {
+      return;
+    }
+    // Clone because we want to modify this map:
+    let state = { placements: new Map(gPlacements),
+                  seen: gSeenWidgets,
+                  dirtyAreaCache: gDirtyAreaCache,
+                  currentVersion: kVersion,
+                  newElementCount: gNewElementCount };
+
+    // Merge in previously saved areas if not present in gPlacements.
+    // This way, state is still persisted for e.g. temporarily disabled
+    // add-ons - see bug 989338.
+    if (gSavedState && gSavedState.placements) {
+      for (let area of Object.keys(gSavedState.placements)) {
+        if (!state.placements.has(area)) {
+          let placements = gSavedState.placements[area];
+          state.placements.set(area, placements);
+        }
+      }
+    }
+
+    log.debug("Saving state.");
+    let serialized = JSON.stringify(state, this.serializerHelper);
+    log.debug("State saved as: " + serialized);
+    Services.prefs.setCharPref(kPrefCustomizationState, serialized);
+    gDirty = false;
+  },
+
+  serializerHelper(aKey, aValue) {
+    if (typeof aValue == "object" && aValue.constructor.name == "Map") {
+      let result = {};
+      for (let [mapKey, mapValue] of aValue)
+        result[mapKey] = mapValue;
+      return result;
+    }
+
+    if (typeof aValue == "object" && aValue.constructor.name == "Set") {
+      return [...aValue];
+    }
+
+    return aValue;
+  },
+
+  beginBatchUpdate() {
+    gInBatchStack++;
+  },
+
+  endBatchUpdate(aForceDirty) {
+    gInBatchStack--;
+    if (aForceDirty === true) {
+      gDirty = true;
+    }
+    if (gInBatchStack == 0) {
+      this.saveState();
+    } else if (gInBatchStack < 0) {
+      throw new Error("The batch editing stack should never reach a negative number.");
+    }
+  },
+
+  addListener(aListener) {
+    gListeners.add(aListener);
+  },
+
+  removeListener(aListener) {
+    if (aListener == this) {
+      return;
+    }
+
+    gListeners.delete(aListener);
+  },
+
+  notifyListeners(aEvent, ...aArgs) {
+    if (gRestoring) {
+      return;
+    }
+
+    for (let listener of gListeners) {
+      try {
+        if (typeof listener[aEvent] == "function") {
+          listener[aEvent].apply(listener, aArgs);
+        }
+      } catch (e) {
+        log.error(e + " -- " + e.fileName + ":" + e.lineNumber);
+      }
+    }
+  },
+
+  _dispatchToolboxEventToWindow(aEventType, aDetails, aWindow) {
+    let evt = new aWindow.CustomEvent(aEventType, {
+      bubbles: true,
+      cancelable: true,
+      detail: aDetails,
+    });
+    aWindow.gNavToolbox.dispatchEvent(evt);
+  },
+
+  dispatchToolboxEvent(aEventType, aDetails = {}, aWindow = null) {
+    if (aWindow) {
+      this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
+      return;
+    }
+    for (let [win ] of gBuildWindows) {
+      this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
+    }
+  },
+
+  createWidget(aProperties) {
+    let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL);
+    // XXXunf This should probably throw.
+    if (!widget) {
+      log.error("unable to normalize widget");
+      return undefined;
+    }
+
+    gPalette.set(widget.id, widget);
+
+    // Clear our caches:
+    gGroupWrapperCache.delete(widget.id);
+    for (let [win ] of gBuildWindows) {
+      let cache = gSingleWrapperCache.get(win);
+      if (cache) {
+        cache.delete(widget.id);
+      }
+    }
+
+    this.notifyListeners("onWidgetCreated", widget.id);
+
+    if (widget.defaultArea) {
+      let addToDefaultPlacements = false;
+      let area = gAreas.get(widget.defaultArea);
+      if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
+          widget.defaultArea != CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
+        addToDefaultPlacements = true;
+      }
+
+      if (addToDefaultPlacements) {
+        if (area.has("defaultPlacements")) {
+          area.get("defaultPlacements").push(widget.id);
+        } else {
+          area.set("defaultPlacements", [widget.id]);
+        }
+      }
+    }
+
+    // Look through previously saved state to see if we're restoring a widget.
+    let seenAreas = new Set();
+    let widgetMightNeedAutoAdding = true;
+    for (let [area ] of gPlacements) {
+      seenAreas.add(area);
+      let areaIsRegistered = gAreas.has(area);
+      let index = gPlacements.get(area).indexOf(widget.id);
+      if (index != -1) {
+        widgetMightNeedAutoAdding = false;
+        if (areaIsRegistered) {
+          widget.currentArea = area;
+          widget.currentPosition = index;
+        }
+        break;
+      }
+    }
+
+    // Also look at saved state data directly in areas that haven't yet been
+    // restored. Can't rely on this for restored areas, as they may have
+    // changed.
+    if (widgetMightNeedAutoAdding && gSavedState) {
+      for (let area of Object.keys(gSavedState.placements)) {
+        if (seenAreas.has(area)) {
+          continue;
+        }
+
+        let areaIsRegistered = gAreas.has(area);
+        let index = gSavedState.placements[area].indexOf(widget.id);
+        if (index != -1) {
+          widgetMightNeedAutoAdding = false;
+          if (areaIsRegistered) {
+            widget.currentArea = area;
+            widget.currentPosition = index;
+          }
+          break;
+        }
+      }
+    }
+
+    // If we're restoring the widget to it's old placement, fire off the
+    // onWidgetAdded event - our own handler will take care of adding it to
+    // any build areas.
+    this.beginBatchUpdate();
+    try {
+      if (widget.currentArea) {
+        this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea,
+                             widget.currentPosition);
+      } else if (widgetMightNeedAutoAdding) {
+        let autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd, true);
+
+        // If the widget doesn't have an existing placement, and it hasn't been
+        // seen before, then add it to its default area so it can be used.
+        // If the widget is not removable, we *have* to add it to its default
+        // area here.
+        let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
+        if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
+          if (widget.defaultArea) {
+            if (this.isAreaLazy(widget.defaultArea)) {
+              gFuturePlacements.get(widget.defaultArea).add(widget.id);
+            } else {
+              this.addWidgetToArea(widget.id, widget.defaultArea);
+            }
+          }
+        }
+      }
+    } finally {
+      // Ensure we always have this widget in gSeenWidgets, and save
+      // state in case this needs to be done here.
+      gSeenWidgets.add(widget.id);
+      this.endBatchUpdate(true);
+    }
+
+    this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea);
+    return widget.id;
+  },
+
+  createBuiltinWidget(aData) {
+    // This should only ever be called on startup, before any windows are
+    // opened - so we know there's no build areas to handle. Also, builtin
+    // widgets are expected to be (mostly) static, so shouldn't affect the
+    // current placement settings.
+
+    // This allows a widget to be both built-in by default but also able to be
+    // destroyed and removed from the area based on criteria that may not be
+    // available when the widget is created -- for example, because some other
+    // feature in the browser supersedes the widget.
+    let conditionalDestroyPromise = aData.conditionalDestroyPromise || null;
+    delete aData.conditionalDestroyPromise;
+
+    let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
+    if (!widget) {
+      log.error("Error creating builtin widget: " + aData.id);
+      return;
+    }
+
+    log.debug("Creating built-in widget with id: " + widget.id);
+    gPalette.set(widget.id, widget);
+
+    if (conditionalDestroyPromise) {
+      conditionalDestroyPromise.then(shouldDestroy => {
+        if (shouldDestroy) {
+          this.destroyWidget(widget.id);
+          this.removeWidgetFromArea(widget.id);
+        }
+      }, err => {
+        Cu.reportError(err);
+      });
+    }
+  },
+
+  // Returns true if the area will eventually lazily restore (but hasn't yet).
+  isAreaLazy(aArea) {
+    if (gPlacements.has(aArea)) {
+      return false;
+    }
+    return gAreas.get(aArea).get("type") == CustomizableUI.TYPE_TOOLBAR;
+  },
+
+  // XXXunf Log some warnings here, when the data provided isn't up to scratch.
+  normalizeWidget(aData, aSource) {
+    let widget = {
+      implementation: aData,
+      source: aSource || CustomizableUI.SOURCE_EXTERNAL,
+      instances: new Map(),
+      currentArea: null,
+      localized: true,
+      removable: true,
+      overflows: true,
+      defaultArea: null,
+      shortcutId: null,
+      tabSpecific: false,
+      tooltiptext: null,
+      showInPrivateBrowsing: true,
+      _introducedInVersion: -1,
+    };
+
+    if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
+      log.error("Given an illegal id in normalizeWidget: " + aData.id);
+      return null;
+    }
+
+    delete widget.implementation.currentArea;
+    widget.implementation.__defineGetter__("currentArea", () => widget.currentArea);
+
+    const kReqStringProps = ["id"];
+    for (let prop of kReqStringProps) {
+      if (typeof aData[prop] != "string") {
+        log.error("Missing required property '" + prop + "' in normalizeWidget: "
+                  + aData.id);
+        return null;
+      }
+      widget[prop] = aData[prop];
+    }
+
+    const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
+    for (let prop of kOptStringProps) {
+      if (typeof aData[prop] == "string") {
+        widget[prop] = aData[prop];
+      }
+    }
+
+    const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows", "tabSpecific",
+                           "localized"];
+    for (let prop of kOptBoolProps) {
+      if (typeof aData[prop] == "boolean") {
+        widget[prop] = aData[prop];
+      }
+    }
+
+    // When we normalize builtin widgets, areas have not yet been registered:
+    if (aData.defaultArea &&
+        (aSource == CustomizableUI.SOURCE_BUILTIN || gAreas.has(aData.defaultArea))) {
+      widget.defaultArea = aData.defaultArea;
+    } else if (!widget.removable) {
+      log.error("Widget '" + widget.id + "' is not removable but does not specify " +
+                "a valid defaultArea. That's not possible; it must specify a " +
+                "valid defaultArea as well.");
+      return null;
+    }
+
+    if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
+      widget.type = aData.type;
+    } else {
+      widget.type = "button";
+    }
+
+    widget.disabled = aData.disabled === true;
+
+    if (aSource == CustomizableUI.SOURCE_BUILTIN) {
+      widget._introducedInVersion = aData.introducedInVersion || 0;
+    }
+
+    this.wrapWidgetEventHandler("onBeforeCreated", widget);
+    this.wrapWidgetEventHandler("onClick", widget);
+    this.wrapWidgetEventHandler("onCreated", widget);
+    this.wrapWidgetEventHandler("onDestroyed", widget);
+
+    if (typeof aData.onBeforeCommand == "function") {
+      widget.onBeforeCommand = aData.onBeforeCommand;
+    }
+
+    if (widget.type == "button") {
+      widget.onCommand = typeof aData.onCommand == "function" ?
+                           aData.onCommand :
+                           null;
+    } else if (widget.type == "view") {
+      if (typeof aData.viewId != "string") {
+        log.error("Expected a string for widget " + widget.id + " viewId, but got "
+                  + aData.viewId);
+        return null;
+      }
+      widget.viewId = aData.viewId;
+
+      this.wrapWidgetEventHandler("onViewShowing", widget);
+      this.wrapWidgetEventHandler("onViewHiding", widget);
+    } else if (widget.type == "custom") {
+      this.wrapWidgetEventHandler("onBuild", widget);
+    }
+
+    if (gPalette.has(widget.id)) {
+      return null;
+    }
+
+    return widget;
+  },
+
+  wrapWidgetEventHandler(aEventName, aWidget) {
+    if (typeof aWidget.implementation[aEventName] != "function") {
+      aWidget[aEventName] = null;
+      return;
+    }
+    aWidget[aEventName] = function(...aArgs) {
+      // Wrap inside a try...catch to properly log errors, until bug 862627 is
+      // fixed, which in turn might help bug 503244.
+      try {
+        // Don't copy the function to the normalized widget object, instead
+        // keep it on the original object provided to the API so that
+        // additional methods can be implemented and used by the event
+        // handlers.
+        return aWidget.implementation[aEventName].apply(aWidget.implementation,
+                                                        aArgs);
+      } catch (e) {
+        Cu.reportError(e);
+        return undefined;
+      }
+    };
+  },
+
+  destroyWidget(aWidgetId) {
+    let widget = gPalette.get(aWidgetId);
+    if (!widget) {
+      gGroupWrapperCache.delete(aWidgetId);
+      for (let [window ] of gBuildWindows) {
+        let windowCache = gSingleWrapperCache.get(window);
+        if (windowCache) {
+          windowCache.delete(aWidgetId);
+        }
+      }
+      return;
+    }
+
+    // Remove it from the default placements of an area if it was added there:
+    if (widget.defaultArea) {
+      let area = gAreas.get(widget.defaultArea);
+      if (area) {
+        let defaultPlacements = area.get("defaultPlacements");
+        // We can assume this is present because if a widget has a defaultArea,
+        // we automatically create a defaultPlacements array for that area.
+        let widgetIndex = defaultPlacements.indexOf(aWidgetId);
+        if (widgetIndex != -1) {
+          defaultPlacements.splice(widgetIndex, 1);
+        }
+      }
+    }
+
+    // This will not remove the widget from gPlacements - we want to keep the
+    // setting so the widget gets put back in it's old position if/when it
+    // returns.
+    for (let [window ] of gBuildWindows) {
+      let windowCache = gSingleWrapperCache.get(window);
+      if (windowCache) {
+        windowCache.delete(aWidgetId);
+      }
+      let widgetNode = window.document.getElementById(aWidgetId) ||
+                       window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
+      if (widgetNode) {
+        let container = widgetNode.parentNode;
+        this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null,
+                             container, true);
+        widgetNode.remove();
+        this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null,
+                             container, true);
+      }
+      if (widget.type == "view") {
+        let viewNode = window.document.getElementById(widget.viewId);
+        if (viewNode) {
+          for (let eventName of kSubviewEvents) {
+            let handler = "on" + eventName;
+            if (typeof widget[handler] == "function") {
+              viewNode.removeEventListener(eventName, widget[handler]);
+            }
+          }
+        }
+      }
+      if (widgetNode && widget.onDestroyed) {
+        widget.onDestroyed(window.document);
+      }
+    }
+
+    gPalette.delete(aWidgetId);
+    gGroupWrapperCache.delete(aWidgetId);
+
+    this.notifyListeners("onWidgetDestroyed", aWidgetId);
+  },
+
+  getCustomizeTargetForArea(aArea, aWindow) {
+    let buildAreaNodes = gBuildAreas.get(aArea);
+    if (!buildAreaNodes) {
+      return null;
+    }
+
+    for (let node of buildAreaNodes) {
+      if (node.ownerGlobal == aWindow) {
+        return this.getCustomizationTarget(node) || node;
+      }
+    }
+
+    return null;
+  },
+
+  reset() {
+    gResetting = true;
+    this._resetUIState();
+
+    // Rebuild each registered area (across windows) to reflect the state that
+    // was reset above.
+    this._rebuildRegisteredAreas();
+
+    for (let [widgetId, widget] of gPalette) {
+      if (widget.source == CustomizableUI.SOURCE_EXTERNAL) {
+        gSeenWidgets.add(widgetId);
+      }
+    }
+    if (gSeenWidgets.size || gNewElementCount) {
+      gDirty = true;
+      this.saveState();
+    }
+
+    gResetting = false;
+  },
+
+  _resetUIState() {
+    try {
+      // kPrefDrawInTitlebar may not be defined on Linux/Gtk+ which throws an exception
+      // and leads to whole test failure. Let's set a fallback default value to avoid that,
+      // both titlebar states are tested anyway and it's not important which state is tested first.
+      gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar, false);
+      gUIStateBeforeReset.extraDragSpace = Services.prefs.getBoolPref(kPrefExtraDragSpace);
+      gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
+      gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity);
+      gUIStateBeforeReset.autoTouchMode = Services.prefs.getBoolPref(kPrefAutoTouchMode);
+      gUIStateBeforeReset.currentTheme = gSelectedTheme;
+      gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref(kPrefAutoHideDownloadsButton);
+      gUIStateBeforeReset.newElementCount = gNewElementCount;
+    } catch (e) { }
+
+    Services.prefs.clearUserPref(kPrefCustomizationState);
+    Services.prefs.clearUserPref(kPrefDrawInTitlebar);
+    Services.prefs.clearUserPref(kPrefExtraDragSpace);
+    Services.prefs.clearUserPref(kPrefUIDensity);
+    Services.prefs.clearUserPref(kPrefAutoTouchMode);
+    Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton);
+    gDefaultTheme.enable();
+    gNewElementCount = 0;
+    log.debug("State reset");
+
+    // Reset placements to make restoring default placements possible.
+    gPlacements = new Map();
+    gDirtyAreaCache = new Set();
+    gSeenWidgets = new Set();
+    // Clear the saved state to ensure that defaults will be used.
+    gSavedState = null;
+    // Restore the state for each area to its defaults
+    for (let [areaId ] of gAreas) {
+      this.restoreStateForArea(areaId);
+    }
+  },
+
+  _rebuildRegisteredAreas() {
+    for (let [areaId, areaNodes] of gBuildAreas) {
+      let placements = gPlacements.get(areaId);
+      let isFirstChangedToolbar = true;
+      for (let areaNode of areaNodes) {
+        this.buildArea(areaId, placements, areaNode);
+
+        let area = gAreas.get(areaId);
+        if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
+          let defaultCollapsed = area.get("defaultCollapsed");
+          let win = areaNode.ownerGlobal;
+          if (defaultCollapsed !== null) {
+            win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar);
+          }
+        }
+        isFirstChangedToolbar = false;
+      }
+    }
+  },
+
+  /**
+   * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
+   */
+  undoReset() {
+    if (gUIStateBeforeReset.uiCustomizationState == null ||
+        gUIStateBeforeReset.drawInTitlebar == null) {
+      return;
+    }
+    gUndoResetting = true;
+
+    const {
+      uiCustomizationState, drawInTitlebar, currentTheme, uiDensity,
+      autoTouchMode, autoHideDownloadsButton, extraDragSpace,
+    } = 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.setBoolPref(kPrefExtraDragSpace, extraDragSpace);
+    Services.prefs.setIntPref(kPrefUIDensity, uiDensity);
+    Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode);
+    Services.prefs.setBoolPref(kPrefAutoHideDownloadsButton, autoHideDownloadsButton);
+    currentTheme.enable();
+    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);
+      }
+      this._rebuildRegisteredAreas();
+    }
+
+    gUndoResetting = false;
+  },
+
+  _clearPreviousUIState() {
+    Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
+      gUIStateBeforeReset[prop] = null;
+    });
+  },
+
+  /**
+   * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
+   * @return {Boolean} whether the widget is removable
+   */
+  isWidgetRemovable(aWidget) {
+    let widgetId;
+    let widgetNode;
+    if (typeof aWidget == "string") {
+      widgetId = aWidget;
+    } else {
+      // Skipped items could just not have ids.
+      if (!aWidget.id && aWidget.getAttribute("skipintoolbarset") == "true") {
+        return false;
+      }
+      if (!aWidget.id &&
+          !["toolbarspring", "toolbarspacer", "toolbarseparator"].includes(aWidget.nodeName)) {
+        throw new Error("No nodes without ids that aren't special widgets should ever come into contact with CUI");
+      }
+      // Use "spring" / "spacer" / "separator" for special widgets without ids
+      widgetId = aWidget.id || aWidget.nodeName.substring(7 /* "toolbar".length */);
+      widgetNode = aWidget;
+    }
+    let provider = this.getWidgetProvider(widgetId);
+
+    if (provider == CustomizableUI.PROVIDER_API) {
+      return gPalette.get(widgetId).removable;
+    }
+
+    if (provider == CustomizableUI.PROVIDER_XUL) {
+      if (gBuildWindows.size == 0) {
+        // We don't have any build windows to look at, so just assume for now
+        // that its removable.
+        return true;
+      }
+
+      if (!widgetNode) {
+        // Pick any of the build windows to look at.
+        let [window ] = [...gBuildWindows][0];
+        [, widgetNode] = this.getWidgetNode(widgetId, window);
+      }
+      // If we don't have a node, we assume it's removable. This can happen because
+      // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen
+      // for API-provided widgets which have been destroyed.
+      if (!widgetNode) {
+        return true;
+      }
+      return widgetNode.getAttribute("removable") == "true";
+    }
+
+    // Otherwise this is either a special widget, which is always removable, or
+    // an API widget which has already been removed from gPalette. Returning true
+    // here allows us to then remove its ID from any placements where it might
+    // still occur.
+    return true;
+  },
+
+  canWidgetMoveToArea(aWidgetId, aArea) {
+    // Special widgets can't move to the menu panel.
+    if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) &&
+        gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) {
+      return false;
+    }
+    let placement = this.getPlacementOfWidget(aWidgetId);
+    // Items in the palette can move, and items can move within their area:
+    if (!placement || placement.area == aArea) {
+      return true;
+    }
+    // For everything else, just return whether the widget is removable.
+    return this.isWidgetRemovable(aWidgetId);
+  },
+
+  ensureWidgetPlacedInWindow(aWidgetId, aWindow) {
+    let placement = this.getPlacementOfWidget(aWidgetId);
+    if (!placement) {
+      return false;
+    }
+    let areaNodes = gBuildAreas.get(placement.area);
+    if (!areaNodes) {
+      return false;
+    }
+    let container = [...areaNodes].filter((n) => n.ownerGlobal == aWindow);
+    if (!container.length) {
+      return false;
+    }
+    let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
+    if (existingNode) {
+      return true;
+    }
+
+    this.insertNodeInWindow(aWidgetId, container[0], true);
+    return true;
+  },
+
+  _getCurrentWidgetsInContainer(container) {
+    // Get a list of all the widget IDs in this container, including any that
+    // are overflown.
+    let currentWidgets = new Set();
+    function addUnskippedChildren(parent) {
+      for (let node of parent.children) {
+        let realNode = node.localName == "toolbarpaletteitem" ? node.firstElementChild : node;
+        if (realNode.getAttribute("skipintoolbarset") != "true") {
+          currentWidgets.add(realNode.id);
+        }
+      }
+    }
+    addUnskippedChildren(this.getCustomizationTarget(container));
+    if (container.getAttribute("overflowing") == "true") {
+      let overflowTarget = container.getAttribute("overflowtarget");
+      addUnskippedChildren(container.ownerDocument.getElementById(overflowTarget));
+    }
+    // Then get the sorted list of placements, and filter based on the nodes
+    // that are present. This avoids including items that don't exist (e.g. ids
+    // of add-on items that the user has uninstalled).
+    let orderedPlacements = CustomizableUI.getWidgetIdsInArea(container.id);
+    return orderedPlacements.filter(w => currentWidgets.has(w));
+  },
+
+  get inDefaultState() {
+    for (let [areaId, props] of gAreas) {
+      let defaultPlacements = props.get("defaultPlacements");
+      let currentPlacements = gPlacements.get(areaId);
+      // We're excluding all of the placement IDs for items that do not exist,
+      // and items that have removable="false",
+      // because we don't want to consider them when determining if we're
+      // in the default state. This way, if an add-on introduces a widget
+      // and is then uninstalled, the leftover placement doesn't cause us to
+      // automatically assume that the buttons are not in the default state.
+      let buildAreaNodes = gBuildAreas.get(areaId);
+      if (buildAreaNodes && buildAreaNodes.size) {
+        let container = [...buildAreaNodes][0];
+        let removableOrDefault = (itemNodeOrItem) => {
+          let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
+          let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
+          let isInDefault = defaultPlacements.includes(item);
+          return isRemovable || isInDefault;
+        };
+        // Toolbars need to deal with overflown widgets (if any) - so
+        // specialcase them:
+        if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
+          currentPlacements =
+            this._getCurrentWidgetsInContainer(container).filter(removableOrDefault);
+        } else {
+          currentPlacements = currentPlacements.filter((item) => {
+            let itemNode = container.getElementsByAttribute("id", item)[0];
+            return itemNode && removableOrDefault(itemNode || item);
+          });
+        }
+
+        if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
+          let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
+          let collapsed = container.getAttribute(attribute) == "true";
+          let defaultCollapsed = props.get("defaultCollapsed");
+          if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
+            log.debug("Found " + areaId + " had non-default toolbar visibility" +
+                      "(expected " + defaultCollapsed + ", was " + collapsed + ")");
+            return false;
+          }
+        }
+      }
+      log.debug("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
+                "\nvs.\n" + defaultPlacements.join(","));
+
+      if (currentPlacements.length != defaultPlacements.length) {
+        return false;
+      }
+
+      for (let i = 0; i < currentPlacements.length; ++i) {
+        if (currentPlacements[i] != defaultPlacements[i] &&
+            !this.matchingSpecials(currentPlacements[i], defaultPlacements[i])) {
+          log.debug("Found " + currentPlacements[i] + " in " + areaId + " where " +
+                    defaultPlacements[i] + " was expected!");
+          return false;
+        }
+      }
+    }
+
+    if (Services.prefs.prefHasUserValue(kPrefUIDensity)) {
+      log.debug(kPrefUIDensity + " pref is non-default");
+      return false;
+    }
+
+    if (Services.prefs.prefHasUserValue(kPrefAutoTouchMode)) {
+      log.debug(kPrefAutoTouchMode + " pref is non-default");
+      return false;
+    }
+
+    if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
+      log.debug(kPrefDrawInTitlebar + " pref is non-default");
+      return false;
+    }
+
+    if (Services.prefs.prefHasUserValue(kPrefExtraDragSpace)) {
+      log.debug(kPrefExtraDragSpace + " pref is non-default");
+      return false;
+    }
+
+    // This should just be `gDefaultTheme.isActive`, but bugs...
+    if (gDefaultTheme && gDefaultTheme.id != gSelectedTheme.id) {
+      log.debug(gSelectedTheme.id + " theme is non-default");
+      return false;
+    }
+
+    return true;
+  },
+
+  setToolbarVisibility(aToolbarId, aIsVisible) {
+    // We only persist the attribute the first time.
+    let isFirstChangedToolbar = true;
+    for (let window of CustomizableUI.windows) {
+      let toolbar = window.document.getElementById(aToolbarId);
+      if (toolbar) {
+        window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
+        isFirstChangedToolbar = false;
+      }
+    }
+  },
+};
+Object.freeze(CustomizableUIInternal);
+
+var CustomizableUI = {
+  /**
+   * Constant reference to the ID of the navigation toolbar.
+   */
+  AREA_NAVBAR: "nav-bar",
+  /**
+   * Constant reference to the ID of the menubar's toolbar.
+   */
+  AREA_MENUBAR: "toolbar-menubar",
+  /**
+   * Constant reference to the ID of the tabstrip toolbar.
+   */
+  AREA_TABSTRIP: "TabsToolbar",
+  /**
+   * Constant reference to the ID of the bookmarks toolbar.
+   */
+  AREA_BOOKMARKS: "PersonalToolbar",
+  /**
+   * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
+   */
+  AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
+
+  /**
+   * Constant indicating the area is a menu panel.
+   */
+  TYPE_MENU_PANEL: "menu-panel",
+  /**
+   * Constant indicating the area is a toolbar.
+   */
+  TYPE_TOOLBAR: "toolbar",
+
+  /**
+   * Constant indicating a XUL-type provider.
+   */
+  PROVIDER_XUL: "xul",
+  /**
+   * Constant indicating an API-type provider.
+   */
+  PROVIDER_API: "api",
+  /**
+   * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
+   */
+  PROVIDER_SPECIAL: "special",
+
+  /**
+   * Constant indicating the widget is built-in
+   */
+  SOURCE_BUILTIN: "builtin",
+  /**
+   * Constant indicating the widget is externally provided
+   * (e.g. by add-ons or other items not part of the builtin widget set).
+   */
+  SOURCE_EXTERNAL: "external",
+
+  /**
+   * Constant indicating the reason the event was fired was a window closing
+   */
+  REASON_WINDOW_CLOSED: "window-closed",
+  /**
+   * Constant indicating the reason the event was fired was an area being
+   * unregistered separately from window closing mechanics.
+   */
+  REASON_AREA_UNREGISTERED: "area-unregistered",
+
+
+  /**
+   * An iteratable property of windows managed by CustomizableUI.
+   * Note that this can *only* be used as an iterator. ie:
+   *     for (let window of CustomizableUI.windows) { ... }
+   */
+  windows: {
+    * [Symbol.iterator]() {
+      for (let [window ] of gBuildWindows)
+        yield window;
+    },
+  },
+
+  /**
+   * Add a listener object that will get fired for various events regarding
+   * customization.
+   *
+   * @param aListener the listener object to add
+   *
+   * Not all event handler methods need to be defined.
+   * CustomizableUI will catch exceptions. Events are dispatched
+   * synchronously on the UI thread, so if you can delay any/some of your
+   * processing, that is advisable. The following event handlers are supported:
+   *   - onWidgetAdded(aWidgetId, aArea, aPosition)
+   *     Fired when a widget is added to an area. aWidgetId is the widget that
+   *     was added, aArea the area it was added to, and aPosition the position
+   *     in which it was added.
+   *   - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition)
+   *     Fired when a widget is moved within its area. aWidgetId is the widget
+   *     that was moved, aArea the area it was moved in, aOldPosition its old
+   *     position, and aNewPosition its new position.
+   *   - onWidgetRemoved(aWidgetId, aArea)
+   *     Fired when a widget is removed from its area. aWidgetId is the widget
+   *     that was removed, aArea the area it was removed from.
+   *
+   *   - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval)
+   *     Fired *before* a widget's DOM node is acted upon by CustomizableUI
+   *     (to add, move or remove it). aNode is the DOM node changed, aNextNode
+   *     the DOM node (if any) before which a widget will be inserted,
+   *     aContainer the *actual* DOM container (could be an overflow panel in
+   *     case of an overflowable toolbar), and aWasRemoval is true iff the
+   *     action about to happen is the removal of the DOM node.
+   *   - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval)
+   *     Like onWidgetBeforeDOMChange, but fired after the change to the DOM
+   *     node of the widget.
+   *
+   *   - onWidgetReset(aNode, aContainer)
+   *     Fired after a reset to default placements moves a widget's node to a
+   *     different location. aNode is the widget's node, aContainer is the
+   *     area it was moved into (NB: it might already have been there and been
+   *     moved to a different position!)
+   *   - onWidgetUndoMove(aNode, aContainer)
+   *     Fired after undoing a reset to default placements moves a widget's
+   *     node to a different location. aNode is the widget's node, aContainer
+   *     is the area it was moved into (NB: it might already have been there
+   *     and been moved to a different position!)
+   *   - onAreaReset(aArea, aContainer)
+   *     Fired after a reset to default placements is complete on an area's
+   *     DOM node. Note that this is fired for each DOM node. aArea is the area
+   *     that was reset, aContainer the DOM node that was reset.
+   *
+   *   - onWidgetCreated(aWidgetId)
+   *     Fired when a widget with id aWidgetId has been created, but before it
+   *     is added to any placements or any DOM nodes have been constructed.
+   *     Only fired for API-based widgets.
+   *   - onWidgetAfterCreation(aWidgetId, aArea)
+   *     Fired after a widget with id aWidgetId has been created, and has been
+   *     added to either its default area or the area in which it was placed
+   *     previously. If the widget has no default area and/or it has never
+   *     been placed anywhere, aArea may be null. Only fired for API-based
+   *     widgets.
+   *   - onWidgetDestroyed(aWidgetId)
+   *     Fired when widgets are destroyed. aWidgetId is the widget that is
+   *     being destroyed. Only fired for API-based widgets.
+   *   - onWidgetInstanceRemoved(aWidgetId, aDocument)
+   *     Fired when a window is unloaded and a widget's instance is destroyed
+   *     because of this. Only fired for API-based widgets.
+   *
+   *   - onWidgetDrag(aWidgetId, aArea)
+   *     Fired both when and after customize mode drag handling system tries
+   *     to determine the width and height of widget aWidgetId when dragged to a
+   *     different area. aArea will be the area the item is dragged to, or
+   *     undefined after the measurements have been done and the node has been
+   *     moved back to its 'regular' area.
+   *
+   *   - onCustomizeStart(aWindow)
+   *     Fired when opening customize mode in aWindow.
+   *   - onCustomizeEnd(aWindow)
+   *     Fired when exiting customize mode in aWindow.
+   *
+   *   - onWidgetOverflow(aNode, aContainer)
+   *     Fired when a widget's DOM node is overflowing its container, a toolbar,
+   *     and will be displayed in the overflow panel.
+   *   - onWidgetUnderflow(aNode, aContainer)
+   *     Fired when a widget's DOM node is *not* overflowing its container, a
+   *     toolbar, anymore.
+   *   - onWindowOpened(aWindow)
+   *     Fired when a window has been opened that is managed by CustomizableUI,
+   *     once all of the prerequisite setup has been done.
+   *   - onWindowClosed(aWindow)
+   *     Fired when a window that has been managed by CustomizableUI has been
+   *     closed.
+   *   - onAreaNodeRegistered(aArea, aContainer)
+   *     Fired after an area node is first built when it is registered. This
+   *     is often when the window has opened, but in the case of add-ons,
+   *     could fire when the node has just been registered with CustomizableUI
+   *     after an add-on update or disable/enable sequence.
+   *   - onAreaNodeUnregistered(aArea, aContainer, aReason)
+   *     Fired when an area node is explicitly unregistered by an API caller,
+   *     or by a window closing. The aReason parameter indicates which of
+   *     these is the case.
+   */
+  addListener(aListener) {
+    CustomizableUIInternal.addListener(aListener);
+  },
+  /**
+   * Remove a listener added with addListener
+   * @param aListener the listener object to remove
+   */
+  removeListener(aListener) {
+    CustomizableUIInternal.removeListener(aListener);
+  },
+
+  /**
+   * Register a customizable area with CustomizableUI.
+   * @param aName   the name of the area to register. Can only contain
+   *                alphanumeric characters, dashes (-) and underscores (_).
+   * @param aProps  the properties of the area. The following properties are
+   *                recognized:
+   *                - type:   the type of area. Either TYPE_TOOLBAR (default) or
+   *                          TYPE_MENU_PANEL;
+   *                - anchor: for a menu panel or overflowable toolbar, the
+   *                          anchoring node for the panel.
+   *                - overflowable: set to true if your toolbar is overflowable.
+   *                                This requires an anchor, and only has an
+   *                                effect for toolbars.
+   *                - defaultPlacements: an array of widget IDs making up the
+   *                                     default contents of the area
+   *                - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
+   *                                    if toolbar is collapsed by default (default to true).
+   *                                    Specify null to ensure that reset/inDefaultArea don't care
+   *                                    about a toolbar's collapsed state
+   */
+  registerArea(aName, aProperties) {
+    CustomizableUIInternal.registerArea(aName, aProperties);
+  },
+  /**
+   * Register a concrete node for a registered area. This method needs to be called
+   * with any toolbar in the main browser window that has its "customizable" attribute
+   * set to true.
+   *
+   * Note that ideally, you should register your toolbar using registerArea
+   * before calling this. If you don't, the node will be saved for processing when
+   * you call registerArea. Note that CustomizableUI won't restore state in the area,
+   * allow the user to customize it in customize mode, or otherwise deal
+   * with it, until the area has been registered.
+   */
+  registerToolbarNode(aToolbar) {
+    CustomizableUIInternal.registerToolbarNode(aToolbar);
+  },
+  /**
+   * Register the menu panel node. This method should not be called by anyone
+   * apart from the built-in PanelUI.
+   * @param aPanelContents the panel contents DOM node being registered.
+   * @param aArea the area for which to register this node.
+   */
+  registerMenuPanel(aPanelContents, aArea) {
+    CustomizableUIInternal.registerMenuPanel(aPanelContents, aArea);
+  },
+  /**
+   * Unregister a customizable area. The inverse of registerArea.
+   *
+   * Unregistering an area will remove all the (removable) widgets in the
+   * area, which will return to the panel, and destroy all other traces
+   * of the area within CustomizableUI. Note that this means the *contents*
+   * of the area's DOM nodes will be moved to the panel or removed, but
+   * the area's DOM nodes *themselves* will stay.
+   *
+   * Furthermore, by default the placements of the area will be kept in the
+   * saved state (!) and restored if you re-register the area at a later
+   * point. This is useful for e.g. add-ons that get disabled and then
+   * re-enabled (e.g. when they update).
+   *
+   * You can override this last behaviour (and destroy the placements
+   * information in the saved state) by passing true for aDestroyPlacements.
+   *
+   * @param aName              the name of the area to unregister
+   * @param aDestroyPlacements whether to destroy the placements information
+   *                           for the area, too.
+   */
+  unregisterArea(aName, aDestroyPlacements) {
+    CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
+  },
+  /**
+   * Add a widget to an area.
+   * If the area to which you try to add is not known to CustomizableUI,
+   * this will throw.
+   * If the area to which you try to add is the same as the area in which
+   * the widget is currently placed, this will do the same as
+   * moveWidgetWithinArea.
+   * If the widget cannot be removed from its original location, this will
+   * no-op.
+   *
+   * This will fire an onWidgetAdded notification,
+   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
+   * for each window CustomizableUI knows about.
+   *
+   * @param aWidgetId the ID of the widget to add
+   * @param aArea     the ID of the area to add the widget to
+   * @param aPosition the position at which to add the widget. If you do not
+   *                  pass a position, the widget will be added to the end
+   *                  of the area.
+   */
+  addWidgetToArea(aWidgetId, aArea, aPosition) {
+    CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
+  },
+  /**
+   * Remove a widget from its area. If the widget cannot be removed from its
+   * area, or is not in any area, this will no-op. Otherwise, this will fire an
+   * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
+   * onWidgetAfterDOMChange notification for each window CustomizableUI knows
+   * about.
+   *
+   * @param aWidgetId the ID of the widget to remove
+   */
+  removeWidgetFromArea(aWidgetId) {
+    CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
+  },
+  /**
+   * Move a widget within an area.
+   * If the widget is not in any area, this will no-op.
+   * If the widget is already at the indicated position, this will no-op.
+   *
+   * Otherwise, this will move the widget and fire an onWidgetMoved notification,
+   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
+   * each window CustomizableUI knows about.
+   *
+   * @param aWidgetId the ID of the widget to move
+   * @param aPosition the position to move the widget to.
+   *                  Negative values or values greater than the number of
+   *                  widgets will be interpreted to mean moving the widget to
+   *                  respectively the first or last position.
+   */
+  moveWidgetWithinArea(aWidgetId, aPosition) {
+    CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
+  },
+  /**
+   * Ensure a XUL-based widget created in a window after areas were
+   * initialized moves to its correct position.
+   * Always prefer this over moving items in the DOM yourself.
+   *
+   * @param aWidgetId the ID of the widget that was just created
+   * @param aWindow the window in which you want to ensure it was added.
+   *
+   * NB: why is this API per-window, you wonder? Because if you need this,
+   * presumably you yourself need to create the widget in all the windows
+   * and need to loop through them anyway.
+   */
+  ensureWidgetPlacedInWindow(aWidgetId, aWindow) {
+    return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow);
+  },
+  /**
+   * Start a batch update of items.
+   * During a batch update, the customization state is not saved to the user's
+   * preferences file, in order to reduce (possibly sync) IO.
+   * Calls to begin/endBatchUpdate may be nested.
+   *
+   * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
+   * for each call to beginBatchUpdate, even if there are exceptions in the
+   * code in the batch update. Otherwise, for the duration of the
+   * Firefox session, customization state is never saved. Typically, you
+   * would do this using a try...finally block.
+   */
+  beginBatchUpdate() {
+    CustomizableUIInternal.beginBatchUpdate();
+  },
+  /**
+   * End a batch update. See the documentation for beginBatchUpdate above.
+   *
+   * State is not saved if we believe it is identical to the last known
+   * saved state. State is only ever saved when all batch updates have
+   * finished (ie there has been 1 endBatchUpdate call for each
+   * beginBatchUpdate call). If any of the endBatchUpdate calls pass
+   * aForceDirty=true, we will flush to the prefs file.
+   *
+   * @param aForceDirty force CustomizableUI to flush to the prefs file when
+   *                    all batch updates have finished.
+   */
+  endBatchUpdate(aForceDirty) {
+    CustomizableUIInternal.endBatchUpdate(aForceDirty);
+  },
+  /**
+   * Create a widget.
+   *
+   * To create a widget, you should pass an object with its desired
+   * properties. The following properties are supported:
+   *
+   * - id:            the ID of the widget (required).
+   * - type:          a string indicating the type of widget. Possible types
+   *                  are:
+   *                  'button' - for simple button widgets (the default)
+   *                  'view'   - for buttons that open a panel or subview,
+   *                             depending on where they are placed.
+   *                  'custom' - for fine-grained control over the creation
+   *                             of the widget.
+   * - viewId:        Only useful for views (and required there): the id of the
+   *                  <panelview> that should be shown when clicking the widget.
+   * - onBuild(aDoc): Only useful for custom widgets (and required there); a
+   *                  function that will be invoked with the document in which
+   *                  to build a widget. Should return the DOM node that has
+   *                  been constructed.
+   * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function
+   *                  that will be invoked before the widget gets a DOM node
+   *                  constructed, passing the document in which that will happen.
+   *                  This is useful especially for 'view' type widgets that need
+   *                  to construct their views on the fly (e.g. from bootstrapped
+   *                  add-ons)
+   * - onCreated(aNode): Attached to all widgets; a function that will be invoked
+   *                  whenever the widget has a DOM node constructed, passing the
+   *                  constructed node as an argument.
+   * - onDestroyed(aDoc): Attached to all non-custom widgets; a function that
+   *                  will be invoked after the widget has a DOM node destroyed,
+   *                  passing the document from which it was removed. This is
+   *                  useful especially for 'view' type widgets that need to
+   *                  cleanup after views that were constructed on the fly.
+   * - onBeforeCommand(aEvt): A function that will be invoked when the user
+   *                          activates the button but before the command
+   *                          is evaluated. Useful if code needs to run to
+   *                          change the button's icon in preparation to the
+   *                          pending command action. Called for both type=button
+   *                          and type=view.
+   * - onCommand(aEvt): Only useful for button widgets; a function that will be
+   *                    invoked when the user activates the button.
+   * - onClick(aEvt): Attached to all widgets; a function that will be invoked
+   *                  when the user clicks the widget.
+   * - onViewShowing(aEvt): Only useful for views; a function that will be
+   *                  invoked when a user shows your view. If any event
+   *                  handler calls aEvt.preventDefault(), the view will
+   *                  not be shown.
+   *
+   *                  The event's `detail` property is an object with an
+   *                  `addBlocker` method. Handlers which need to
+   *                  perform asynchronous operations before the view is
+   *                  shown may pass this method a Promise, which will
+   *                  prevent the view from showing until it resolves.
+   *                  Additionally, if the promise resolves to the exact
+   *                  value `false`, the view will not be shown.
+   * - onViewHiding(aEvt): Only useful for views; a function that will be
+   *                  invoked when a user hides your view.
+   * - tooltiptext:   string to use for the tooltip of the widget
+   * - label:         string to use for the label of the widget
+   * - localized:     If true, or undefined, attempt to retrieve the
+   *                  widget's string properties from the customizable
+   *                  widgets string bundle.
+   * - removable:     whether the widget is removable (optional, default: true)
+   *                  NB: if you specify false here, you must provide a
+   *                  defaultArea, too.
+   * - overflows:     whether widget can overflow when in an overflowable
+   *                  toolbar (optional, default: true)
+   * - defaultArea:   default area to add the widget to
+   *                  (optional, default: none; required if non-removable)
+   * - shortcutId:    id of an element that has a shortcut for this widget
+   *                  (optional, default: null). This is only used to display
+   *                  the shortcut as part of the tooltip for builtin widgets
+   *                  (which have strings inside
+   *                  customizableWidgets.properties). If you're in an add-on,
+   *                  you should not set this property.
+   * - showInPrivateBrowsing: whether to show the widget in private browsing
+   *                          mode (optional, default: true)
+   *
+   * @param aProperties the specifications for the widget.
+   * @return a wrapper around the created widget (see getWidget)
+   */
+  createWidget(aProperties) {
+    return CustomizableUIInternal.wrapWidget(
+      CustomizableUIInternal.createWidget(aProperties)
+    );
+  },
+  /**
+   * Destroy a widget
+   *
+   * If the widget is part of the default placements in an area, this will
+   * remove it from there. It will also remove any DOM instances. However,
+   * it will keep the widget in the placements for whatever area it was
+   * in at the time. You can remove it from there yourself by calling
+   * CustomizableUI.removeWidgetFromArea(aWidgetId).
+   *
+   * @param aWidgetId the ID of the widget to destroy
+   */
+  destroyWidget(aWidgetId) {
+    CustomizableUIInternal.destroyWidget(aWidgetId);
+  },
+  /**
+   * Get a wrapper object with information about the widget.
+   * The object provides the following properties
+   * (all read-only unless otherwise indicated):
+   *
+   * - id:            the widget's ID;
+   * - type:          the type of widget (button, view, custom). For
+   *                  XUL-provided widgets, this is always 'custom';
+   * - provider:      the provider type of the widget, id est one of
+   *                  PROVIDER_API or PROVIDER_XUL;
+   * - forWindow(w):  a method to obtain a single window wrapper for a widget,
+   *                  in the window w passed as the only argument;
+   * - instances:     an array of all instances (single window wrappers)
+   *                  of the widget. This array is NOT live;
+   * - areaType:      the type of the widget's current area
+   * - isGroup:       true; will be false for wrappers around single widget nodes;
+   * - source:        for API-provided widgets, whether they are built-in to
+   *                  Firefox or add-on-provided;
+   * - disabled:      for API-provided widgets, whether the widget is currently
+   *                  disabled. NB: this property is writable, and will toggle
+   *                  all the widgets' nodes' disabled states;
+   * - label:         for API-provied widgets, the label of the widget;
+   * - tooltiptext:   for API-provided widgets, the tooltip of the widget;
+   * - showInPrivateBrowsing: for API-provided widgets, whether the widget is
+   *                          visible in private browsing;
+   *
+   * Single window wrappers obtained through forWindow(someWindow) or from the
+   * instances array have the following properties
+   * (all read-only unless otherwise indicated):
+   *
+   * - id:            the widget's ID;
+   * - type:          the type of widget (button, view, custom). For
+   *                  XUL-provided widgets, this is always 'custom';
+   * - provider:      the provider type of the widget, id est one of
+   *                  PROVIDER_API or PROVIDER_XUL;
+   * - node:          reference to the corresponding DOM node;
+   * - anchor:        the anchor on which to anchor panels opened from this
+   *                  node. This will point to the overflow chevron on
+   *                  overflowable toolbars if and only if your widget node
+   *                  is overflowed, to the anchor for the panel menu
+   *                  if your widget is inside the panel menu, and to the
+   *                  node itself in all other cases;
+   * - overflowed:    boolean indicating whether the node is currently in the
+   *                  overflow panel of the toolbar;
+   * - isGroup:       false; will be true for the group widget;
+   * - label:         for API-provided widgets, convenience getter for the
+   *                  label attribute of the DOM node;
+   * - tooltiptext:   for API-provided widgets, convenience getter for the
+   *                  tooltiptext attribute of the DOM node;
+   * - disabled:      for API-provided widgets, convenience getter *and setter*
+   *                  for the disabled state of this single widget. Note that
+   *                  you may prefer to use the group wrapper's getter/setter
+   *                  instead.
+   *
+   * @param aWidgetId the ID of the widget whose information you need
+   * @return a wrapper around the widget as described above, or null if the
+   *         widget is known not to exist (anymore). NB: non-null return
+   *         is no guarantee the widget exists because we cannot know in
+   *         advance if a XUL widget exists or not.
+   */
+  getWidget(aWidgetId) {
+    return CustomizableUIInternal.wrapWidget(aWidgetId);
+  },
+  /**
+   * Get an array of widget wrappers (see getWidget) for all the widgets
+   * which are currently not in any area (so which are in the palette).
+   *
+   * @param aWindowPalette the palette (and by extension, the window) in which
+   *                       CustomizableUI should look. This matters because of
+   *                       course XUL-provided widgets could be available in
+   *                       some windows but not others, and likewise
+   *                       API-provided widgets might not exist in a private
+   *                       window (because of the showInPrivateBrowsing
+   *                       property).
+   *
+   * @return an array of widget wrappers (see getWidget)
+   */
+  getUnusedWidgets(aWindowPalette) {
+    return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
+      CustomizableUIInternal.wrapWidget,
+      CustomizableUIInternal
+    );
+  },
+  /**
+   * Get an array of all the widget IDs placed in an area.
+   * Modifying the array will not affect CustomizableUI.
+   *
+   * @param aArea the ID of the area whose placements you want to obtain.
+   * @return an array containing the widget IDs that are in the area.
+   *
+   * NB: will throw if called too early (before placements have been fetched)
+   *     or if the area is not currently known to CustomizableUI.
+   */
+  getWidgetIdsInArea(aArea) {
+    if (!gAreas.has(aArea)) {
+      throw new Error("Unknown customization area: " + aArea);
+    }
+    if (!gPlacements.has(aArea)) {
+      throw new Error("Area not yet restored");
+    }
+
+    // We need to clone this, as we don't want to let consumers muck with placements
+    return [...gPlacements.get(aArea)];
+  },
+  /**
+   * Get an array of widget wrappers for all the widgets in an area. This is
+   * the same as calling getWidgetIdsInArea and .map() ing the result through
+   * CustomizableUI.getWidget. Careful: this means that if there are IDs in there
+   * which don't have corresponding DOM nodes, there might be nulls in this array,
+   * or items for which wrapper.forWindow(win) will return null.
+   *
+   * @param aArea the ID of the area whose widgets you want to obtain.
+   * @return an array of widget wrappers and/or null values for the widget IDs
+   *         placed in an area.
+   *
+   * NB: will throw if called too early (before placements have been fetched)
+   *     or if the area is not currently known to CustomizableUI.
+   */
+  getWidgetsInArea(aArea) {
+    return this.getWidgetIdsInArea(aArea).map(
+      CustomizableUIInternal.wrapWidget,
+      CustomizableUIInternal
+    );
+  },
+
+  /**
+   * Ensure the customizable widget that matches up with this view node
+   * will get the right subview showing/shown/hiding/hidden events when
+   * they fire.
+   * @param aViewNode the view node to add listeners to if they haven't
+   *                  been added already.
+   */
+  ensureSubviewListeners(aViewNode) {
+    return CustomizableUIInternal.ensureSubviewListeners(aViewNode);
+  },
+  /**
+   * Obtain an array of all the area IDs known to CustomizableUI.
+   * This array is created for you, so is modifiable without CustomizableUI
+   * being affected.
+   */
+  get areas() {
+    return [...gAreas.keys()];
+  },
+  /**
+   * Check what kind of area (toolbar or menu panel) an area is. This is
+   * useful if you have a widget that needs to behave differently depending
+   * on its location. Note that widget wrappers have a convenience getter
+   * property (areaType) for this purpose.
+   *
+   * @param aArea the ID of the area whose type you want to know
+   * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if
+   *         the area is unknown.
+   */
+  getAreaType(aArea) {
+    let area = gAreas.get(aArea);
+    return area ? area.get("type") : null;
+  },
+  /**
+   * Check if a toolbar is collapsed by default.
+   *
+   * @param aArea the ID of the area whose default-collapsed state you want to know.
+   * @return `true` or `false` depending on the area, null if the area is unknown,
+   *         or its collapsed state cannot normally be controlled by the user
+   */
+  isToolbarDefaultCollapsed(aArea) {
+    let area = gAreas.get(aArea);
+    return area ? area.get("defaultCollapsed") : null;
+  },
+  /**
+   * Obtain the DOM node that is the customize target for an area in a
+   * specific window.
+   *
+   * Areas can have a customization target that does not correspond to the
+   * node itself. In particular, toolbars that have a customizationtarget
+   * attribute set will have their customization target set to that node.
+   * This means widgets will end up in the customization target, not in the
+   * DOM node with the ID that corresponds to the area ID. This is useful
+   * because it lets you have fixed content in a toolbar (e.g. the panel
+   * menu item in the navbar) and have all the customizable widgets use
+   * the customization target.
+   *
+   * Using this API yourself is discouraged; you should generally not need
+   * to be asking for the DOM container node used for a particular area.
+   * In particular, if you're wanting to check it in relation to a widget's
+   * node, your DOM node might not be a direct child of the customize target
+   * in a window if, for instance, the window is in customization mode, or if
+   * this is an overflowable toolbar and the widget has been overflowed.
+   *
+   * @param aArea   the ID of the area whose customize target you want to have
+   * @param aWindow the window where you want to fetch the DOM node.
+   * @return the customize target DOM node for aArea in aWindow
+   */
+  getCustomizeTargetForArea(aArea, aWindow) {
+    return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
+  },
+  /**
+   * Reset the customization state back to its default.
+   *
+   * This is the nuclear option. You should never call this except if the user
+   * explicitly requests it. Firefox does this when the user clicks the
+   * "Restore Defaults" button in customize mode.
+   */
+  reset() {
+    CustomizableUIInternal.reset();
+  },
+
+  /**
+   * Undo the previous reset, can only be called immediately after a reset.
+   * @return a promise that will be resolved when the operation is complete.
+   */
+  undoReset() {
+    CustomizableUIInternal.undoReset();
+  },
+
+  /**
+   * Remove a custom toolbar added in a previous version of Firefox or using
+   * an add-on. NB: only works on the customizable toolbars generated by
+   * the toolbox itself. Intended for use from CustomizeMode, not by
+   * other consumers.
+   * @param aToolbarId the ID of the toolbar to remove
+   */
+  removeExtraToolbar(aToolbarId) {
+    CustomizableUIInternal.removeExtraToolbar(aToolbarId);
+  },
+
+  /**
+   * Can the last Restore Defaults operation be undone.
+   *
+   * @return A boolean stating whether an undo of the
+   *         Restore Defaults can be performed.
+   */
+  get canUndoReset() {
+    return gUIStateBeforeReset.uiCustomizationState != null ||
+           gUIStateBeforeReset.drawInTitlebar != null ||
+           gUIStateBeforeReset.extraDragSpace != null ||
+           gUIStateBeforeReset.currentTheme != null ||
+           gUIStateBeforeReset.autoTouchMode != null ||
+           gUIStateBeforeReset.uiDensity != null;
+  },
+
+  /**
+   * Get the placement of a widget. This is by far the best way to obtain
+   * information about what the state of your widget is. The internals of
+   * this call are cheap (no DOM necessary) and you will know where the user
+   * has put your widget.
+   *
+   * @param aWidgetId the ID of the widget whose placement you want to know
+   * @return
+   *   {
+   *     area: "somearea", // The ID of the area where the widget is placed
+   *     position: 42 // the index in the placements array corresponding to
+   *                  // your widget.
+   *   }
+   *
+   *   OR
+   *
+   *   null // if the widget is not placed anywhere (ie in the palette)
+   */
+  getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
+    return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas);
+  },
+  /**
+   * Check if a widget can be removed from the area it's in.
+   *
+   * Note that if you're wanting to move the widget somewhere, you should
+   * generally be checking canWidgetMoveToArea, because that will return
+   * true if the widget is already in the area where you want to move it (!).
+   *
+   * NB: oh, also, this method might lie if the widget in question is a
+   *     XUL-provided widget and there are no windows open, because it
+   *     can obviously not check anything in this case. It will return
+   *     true. You will be able to move the widget elsewhere. However,
+   *     once the user reopens a window, the widget will move back to its
+   *     'proper' area automagically.
+   *
+   * @param aWidgetId a widget ID or DOM node to check
+   * @return true if the widget can be removed from its area,
+   *          false otherwise.
+   */
+  isWidgetRemovable(aWidgetId) {
+    return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
+  },
+  /**
+   * Check if a widget can be moved to a particular area. Like
+   * isWidgetRemovable but better, because it'll return true if the widget
+   * is already in the right area.
+   *
+   * @param aWidgetId the widget ID or DOM node you want to move somewhere
+   * @param aArea     the area ID you want to move it to.
+   * @return true if this is possible, false if it is not. The same caveats as
+   *              for isWidgetRemovable apply, however, if no windows are open.
+   */
+  canWidgetMoveToArea(aWidgetId, aArea) {
+    return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
+  },
+  /**
+   * Whether we're in a default state. Note that non-removable non-default
+   * widgets and non-existing widgets are not taken into account in determining
+   * whether we're in the default state.
+   *
+   * NB: this is a property with a getter. The getter is NOT cheap, because
+   * it does smart things with non-removable non-default items, non-existent
+   * items, and so forth. Please don't call unless necessary.
+   */
+  get inDefaultState() {
+    return CustomizableUIInternal.inDefaultState;
+  },
+
+  /**
+   * Set a toolbar's visibility state in all windows.
+   * @param aToolbarId    the toolbar whose visibility should be adjusted
+   * @param aIsVisible    whether the toolbar should be visible
+   */
+  setToolbarVisibility(aToolbarId, aIsVisible) {
+    CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
+  },
+
+  /**
+   * Get a localized property off a (widget?) object.
+   *
+   * NB: this is unlikely to be useful unless you're in Firefox code, because
+   *     this code uses the builtin widget stringbundle, and can't be told
+   *     to use add-on-provided strings. It's mainly here as convenience for
+   *     custom builtin widgets that build their own DOM but use the same
+   *     stringbundle as the other builtin widgets.
+   *
+   * @param aWidget     the object whose property we should use to fetch a
+   *                    localizable string;
+   * @param aProp       the property on the object to use for the fetching;
+   * @param aFormatArgs (optional) any extra arguments to use for a formatted
+   *                    string;
+   * @param aDef        (optional) the default to return if we don't find the
+   *                    string in the stringbundle;
+   *
+   * @return the localized string, or aDef if the string isn't in the bundle.
+   *         If no default is provided,
+   *           if aProp exists on aWidget, we'll return that,
+   *           otherwise we'll return the empty string
+   *
+   */
+  getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
+    return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp,
+      aFormatArgs, aDef);
+  },
+  /**
+   * Utility function to detect, find and set a keyboard shortcut for a menuitem
+   * or (toolbar)button.
+   *
+   * @param aShortcutNode the XUL node where the shortcut will be derived from;
+   * @param aTargetNode   (optional) the XUL node on which the `shortcut`
+   *                      attribute will be set. If NULL, the shortcut will be
+   *                      set on aShortcutNode;
+   */
+  addShortcut(aShortcutNode, aTargetNode) {
+    return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode);
+  },
+  /**
+   * Given a node, walk up to the first panel in its ancestor chain, and
+   * close it.
+   *
+   * @param aNode a node whose panel should be closed;
+   */
+  hidePanelForNode(aNode) {
+    CustomizableUIInternal.hidePanelForNode(aNode);
+  },
+  /**
+   * Check if a widget is a "special" widget: a spring, spacer or separator.
+   *
+   * @param aWidgetId the widget ID to check.
+   * @return true if the widget is 'special', false otherwise.
+   */
+  isSpecialWidget(aWidgetId) {
+    return CustomizableUIInternal.isSpecialWidget(aWidgetId);
+  },
+  /**
+   * Add listeners to a panel that will close it. For use from the menu panel
+   * and overflowable toolbar implementations, unlikely to be useful for
+   * consumers.
+   *
+   * @param aPanel the panel to which listeners should be attached.
+   */
+  addPanelCloseListeners(aPanel) {
+    CustomizableUIInternal.addPanelCloseListeners(aPanel);
+  },
+  /**
+   * Remove close listeners that have been added to a panel with
+   * addPanelCloseListeners. For use from the menu panel and overflowable
+   * toolbar implementations, unlikely to be useful for consumers.
+   *
+   * @param aPanel the panel from which listeners should be removed.
+   */
+  removePanelCloseListeners(aPanel) {
+    CustomizableUIInternal.removePanelCloseListeners(aPanel);
+  },
+  /**
+   * Notify listeners a widget is about to be dragged to an area. For use from
+   * Customize Mode only, do not use otherwise.
+   *
+   * @param aWidgetId the ID of the widget that is being dragged to an area.
+   * @param aArea     the ID of the area to which the widget is being dragged.
+   */
+  onWidgetDrag(aWidgetId, aArea) {
+    CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
+  },
+  /**
+   * Notify listeners that a window is entering customize mode. For use from
+   * Customize Mode only, do not use otherwise.
+   * @param aWindow the window entering customize mode
+   */
+  notifyStartCustomizing(aWindow) {
+    CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
+  },
+  /**
+   * Notify listeners that a window is exiting customize mode. For use from
+   * Customize Mode only, do not use otherwise.
+   * @param aWindow the window exiting customize mode
+   */
+  notifyEndCustomizing(aWindow) {
+    CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
+  },
+
+  /**
+   * Notify toolbox(es) of a particular event. If you don't pass aWindow,
+   * all toolboxes will be notified. For use from Customize Mode only,
+   * do not use otherwise.
+   * @param aEvent the name of the event to send.
+   * @param aDetails optional, the details of the event.
+   * @param aWindow optional, the window in which to send the event.
+   */
+  dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {
+    CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
+  },
+
+  /**
+   * Check whether an area is overflowable.
+   *
+   * @param aAreaId the ID of an area to check for overflowable-ness
+   * @return true if the area is overflowable, false otherwise.
+   */
+  isAreaOverflowable(aAreaId) {
+    let area = gAreas.get(aAreaId);
+    return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
+                : false;
+  },
+  /**
+   * Obtain a string indicating the place of an element. This is intended
+   * for use from customize mode; You should generally use getPlacementOfWidget
+   * instead, which is cheaper because it does not use the DOM.
+   *
+   * @param aElement the DOM node whose place we need to check
+   * @return "toolbar" if the node is in a toolbar, "panel" if it is in the
+   *         menu panel, "palette" if it is in the (visible!) customization
+   *         palette, undefined otherwise.
+   */
+  getPlaceForItem(aElement) {
+    let place;
+    let node = aElement;
+    while (node && !place) {
+      if (node.localName == "toolbar")
+        place = "toolbar";
+      else if (node.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL)
+        place = "menu-panel";
+      else if (node.id == "customization-palette")
+        place = "palette";
+
+      node = node.parentNode;
+    }
+    return place;
+  },
+
+  /**
+   * Check if a toolbar is builtin or not.
+   * @param aToolbarId the ID of the toolbar you want to check
+   */
+  isBuiltinToolbar(aToolbarId) {
+    return CustomizableUIInternal._builtinToolbars.has(aToolbarId);
+  },
+
+  /**
+   * Create an instance of a spring, spacer or separator.
+   * @param aId       the type of special widget (spring, spacer or separator)
+   * @param aDocument the document in which to create it.
+   */
+  createSpecialWidget(aId, aDocument) {
+    return CustomizableUIInternal.createSpecialWidget(aId, aDocument);
+  },
+
+  /**
+   * Fills a submenu with menu items.
+   * @param aMenuItems the menu items to display.
+   * @param aSubview   the subview to fill.
+   */
+  fillSubviewFromMenuItems(aMenuItems, aSubview) {
+    let attrs = ["oncommand", "onclick", "label", "key", "disabled",
+                 "command", "observes", "hidden", "class", "origin",
+                 "image", "checked", "style"];
+
+    let doc = aSubview.ownerDocument;
+    let fragment = doc.createDocumentFragment();
+    for (let menuChild of aMenuItems) {
+      if (menuChild.hidden)
+        continue;
+
+      let subviewItem;
+      if (menuChild.localName == "menuseparator") {
+        // Don't insert duplicate or leading separators. This can happen if there are
+        // menus (which we don't copy) above the separator.
+        if (!fragment.lastElementChild || fragment.lastElementChild.localName == "menuseparator") {
+          continue;
+        }
+        subviewItem = doc.createXULElement("menuseparator");
+      } else if (menuChild.localName == "menuitem") {
+        subviewItem = doc.createXULElement("toolbarbutton");
+        CustomizableUI.addShortcut(menuChild, subviewItem);
+
+        let item = menuChild;
+        if (!item.hasAttribute("onclick")) {
+          subviewItem.addEventListener("click", event => {
+            let newEvent = new doc.defaultView.MouseEvent(event.type, event);
+            item.dispatchEvent(newEvent);
+          });
+        }
+
+        if (!item.hasAttribute("oncommand")) {
+          subviewItem.addEventListener("command", event => {
+            let newEvent = doc.createEvent("XULCommandEvent");
+            newEvent.initCommandEvent(
+              event.type, event.bubbles, event.cancelable, event.view,
+              event.detail, event.ctrlKey, event.altKey, event.shiftKey,
+              event.metaKey, event.sourceEvent, 0);
+            item.dispatchEvent(newEvent);
+          });
+        }
+      } else {
+        continue;
+      }
+      for (let attr of attrs) {
+        let attrVal = menuChild.getAttribute(attr);
+        if (attrVal)
+          subviewItem.setAttribute(attr, attrVal);
+      }
+      // We do this after so the .subviewbutton class doesn't get overriden.
+      if (menuChild.localName == "menuitem") {
+        subviewItem.classList.add("subviewbutton");
+      }
+      fragment.appendChild(subviewItem);
+    }
+    aSubview.appendChild(fragment);
+  },
+
+  /**
+   * A helper function for clearing subviews.
+   * @param aSubview the subview to clear.
+   */
+  clearSubview(aSubview) {
+    let parent = aSubview.parentNode;
+    // We'll take the container out of the document before cleaning it out
+    // to avoid reflowing each time we remove something.
+    parent.removeChild(aSubview);
+
+    while (aSubview.firstChild) {
+      aSubview.firstChild.remove();
+    }
+
+    parent.appendChild(aSubview);
+  },
+
+  getCustomizationTarget(aElement) {
+    return CustomizableUIInternal.getCustomizationTarget(aElement);
+  },
+};
+Object.freeze(this.CustomizableUI);
+Object.freeze(this.CustomizableUI.windows);
+
+/**
+ * All external consumers of widgets are really interacting with these wrappers
+ * which provide a common interface.
+ */
+
+/**
+ * WidgetGroupWrapper is the common interface for interacting with an entire
+ * widget group - AKA, all instances of a widget across a series of windows.
+ * This particular wrapper is only used for widgets created via the provider
+ * API.
+ */
+function WidgetGroupWrapper(aWidget) {
+  this.isGroup = true;
+
+  const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext",
+                      "showInPrivateBrowsing", "viewId"];
+  for (let prop of kBareProps) {
+    let propertyName = prop;
+    this.__defineGetter__(propertyName, () => aWidget[propertyName]);
+  }
+
+  this.__defineGetter__("provider", () => CustomizableUI.PROVIDER_API);
+
+  this.__defineSetter__("disabled", function(aValue) {
+    aValue = !!aValue;
+    aWidget.disabled = aValue;
+    for (let [, instance] of aWidget.instances) {
+      instance.disabled = aValue;
+    }
+  });
+
+  this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
+    let wrapperMap;
+    if (!gSingleWrapperCache.has(aWindow)) {
+      wrapperMap = new Map();
+      gSingleWrapperCache.set(aWindow, wrapperMap);
+    } else {
+      wrapperMap = gSingleWrapperCache.get(aWindow);
+    }
+    if (wrapperMap.has(aWidget.id)) {
+      return wrapperMap.get(aWidget.id);
+    }
+
+    let instance = aWidget.instances.get(aWindow.document);
+    if (!instance) {
+      instance = CustomizableUIInternal.buildWidget(aWindow.document,
+                                                    aWidget);
+    }
+
+    let wrapper = new WidgetSingleWrapper(aWidget, instance);
+    wrapperMap.set(aWidget.id, wrapper);
+    return wrapper;
+  };
+
+  this.__defineGetter__("instances", function() {
+    // Can't use gBuildWindows here because some areas load lazily:
+    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
+    if (!placement) {
+      return [];
+    }
+    let area = placement.area;
+    let buildAreas = gBuildAreas.get(area);
+    if (!buildAreas) {
+      return [];
+    }
+    return Array.from(buildAreas, (node) => this.forWindow(node.ownerGlobal));
+  });
+
+  this.__defineGetter__("areaType", function() {
+    let areaProps = gAreas.get(aWidget.currentArea);
+    return areaProps && areaProps.get("type");
+  });
+
+  Object.freeze(this);
+}
+
+/**
+ * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
+ * a particular window.
+ */
+function WidgetSingleWrapper(aWidget, aNode) {
+  this.isGroup = false;
+
+  this.node = aNode;
+  this.provider = CustomizableUI.PROVIDER_API;
+
+  const kGlobalProps = ["id", "type"];
+  for (let prop of kGlobalProps) {
+    this[prop] = aWidget[prop];
+  }
+
+  const kNodeProps = ["label", "tooltiptext"];
+  for (let prop of kNodeProps) {
+    let propertyName = prop;
+    // Look at the node for these, instead of the widget data, to ensure the
+    // wrapper always reflects this live instance.
+    this.__defineGetter__(propertyName,
+                          () => aNode.getAttribute(propertyName));
+  }
+
+  this.__defineGetter__("disabled", () => aNode.disabled);
+  this.__defineSetter__("disabled", function(aValue) {
+    aNode.disabled = !!aValue;
+  });
+
+  this.__defineGetter__("anchor", function() {
+    let anchorId;
+    // First check for an anchor for the area:
+    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
+    if (placement) {
+      anchorId = gAreas.get(placement.area).get("anchor");
+    }
+    if (!anchorId) {
+      anchorId = aNode.getAttribute("cui-anchorid");
+    }
+
+    return anchorId ? aNode.ownerDocument.getElementById(anchorId)
+                    : aNode;
+  });
+
+  this.__defineGetter__("overflowed", function() {
+    return aNode.getAttribute("overflowedItem") == "true";
+  });
+
+  Object.freeze(this);
+}
+
+/**
+ * XULWidgetGroupWrapper is the common interface for interacting with an entire
+ * widget group - AKA, all instances of a widget across a series of windows.
+ * This particular wrapper is only used for widgets created via the old-school
+ * XUL method (overlays, or programmatically injecting toolbaritems, or other
+ * such things).
+ */
+// XXXunf Going to need to hook this up to some events to keep it all live.
+function XULWidgetGroupWrapper(aWidgetId) {
+  this.isGroup = true;
+  this.id = aWidgetId;
+  this.type = "custom";
+  this.provider = CustomizableUI.PROVIDER_XUL;
+
+  this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) {
+    let wrapperMap;
+    if (!gSingleWrapperCache.has(aWindow)) {
+      wrapperMap = new Map();
+      gSingleWrapperCache.set(aWindow, wrapperMap);
+    } else {
+      wrapperMap = gSingleWrapperCache.get(aWindow);
+    }
+    if (wrapperMap.has(aWidgetId)) {
+      return wrapperMap.get(aWidgetId);
+    }
+
+    let instance = aWindow.document.getElementById(aWidgetId);
+    if (!instance) {
+      // Toolbar palettes aren't part of the document, so elements in there
+      // won't be found via document.getElementById().
+      instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
+    }
+
+    let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document);
+    wrapperMap.set(aWidgetId, wrapper);
+    return wrapper;
+  };
+
+  this.__defineGetter__("areaType", function() {
+    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
+    if (!placement) {
+      return null;
+    }
+
+    let areaProps = gAreas.get(placement.area);
+    return areaProps && areaProps.get("type");
+  });
+
+  this.__defineGetter__("instances", function() {
+    return Array.from(gBuildWindows, (wins) => this.forWindow(wins[0]));
+  });
+
+  Object.freeze(this);
+}
+
+/**
+ * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL
+ * widget in a particular window.
+ */
+function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) {
+  this.isGroup = false;
+
+  this.id = aWidgetId;
+  this.type = "custom";
+  this.provider = CustomizableUI.PROVIDER_XUL;
+
+  let weakDoc = Cu.getWeakReference(aDocument);
+  // If we keep a strong ref, the weak ref will never die, so null it out:
+  aDocument = null;
+
+  this.__defineGetter__("node", function() {
+    // If we've set this to null (further down), we're sure there's nothing to
+    // be gotten here, so bail out early:
+    if (!weakDoc) {
+      return null;
+    }
+    if (aNode) {
+      // Return the last known node if it's still in the DOM...
+      if (aNode.ownerDocument.contains(aNode)) {
+        return aNode;
+      }
+      // ... or the toolbox
+      let toolbox = aNode.ownerGlobal.gNavToolbox;
+      if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
+        return aNode;
+      }
+      // If it isn't, clear the cached value and fall through to the "slow" case:
+      aNode = null;
+    }
+
+    let doc = weakDoc.get();
+    if (doc) {
+      // Store locally so we can cache the result:
+      aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView);
+      return aNode;
+    }
+    // The weakref to the document is dead, we're done here forever more:
+    weakDoc = null;
+    return null;
+  });
+
+  this.__defineGetter__("anchor", function() {
+    let anchorId;
+    // First check for an anchor for the area:
+    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
+    if (placement) {
+      anchorId = gAreas.get(placement.area).get("anchor");
+    }
+
+    let node = this.node;
+    if (!anchorId && node) {
+      anchorId = node.getAttribute("cui-anchorid");
+    }
+
+    return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node;
+  });
+
+  this.__defineGetter__("overflowed", function() {
+    let node = this.node;
+    if (!node) {
+      return false;
+    }
+    return node.getAttribute("overflowedItem") == "true";
+  });
+
+  Object.freeze(this);
+}
+
+const LAZY_RESIZE_INTERVAL_MS = 200;
+const OVERFLOW_PANEL_HIDE_DELAY_MS = 500;
+
+function OverflowableToolbar(aToolbarNode) {
+  this._toolbar = aToolbarNode;
+  this._collapsed = new Map();
+  this._enabled = true;
+  this._toolbar.addEventListener("overflow", this);
+  this._toolbar.addEventListener("underflow", this);
+
+  this._toolbar.setAttribute("overflowable", "true");
+  let doc = this._toolbar.ownerDocument;
+  this._target = CustomizableUI.getCustomizationTarget(this._toolbar);
+  this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
+  this._list._customizationTarget = this._list;
+
+  let window = this._toolbar.ownerGlobal;
+  if (window.gBrowserInit.delayedStartupFinished) {
+    this.init();
+  } else {
+    Services.obs.addObserver(this, "browser-delayed-startup-finished");
+  }
+}
+
+OverflowableToolbar.prototype = {
+  initialized: false,
+  _forceOnOverflow: false,
+  _addedListener: false,
+
+  observe(aSubject, aTopic, aData) {
+    if (aTopic == "browser-delayed-startup-finished" &&
+        aSubject == this._toolbar.ownerGlobal) {
+      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
+      this.init();
+    }
+  },
+
+  init() {
+    let doc = this._toolbar.ownerDocument;
+    let window = doc.defaultView;
+    window.addEventListener("resize", this);
+    window.gNavToolbox.addEventListener("customizationstarting", this);
+    window.gNavToolbox.addEventListener("aftercustomization", this);
+
+    let chevronId = this._toolbar.getAttribute("overflowbutton");
+    this._chevron = doc.getElementById(chevronId);
+    this._chevron.addEventListener("mousedown", this);
+    this._chevron.addEventListener("keypress", this);
+    this._chevron.addEventListener("dragover", this);
+    this._chevron.addEventListener("dragend", this);
+
+    let panelId = this._toolbar.getAttribute("overflowpanel");
+    this._panel = doc.getElementById(panelId);
+    this._panel.addEventListener("popuphiding", this);
+    CustomizableUIInternal.addPanelCloseListeners(this._panel);
+
+    CustomizableUI.addListener(this);
+    this._addedListener = true;
+
+    // The 'overflow' event may have been fired before init was called.
+    if (this.overflowedDuringConstruction) {
+      this.onOverflow(this.overflowedDuringConstruction);
+      this.overflowedDuringConstruction = null;
+    }
+
+    this.initialized = true;
+  },
+
+  uninit() {
+    this._toolbar.removeEventListener("overflow", this._toolbar);
+    this._toolbar.removeEventListener("underflow", this._toolbar);
+    this._toolbar.removeAttribute("overflowable");
+
+    if (!this.initialized) {
+      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
+      return;
+    }
+
+    this._disable();
+
+    let window = this._toolbar.ownerGlobal;
+    window.removeEventListener("resize", this);
+    window.gNavToolbox.removeEventListener("customizationstarting", this);
+    window.gNavToolbox.removeEventListener("aftercustomization", this);
+    this._chevron.removeEventListener("mousedown", this);
+    this._chevron.removeEventListener("keypress", this);
+    this._chevron.removeEventListener("dragover", this);
+    this._chevron.removeEventListener("dragend", this);
+    this._panel.removeEventListener("popuphiding", this);
+    CustomizableUI.removeListener(this);
+    this._addedListener = false;
+    CustomizableUIInternal.removePanelCloseListeners(this._panel);
+  },
+
+  handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "overflow":
+        // Ignore vertical overflow and events from from nodes inside the toolbar.
+        if (aEvent.detail > 0 && aEvent.target == this._target) {
+          if (this.initialized) {
+            this.onOverflow(aEvent);
+          } else {
+            this.overflowedDuringConstruction = aEvent;
+          }
+        }
+        break;
+      case "underflow":
+        // Ignore vertical underflow and events from from nodes inside the toolbar.
+        if (aEvent.detail > 0 && aEvent.target == this._target) {
+          this.overflowedDuringConstruction = null;
+        }
+        break;
+      case "aftercustomization":
+        this._enable();
+        break;
+      case "mousedown":
+        if (aEvent.button != 0) {
+          break;
+        }
+        if (aEvent.target == this._chevron) {
+          this._onClickChevron(aEvent);
+        } else {
+          PanelMultiView.hidePopup(this._panel);
+        }
+        break;
+      case "keypress":
+        if (aEvent.target == this._chevron &&
+            (aEvent.key == " " || aEvent.key == "Enter")) {
+          this._onClickChevron(aEvent);
+        }
+        break;
+      case "customizationstarting":
+        this._disable();
+        break;
+      case "dragover":
+        if (this._enabled) {
+          this._showWithTimeout();
+        }
+        break;
+      case "dragend":
+        PanelMultiView.hidePopup(this._panel);
+        break;
+      case "popuphiding":
+        this._onPanelHiding(aEvent);
+        break;
+      case "resize":
+        this._onResize(aEvent);
+    }
+  },
+
+  show(aEvent) {
+    if (this._panel.state == "open") {
+      return Promise.resolve();
+    }
+    return new Promise(resolve => {
+      let doc = this._panel.ownerDocument;
+      this._panel.hidden = false;
+      let multiview = this._panel.querySelector("panelmultiview");
+      let mainViewId = multiview.getAttribute("mainViewId");
+      let mainView = doc.getElementById(mainViewId);
+      let contextMenu = doc.getElementById(mainView.getAttribute("context"));
+      gELS.addSystemEventListener(contextMenu, "command", this, true);
+      let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
+      // Ensure we update the gEditUIVisible flag when opening the popup, in
+      // case the edit controls are in it.
+      this._panel.addEventListener("popupshowing", () => doc.defaultView.updateEditUIVisibility(), {once: true});
+      PanelMultiView.openPopup(this._panel, anchor || this._chevron, {
+        triggerEvent: aEvent,
+      }).catch(Cu.reportError);
+      this._chevron.open = true;
+
+      this._panel.addEventListener("popupshown", () => {
+        this._panel.addEventListener("dragover", this);
+        this._panel.addEventListener("dragend", this);
+        // Wait until the next tick to resolve so all popupshown
+        // handlers have a chance to run before our promise resolution
+        // handlers do.
+        Services.tm.dispatchToMainThread(resolve);
+      }, {once: true});
+    });
+  },
+
+  _onClickChevron(aEvent) {
+    if (this._chevron.open) {
+      this._chevron.open = false;
+      PanelMultiView.hidePopup(this._panel);
+    } else if (this._panel.state != "hiding" && !this._chevron.disabled) {
+      this.show(aEvent);
+    }
+  },
+
+  _onPanelHiding(aEvent) {
+    if (aEvent.target != this._panel) {
+      // Ignore context menus, <select> popups, etc.
+      return;
+    }
+    this._chevron.open = false;
+    this._panel.removeEventListener("dragover", this);
+    this._panel.removeEventListener("dragend", this);
+    let doc = aEvent.target.ownerDocument;
+    doc.defaultView.updateEditUIVisibility();
+    let contextMenuId = this._panel.getAttribute("context");
+    if (contextMenuId) {
+      let contextMenu = doc.getElementById(contextMenuId);
+      gELS.removeSystemEventListener(contextMenu, "command", this, true);
+    }
+  },
+
+  /**
+   * Avoid re-entrancy in the overflow handling by keeping track of invocations:
+   */
+  _lastOverflowCounter: 0,
+
+  /**
+   * Handle overflow in the toolbar by moving items to the overflow menu.
+   * @param {Event} aEvent
+   *        The overflow event that triggered handling overflow. May be omitted
+   *        in some cases (e.g. when we run this method after overflow handling
+   *        is re-enabled from customize mode, to ensure correct handling of
+   *        initial overflow).
+   */
+  async onOverflow(aEvent) {
+    if (!this._enabled)
+      return;
+
+    let child = this._target.lastElementChild;
+
+    let thisOverflowResponse = ++this._lastOverflowCounter;
+
+    let win = this._target.ownerGlobal;
+    let [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
+      return [this._target.scrollLeftMin, this._target.scrollLeftMax];
+    });
+    if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
+      return;
+    }
+
+    while (child && scrollLeftMin != scrollLeftMax) {
+      let prevChild = child.previousElementSibling;
+
+      if (child.getAttribute("overflows") != "false") {
+        this._collapsed.set(child.id, this._target.clientWidth);
+        child.setAttribute("overflowedItem", true);
+        child.setAttribute("cui-anchorid", this._chevron.id);
+        CustomizableUIInternal.ensureButtonContextMenu(child, this._toolbar, true);
+        CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);
+
+        this._list.insertBefore(child, this._list.firstElementChild);
+        if (!this._addedListener) {
+          CustomizableUI.addListener(this);
+        }
+        if (!CustomizableUI.isSpecialWidget(child.id)) {
+          this._toolbar.setAttribute("overflowing", "true");
+        }
+      }
+      child = prevChild;
+      [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
+        return [this._target.scrollLeftMin, this._target.scrollLeftMax];
+      });
+      // If the window has closed or if we re-enter because we were waiting
+      // for layout, stop.
+      if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
+        return;
+      }
+    }
+
+    win.UpdateUrlbarSearchSplitterState();
+    // Reset the counter because we finished handling overflow.
+    this._lastOverflowCounter = 0;
+  },
+
+  _onResize(aEvent) {
+    // Ignore bubbled-up resize events.
+    if (aEvent.target != aEvent.target.ownerGlobal.top) {
+      return;
+    }
+    if (!this._lazyResizeHandler) {
+      this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
+                                                 LAZY_RESIZE_INTERVAL_MS, 0);
+    }
+    this._lazyResizeHandler.arm();
+  },
+
+  /**
+   * Try to move toolbar items back to the toolbar from the overflow menu.
+   * @param {boolean} shouldMoveAllItems
+   *        Whether we should move everything (e.g. because we're being disabled)
+   * @param {number} targetWidth
+   *        Optional; the width of the toolbar in which we can put things.
+   *        Some consumers pass this to avoid reflows.
+   *        While there are items in the list, this width won't change, and so
+   *        we can avoid flushing layout by providing it and/or caching it.
+   *        Note that if `shouldMoveAllItems` is true, we never need the width
+   *        anyway.
+   */
+  _moveItemsBackToTheirOrigin(shouldMoveAllItems, targetWidth) {
+    let placements = gPlacements.get(this._toolbar.id);
+    let win = this._target.ownerGlobal;
+    while (this._list.firstElementChild) {
+      let child = this._list.firstElementChild;
+      let minSize = this._collapsed.get(child.id);
+
+      if (!shouldMoveAllItems && minSize) {
+        if (!targetWidth) {
+          let dwu = win.windowUtils;
+          targetWidth = Math.floor(dwu.getBoundsWithoutFlushing(this._target).width);
+        }
+        if (targetWidth <= minSize) {
+          break;
+        }
+      }
+
+      this._collapsed.delete(child.id);
+      let beforeNodeIndex = placements.indexOf(child.id) + 1;
+      // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
+      // we're inserting it at the end. This will mean first-in, first-out (more or less)
+      // leading to as little change in order as possible.
+      if (beforeNodeIndex == 0) {
+        beforeNodeIndex = placements.length;
+      }
+      let inserted = false;
+      for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
+        let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0];
+        // Unfortunately, XUL add-ons can mess with nodes after they are inserted,
+        // and this breaks the following code if the button isn't where we expect
+        // it to be (ie not a child of the target). In this case, ignore the node.
+        if (beforeNode && this._target == beforeNode.parentElement) {
+          this._target.insertBefore(child, beforeNode);
+          inserted = true;
+          break;
+        }
+      }
+      if (!inserted) {
+        this._target.appendChild(child);
+      }
+      child.removeAttribute("cui-anchorid");
+      child.removeAttribute("overflowedItem");
+      CustomizableUIInternal.ensureButtonContextMenu(child, this._target);
+      CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
+    }
+
+    win.UpdateUrlbarSearchSplitterState();
+
+    let collapsedWidgetIds = Array.from(this._collapsed.keys());
+    if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
+      this._toolbar.removeAttribute("overflowing");
+    }
+    if (this._addedListener && !this._collapsed.size) {
+      CustomizableUI.removeListener(this);
+      this._addedListener = false;
+    }
+  },
+
+  async _onLazyResize() {
+    if (!this._enabled)
+      return;
+
+    let win = this._target.ownerGlobal;
+    let [min, max, targetWidth] = await win.promiseDocumentFlushed(() => {
+      return [this._target.scrollLeftMin, this._target.scrollLeftMax, this._target.clientWidth];
+    });
+    if (win.closed) {
+      return;
+    }
+    if (min != max) {
+      this.onOverflow();
+    } else {
+      this._moveItemsBackToTheirOrigin(false, targetWidth);
+    }
+  },
+
+  _disable() {
+    this._enabled = false;
+    this._moveItemsBackToTheirOrigin(true);
+    if (this._lazyResizeHandler) {
+      this._lazyResizeHandler.disarm();
+    }
+  },
+
+  _enable() {
+    this._enabled = true;
+    this.onOverflow();
+  },
+
+  onWidgetBeforeDOMChange(aNode, aNextNode, aContainer) {
+    if (aContainer != this._target && aContainer != this._list) {
+      return;
+    }
+    // When we (re)move an item, update all the items that come after it in the list
+    // with the minsize *of the item before the to-be-removed node*. This way, we
+    // ensure that we try to move items back as soon as that's possible.
+    if (aNode.parentNode == this._list) {
+      let updatedMinSize;
+      if (aNode.previousElementSibling) {
+        updatedMinSize = this._collapsed.get(aNode.previousElementSibling.id);
+      } else {
+        // Force (these) items to try to flow back into the bar:
+        updatedMinSize = 1;
+      }
+      let nextItem = aNode.nextElementSibling;
+      while (nextItem) {
+        this._collapsed.set(nextItem.id, updatedMinSize);
+        nextItem = nextItem.nextElementSibling;
+      }
+    }
+  },
+
+  onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
+    if (aContainer != this._target && aContainer != this._list) {
+      return;
+    }
+
+    let nowInBar = aNode.parentNode == aContainer;
+    let nowOverflowed = aNode.parentNode == this._list;
+    let wasOverflowed = this._collapsed.has(aNode.id);
+
+    // If this wasn't overflowed before...
+    if (!wasOverflowed) {
+      // ... but it is now, then we added to the overflow panel.
+      if (nowOverflowed) {
+        // We could be the first item in the overflow panel if we're being inserted
+        // before the previous first item in it. We can't assume the minimum
+        // size is the same (because the other item might be much wider), so if
+        // there is no previous item, just allow this item to be put back in the
+        // toolbar immediately by specifying a very low minimum size.
+        let sourceOfMinSize = aNode.previousElementSibling;
+        let minSize = sourceOfMinSize ? this._collapsed.get(sourceOfMinSize.id) : 1;
+        this._collapsed.set(aNode.id, minSize);
+        aNode.setAttribute("cui-anchorid", this._chevron.id);
+        aNode.setAttribute("overflowedItem", true);
+        CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer, true);
+        CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target);
+      } else if (!nowInBar) {
+        // If it is not overflowed and not in the toolbar, and was not overflowed
+        // either, it moved out of the toolbar. That means there's now space in there!
+        // Let's try to move stuff back:
+        this._moveItemsBackToTheirOrigin(true);
+      }
+      // If it's in the toolbar now, then we don't care. An overflow event may
+      // fire afterwards; that's ok!
+    } else if (!nowOverflowed) {
+      // If it used to be overflowed...
+      // ... and isn't anymore, let's remove our bookkeeping:
+      this._collapsed.delete(aNode.id);
+      aNode.removeAttribute("cui-anchorid");
+      aNode.removeAttribute("overflowedItem");
+      CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer);
+      CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target);
+
+      let collapsedWidgetIds = Array.from(this._collapsed.keys());
+      if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
+        this._toolbar.removeAttribute("overflowing");
+      }
+      if (this._addedListener && !this._collapsed.size) {
+        CustomizableUI.removeListener(this);
+        this._addedListener = false;
+      }
+    } else if (aNode.previousElementSibling) {
+      // but if it still is, it must have changed places. Bookkeep:
+      let prevId = aNode.previousElementSibling.id;
+      let minSize = this._collapsed.get(prevId);
+      this._collapsed.set(aNode.id, minSize);
+    } else {
+      // If it's now the first item in the overflow list,
+      // maybe we can return it:
+      this._moveItemsBackToTheirOrigin(false);
+    }
+  },
+
+  findOverflowedInsertionPoints(aNode) {
+    let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
+    let areaId = this._toolbar.id;
+    let placements = gPlacements.get(areaId);
+    let nodeIndex = placements.indexOf(aNode.id);
+    let nodeBeforeNewNodeIsOverflown = false;
+
+    let loopIndex = -1;
+    // Loop through placements to find where to insert this item.
+    // As soon as we find an overflown widget, we will only
+    // insert in the overflow panel (this is why we check placements
+    // before the desired location for the new node). Once we pass
+    // the desired location of the widget, we look for placement ids
+    // that actually have DOM equivalents to insert before. If all
+    // else fails, we insert at the end of either the overflow list
+    // or the toolbar target.
+    while (++loopIndex < placements.length) {
+      let nextNodeId = placements[loopIndex];
+      if (loopIndex > nodeIndex) {
+        let nextNode = aNode.ownerDocument.getElementById(nextNodeId);
+        // If the node we're inserting can overflow, and the next node
+        // in the toolbar is overflown, we should insert this node
+        // in the overflow panel before it.
+        if (newNodeCanOverflow && this._collapsed.has(nextNodeId) &&
+            nextNode && nextNode.parentNode == this._list) {
+          return [this._list, nextNode];
+        }
+        // Otherwise (if either we can't overflow, or the previous node
+        // wasn't overflown), and the next node is in the toolbar itself,
+        // insert the node in the toolbar.
+        if ((!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) && nextNode &&
+            (nextNode.parentNode == this._target ||
+             // Also check if the next node is in a customization wrapper
+             // (toolbarpaletteitem). We don't need to do this for the
+             // overflow case because overflow is disabled in customize mode.
+             (nextNode.parentNode.localName == "toolbarpaletteitem" &&
+              nextNode.parentNode.parentNode == this._target))) {
+          return [this._target, nextNode];
+        }
+      } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) {
+        nodeBeforeNewNodeIsOverflown = true;
+      }
+    }
+
+    let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ?
+                                this._list : this._target;
+    return [containerForAppending, null];
+  },
+
+  getContainerFor(aNode) {
+    if (aNode.getAttribute("overflowedItem") == "true") {
+      return this._list;
+    }
+    return this._target;
+  },
+
+  _hideTimeoutId: null,
+  _showWithTimeout() {
+    this.show().then(() => {
+      let window = this._toolbar.ownerGlobal;
+      if (this._hideTimeoutId) {
+        window.clearTimeout(this._hideTimeoutId);
+      }
+      this._hideTimeoutId = window.setTimeout(() => {
+        if (!this._panel.firstElementChild.matches(":hover")) {
+          PanelMultiView.hidePopup(this._panel);
+        }
+      }, OVERFLOW_PANEL_HIDE_DELAY_MS);
+    });
+  },
+};
+
+CustomizableUIInternal.initialize();
new file mode 100644
--- /dev/null
+++ b/mail/components/customizableui/CustomizableWidgets.jsm
@@ -0,0 +1,861 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["CustomizableWidgets"];
+
+const {CustomizableUI} = ChromeUtils.import("resource:///modules/CustomizableUI.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  PanelView: "resource:///modules/PanelMultiView.jsm",
+  RecentlyClosedTabsAndWindowsMenuUtils: "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm",
+  ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
+  CharsetMenu: "resource://gre/modules/CharsetMenu.jsm",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+  Sanitizer: "resource:///modules/Sanitizer.jsm",
+  SyncedTabs: "resource://services-sync/SyncedTabs.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "CharsetBundle", function() {
+  const kCharsetBundle = "chrome://global/locale/charsetMenu.properties";
+  return Services.strings.createBundle(kCharsetBundle);
+});
+
+const kPrefCustomizationDebug = "browser.uiCustomization.debug";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let scope = {};
+  ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
+  let debug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
+  let consoleOptions = {
+    maxLogLevel: debug ? "all" : "log",
+    prefix: "CustomizableWidgets",
+  };
+  return new scope.ConsoleAPI(consoleOptions);
+});
+
+
+
+function setAttributes(aNode, aAttrs) {
+  let doc = aNode.ownerDocument;
+  for (let [name, value] of Object.entries(aAttrs)) {
+    if (!value) {
+      if (aNode.hasAttribute(name))
+        aNode.removeAttribute(name);
+    } else {
+      if (name == "shortcutId") {
+        continue;
+      }
+      if (name == "label" || name == "tooltiptext") {
+        let stringId = (typeof value == "string") ? value : name;
+        let additionalArgs = [];
+        if (aAttrs.shortcutId) {
+          let shortcut = doc.getElementById(aAttrs.shortcutId);
+          if (shortcut) {
+            additionalArgs.push(ShortcutUtils.prettifyShortcut(shortcut));
+          }
+        }
+        value = CustomizableUI.getLocalizedProperty({id: aAttrs.id}, stringId, additionalArgs);
+      }
+      aNode.setAttribute(name, value);
+    }
+  }
+}
+
+const CustomizableWidgets = [
+  {
+    id: "history-panelmenu",
+    type: "view",
+    viewId: "PanelUI-history",
+    shortcutId: "key_gotoHistory",
+    tooltiptext: "history-panelmenu.tooltiptext2",
+    recentlyClosedTabsPanel: "appMenu-library-recentlyClosedTabs",
+    recentlyClosedWindowsPanel: "appMenu-library-recentlyClosedWindows",
+    handleEvent(event) {
+      switch (event.type) {
+        case "PanelMultiViewHidden":
+          this.onPanelMultiViewHidden(event);
+          break;
+        case "ViewShowing":
+          this.onSubViewShowing(event);
+          break;
+        default:
+          throw new Error(`Unsupported event for '${this.id}'`);
+      }
+    },
+    onViewShowing(event) {
+      if (this._panelMenuView)
+        return;
+
+      let panelview = event.target;
+      let document = panelview.ownerDocument;
+      let window = document.defaultView;
+
+      // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42.
+      let query = "place:queryType=" + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY +
+        "&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING +
+        "&maxResults=42&excludeQueries=1";
+
+      this._panelMenuView = new window.PlacesPanelview(document.getElementById("appMenu_historyMenu"),
+        panelview, query);
+      // When either of these sub-subviews show, populate them with recently closed
+      // objects data.
+      document.getElementById(this.recentlyClosedTabsPanel).addEventListener("ViewShowing", this);
+      document.getElementById(this.recentlyClosedWindowsPanel).addEventListener("ViewShowing", this);
+      // When the popup is hidden (thus the panelmultiview node as well), make
+      // sure to stop listening to PlacesDatabase updates.
+      panelview.panelMultiView.addEventListener("PanelMultiViewHidden", this);
+    },
+    onViewHiding(event) {
+      log.debug("History view is being hidden!");
+    },
+    onPanelMultiViewHidden(event) {
+      let panelMultiView = event.target;
+      let document = panelMultiView.ownerDocument;
+      if (this._panelMenuView) {
+        this._panelMenuView.uninit();
+        delete this._panelMenuView;
+        document.getElementById(this.recentlyClosedTabsPanel).removeEventListener("ViewShowing", this);
+        document.getElementById(this.recentlyClosedWindowsPanel).removeEventListener("ViewShowing", this);
+      }
+      panelMultiView.removeEventListener("PanelMultiViewHidden", this);
+    },
+    onSubViewShowing(event) {
+      let panelview = event.target;
+      let document = event.target.ownerDocument;
+      let window = document.defaultView;
+      let viewType = panelview.id == this.recentlyClosedTabsPanel ? "Tabs" : "Windows";
+
+      this._panelMenuView.clearAllContents(panelview);
+
+      let utils = RecentlyClosedTabsAndWindowsMenuUtils;
+      let method = `get${viewType}Fragment`;
+      let fragment = utils[method](window, "toolbarbutton", true);
+      let elementCount = fragment.childElementCount;
+      this._panelMenuView._setEmptyPopupStatus(panelview, !elementCount);
+      if (!elementCount)
+        return;
+
+      let body = document.createXULElement("vbox");
+      body.className = "panel-subview-body";
+      body.appendChild(fragment);
+      let footer;
+      while (--elementCount >= 0) {
+        let element = body.children[elementCount];
+        CustomizableUI.addShortcut(element);
+        element.classList.add("subviewbutton");
+        if (element.classList.contains("restoreallitem")) {
+          footer = element;
+          element.classList.add("panel-subview-footer");
+        } else {
+          element.classList.add("subviewbutton-iconic", "bookmark-item");
+        }
+      }
+      panelview.appendChild(body);
+      panelview.appendChild(footer);
+    },
+  }, {
+    id: "save-page-button",
+    shortcutId: "key_savePage",
+    tooltiptext: "save-page-button.tooltiptext3",
+    onCommand(aEvent) {
+      let win = aEvent.target.ownerGlobal;
+      win.saveBrowser(win.gBrowser.selectedBrowser);
+    },
+  }, {
+    id: "find-button",
+    shortcutId: "key_find",
+    tooltiptext: "find-button.tooltiptext3",
+    onCommand(aEvent) {
+      let win = aEvent.target.ownerGlobal;
+      if (win.gLazyFindCommand) {
+        win.gLazyFindCommand("onFindCommand");
+      }
+    },
+  }, {
+    id: "open-file-button",
+    shortcutId: "openFileKb",
+    tooltiptext: "open-file-button.tooltiptext3",
+    onCommand(aEvent) {
+      let win = aEvent.target.ownerGlobal;
+      win.BrowserOpenFileWindow();
+    },
+  }, {
+    id: "sidebar-button",
+    tooltiptext: "sidebar-button.tooltiptext2",
+    onCommand(aEvent) {
+      let win = aEvent.target.ownerGlobal;
+      win.SidebarUI.toggle();
+    },
+    onCreated(aNode) {
+      // Add an observer so the button is checked while the sidebar is open
+      let doc = aNode.ownerDocument;
+      let obChecked = doc.createXULElement("observes");
+      obChecked.setAttribute("element", "sidebar-box");
+      obChecked.setAttribute("attribute", "checked");
+      let obPosition = doc.createXULElement("observes");
+      obPosition.setAttribute("element", "sidebar-box");
+      obPosition.setAttribute("attribute", "positionend");
+
+      aNode.appendChild(obChecked);
+      aNode.appendChild(obPosition);
+    },
+  }, {
+    id: "add-ons-button",
+    shortcutId: "key_openAddons",
+    tooltiptext: "add-ons-button.tooltiptext3",
+    onCommand(aEvent) {
+      let win = aEvent.target.ownerGlobal;
+      win.BrowserOpenAddonsMgr();
+    },
+  }, {
+    id: "zoom-controls",
+    type: "custom",
+    tooltiptext: "zoom-controls.tooltiptext2",
+    onBuild(aDocument) {
+      let buttons = [{
+        id: "zoom-out-button",
+        command: "cmd_fullZoomReduce",
+        label: true,
+        closemenu: "none",
+        tooltiptext: "tooltiptext2",
+        shortcutId: "key_fullZoomReduce",
+        "class": "toolbarbutton-1 toolbarbutton-combined",
+      }, {
+        id: "zoom-reset-button",
+        command: "cmd_fullZoomReset",
+        closemenu: "none",
+        tooltiptext: "tooltiptext2",
+        shortcutId: "key_fullZoomReset",
+        "class": "toolbarbutton-1 toolbarbutton-combined",
+      }, {
+        id: "zoom-in-button",
+        command: "cmd_fullZoomEnlarge",
+        closemenu: "none",
+        label: true,
+        tooltiptext: "tooltiptext2",
+        shortcutId: "key_fullZoomEnlarge",
+        "class": "toolbarbutton-1 toolbarbutton-combined",
+      }];
+
+      let node = aDocument.createXULElement("toolbaritem");
+      node.setAttribute("id", "zoom-controls");
+      node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
+      node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
+      // Set this as an attribute in addition to the property to make sure we can style correctly.
+      node.setAttribute("removable", "true");
+      node.classList.add("chromeclass-toolbar-additional");
+      node.classList.add("toolbaritem-combined-buttons");
+
+      buttons.forEach(function(aButton, aIndex) {
+        if (aIndex != 0)
+          node.appendChild(aDocument.createXULElement("separator"));
+        let btnNode = aDocument.createXULElement("toolbarbutton");
+        setAttributes(btnNode, aButton);
+        node.appendChild(btnNode);
+      });
+      return node;
+    },
+  }, {
+    id: "edit-controls",
+    type: "custom",
+    tooltiptext: "edit-controls.tooltiptext2",
+    onBuild(aDocument) {
+      let buttons = [{
+        id: "cut-button",
+        command: "cmd_cut",
+        label: true,
+        tooltiptext: "tooltiptext2",
+        shortcutId: "key_cut",
+        "class": "toolbarbutton-1 toolbarbutton-combined",
+      }, {
+        id: "copy-button",
+        command: "cmd_copy",
+        label: true,
+        tooltiptext: "tooltiptext2",
+        shortcutId: "key_copy",
+        "class": "toolbarbutton-1 toolbarbutton-combined",
+      }, {
+        id: "paste-button",
+        command: "cmd_paste",
+        label: true,
+        tooltiptext: "tooltiptext2",
+        shortcutId: "key_paste",
+        "class": "toolbarbutton-1 toolbarbutton-combined",
+      }];
+
+      let node = aDocument.createXULElement("toolbaritem");
+      node.setAttribute("id", "edit-controls");
+      node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
+      node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
+      // Set this as an attribute in addition to the property to make sure we can style correctly.
+      node.setAttribute("removable", "true");
+      node.classList.add("chromeclass-toolbar-additional");
+      node.classList.add("toolbaritem-combined-buttons");
+
+      buttons.forEach(function(aButton, aIndex) {
+        if (aIndex != 0)
+          node.appendChild(aDocument.createXULElement("separator"));
+        let btnNode = aDocument.createXULElement("toolbarbutton");
+        setAttributes(btnNode, aButton);
+        node.appendChild(btnNode);
+      });
+
+      let listener = {
+        onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
+          if (aWidgetId != this.id || aDoc != aDocument)
+            return;
+          CustomizableUI.removeListener(listener);
+        },
+        onWidgetOverflow(aWidgetNode) {
+          if (aWidgetNode == node) {
+            node.ownerGlobal.updateEditUIVisibility();
+          }
+        },
+        onWidgetUnderflow(aWidgetNode) {
+          if (aWidgetNode == node) {
+            node.ownerGlobal.updateEditUIVisibility();
+          }
+        },
+      };
+      CustomizableUI.addListener(listener);
+
+      return node;
+    },
+  },
+  {
+    id: "characterencoding-button",
+    label: "characterencoding-button2.label",
+    type: "view",
+    viewId: "PanelUI-characterEncodingView",
+    tooltiptext: "characterencoding-button2.tooltiptext",
+    maybeDisableMenu(aDocument) {
+      let window = aDocument.defaultView;
+      return !(window.gBrowser &&
+               window.gBrowser.selectedBrowser.mayEnableCharacterEncodingMenu);
+    },
+    populateList(aDocument, aContainerId, aSection) {
+      let containerElem = aDocument.getElementById(aContainerId);
+
+      containerElem.addEventListener("command", this.onCommand);
+
+      let list = this.charsetInfo[aSection];
+
+      for (let item of list) {
+        let elem = aDocument.createXULElement("toolbarbutton");
+        elem.setAttribute("label", item.label);
+        elem.setAttribute("type", "checkbox");
+        elem.section = aSection;
+        elem.value = item.value;
+        elem.setAttribute("class", "subviewbutton");
+        containerElem.appendChild(elem);
+      }
+    },
+    updateCurrentCharset(aDocument) {
+      let currentCharset = aDocument.defaultView.gBrowser.selectedBrowser.characterSet;
+      currentCharset = CharsetMenu.foldCharset(currentCharset);
+
+      let pinnedContainer = aDocument.getElementById("PanelUI-characterEncodingView-pinned");
+      let charsetContainer = aDocument.getElementById("PanelUI-characterEncodingView-charsets");
+      let elements = [...(pinnedContainer.children), ...(charsetContainer.children)];
+
+      this._updateElements(elements, currentCharset);
+    },
+    updateCurrentDetector(aDocument) {
+      let detectorContainer = aDocument.getElementById("PanelUI-characterEncodingView-autodetect");
+      let currentDetector;
+      try {
+        currentDetector = Services.prefs.getComplexValue(
+          "intl.charset.detector", Ci.nsIPrefLocalizedString).data;
+      } catch (e) {}
+
+      this._updateElements(detectorContainer.children, currentDetector);
+    },
+    _updateElements(aElements, aCurrentItem) {
+      if (!aElements.length) {
+        return;
+      }
+      let disabled = this.maybeDisableMenu(aElements[0].ownerDocument);
+      for (let elem of aElements) {
+        if (disabled) {
+          elem.setAttribute("disabled", "true");
+        } else {
+          elem.removeAttribute("disabled");
+        }
+        if (elem.value.toLowerCase() == aCurrentItem.toLowerCase()) {
+          elem.setAttribute("checked", "true");
+        } else {
+          elem.removeAttribute("checked");
+        }
+      }
+    },
+    onViewShowing(aEvent) {
+      if (!this._inited) {
+        this.onInit();
+      }
+      let document = aEvent.target.ownerDocument;
+
+      let autoDetectLabelId = "PanelUI-characterEncodingView-autodetect-label";
+      let autoDetectLabel = document.getElementById(autoDetectLabelId);
+      if (!autoDetectLabel.hasAttribute("value")) {
+        let label = CharsetBundle.GetStringFromName("charsetMenuAutodet");
+        autoDetectLabel.setAttribute("value", label);
+        this.populateList(document,
+                          "PanelUI-characterEncodingView-pinned",
+                          "pinnedCharsets");
+        this.populateList(document,
+                          "PanelUI-characterEncodingView-charsets",
+                          "otherCharsets");
+        this.populateList(document,
+                          "PanelUI-characterEncodingView-autodetect",
+                          "detectors");
+      }
+      this.updateCurrentDetector(document);
+      this.updateCurrentCharset(document);
+    },
+    onCommand(aEvent) {
+      let node = aEvent.target;
+      if (!node.hasAttribute || !node.section) {
+        return;
+      }
+
+      let window = node.ownerGlobal;
+      let section = node.section;
+      let value = node.value;
+
+      // The behavior as implemented here is directly based off of the
+      // `MultiplexHandler()` method in browser.js.
+      if (section != "detectors") {
+        window.BrowserSetForcedCharacterSet(value);
+      } else {
+        // Set the detector pref.
+        try {
+          Services.prefs.setStringPref("intl.charset.detector", value);
+        } catch (e) {
+          Cu.reportError("Failed to set the intl.charset.detector preference.");
+        }
+        // Prepare a browser page reload with a changed charset.
+        window.BrowserCharsetReload();
+      }
+    },
+    onCreated(aNode) {
+      let document = aNode.ownerDocument;
+
+      let updateButton = () => {
+        if (this.maybeDisableMenu(document))
+          aNode.setAttribute("disabled", "true");
+        else
+          aNode.removeAttribute("disabled");
+      };
+
+      let getPanel = () => {
+        let {PanelUI} = document.ownerGlobal;
+        return PanelUI.overflowPanel;
+      };
+
+      if (CustomizableUI.getAreaType(this.currentArea) == CustomizableUI.TYPE_MENU_PANEL) {
+        getPanel().addEventListener("popupshowing", updateButton);
+      }
+
+      let listener = {
+        onWidgetAdded: (aWidgetId, aArea) => {
+          if (aWidgetId != this.id)
+            return;
+          if (CustomizableUI.getAreaType(aArea) == CustomizableUI.TYPE_MENU_PANEL) {
+            getPanel().addEventListener("popupshowing", updateButton);
+          }
+        },
+        onWidgetRemoved: (aWidgetId, aPrevArea) => {
+          if (aWidgetId != this.id)
+            return;
+          aNode.removeAttribute("disabled");
+          if (CustomizableUI.getAreaType(aPrevArea) == CustomizableUI.TYPE_MENU_PANEL) {
+            getPanel().removeEventListener("popupshowing", updateButton);
+          }
+        },
+        onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
+          if (aWidgetId != this.id || aDoc != document)
+            return;
+
+          CustomizableUI.removeListener(listener);
+          getPanel().removeEventListener("popupshowing", updateButton);
+        },
+      };
+      CustomizableUI.addListener(listener);
+      this.onInit();
+    },
+    onInit() {
+      this._inited = true;
+      if (!this.charsetInfo) {
+        this.charsetInfo = CharsetMenu.getData();
+      }
+    },
+  }, {
+    id: "email-link-button",
+    tooltiptext: "email-link-button.tooltiptext3",
+    onCommand(aEvent) {
+      let win = aEvent.view;
+      win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser);
+    },
+  }];
+
+if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+  CustomizableWidgets.push({
+    id: "sync-button",
+    label: "remotetabs-panelmenu.label",
+    tooltiptext: "remotetabs-panelmenu.tooltiptext2",
+    type: "view",
+    viewId: "PanelUI-remotetabs",
+    deckIndices: {
+      DECKINDEX_TABS: 0,
+      DECKINDEX_TABSDISABLED: 1,
+      DECKINDEX_FETCHING: 2,
+      DECKINDEX_NOCLIENTS: 3,
+    },
+    TABS_PER_PAGE: 25,
+    NEXT_PAGE_MIN_TABS: 5, // Minimum number of tabs displayed when we click "Show All"
+    onViewShowing(aEvent) {
+      let doc = aEvent.target.ownerDocument;
+      this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist");
+      Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
+
+      if (SyncedTabs.isConfiguredToSyncTabs) {
+        if (SyncedTabs.hasSyncedThisSession) {
+          this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
+        } else {
+          // Sync hasn't synced tabs yet, so show the "fetching" panel.
+          this.setDeckIndex(this.deckIndices.DECKINDEX_FETCHING);
+        }
+        // force a background sync.
+        SyncedTabs.syncTabs().catch(ex => {
+          Cu.reportError(ex);
+        });
+        // show the current list - it will be updated by our observer.
+        this._showTabs();
+      } else {
+        // not configured to sync tabs, so no point updating the list.
+        this.setDeckIndex(this.deckIndices.DECKINDEX_TABSDISABLED);
+      }
+    },
+    onViewHiding() {
+      Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
+      this._tabsList = null;
+    },
+    _tabsList: null,
+    observe(subject, topic, data) {
+      switch (topic) {
+        case SyncedTabs.TOPIC_TABS_CHANGED:
+          this._showTabs();
+          break;
+        default:
+          break;
+      }
+    },
+    setDeckIndex(index) {
+      let deck = this._tabsList.ownerDocument.getElementById("PanelUI-remotetabs-deck");
+      // We call setAttribute instead of relying on the XBL property setter due
+      // to things going wrong when we try and set the index before the XBL
+      // binding has been created - see bug 1241851 for the gory details.
+      deck.setAttribute("selectedIndex", index);
+    },
+
+    _showTabsPromise: Promise.resolve(),
+    // Update the tab list after any existing in-flight updates are complete.
+    _showTabs(paginationInfo) {
+      this._showTabsPromise = this._showTabsPromise.then(() => {
+        return this.__showTabs(paginationInfo);
+      }, e => {
+        Cu.reportError(e);
+      });
+    },
+    // Return a new promise to update the tab list.
+    __showTabs(paginationInfo) {
+      if (!this._tabsList) {
+        // Closed between the previous `this._showTabsPromise`
+        // resolving and now.
+        return undefined;
+      }
+      let doc = this._tabsList.ownerDocument;
+      return SyncedTabs.getTabClients().then(clients => {
+        // The view may have been hidden while the promise was resolving.
+        if (!this._tabsList) {
+          return;
+        }
+        if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
+          // the "fetching tabs" deck is being shown - let's leave it there.
+          // When that first sync completes we'll be notified and update.
+          return;
+        }
+
+        if (clients.length === 0) {
+          this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS);
+          return;
+        }
+
+        this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
+        this._clearTabList();
+        SyncedTabs.sortTabClientsByLastUsed(clients);
+        let fragment = doc.createDocumentFragment();
+
+        for (let client of clients) {
+          // add a menu separator for all clients other than the first.
+          if (fragment.lastElementChild) {
+            let separator = doc.createXULElement("menuseparator");
+            fragment.appendChild(separator);
+          }
+          if (paginationInfo && paginationInfo.clientId == client.id) {
+            this._appendClient(client, fragment, paginationInfo.maxTabs);
+          } else {
+            this._appendClient(client, fragment);
+          }
+        }
+        this._tabsList.appendChild(fragment);
+        PanelView.forNode(this._tabsList.closest("panelview"))
+                 .descriptionHeightWorkaround();
+      }).catch(err => {
+        Cu.reportError(err);
+      }).then(() => {
+        // an observer for tests.
+        Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated");
+      });
+    },
+    _clearTabList() {
+      let list = this._tabsList;
+      while (list.lastChild) {
+        list.lastChild.remove();
+      }
+    },
+    _showNoClientMessage() {
+      this._appendMessageLabel("notabslabel");
+    },
+    _appendMessageLabel(messageAttr, appendTo = null) {
+      if (!appendTo) {
+        appendTo = this._tabsList;
+      }
+      let message = this._tabsList.getAttribute(messageAttr);
+      let doc = this._tabsList.ownerDocument;
+      let messageLabel = doc.createXULElement("label");
+      messageLabel.textContent = message;
+      appendTo.appendChild(messageLabel);
+      return messageLabel;
+    },
+    _appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
+      let doc = attachFragment.ownerDocument;
+      // Create the element for the remote client.
+      let clientItem = doc.createXULElement("label");
+      clientItem.setAttribute("itemtype", "client");
+      let window = doc.defaultView;
+      clientItem.setAttribute("tooltiptext",
+        window.gSync.formatLastSyncDate(new Date(client.lastModified)));
+      clientItem.textContent = client.name;
+
+      attachFragment.appendChild(clientItem);
+
+      if (client.tabs.length == 0) {
+        let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
+        label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
+      } else {
+        // If this page will display all tabs, show no additional buttons.
+        // If the next page will display all the remaining tabs, show a "Show All" button
+        // Otherwise, show a "Shore More" button
+        let hasNextPage = client.tabs.length > maxTabs;
+        let nextPageIsLastPage = hasNextPage && maxTabs + this.TABS_PER_PAGE >= client.tabs.length;
+        if (nextPageIsLastPage) {
+          // When the user clicks "Show All", try to have at least NEXT_PAGE_MIN_TABS more tabs
+          // to display in order to avoid user frustration
+          maxTabs = Math.min(client.tabs.length - this.NEXT_PAGE_MIN_TABS, maxTabs);
+        }
+        if (hasNextPage) {
+          client.tabs = client.tabs.slice(0, maxTabs);
+        }
+        for (let tab of client.tabs) {
+          let tabEnt = this._createTabElement(doc, tab);
+          attachFragment.appendChild(tabEnt);
+        }
+        if (hasNextPage) {
+          let showAllEnt = this._createShowMoreElement(doc, client.id,
+                                                       nextPageIsLastPage ?
+                                                       Infinity :
+                                                       maxTabs + this.TABS_PER_PAGE);
+          attachFragment.appendChild(showAllEnt);
+        }
+      }
+    },
+    _createTabElement(doc, tabInfo) {
+      let item = doc.createXULElement("toolbarbutton");
+      let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
+      item.setAttribute("itemtype", "tab");
+      item.setAttribute("class", "subviewbutton");
+      item.setAttribute("targetURI", tabInfo.url);
+      item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
+      item.setAttribute("image", tabInfo.icon);
+      item.setAttribute("tooltiptext", tooltipText);
+      // We need to use "click" instead of "command" here so openUILink
+      // respects different buttons (eg, to open in a new tab).
+      item.addEventListener("click", e => {
+        doc.defaultView.openUILink(tabInfo.url, e, {
+          triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+        });
+        if (doc.defaultView.whereToOpenLink(e) != "current") {
+          e.preventDefault();
+          e.stopPropagation();
+        } else {
+          CustomizableUI.hidePanelForNode(item);
+        }
+      });
+      return item;
+    },
+    _createShowMoreElement(doc, clientId, showCount) {
+      let labelAttr, tooltipAttr;
+      if (showCount === Infinity) {
+        labelAttr = "showAllLabel";
+        tooltipAttr = "showAllTooltipText";
+      } else {
+        labelAttr = "showMoreLabel";
+        tooltipAttr = "showMoreTooltipText";
+      }
+      let showAllItem = doc.createXULElement("toolbarbutton");
+      showAllItem.setAttribute("itemtype", "showmorebutton");
+      showAllItem.setAttribute("class", "subviewbutton");
+      let label = this._tabsList.getAttribute(labelAttr);
+      showAllItem.setAttribute("label", label);
+      let tooltipText = this._tabsList.getAttribute(tooltipAttr);
+      showAllItem.setAttribute("tooltiptext", tooltipText);
+      showAllItem.addEventListener("click", e => {
+        e.preventDefault();
+        e.stopPropagation();
+        this._showTabs({ clientId, maxTabs: showCount });
+      });
+      return showAllItem;
+    },
+  });
+}
+
+let preferencesButton = {
+  id: "preferences-button",
+  onCommand(aEvent) {
+    let win = aEvent.target.ownerGlobal;
+    win.openPreferences(undefined);
+  },
+};
+if (AppConstants.platform == "win") {
+  preferencesButton.label = "preferences-button.labelWin";
+  preferencesButton.tooltiptext = "preferences-button.tooltipWin2";
+} else if (AppConstants.platform == "macosx") {
+  preferencesButton.tooltiptext = "preferences-button.tooltiptext.withshortcut";
+  preferencesButton.shortcutId = "key_preferencesCmdMac";
+} else {
+  preferencesButton.tooltiptext = "preferences-button.tooltiptext2";
+}
+CustomizableWidgets.push(preferencesButton);
+
+if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) {
+  CustomizableWidgets.push({
+    id: "panic-button",
+    type: "view",
+    viewId: "PanelUI-panicView",
+
+    forgetButtonCalled(aEvent) {
+      let doc = aEvent.target.ownerDocument;
+      let group = doc.getElementById("PanelUI-panic-timeSpan");
+      let itemsToClear = [
+        "cookies", "history", "openWindows", "formdata", "sessions", "cache", "downloads", "offlineApps",
+      ];
+      let newWindowPrivateState = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView) ?
+                                  "private" : "non-private";
+      let promise = Sanitizer.sanitize(itemsToClear, {
+        ignoreTimespan: false,
+        range: Sanitizer.getClearRange(+group.value),
+        privateStateForNewWindow: newWindowPrivateState,
+      });
+      promise.then(function() {
+        let otherWindow = Services.wm.getMostRecentWindow("navigator:browser");
+        if (otherWindow.closed) {
+          Cu.reportError("Got a closed window!");
+        }
+        if (otherWindow.PanicButtonNotifier) {
+          otherWindow.PanicButtonNotifier.notify();
+        } else {
+          otherWindow.PanicButtonNotifierShouldNotify = true;
+        }
+      });
+    },
+    handleEvent(aEvent) {
+      switch (aEvent.type) {
+        case "command":
+          this.forgetButtonCalled(aEvent);
+          break;
+      }
+    },
+    onViewShowing(aEvent) {
+      let win = aEvent.target.ownerGlobal;
+      let doc = win.document;
+      let eventBlocker = null;
+      if (!doc.querySelector("#PanelUI-panic-timeframe")) {
+        win.MozXULElement.insertFTLIfNeeded("browser/panicButton.ftl");
+        let frag = win.MozXULElement.parseXULToFragment(`
+          <vbox class="panel-subview-body">
+            <hbox id="PanelUI-panic-timeframe">
+              <image id="PanelUI-panic-timeframe-icon" alt=""/>
+              <vbox flex="1">
+                <description data-l10n-id="panic-main-timeframe-desc" id="PanelUI-panic-mainDesc"></description>
+                <radiogroup id="PanelUI-panic-timeSpan" aria-labelledby="PanelUI-panic-mainDesc" closemenu="none">
+                  <radio id="PanelUI-panic-5min" data-l10n-id="panic-button-5min" selected="true"
+                        value="5" class="subviewradio"/>
+                  <radio id="PanelUI-panic-2hr" data-l10n-id="panic-button-2hr"
+                        value="2" class="subviewradio"/>
+                  <radio id="PanelUI-panic-day" data-l10n-id="panic-button-day"
+                        value="6" class="subviewradio"/>
+                </radiogroup>
+              </vbox>
+            </hbox>
+            <vbox id="PanelUI-panic-explanations">
+              <label id="PanelUI-panic-actionlist-main-label" data-l10n-id="panic-button-action-desc"></label>
+
+              <label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-tabs-and-windows"></label>
+              <label id="PanelUI-panic-actionlist-cookies" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-cookies"></label>
+              <label id="PanelUI-panic-actionlist-history" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-delete-history"></label>
+              <label id="PanelUI-panic-actionlist-newwindow" class="PanelUI-panic-actionlist" data-l10n-id="panic-button-open-new-window"></label>
+
+              <label id="PanelUI-panic-warning" data-l10n-id="panic-button-undo-warning"></label>
+            </vbox>
+            <button id="PanelUI-panic-view-button"
+                    data-l10n-id="panic-button-forget-button"/>
+          </vbox>
+        `);
+
+        aEvent.target.appendChild(frag);
+        eventBlocker = doc.l10n.translateElements([aEvent.target]);
+      }
+
+      let forgetButton = aEvent.target.querySelector("#PanelUI-panic-view-button");
+      let group = doc.getElementById("PanelUI-panic-timeSpan");
+      group.selectedItem = doc.getElementById("PanelUI-panic-5min");
+      forgetButton.addEventListener("command", this);
+
+      if (eventBlocker) {
+        aEvent.detail.addBlocker(eventBlocker);
+      }
+    },
+    onViewHiding(aEvent) {
+      let forgetButton = aEvent.target.querySelector("#PanelUI-panic-view-button");
+      forgetButton.removeEventListener("command", this);
+    },
+  });
+}
+
+if (PrivateBrowsingUtils.enabled) {
+  CustomizableWidgets.push({
+    id: "privatebrowsing-button",
+    shortcutId: "key_privatebrowsing",
+    onCommand(e) {
+      let win = e.target.ownerGlobal;
+      win.OpenBrowserWindow({private: true});
+    },
+  });
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/customizableui/CustomizeMode.jsm
@@ -0,0 +1,2645 @@
+/* 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";
+
+var EXPORTED_SYMBOLS = ["CustomizeMode"];
+
+const kPrefCustomizationDebug = "browser.uiCustomization.debug";
+const kPaletteId = "customization-palette";
+const kDragDataTypePrefix = "text/toolbarwrapper-id/";
+const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
+const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
+const kExtraDragSpacePref = "browser.tabs.extraDragSpace";
+const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";
+
+const kPanelItemContextMenu = "customizationPanelItemContextMenu";
+const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
+
+const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
+const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
+const kDownloadAutoHidePref = "browser.download.autohideButton";
+
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {CustomizableUI} = ChromeUtils.import("resource:///modules/CustomizableUI.jsm");
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["CSS"]);
+
+ChromeUtils.defineModuleGetter(this, "AddonManager",
+                               "resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "AMTelemetry",
+                               "resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "DragPositionManager",
+                               "resource:///modules/DragPositionManager.jsm");
+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.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);
+  let consoleOptions = {
+    maxLogLevel: gDebug ? "all" : "log",
+    prefix: "CustomizeMode",
+  };
+  return new scope.ConsoleAPI(consoleOptions);
+});
+
+var gDraggingInToolbars;
+
+var gTab;
+
+function closeGlobalTab() {
+  let win = gTab.ownerGlobal;
+  if (win.gBrowser.browsers.length == 1) {
+    win.BrowserOpenTab();
+  }
+  win.gBrowser.removeTab(gTab);
+  gTab = null;
+}
+
+var gTabsProgressListener = {
+  onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) {
+    // Tear down customize mode when the customize mode tab loads some other page.
+    // Customize mode will be re-entered if "about:blank" is loaded again, so
+    // don't tear down in this case.
+    if (!gTab || gTab.linkedBrowser != aBrowser || aLocation.spec == "about:blank") {
+      return;
+    }
+
+    unregisterGlobalTab();
+  },
+};
+
+function unregisterGlobalTab() {
+  gTab.removeEventListener("TabClose", unregisterGlobalTab);
+  let win = gTab.ownerGlobal;
+  win.removeEventListener("unload", unregisterGlobalTab);
+  win.gBrowser.removeTabsProgressListener(gTabsProgressListener);
+
+  gTab.removeAttribute("customizemode");
+
+  gTab = null;
+}
+
+function CustomizeMode(aWindow) {
+  this.window = aWindow;
+  this.document = aWindow.document;
+  this.browser = aWindow.gBrowser;
+  this.areas = new Set();
+
+  let content = this.$("customization-content-container");
+  if (!content) {
+    this.window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl");
+    let container = this.$("customization-container");
+    container.replaceChild(this.window.MozXULElement.parseXULToFragment(container.firstChild.data), container.lastChild);
+  }
+  // There are two palettes - there's the palette that can be overlayed with
+  // toolbar items in browser.xul. This is invisible, and never seen by the
+  // user. Then there's the visible palette, which gets populated and displayed
+  // to the user when in customizing mode.
+  this.visiblePalette = this.$(kPaletteId);
+  this.pongArena = this.$("customization-pong-arena");
+
+  if (this._canDrawInTitlebar()) {
+    this._updateTitlebarCheckbox();
+    this._updateDragSpaceCheckbox();
+    Services.prefs.addObserver(kDrawInTitlebarPref, this);
+    Services.prefs.addObserver(kExtraDragSpacePref, this);
+  } else {
+    this.$("customization-titlebar-visibility-checkbox").hidden = true;
+    this.$("customization-extra-drag-space-checkbox").hidden = true;
+  }
+
+  this.window.addEventListener("unload", this);
+}
+
+CustomizeMode.prototype = {
+  _changed: false,
+  _transitioning: false,
+  window: null,
+  document: null,
+  // areas is used to cache the customizable areas when in customization mode.
+  areas: null,
+  // When in customizing mode, we swap out the reference to the invisible
+  // palette in gNavToolbox.palette for our visiblePalette. This way, for the
+  // customizing browser window, when widgets are removed from customizable
+  // areas and added to the palette, they're added to the visible palette.
+  // _stowedPalette is a reference to the old invisible palette so we can
+  // restore gNavToolbox.palette to its original state after exiting
+  // customization mode.
+  _stowedPalette: null,
+  _dragOverItem: null,
+  _customizing: false,
+  _skipSourceNodeCheck: null,
+  _mainViewContext: null,
+
+  get _handler() {
+    return this.window.CustomizationHandler;
+  },
+
+  uninit() {
+    if (this._canDrawInTitlebar()) {
+      Services.prefs.removeObserver(kDrawInTitlebarPref, this);
+      Services.prefs.removeObserver(kExtraDragSpacePref, this);
+    }
+  },
+
+  $(id) {
+    return this.document.getElementById(id);
+  },
+
+  toggle() {
+    if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
+      this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
+      return;
+    }
+    if (this._customizing) {
+      this.exit();
+    } else {
+      this.enter();
+    }
+  },
+
+  async _updateThemeButtonIcon() {
+    let lwthemeButton = this.$("customization-lwtheme-button");
+    let lwthemeIcon = lwthemeButton.icon;
+    let theme = (await AddonManager.getAddonsByTypes(["theme"])).find(addon => addon.isActive);
+    lwthemeIcon.style.backgroundImage = (theme && theme.iconURL) ? "url(" + theme.iconURL + ")" : "";
+  },
+
+  setTab(aTab) {
+    if (gTab == aTab) {
+      return;
+    }
+
+    if (gTab) {
+      closeGlobalTab();
+    }
+
+    gTab = aTab;
+
+    gTab.setAttribute("customizemode", "true");
+    SessionStore.persistTabAttribute("customizemode");
+
+    if (gTab.linkedPanel) {
+      gTab.linkedBrowser.stop();
+    }
+
+    let win = gTab.ownerGlobal;
+
+    win.gBrowser.setTabTitle(gTab);
+    win.gBrowser.setIcon(gTab,
+                         "chrome://browser/skin/customize.svg");
+
+    gTab.addEventListener("TabClose", unregisterGlobalTab);
+
+    win.gBrowser.addTabsProgressListener(gTabsProgressListener);
+
+    win.addEventListener("unload", unregisterGlobalTab);
+
+    if (gTab.selected) {
+      win.gCustomizeMode.enter();
+    }
+  },
+
+  enter() {
+    if (!this.window.toolbar.visible) {
+      let w = this.window.getTopWin(true);
+      if (w) {
+        w.gCustomizeMode.enter();
+        return;
+      }
+      let obs = () => {
+        Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
+        w = this.window.getTopWin(true);
+        w.gCustomizeMode.enter();
+      };
+      Services.obs.addObserver(obs, "browser-delayed-startup-finished");
+      this.window.openTrustedLinkIn("about:newtab", "window");
+      return;
+    }
+    this._wantToBeInCustomizeMode = true;
+
+    if (this._customizing || this._handler.isEnteringCustomizeMode) {
+      return;
+    }
+
+    // Exiting; want to re-enter once we've done that.
+    if (this._handler.isExitingCustomizeMode) {
+      log.debug("Attempted to enter while we're in the middle of exiting. " +
+                "We'll exit after we've entered");
+      return;
+    }
+
+    if (!gTab) {
+      this.setTab(this.browser.loadOneTab("about:blank", {
+        inBackground: false,
+        forceNotRemote: true,
+        skipAnimation: true,
+        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+      }));
+      return;
+    }
+    if (!gTab.selected) {
+      // This will force another .enter() to be called via the
+      // onlocationchange handler of the tabbrowser, so we return early.
+      gTab.ownerGlobal.gBrowser.selectedTab = gTab;
+      return;
+    }
+    gTab.ownerGlobal.focus();
+    if (gTab.ownerDocument != this.document) {
+      return;
+    }
+
+    let window = this.window;
+    let document = this.document;
+
+    this._handler.isEnteringCustomizeMode = true;
+
+    // Always disable the reset button at the start of customize mode, it'll be re-enabled
+    // if necessary when we finish entering:
+    let resetButton = this.$("customization-reset-button");
+    resetButton.setAttribute("disabled", "true");
+
+    (async () => {
+      // We shouldn't start customize mode until after browser-delayed-startup has finished:
+      if (!this.window.gBrowserInit.delayedStartupFinished) {
+        await new Promise(resolve => {
+          let delayedStartupObserver = aSubject => {
+            if (aSubject == this.window) {
+              Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
+              resolve();
+            }
+          };
+
+          Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished");
+        });
+      }
+
+      CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
+      CustomizableUI.notifyStartCustomizing(this.window);
+
+      // Add a keypress listener to the document so that we can quickly exit
+      // customization mode when pressing ESC.
+      document.addEventListener("keypress", this);
+
+      // Same goes for the menu button - if we're customizing, a click on the
+      // menu button means a quick exit from customization mode.
+      window.PanelUI.hide();
+
+      let panelHolder = document.getElementById("customization-panelHolder");
+      let panelContextMenu = document.getElementById(kPanelItemContextMenu);
+      this._previousPanelContextMenuParent = panelContextMenu.parentNode;
+      document.getElementById("mainPopupSet").appendChild(panelContextMenu);
+      panelHolder.appendChild(window.PanelUI.overflowFixedList);
+
+      window.PanelUI.overflowFixedList.setAttribute("customizing", true);
+      window.PanelUI.menuButton.disabled = true;
+      document.getElementById("nav-bar-overflow-button").disabled = true;
+
+      this._transitioning = true;
+
+      let customizer = document.getElementById("customization-container");
+      customizer.parentNode.selectedPanel = customizer;
+      customizer.hidden = false;
+
+      this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP);
+
+      let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
+      for (let toolbar of customizableToolbars)
+        toolbar.setAttribute("customizing", true);
+
+      await this._doTransition(true);
+
+      // Let everybody in this window know that we're about to customize.
+      CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
+
+      await this._wrapToolbarItems();
+      this.populatePalette();
+
+      this._setupPaletteDragging();
+
+      window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
+
+      this._updateResetButton();
+      this._updateUndoResetButton();
+
+      this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
+                                  Services.prefs.getBoolPref(kSkipSourceNodePref);
+
+      CustomizableUI.addListener(this);
+      this._customizing = true;
+      this._transitioning = false;
+
+      // Show the palette now that the transition has finished.
+      this.visiblePalette.hidden = false;
+      window.setTimeout(() => {
+        // Force layout reflow to ensure the animation runs,
+        // and make it async so it doesn't affect the timing.
+        this.visiblePalette.clientTop;
+        this.visiblePalette.setAttribute("showing", "true");
+      }, 0);
+      this._updateEmptyPaletteNotice();
+
+      this._updateThemeButtonIcon();
+      AddonManager.addAddonListener(this);
+
+      this._setupDownloadAutoHideToggle();
+
+      this._handler.isEnteringCustomizeMode = false;
+
+      CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
+
+      if (!this._wantToBeInCustomizeMode) {
+        this.exit();
+      }
+    })().catch(e => {
+      log.error("Error entering customize mode", e);
+      this._handler.isEnteringCustomizeMode = false;
+      // Exit customize mode to ensure proper clean-up when entering failed.
+      this.exit();
+    });
+  },
+
+  exit() {
+    this._wantToBeInCustomizeMode = false;
+
+    if (!this._customizing || this._handler.isExitingCustomizeMode) {
+      return;
+    }
+
+    // Entering; want to exit once we've done that.
+    if (this._handler.isEnteringCustomizeMode) {
+      log.debug("Attempted to exit while we're in the middle of entering. " +
+                "We'll exit after we've entered");
+      return;
+    }
+
+    if (this.resetting) {
+      log.debug("Attempted to exit while we're resetting. " +
+                "We'll exit after resetting has finished.");
+      return;
+    }
+
+    this._handler.isExitingCustomizeMode = true;
+
+    this._teardownDownloadAutoHideToggle();
+
+    AddonManager.removeAddonListener(this);
+    CustomizableUI.removeListener(this);
+
+    this.document.removeEventListener("keypress", this);
+
+    let window = this.window;
+    let document = this.document;
+
+    this.togglePong(false);
+
+    // Disable the reset and undo reset buttons while transitioning:
+    let resetButton = this.$("customization-reset-button");
+    let undoResetButton = this.$("customization-undo-reset-button");
+    undoResetButton.hidden = resetButton.disabled = true;
+
+    this._transitioning = true;
+
+    (async () => {
+      await this.depopulatePalette();
+
+      await this._doTransition(false);
+
+      if (this.browser.selectedTab == gTab) {
+        if (gTab.linkedBrowser.currentURI.spec == "about:blank") {
+          closeGlobalTab();
+        } else {
+          unregisterGlobalTab();
+        }
+      }
+      let browser = document.getElementById("browser");
+      browser.parentNode.selectedPanel = browser;
+      let customizer = document.getElementById("customization-container");
+      customizer.hidden = true;
+
+      window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
+
+      this._teardownPaletteDragging();
+
+      await this._unwrapToolbarItems();
+
+      // And drop all area references.
+      this.areas.clear();
+
+      // Let everybody in this window know that we're starting to
+      // exit customization mode.
+      CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
+
+      window.PanelUI.menuButton.disabled = false;
+      let overflowContainer = document.getElementById("widget-overflow-mainView").firstElementChild;
+      overflowContainer.appendChild(window.PanelUI.overflowFixedList);
+      document.getElementById("nav-bar-overflow-button").disabled = false;
+      let panelContextMenu = document.getElementById(kPanelItemContextMenu);
+      this._previousPanelContextMenuParent.appendChild(panelContextMenu);
+
+      // We need to set this._customizing to false before removing the tab
+      // or the TabSelect event handler will think that we are exiting
+      // customization mode for a second time.
+      this._customizing = false;
+
+      let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])");
+      for (let toolbar of customizableToolbars)
+        toolbar.removeAttribute("customizing");
+
+      this._maybeMoveDownloadsButtonToNavBar();
+
+      delete this._lastLightweightTheme;
+      this._changed = false;
+      this._transitioning = false;
+      this._handler.isExitingCustomizeMode = false;
+      CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
+      CustomizableUI.notifyEndCustomizing(window);
+
+      if (this._wantToBeInCustomizeMode) {
+        this.enter();
+      }
+    })().catch(e => {
+      log.error("Error exiting customize mode", e);
+      this._handler.isExitingCustomizeMode = false;
+    });
+  },
+
+  /**
+   * The customize mode transition has 4 phases when entering:
+   * 1) Pre-customization mode
+   *    This is the starting phase of the browser.
+   * 2) LWT swapping
+   *    This is where we swap some of the lightweight theme styles in order
+   *    to make them work in customize mode. We set/unset a customization-
+   *    lwtheme attribute iff we're using a lightweight theme.
+   * 3) customize-entering
+   *    This phase is a transition, optimized for smoothness.
+   * 4) customize-entered
+   *    After the transition completes, this phase draws all of the
+   *    expensive detail that isn't necessary during the second phase.
+   *
+   * Exiting customization mode has a similar set of phases, but in reverse
+   * order - customize-entered, customize-exiting, remove LWT swapping,
+   * pre-customization mode.
+   *
+   * When in the customize-entering, customize-entered, or customize-exiting
+   * phases, there is a "customizing" attribute set on the main-window to simplify
+   * excluding certain styles while in any phase of customize mode.
+   */
+  _doTransition(aEntering) {
+    let docEl = this.document.documentElement;
+    if (aEntering) {
+      docEl.setAttribute("customizing", true);
+      docEl.setAttribute("customize-entered", true);
+    } else {
+      docEl.removeAttribute("customizing");
+      docEl.removeAttribute("customize-entered");
+    }
+    return Promise.resolve();
+  },
+
+  _getCustomizableChildForNode(aNode) {
+    // NB: adjusted from _getCustomizableParent to keep that method fast
+    // (it's used during drags), and avoid multiple DOM loops
+    let areas = CustomizableUI.areas;
+    // Caching this length is important because otherwise we'll also iterate
+    // over items we add to the end from within the loop.
+    let numberOfAreas = areas.length;
+    for (let i = 0; i < numberOfAreas; i++) {
+      let area = areas[i];
+      let areaNode = aNode.ownerDocument.getElementById(area);
+      let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode);
+      if (customizationTarget && customizationTarget != areaNode) {
+        areas.push(customizationTarget.id);
+      }
+      let overflowTarget = areaNode && areaNode.getAttribute("overflowtarget");
+      if (overflowTarget) {
+        areas.push(overflowTarget);
+      }
+    }
+    areas.push(kPaletteId);
+
+    while (aNode && aNode.parentNode) {
+      let parent = aNode.parentNode;
+      if (areas.includes(parent.id)) {
+        return aNode;
+      }
+      aNode = parent;
+    }
+    return null;
+  },
+
+  _promiseWidgetAnimationOut(aNode) {
+    if (!gCosmeticAnimationsEnabled ||
+        aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" ||
+        (aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") ||
+        (aNode.id == "downloads-button" && aNode.hidden)) {
+      return null;
+    }
+
+    let animationNode;
+    if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
+      animationNode = aNode.parentNode;
+    } else {
+      animationNode = aNode;
+    }
+    return new Promise(resolve => {
+      function cleanupCustomizationExit() {
+        resolveAnimationPromise();
+      }
+
+      function cleanupWidgetAnimationEnd(e) {
+        if (e.animationName == "widget-animate-out" && e.target.id == animationNode.id) {
+          resolveAnimationPromise();
+        }
+      }
+
+      function resolveAnimationPromise() {
+        animationNode.removeEventListener("animationend", cleanupWidgetAnimationEnd);
+        animationNode.removeEventListener("customizationending", cleanupCustomizationExit);
+        resolve();
+      }
+
+      // Wait until the next frame before setting the class to ensure
+      // we do start the animation.
+      this.window.requestAnimationFrame(() => {
+        this.window.requestAnimationFrame(() => {
+          animationNode.classList.add("animate-out");
+          animationNode.ownerGlobal.gNavToolbox.addEventListener("customizationending", cleanupCustomizationExit);
+          animationNode.addEventListener("animationend", cleanupWidgetAnimationEnd);
+        });
+      });
+    });
+  },
+
+  async addToToolbar(aNode) {
+    aNode = this._getCustomizableChildForNode(aNode);
+    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
+      aNode = aNode.firstElementChild;
+    }
+    let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
+    if (widgetAnimationPromise) {
+      await widgetAnimationPromise;
+    }
+
+    let widgetToAdd = aNode.id;
+    if (CustomizableUI.isSpecialWidget(widgetToAdd) && aNode.closest("#customization-palette")) {
+      widgetToAdd = widgetToAdd.match(/^customizableui-special-(spring|spacer|separator)/)[1];
+    }
+
+    CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
+    if (!this._customizing) {
+      CustomizableUI.dispatchToolboxEvent("customizationchange");
+    }
+
+    // If the user explicitly moves this item, turn off autohide.
+    if (aNode.id == "downloads-button") {
+      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+      if (this._customizing) {
+        this._showDownloadsAutoHidePanel();
+      }
+    }
+
+    if (widgetAnimationPromise) {
+      if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
+        aNode.parentNode.classList.remove("animate-out");
+      } else {
+        aNode.classList.remove("animate-out");
+      }
+    }
+  },
+
+  async addToPanel(aNode) {
+    aNode = this._getCustomizableChildForNode(aNode);
+    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
+      aNode = aNode.firstElementChild;
+    }
+    let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
+    if (widgetAnimationPromise) {
+      await widgetAnimationPromise;
+    }
+
+    let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
+    CustomizableUI.addWidgetToArea(aNode.id, panel);
+    if (!this._customizing) {
+      CustomizableUI.dispatchToolboxEvent("customizationchange");
+    }
+
+    // If the user explicitly moves this item, turn off autohide.
+    if (aNode.id == "downloads-button") {
+      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+      if (this._customizing) {
+        this._showDownloadsAutoHidePanel();
+      }
+    }
+
+    if (widgetAnimationPromise) {
+      if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
+        aNode.parentNode.classList.remove("animate-out");
+      } else {
+        aNode.classList.remove("animate-out");
+      }
+    }
+    if (gCosmeticAnimationsEnabled) {
+      let overflowButton = this.$("nav-bar-overflow-button");
+      BrowserUtils.setToolbarButtonHeightProperty(overflowButton).then(() => {
+        overflowButton.setAttribute("animate", "true");
+        overflowButton.addEventListener("animationend", function onAnimationEnd(event) {
+          if (event.animationName.startsWith("overflow-animation")) {
+            this.setAttribute("fade", "true");
+          } else if (event.animationName == "overflow-fade") {
+            this.removeEventListener("animationend", onAnimationEnd);
+            this.removeAttribute("animate");
+            this.removeAttribute("fade");
+          }
+        });
+      });
+    }
+  },
+
+  async removeFromArea(aNode) {
+    aNode = this._getCustomizableChildForNode(aNode);
+    if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
+      aNode = aNode.firstElementChild;
+    }
+    let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
+    if (widgetAnimationPromise) {
+      await widgetAnimationPromise;
+    }
+
+    CustomizableUI.removeWidgetFromArea(aNode.id);
+    if (!this._customizing) {
+      CustomizableUI.dispatchToolboxEvent("customizationchange");
+    }
+
+    // If the user explicitly removes this item, turn off autohide.
+    if (aNode.id == "downloads-button") {
+      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+      if (this._customizing) {
+        this._showDownloadsAutoHidePanel();
+      }
+    }
+    if (widgetAnimationPromise) {
+      if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
+        aNode.parentNode.classList.remove("animate-out");
+      } else {
+        aNode.classList.remove("animate-out");
+      }
+    }
+  },
+
+  populatePalette() {
+    let fragment = this.document.createDocumentFragment();
+    let toolboxPalette = this.window.gNavToolbox.palette;
+
+    try {
+      let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
+      for (let widget of unusedWidgets) {
+        let paletteItem = this.makePaletteItem(widget, "palette");
+        if (!paletteItem) {
+          continue;
+        }
+        fragment.appendChild(paletteItem);
+      }
+
+      let flexSpace = CustomizableUI.createSpecialWidget("spring", this.document);
+      fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette"));
+
+      this.visiblePalette.appendChild(fragment);
+      this._stowedPalette = this.window.gNavToolbox.palette;
+      this.window.gNavToolbox.palette = this.visiblePalette;
+    } catch (ex) {
+      log.error(ex);
+    }
+  },
+
+  // XXXunf Maybe this should use -moz-element instead of wrapping the node?
+  //       Would ensure no weird interactions/event handling from original node,
+  //       and makes it possible to put this in a lazy-loaded iframe/real tab
+  //       while still getting rid of the need for overlays.
+  makePaletteItem(aWidget, aPlace) {
+    let widgetNode = aWidget.forWindow(this.window).node;
+    if (!widgetNode) {
+      log.error("Widget with id " + aWidget.id + " does not return a valid node");
+      return null;
+    }
+    // Do not build a palette item for hidden widgets; there's not much to show.
+    if (widgetNode.hidden) {
+      return null;
+    }
+
+    let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
+    wrapper.appendChild(widgetNode);
+    return wrapper;
+  },
+
+  depopulatePalette() {
+    return (async () => {
+      this.visiblePalette.hidden = true;
+      let paletteChild = this.visiblePalette.firstElementChild;
+      let nextChild;
+      while (paletteChild) {
+        nextChild = paletteChild.nextElementSibling;
+        let itemId = paletteChild.firstElementChild.id;
+        if (CustomizableUI.isSpecialWidget(itemId)) {
+          this.visiblePalette.removeChild(paletteChild);
+        } else {
+          // XXXunf Currently this doesn't destroy the (now unused) node in the
+          //       API provider case. It would be good to do so, but we need to
+          //       keep strong refs to it in CustomizableUI (can't iterate of
+          //       WeakMaps), and there's the question of what behavior
+          //       wrappers should have if consumers keep hold of them.
+          let unwrappedPaletteItem =
+            await this.deferredUnwrapToolbarItem(paletteChild);
+          this._stowedPalette.appendChild(unwrappedPaletteItem);
+        }
+
+        paletteChild = nextChild;
+      }
+      this.visiblePalette.hidden = false;
+      this.window.gNavToolbox.palette = this._stowedPalette;
+    })().catch(log.error);
+  },
+
+  isCustomizableItem(aNode) {
+    return aNode.localName == "toolbarbutton" ||
+           aNode.localName == "toolbaritem" ||
+           aNode.localName == "toolbarseparator" ||
+           aNode.localName == "toolbarspring" ||
+           aNode.localName == "toolbarspacer";
+  },
+
+  isWrappedToolbarItem(aNode) {
+    return aNode.localName == "toolbarpaletteitem";
+  },
+
+  deferredWrapToolbarItem(aNode, aPlace) {
+    return new Promise(resolve => {
+      dispatchFunction(() => {
+        let wrapper = this.wrapToolbarItem(aNode, aPlace);
+        resolve(wrapper);
+      });
+    });
+  },
+
+  wrapToolbarItem(aNode, aPlace) {
+    if (!this.isCustomizableItem(aNode)) {
+      return aNode;
+    }
+    let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
+
+    // It's possible that this toolbar node is "mid-flight" and doesn't have
+    // a parent, in which case we skip replacing it. This can happen if a
+    // toolbar item has been dragged into the palette. In that case, we tell
+    // CustomizableUI to remove the widget from its area before putting the
+    // widget in the palette - so the node will have no parent.
+    if (aNode.parentNode) {
+      aNode = aNode.parentNode.replaceChild(wrapper, aNode);
+    }
+    wrapper.appendChild(aNode);
+    return wrapper;
+  },
+
+  createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
+    let wrapper;
+    if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") {
+      wrapper = aNode.parentNode;
+      aPlace = wrapper.getAttribute("place");
+    } else {
+      wrapper = this.document.createXULElement("toolbarpaletteitem");
+      // "place" is used to show the label when it's sitting in the palette.
+      wrapper.setAttribute("place", aPlace);
+    }
+
+
+    // Ensure the wrapped item doesn't look like it's in any special state, and
+    // can't be interactved with when in the customization palette.
+    // Note that some buttons opt out of this with the
+    // keepbroadcastattributeswhencustomizing attribute.
+    if (aNode.hasAttribute("command") &&
+        aNode.getAttribute(kKeepBroadcastAttributes) != "true") {
+      wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
+      aNode.removeAttribute("command");
+    }
+
+    if (aNode.hasAttribute("observes") &&
+        aNode.getAttribute(kKeepBroadcastAttributes) != "true") {
+      wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
+      aNode.removeAttribute("observes");
+    }
+
+    if (aNode.getAttribute("checked") == "true") {
+      wrapper.setAttribute("itemchecked", "true");
+      aNode.removeAttribute("checked");
+    }
+
+    if (aNode.hasAttribute("id")) {
+      wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
+    }
+
+    if (aNode.hasAttribute("label")) {
+      wrapper.setAttribute("title", aNode.getAttribute("label"));
+      wrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
+    } else if (aNode.hasAttribute("title")) {
+      wrapper.setAttribute("title", aNode.getAttribute("title"));
+      wrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
+    }
+
+    if (aNode.hasAttribute("flex")) {
+      wrapper.setAttribute("flex", aNode.getAttribute("flex"));
+    }
+
+    let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
+    wrapper.setAttribute("removable", removable);
+
+    // Allow touch events to initiate dragging in customize mode.
+    // This is only supported on Windows for now.
+    wrapper.setAttribute("touchdownstartsdrag", "true");
+
+    let contextMenuAttrName = "";
+    if (aNode.getAttribute("context")) {
+      contextMenuAttrName = "context";
+    } else if (aNode.getAttribute("contextmenu")) {
+      contextMenuAttrName = "contextmenu";
+    }
+    let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
+    let contextMenuForPlace = aPlace == "menu-panel" ?
+                                kPanelItemContextMenu :
+                                kPaletteItemContextMenu;
+    if (aPlace != "toolbar") {
+      wrapper.setAttribute("context", contextMenuForPlace);
+    }
+    // Only keep track of the menu if it is non-default.
+    if (currentContextMenu &&
+        currentContextMenu != contextMenuForPlace) {
+      aNode.setAttribute("wrapped-context", currentContextMenu);
+      aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName);
+      aNode.removeAttribute(contextMenuAttrName);
+    } else if (currentContextMenu == contextMenuForPlace) {
+      aNode.removeAttribute(contextMenuAttrName);
+    }
+
+    // Only add listeners for newly created wrappers:
+    if (!aIsUpdate) {
+      wrapper.addEventListener("mousedown", this);
+      wrapper.addEventListener("mouseup", this);
+    }
+
+    if (CustomizableUI.isSpecialWidget(aNode.id)) {
+      wrapper.setAttribute("title", gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label"));
+    }
+
+    return wrapper;
+  },
+
+  deferredUnwrapToolbarItem(aWrapper) {
+    return new Promise(resolve => {
+      dispatchFunction(() => {
+        let item = null;
+        try {
+          item = this.unwrapToolbarItem(aWrapper);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+        resolve(item);
+      });
+    });
+  },
+
+  unwrapToolbarItem(aWrapper) {
+    if (aWrapper.nodeName != "toolbarpaletteitem") {
+      return aWrapper;
+    }
+    aWrapper.removeEventListener("mousedown", this);
+    aWrapper.removeEventListener("mouseup", this);
+
+    let place = aWrapper.getAttribute("place");
+
+    let toolbarItem = aWrapper.firstElementChild;
+    if (!toolbarItem) {
+      log.error("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id);
+      aWrapper.remove();
+      return null;
+    }
+
+    if (aWrapper.hasAttribute("itemobserves")) {
+      toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves"));
+    }
+
+    if (aWrapper.hasAttribute("itemchecked")) {
+      toolbarItem.checked = true;
+    }
+
+    if (aWrapper.hasAttribute("itemcommand")) {
+      let commandID = aWrapper.getAttribute("itemcommand");
+      toolbarItem.setAttribute("command", commandID);
+
+      // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
+      let command = this.$(commandID);
+      if (command && command.hasAttribute("disabled")) {
+        toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
+      }
+    }
+
+    let wrappedContext = toolbarItem.getAttribute("wrapped-context");
+    if (wrappedContext) {
+      let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
+      toolbarItem.setAttribute(contextAttrName, wrappedContext);
+      toolbarItem.removeAttribute("wrapped-contextAttrName");
+      toolbarItem.removeAttribute("wrapped-context");
+    } else if (place == "menu-panel") {
+      toolbarItem.setAttribute("context", kPanelItemContextMenu);
+    }
+
+    if (aWrapper.parentNode) {
+      aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
+    }
+    return toolbarItem;
+  },
+
+  async _wrapToolbarItem(aArea) {
+    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
+    if (!target || this.areas.has(target)) {
+      return null;
+    }
+
+    this._addDragHandlers(target);
+    for (let child of target.children) {
+      if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
+        await this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)).catch(log.error);
+      }
+    }
+    this.areas.add(target);
+    return target;
+  },
+
+  _wrapToolbarItemSync(aArea) {
+    let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
+    if (!target || this.areas.has(target)) {
+      return null;
+    }
+
+    this._addDragHandlers(target);
+    try {
+      for (let child of target.children) {
+        if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
+          this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
+        }
+      }
+    } catch (ex) {
+      log.error(ex, ex.stack);
+    }
+
+    this.areas.add(target);
+    return target;
+  },
+
+  async _wrapToolbarItems() {
+    for (let area of CustomizableUI.areas) {
+      await this._wrapToolbarItem(area);
+    }
+  },
+
+  _addDragHandlers(aTarget) {
+    // Allow dropping on the padding of the arrow panel.
+    if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
+      aTarget = this.$("customization-panelHolder");
+    }
+    aTarget.addEventListener("dragstart", this, true);
+    aTarget.addEventListener("dragover", this, true);
+    aTarget.addEventListener("dragexit", this, true);
+    aTarget.addEventListener("drop", this, true);
+    aTarget.addEventListener("dragend", this, true);
+  },
+
+  _wrapItemsInArea(target) {
+    for (let child of target.children) {
+      if (this.isCustomizableItem(child)) {
+        this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
+      }
+    }
+  },
+
+  _removeDragHandlers(aTarget) {
+    // Remove handler from different target if it was added to
+    // allow dropping on the padding of the arrow panel.
+    if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
+      aTarget = this.$("customization-panelHolder");
+    }
+    aTarget.removeEventListener("dragstart", this, true);
+    aTarget.removeEventListener("dragover", this, true);
+    aTarget.removeEventListener("dragexit", this, true);
+    aTarget.removeEventListener("drop", this, true);
+    aTarget.removeEventListener("dragend", this, true);
+  },
+
+  _unwrapItemsInArea(target) {
+    for (let toolbarItem of target.children) {
+      if (this.isWrappedToolbarItem(toolbarItem)) {
+        this.unwrapToolbarItem(toolbarItem);
+      }
+    }
+  },
+
+  _unwrapToolbarItems() {
+    return (async () => {
+      for (let target of this.areas) {
+        for (let toolbarItem of target.children) {
+          if (this.isWrappedToolbarItem(toolbarItem)) {
+            await this.deferredUnwrapToolbarItem(toolbarItem);
+          }
+        }
+        this._removeDragHandlers(target);
+      }
+      this.areas.clear();
+    })().catch(log.error);
+  },
+
+  reset() {
+    this.resetting = true;
+    // Disable the reset button temporarily while resetting:
+    let btn = this.$("customization-reset-button");
+    btn.disabled = true;
+    return (async () => {
+      await this.depopulatePalette();
+      await this._unwrapToolbarItems();
+
+      CustomizableUI.reset();
+
+      await this._wrapToolbarItems();
+      this.populatePalette();
+
+      this._updateResetButton();
+      this._updateUndoResetButton();
+      this._updateEmptyPaletteNotice();
+      this._moveDownloadsButtonToNavBar = false;
+      this.resetting = false;
+      if (!this._wantToBeInCustomizeMode) {
+        this.exit();
+      }
+    })().catch(log.error);
+  },
+
+  undoReset() {
+    this.resetting = true;
+
+    return (async () => {
+      await this.depopulatePalette();
+      await this._unwrapToolbarItems();
+
+      CustomizableUI.undoReset();
+
+      await this._wrapToolbarItems();
+      this.populatePalette();
+
+      this._updateResetButton();
+      this._updateUndoResetButton();
+      this._updateEmptyPaletteNotice();
+      this._moveDownloadsButtonToNavBar = false;
+      this.resetting = false;
+    })().catch(log.error);
+  },
+
+  _onToolbarVisibilityChange(aEvent) {
+    let toolbar = aEvent.target;
+    if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
+      toolbar.setAttribute("customizing", "true");
+    } else {
+      toolbar.removeAttribute("customizing");
+    }
+    this._onUIChange();
+  },
+
+  onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
+    this._onUIChange();
+  },
+
+  onWidgetAdded(aWidgetId, aArea, aPosition) {
+    this._onUIChange();
+  },
+
+  onWidgetRemoved(aWidgetId, aArea) {
+    this._onUIChange();
+  },
+
+  onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
+    if (aContainer.ownerGlobal != this.window || this.resetting) {
+      return;
+    }
+    // If we get called for widgets that aren't in the window yet, they might not have
+    // a parentNode at all.
+    if (aNodeToChange.parentNode) {
+      this.unwrapToolbarItem(aNodeToChange.parentNode);
+    }
+    if (aSecondaryNode) {
+      this.unwrapToolbarItem(aSecondaryNode.parentNode);
+    }
+  },
+
+  onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
+    if (aContainer.ownerGlobal != this.window || this.resetting) {
+      return;
+    }
+    // If the node is still attached to the container, wrap it again:
+    if (aNodeToChange.parentNode) {
+      let place = CustomizableUI.getPlaceForItem(aNodeToChange);
+      this.wrapToolbarItem(aNodeToChange, place);
+      if (aSecondaryNode) {
+        this.wrapToolbarItem(aSecondaryNode, place);
+      }
+    } else {
+      // If not, it got removed.
+
+      // If an API-based widget is removed while customizing, append it to the palette.
+      // The _applyDrop code itself will take care of positioning it correctly, if
+      // applicable. We need the code to be here so removing widgets using CustomizableUI's
+      // API also does the right thing (and adds it to the palette)
+      let widgetId = aNodeToChange.id;
+      let widget = CustomizableUI.getWidget(widgetId);
+      if (widget.provider == CustomizableUI.PROVIDER_API) {
+        let paletteItem = this.makePaletteItem(widget, "palette");
+        this.visiblePalette.appendChild(paletteItem);
+      }
+    }
+  },
+
+  onWidgetDestroyed(aWidgetId) {
+    let wrapper = this.$("wrapper-" + aWidgetId);
+    if (wrapper) {
+      wrapper.remove();
+    }
+  },
+
+  onWidgetAfterCreation(aWidgetId, aArea) {
+    // If the node was added to an area, we would have gotten an onWidgetAdded notification,
+    // plus associated DOM change notifications, so only do stuff for the palette:
+    if (!aArea) {
+      let widgetNode = this.$(aWidgetId);
+      if (widgetNode) {
+        this.wrapToolbarItem(widgetNode, "palette");
+      } else {
+        let widget = CustomizableUI.getWidget(aWidgetId);
+        this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
+      }
+    }
+  },
+
+  onAreaNodeRegistered(aArea, aContainer) {
+    if (aContainer.ownerDocument == this.document) {
+      this._wrapItemsInArea(aContainer);
+      this._addDragHandlers(aContainer);
+      this.areas.add(aContainer);
+    }
+  },
+
+  onAreaNodeUnregistered(aArea, aContainer, aReason) {
+    if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) {
+      this._unwrapItemsInArea(aContainer);
+      this._removeDragHandlers(aContainer);
+      this.areas.delete(aContainer);
+    }
+  },
+
+  openAddonsManagerThemes(aEvent) {
+    aEvent.target.parentNode.parentNode.hidePopup();
+    AMTelemetry.recordLinkEvent({object: "customize", value: "manageThemes"});
+    this.window.BrowserOpenAddonsMgr("addons://list/theme");
+  },
+
+  getMoreThemes(aEvent) {
+    aEvent.target.parentNode.parentNode.hidePopup();
+    AMTelemetry.recordLinkEvent({object: "customize", value: "getThemes"});
+    let getMoreURL = Services.urlFormatter.formatURLPref("lightweightThemes.getMoreURL");
+    this.window.openTrustedLinkIn(getMoreURL, "tab");
+  },
+
+  updateUIDensity(mode) {
+    this.window.gUIDensity.update(mode);
+  },
+
+  setUIDensity(mode) {
+    let win = this.window;
+    let gUIDensity = win.gUIDensity;
+    let currentDensity = gUIDensity.getCurrentDensity();
+    let panel = win.document.getElementById("customization-uidensity-menu");
+
+    Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode);
+
+    // If the user is choosing a different UI density mode while
+    // the mode is overriden to Touch, remove the override.
+    if (currentDensity.overridden) {
+      Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false);
+    }
+
+    this._onUIChange();
+    panel.hidePopup();
+  },
+
+  resetUIDensity() {
+    this.window.gUIDensity.update();
+  },
+
+  onUIDensityMenuShowing() {
+    let win = this.window;
+    let doc = win.document;
+    let gUIDensity = win.gUIDensity;
+    let currentDensity = gUIDensity.getCurrentDensity();
+
+    let normalItem = doc.getElementById("customization-uidensity-menuitem-normal");
+    normalItem.mode = gUIDensity.MODE_NORMAL;
+
+    let compactItem = doc.getElementById("customization-uidensity-menuitem-compact");
+    compactItem.mode = gUIDensity.MODE_COMPACT;
+
+    let items = [normalItem, compactItem];
+
+    let touchItem = doc.getElementById("customization-uidensity-menuitem-touch");
+    // Touch mode can not be enabled in OSX right now.
+    if (touchItem) {
+      touchItem.mode = gUIDensity.MODE_TOUCH;
+      items.push(touchItem);
+    }
+
+    // Mark the active mode menuitem.
+    for (let item of items) {
+      if (item.mode == currentDensity.mode) {
+        item.setAttribute("aria-checked", "true");
+        item.setAttribute("active", "true");
+      } else {
+        item.removeAttribute("aria-checked");
+        item.removeAttribute("active");
+      }
+    }
+
+    // Add menu items for automatically switching to Touch mode in Windows Tablet Mode,
+    // which is only available in Windows 10.
+    if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
+      let spacer = doc.getElementById("customization-uidensity-touch-spacer");
+      let checkbox = doc.getElementById("customization-uidensity-autotouchmode-checkbox");
+      spacer.removeAttribute("hidden");
+      checkbox.removeAttribute("hidden");
+
+      // Show a hint that the UI density was overridden automatically.
+      if (currentDensity.overridden) {
+        let sb = Services.strings.createBundle("chrome://browser/locale/uiDensity.properties");
+        touchItem.setAttribute("acceltext",
+                                 sb.GetStringFromName("uiDensity.menuitem-touch.acceltext"));
+      } else {
+        touchItem.removeAttribute("acceltext");
+      }
+
+      let autoTouchMode = Services.prefs.getBoolPref(win.gUIDensity.autoTouchModePref);
+      if (autoTouchMode) {
+        checkbox.setAttribute("checked", "true");
+      } else {
+        checkbox.removeAttribute("checked");
+      }
+    }
+  },
+
+  updateAutoTouchMode(checked) {
+    Services.prefs.setBoolPref("browser.touchmode.auto", checked);
+    // Re-render the menu items since the active mode might have
+    // change because of this.
+    this.onUIDensityMenuShowing();
+    this._onUIChange();
+  },
+
+  async onThemesMenuShowing(aEvent) {
+    const DEFAULT_THEME_ID = "default-theme@mozilla.org";
+    const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org";
+    const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
+    const MAX_THEME_COUNT = 6;
+
+    this._clearThemesMenu(aEvent.target);
+
+    let onThemeSelected = panel => {
+      // This causes us to call _onUIChange when the LWT actually changes,
+      // so the restore defaults / undo reset button is updated correctly.
+      this._nextThemeChangeUserTriggered = true;
+      panel.hidePopup();
+    };
+
+    let doc = this.window.document;
+
+    function buildToolbarButton(aTheme) {
+      let tbb = doc.createXULElement("toolbarbutton");
+      tbb.theme = aTheme;
+      tbb.setAttribute("label", aTheme.name);
+      tbb.setAttribute("image", aTheme.iconURL || "chrome://mozapps/skin/extensions/themeGeneric.svg");
+      if (aTheme.description)
+        tbb.setAttribute("tooltiptext", aTheme.description);
+      tbb.setAttribute("tabindex", "0");
+      tbb.classList.add("customization-lwtheme-menu-theme");
+      let isActive = aTheme.isActive;
+      tbb.setAttribute("aria-checked", isActive);
+      tbb.setAttribute("role", "menuitemradio");
+      if (isActive) {
+        tbb.setAttribute("active", "true");
+      }
+
+      return tbb;
+    }
+
+    let themes = await AddonManager.getAddonsByTypes(["theme"]);
+    let currentTheme = themes.find(theme => theme.isActive);
+
+    // Move the current theme (if any) and the light/dark themes to the start:
+    let importantThemes = new Set([DEFAULT_THEME_ID, LIGHT_THEME_ID, DARK_THEME_ID]);
+    if (currentTheme) {
+      importantThemes.add(currentTheme.id);
+    }
+    let importantList = [];
+    for (let importantTheme of importantThemes) {
+      importantList.push(...themes.splice(themes.findIndex(theme => theme.id == importantTheme), 1));
+    }
+
+    // Sort the remainder alphabetically:
+    themes.sort((a, b) => a.name.localeCompare(b.name));
+    themes = importantList.concat(themes);
+
+    if (themes.length > MAX_THEME_COUNT)
+      themes.length = MAX_THEME_COUNT;
+
+    let footer = doc.getElementById("customization-lwtheme-menu-footer");
+    let panel = footer.parentNode;
+    for (let theme of themes) {
+      let button = buildToolbarButton(theme);
+      button.addEventListener("command", async () => {
+        await button.theme.enable();
+        onThemeSelected(panel);
+        AMTelemetry.recordActionEvent({
+          object: "customize",
+          action: "enable",
+          extra: {type: "theme", addonId: theme.id},
+        });
+      });
+      panel.insertBefore(button, footer);
+    }
+  },
+
+  _clearThemesMenu(panel) {
+    let footer = this.$("customization-lwtheme-menu-footer");
+    let element = footer;
+    while (element.previousElementSibling &&
+           element.previousElementSibling.localName == "toolbarbutton") {
+      element.previousElementSibling.remove();
+    }
+
+    // Workaround for bug 1059934
+    panel.removeAttribute("height");
+  },
+
+  _onUIChange() {
+    this._changed = true;
+    if (!this.resetting) {
+      this._updateResetButton();
+      this._updateUndoResetButton();
+      this._updateEmptyPaletteNotice();
+    }
+    CustomizableUI.dispatchToolboxEvent("customizationchange");
+  },
+
+  _updateEmptyPaletteNotice() {
+    let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
+    let whimsyButton = this.$("whimsy-button");
+
+    if (paletteItems.length == 1 &&
+        paletteItems[0].id.includes("wrapper-customizableui-special-spring")) {
+      whimsyButton.hidden = false;
+    } else {
+      this.togglePong(false);
+      whimsyButton.hidden = true;
+    }
+  },
+
+  _updateResetButton() {
+    let btn = this.$("customization-reset-button");
+    btn.disabled = CustomizableUI.inDefaultState;
+  },
+
+  _updateUndoResetButton() {
+    let undoResetButton =  this.$("customization-undo-reset-button");
+    undoResetButton.hidden = !CustomizableUI.canUndoReset;
+  },
+
+  handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "toolbarvisibilitychange":
+        this._onToolbarVisibilityChange(aEvent);
+        break;
+      case "dragstart":
+        this._onDragStart(aEvent);
+        break;
+      case "dragover":
+        this._onDragOver(aEvent);
+        break;
+      case "drop":
+        this._onDragDrop(aEvent);
+        break;
+      case "dragexit":
+        this._onDragExit(aEvent);
+        break;
+      case "dragend":
+        this._onDragEnd(aEvent);
+        break;
+      case "mousedown":
+        this._onMouseDown(aEvent);
+        break;
+      case "mouseup":
+        this._onMouseUp(aEvent);
+        break;
+      case "keypress":
+        if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+          this.exit();
+        }
+        break;
+      case "unload":
+        this.uninit();
+        break;
+    }
+  },
+
+  /**
+   * We handle dragover/drop on the outer palette separately
+   * to avoid overlap with other drag/drop handlers.
+   */
+  _setupPaletteDragging() {
+    this._addDragHandlers(this.visiblePalette);
+
+    this.paletteDragHandler = (aEvent) => {
+      let originalTarget = aEvent.originalTarget;
+      if (this._isUnwantedDragDrop(aEvent) ||
+          this.visiblePalette.contains(originalTarget) ||
+          this.$("customization-panelHolder").contains(originalTarget)) {
+        return;
+      }
+      // We have a dragover/drop on the palette.
+      if (aEvent.type == "dragover") {
+        this._onDragOver(aEvent, this.visiblePalette);
+      } else {
+        this._onDragDrop(aEvent, this.visiblePalette);
+      }
+    };
+    let contentContainer = this.$("customization-content-container");
+    contentContainer.addEventListener("dragover", this.paletteDragHandler, true);
+    contentContainer.addEventListener("drop", this.paletteDragHandler, true);
+  },
+
+  _teardownPaletteDragging() {
+    DragPositionManager.stop();
+    this._removeDragHandlers(this.visiblePalette);
+
+    let contentContainer = this.$("customization-content-container");
+    contentContainer.removeEventListener("dragover", this.paletteDragHandler, true);
+    contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
+    delete this.paletteDragHandler;
+  },
+
+  observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "nsPref:changed":
+        this._updateResetButton();
+        this._updateUndoResetButton();
+        if (this._canDrawInTitlebar()) {
+          this._updateTitlebarCheckbox();
+          this._updateDragSpaceCheckbox();
+        }
+        break;
+    }
+  },
+
+  async onInstalled(addon) {
+    await this.onEnabled(addon);
+  },
+
+  async onEnabled(addon) {
+    if (addon.type != "theme") {
+      return;
+    }
+
+    await this._updateThemeButtonIcon();
+    if (this._nextThemeChangeUserTriggered) {
+      this._onUIChange();
+    }
+    this._nextThemeChangeUserTriggered = false;
+  },
+
+  _canDrawInTitlebar() {
+    return this.window.TabsInTitlebar.systemSupported;
+  },
+
+  _updateTitlebarCheckbox() {
+    let drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref,
+      this.window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches);
+    let checkbox = this.$("customization-titlebar-visibility-checkbox");
+    // Drawing in the titlebar means 'hiding' the titlebar.
+    // We use the attribute rather than a property because if we're not in
+    // customize mode the button is hidden and properties don't work.
+    if (drawInTitlebar) {
+      checkbox.removeAttribute("checked");
+    } else {
+      checkbox.setAttribute("checked", "true");
+    }
+  },
+
+  _updateDragSpaceCheckbox() {
+    let extraDragSpace = Services.prefs.getBoolPref(kExtraDragSpacePref);
+    let drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref,
+      this.window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches);
+    let menuBar = this.$("toolbar-menubar");
+    let menuBarEnabled = menuBar
+      && AppConstants.platform != "macosx"
+      && menuBar.getAttribute("autohide") != "true";
+
+    let checkbox = this.$("customization-extra-drag-space-checkbox");
+    if (extraDragSpace) {
+      checkbox.setAttribute("checked", "true");
+    } else {
+      checkbox.removeAttribute("checked");
+    }
+
+    if (!drawInTitlebar || menuBarEnabled) {
+      checkbox.setAttribute("disabled", "true");
+    } else {
+      checkbox.removeAttribute("disabled");
+    }
+  },
+
+  toggleTitlebar(aShouldShowTitlebar) {
+    // Drawing in the titlebar means not showing the titlebar, hence the negation:
+    Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
+    this._updateDragSpaceCheckbox();
+  },
+
+  toggleDragSpace(aShouldShowDragSpace) {
+    Services.prefs.setBoolPref(kExtraDragSpacePref, aShouldShowDragSpace);
+  },
+
+  _getBoundsWithoutFlushing(element) {
+    return this.window.windowUtils.getBoundsWithoutFlushing(element);
+  },
+
+  _onDragStart(aEvent) {
+    __dumpDragData(aEvent);
+    let item = aEvent.target;
+    while (item && item.localName != "toolbarpaletteitem") {
+      if (item.localName == "toolbar" || item.id == kPaletteId ||
+          item.id == "customization-panelHolder") {
+        return;
+      }
+      item = item.parentNode;
+    }
+
+    let draggedItem = item.firstElementChild;
+    let placeForItem = CustomizableUI.getPlaceForItem(item);
+
+    let dt = aEvent.dataTransfer;
+    let documentId = aEvent.target.ownerDocument.documentElement.id;
+
+    dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
+    dt.effectAllowed = "move";
+
+    let itemRect = this._getBoundsWithoutFlushing(draggedItem);
+    let itemCenter = {x: itemRect.left + itemRect.width / 2,
+                      y: itemRect.top + itemRect.height / 2};
+    this._dragOffset = {x: aEvent.clientX - itemCenter.x,
+                        y: aEvent.clientY - itemCenter.y};
+
+    let toolbarParent = draggedItem.closest("toolbar");
+    if (toolbarParent) {
+      let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent);
+      toolbarParent.style.minHeight = toolbarRect.height + "px";
+    }
+
+    gDraggingInToolbars = new Set();
+
+    // Hack needed so that the dragimage will still show the
+    // item as it appeared before it was hidden.
+    this._initializeDragAfterMove = () => {
+      // For automated tests, we sometimes start exiting customization mode
+      // before this fires, which leaves us with placeholders inserted after
+      // we've exited. So we need to check that we are indeed customizing.
+      if (this._customizing && !this._transitioning) {
+        item.hidden = true;
+        DragPositionManager.start(this.window);
+        let canUsePrevSibling = placeForItem == "toolbar" || placeForItem == "menu-panel";
+        if (item.nextElementSibling) {
+          this._setDragActive(item.nextElementSibling, "before", draggedItem.id, placeForItem);
+          this._dragOverItem = item.nextElementSibling;
+        } else if (canUsePrevSibling && item.previousElementSibling) {
+          this._setDragActive(item.previousElementSibling, "after", draggedItem.id, placeForItem);
+          this._dragOverItem = item.previousElementSibling;
+        }
+        let currentArea = this._getCustomizableParent(item);
+        currentArea.setAttribute("draggingover", "true");
+      }
+      this._initializeDragAfterMove = null;
+      this.window.clearTimeout(this._dragInitializeTimeout);
+    };
+    this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
+  },
+
+  _onDragOver(aEvent, aOverrideTarget) {
+    if (this._isUnwantedDragDrop(aEvent)) {
+      return;
+    }
+    if (this._initializeDragAfterMove) {
+      this._initializeDragAfterMove();
+    }
+
+    __dumpDragData(aEvent);
+
+    let document = aEvent.target.ownerDocument;
+    let documentId = document.documentElement.id;
+    if (!aEvent.dataTransfer.mozTypesAt(0)) {
+      return;
+    }
+
+    let draggedItemId =
+      aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
+    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
+    let targetArea = this._getCustomizableParent(aOverrideTarget || aEvent.currentTarget);
+    let originArea = this._getCustomizableParent(draggedWrapper);
+
+    // Do nothing if the target or origin are not customizable.
+    if (!targetArea || !originArea) {
+      return;
+    }
+
+    // Do nothing if the widget is not allowed to be removed.
+    if (targetArea.id == kPaletteId &&
+       !CustomizableUI.isWidgetRemovable(draggedItemId)) {
+      return;
+    }
+
+    // Do nothing if the widget is not allowed to move to the target area.
+    if (targetArea.id != kPaletteId &&
+        !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
+      return;
+    }
+
+    let targetAreaType = CustomizableUI.getPlaceForItem(targetArea);
+    let targetNode = this._getDragOverNode(aEvent, targetArea, targetAreaType, draggedItemId);
+
+    // We need to determine the place that the widget is being dropped in
+    // the target.
+    let dragOverItem, dragValue;
+    if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) {
+      // We'll assume if the user is dragging directly over the target, that
+      // they're attempting to append a child to that target.
+      dragOverItem = (targetAreaType == "toolbar"
+                        ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
+                        : targetNode.lastElementChild) ||
+                     targetNode;
+      dragValue = "after";
+    } else {
+      let targetParent = targetNode.parentNode;
+      let position = Array.prototype.indexOf.call(targetParent.children, targetNode);
+      if (position == -1) {
+        dragOverItem = (targetAreaType == "toolbar"
+                          ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
+                          : targetNode.lastElementChild);
+        dragValue = "after";
+      } else {
+        dragOverItem = targetParent.children[position];
+        if (targetAreaType == "toolbar") {
+          // Check if the aDraggedItem is hovered past the first half of dragOverItem
+          let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
+          let dropTargetCenter = itemRect.left + (itemRect.width / 2);
+          let existingDir = dragOverItem.getAttribute("dragover");
+          let dirFactor = this.window.RTL_UI ? -1 : 1;
+          if (existingDir == "before") {
+            dropTargetCenter += (parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2 * dirFactor;
+          } else {
+            dropTargetCenter -= (parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2 * dirFactor;
+          }
+          let before = this.window.RTL_UI ? aEvent.clientX > dropTargetCenter : aEvent.clientX < dropTargetCenter;
+          dragValue = before ? "before" : "after";
+        } else if (targetAreaType == "menu-panel") {
+          let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
+          let dropTargetCenter = itemRect.top + (itemRect.height / 2);
+          let existingDir = dragOverItem.getAttribute("dragover");
+          if (existingDir == "before") {
+            dropTargetCenter += (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2;
+          } else {
+            dropTargetCenter -= (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2;
+          }
+          dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after";
+        } else {
+          dragValue = "before";
+        }
+      }
+    }
+
+    if (this._dragOverItem && dragOverItem != this._dragOverItem) {
+      this._cancelDragActive(this._dragOverItem, dragOverItem);
+    }
+
+    if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) {
+      if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) {
+        this._setDragActive(dragOverItem, dragValue, draggedItemId, targetAreaType);
+      }
+      this._dragOverItem = dragOverItem;
+      targetArea.setAttribute("draggingover", "true");
+    }
+
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
+  },
+
+  _onDragDrop(aEvent, aOverrideTarget) {
+    if (this._isUnwantedDragDrop(aEvent)) {
+      return;
+    }
+
+    __dumpDragData(aEvent);
+    this._initializeDragAfterMove = null;
+    this.window.clearTimeout(this._dragInitializeTimeout);
+
+    let targetArea = this._getCustomizableParent(aOverrideTarget || aEvent.currentTarget);
+    let document = aEvent.target.ownerDocument;
+    let documentId = document.documentElement.id;
+    let draggedItemId =
+      aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
+    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
+    let originArea = this._getCustomizableParent(draggedWrapper);
+    if (this._dragSizeMap) {
+      this._dragSizeMap = new WeakMap();
+    }
+    // Do nothing if the target area or origin area are not customizable.
+    if (!targetArea || !originArea) {
+      return;
+    }
+    let targetNode = this._dragOverItem;
+    let dropDir = targetNode.getAttribute("dragover");
+    // Need to insert *after* this node if we promised the user that:
+    if (targetNode != targetArea && dropDir == "after") {
+      if (targetNode.nextElementSibling) {
+        targetNode = targetNode.nextElementSibling;
+      } else {
+        targetNode = targetArea;
+      }
+    }
+    if (targetNode.tagName == "toolbarpaletteitem") {
+      targetNode = targetNode.firstElementChild;
+    }
+
+    this._cancelDragActive(this._dragOverItem, null, true);
+
+    try {
+      this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode);
+    } catch (ex) {
+      log.error(ex, ex.stack);
+    }
+
+    // If the user explicitly moves this item, turn off autohide.
+    if (draggedItemId == "downloads-button") {
+      Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
+      this._showDownloadsAutoHidePanel();
+    }
+  },
+
+  _applyDrop(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
+    let document = aEvent.target.ownerDocument;
+    let draggedItem = document.getElementById(aDraggedItemId);
+    draggedItem.hidden = false;
+    draggedItem.removeAttribute("mousedown");
+
+    let toolbarParent = draggedItem.closest("toolbar");
+    if (toolbarParent) {
+      toolbarParent.style.removeProperty("min-height");
+    }
+
+    // Do nothing if the target was dropped onto itself (ie, no change in area
+    // or position).
+    if (draggedItem == aTargetNode) {
+      return;
+    }
+
+    // Is the target area the customization palette?
+    if (aTargetArea.id == kPaletteId) {
+      // Did we drag from outside the palette?
+      if (aOriginArea.id !== kPaletteId) {
+        if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) {
+          return;
+        }
+
+        CustomizableUI.removeWidgetFromArea(aDraggedItemId);
+        // Special widgets are removed outright, we can return here:
+        if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
+          return;
+        }
+      }
+      draggedItem = draggedItem.parentNode;
+
+      // If the target node is the palette itself, just append
+      if (aTargetNode == this.visiblePalette) {
+        this.visiblePalette.appendChild(draggedItem);
+      } else {
+        // The items in the palette are wrapped, so we need the target node's parent here:
+        this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
+      }
+      this._onDragEnd(aEvent);
+      return;
+    }
+
+    if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
+      return;
+    }
+
+    // Skipintoolbarset items won't really be moved:
+    let areaCustomizationTarget =
+      CustomizableUI.getCustomizationTarget(aTargetArea);
+    if (draggedItem.getAttribute("skipintoolbarset") == "true") {
+      // These items should never leave their area:
+      if (aTargetArea != aOriginArea) {
+        return;
+      }
+      let place = draggedItem.parentNode.getAttribute("place");
+      this.unwrapToolbarItem(draggedItem.parentNode);
+      if (aTargetNode == areaCustomizationTarget) {
+        areaCustomizationTarget.appendChild(draggedItem);
+      } else {
+        this.unwrapToolbarItem(aTargetNode.parentNode);
+        areaCustomizationTarget.insertBefore(draggedItem, aTargetNode);
+        this.wrapToolbarItem(aTargetNode, place);
+      }
+      this.wrapToolbarItem(draggedItem, place);
+      return;
+    }
+
+    // Is the target the customization area itself? If so, we just add the
+    // widget to the end of the area.
+    if (aTargetNode == areaCustomizationTarget) {
+      CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
+      this._onDragEnd(aEvent);
+      return;
+    }
+
+    // We need to determine the place that the widget is being dropped in
+    // the target.
+    let placement;
+    let itemForPlacement = aTargetNode;
+    // Skip the skipintoolbarset items when determining the place of the item:
+    while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
+           itemForPlacement.parentNode &&
+           itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") {
+      itemForPlacement = itemForPlacement.parentNode.nextElementSibling;
+      if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") {
+        itemForPlacement = itemForPlacement.firstElementChild;
+      }
+    }
+    if (itemForPlacement) {
+      let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ?
+                            itemForPlacement.firstElementChild && itemForPlacement.firstElementChild.id :
+                            itemForPlacement.id;
+      placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
+    }
+    if (!placement) {
+      log.debug("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className);
+    }
+    let position = placement ? placement.position : null;
+
+    // Force creating a new spacer/spring/separator if dragging from the palette
+    if (CustomizableUI.isSpecialWidget(aDraggedItemId) && aOriginArea.id == kPaletteId) {
+      aDraggedItemId = aDraggedItemId.match(/^customizableui-special-(spring|spacer|separator)/)[1];
+    }
+
+    // Is the target area the same as the origin? Since we've already handled
+    // the possibility that the target is the customization palette, we know
+    // that the widget is moving within a customizable area.
+    if (aTargetArea == aOriginArea) {
+      CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position);
+    } else {
+      CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
+    }
+
+    this._onDragEnd(aEvent);
+
+    // If we dropped onto a skipintoolbarset item, manually correct the drop location:
+    if (aTargetNode != itemForPlacement) {
+      let draggedWrapper = draggedItem.parentNode;
+      let container = draggedWrapper.parentNode;
+      container.insertBefore(draggedWrapper, aTargetNode.parentNode);
+    }
+  },
+
+  _onDragExit(aEvent) {
+    if (this._isUnwantedDragDrop(aEvent)) {
+      return;
+    }
+
+    __dumpDragData(aEvent);
+
+    // When leaving customization areas, cancel the drag on the last dragover item
+    // We've attached the listener to areas, so aEvent.currentTarget will be the area.
+    // We don't care about dragexit events fired on descendants of the area,
+    // so we check that the event's target is the same as the area to which the listener
+    // was attached.
+    if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
+      this._cancelDragActive(this._dragOverItem);
+      this._dragOverItem = null;
+    }
+  },
+
+  /**
+   * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
+   *
+   * Note that that means that this function may be called multiple times by a single drag operation.
+   */
+  _onDragEnd(aEvent) {
+    if (this._isUnwantedDragDrop(aEvent)) {
+      return;
+    }
+    this._initializeDragAfterMove = null;
+    this.window.clearTimeout(this._dragInitializeTimeout);
+    __dumpDragData(aEvent, "_onDragEnd");
+
+    let document = aEvent.target.ownerDocument;
+    document.documentElement.removeAttribute("customizing-movingItem");
+
+    let documentId = document.documentElement.id;
+    if (!aEvent.dataTransfer.mozTypesAt(0)) {
+      return;
+    }
+
+    let draggedItemId =
+      aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
+
+    let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
+
+    // DraggedWrapper might no longer available if a widget node is
+    // destroyed after starting (but before stopping) a drag.
+    if (draggedWrapper) {
+      draggedWrapper.hidden = false;
+      draggedWrapper.removeAttribute("mousedown");
+
+      let toolbarParent = draggedWrapper.closest("toolbar");
+      if (toolbarParent) {
+        toolbarParent.style.removeProperty("min-height");
+      }
+    }
+
+    if (this._dragOverItem) {
+      this._cancelDragActive(this._dragOverItem);
+      this._dragOverItem = null;
+    }
+    DragPositionManager.stop();
+  },
+
+  _isUnwantedDragDrop(aEvent) {
+    // The simulated events generated by synthesizeDragStart/synthesizeDrop in
+    // mochitests are used only for testing whether the right data is being put
+    // into the dataTransfer. Neither cause a real drop to occur, so they don't
+    // set the source node. There isn't a means of testing real drag and drops,
+    // so this pref skips the check but it should only be set by test code.
+    if (this._skipSourceNodeCheck) {
+      return false;
+    }
+
+    /* Discard drag events that originated from a separate window to
+       prevent content->chrome privilege escalations. */
+    let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
+    // mozSourceNode is null in the dragStart event handler or if
+    // the drag event originated in an external application.
+    return !mozSourceNode ||
+           mozSourceNode.ownerGlobal != this.window;
+  },
+
+  _setDragActive(aItem, aValue, aDraggedItemId, aAreaType) {
+    if (!aItem) {
+      return;
+    }
+
+    if (aItem.getAttribute("dragover") != aValue) {
+      aItem.setAttribute("dragover", aValue);
+
+      let window = aItem.ownerGlobal;
+      let draggedItem = window.document.getElementById(aDraggedItemId);
+      if (aAreaType == "palette") {
+        this._setGridDragActive(aItem, draggedItem, aValue);
+      } else {
+        let targetArea = this._getCustomizableParent(aItem);
+        let makeSpaceImmediately = false;
+        if (!gDraggingInToolbars.has(targetArea.id)) {
+          gDraggingInToolbars.add(targetArea.id);
+          let draggedWrapper = this.$("wrapper-" + aDraggedItemId);
+          let originArea = this._getCustomizableParent(draggedWrapper);
+          makeSpaceImmediately = originArea == targetArea;
+        }
+        let propertyToMeasure = aAreaType == "toolbar" ? "width" : "height";
+        // Calculate width/height of the item when it'd be dropped in this position.
+        let borderWidth = this._getDragItemSize(aItem, draggedItem)[propertyToMeasure];
+        let layoutSide = aAreaType == "toolbar" ? "Inline" : "Block";
+        let prop, otherProp;
+        if (aValue == "before") {
+          prop = "border" + layoutSide + "StartWidth";
+          otherProp = "border-" + layoutSide.toLowerCase() + "-end-width";
+        } else {
+          prop = "border" + layoutSide + "EndWidth";
+          otherProp = "border-" + layoutSide.toLowerCase() + "-start-width";
+        }
+        if (makeSpaceImmediately) {
+          aItem.setAttribute("notransition", "true");
+        }
+        aItem.style[prop] = borderWidth + "px";
+        aItem.style.removeProperty(otherProp);
+        if (makeSpaceImmediately) {
+          // Force a layout flush:
+          aItem.getBoundingClientRect();
+          aItem.removeAttribute("notransition");
+        }
+      }
+    }
+  },
+  _cancelDragActive(aItem, aNextItem, aNoTransition) {
+    let currentArea = this._getCustomizableParent(aItem);
+    if (!currentArea) {
+      return;
+    }
+    let nextArea = aNextItem ? this._getCustomizableParent(aNextItem) : null;
+    if (currentArea != nextArea) {
+      currentArea.removeAttribute("draggingover");
+    }
+    let areaType = CustomizableUI.getAreaType(currentArea.id);
+    if (areaType) {
+      if (aNoTransition) {
+        aItem.setAttribute("notransition", "true");
+      }
+      aItem.removeAttribute("dragover");
+      // Remove all property values in the case that the end padding
+      // had been set.
+      aItem.style.removeProperty("border-inline-start-width");
+      aItem.style.removeProperty("border-inline-end-width");
+      aItem.style.removeProperty("border-block-start-width");
+      aItem.style.removeProperty("border-block-end-width");
+      if (aNoTransition) {
+        // Force a layout flush:
+        aItem.getBoundingClientRect();
+        aItem.removeAttribute("notransition");
+      }
+    } else {
+      aItem.removeAttribute("dragover");
+      if (aNextItem) {
+        if (nextArea == currentArea) {
+          // No need to do anything if we're still dragging in this area:
+          return;
+        }
+      }
+      // Otherwise, clear everything out:
+      let positionManager = DragPositionManager.getManagerForArea(currentArea);
+      positionManager.clearPlaceholders(currentArea, aNoTransition);
+    }
+  },
+
+  _setGridDragActive(aDragOverNode, aDraggedItem, aValue) {
+    let targetArea = this._getCustomizableParent(aDragOverNode);
+    let draggedWrapper = this.$("wrapper-" + aDraggedItem.id);
+    let originArea = this._getCustomizableParent(draggedWrapper);
+    let positionManager = DragPositionManager.getManagerForArea(targetArea);
+    let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
+    positionManager.insertPlaceholder(targetArea, aDragOverNode, draggedSize,
+                                      originArea == targetArea);
+  },
+
+  _getDragItemSize(aDragOverNode, aDraggedItem) {
+    // Cache it good, cache it real good.
+    if (!this._dragSizeMap)
+      this._dragSizeMap = new WeakMap();
+    if (!this._dragSizeMap.has(aDraggedItem))
+      this._dragSizeMap.set(aDraggedItem, new WeakMap());
+    let itemMap = this._dragSizeMap.get(aDraggedItem);
+    let targetArea = this._getCustomizableParent(aDragOverNode);
+    let currentArea = this._getCustomizableParent(aDraggedItem);
+    // Return the size for this target from cache, if it exists.
+    let size = itemMap.get(targetArea);
+    if (size)
+      return size;
+
+    // Calculate size of the item when it'd be dropped in this position.
+    let currentParent = aDraggedItem.parentNode;
+    let currentSibling = aDraggedItem.nextElementSibling;
+    const kAreaType = "cui-areatype";
+    let areaType, currentType;
+
+    if (targetArea != currentArea) {
+      // Move the widget temporarily next to the placeholder.
+      aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
+      // Update the node's areaType.
+      areaType = CustomizableUI.getAreaType(targetArea.id);
+      currentType = aDraggedItem.hasAttribute(kAreaType) &&
+                    aDraggedItem.getAttribute(kAreaType);
+      if (areaType)
+        aDraggedItem.setAttribute(kAreaType, areaType);
+      this.wrapToolbarItem(aDraggedItem, areaType || "palette");
+      CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
+    } else {
+      aDraggedItem.parentNode.hidden = false;
+    }
+
+    // Fetch the new size.
+    let rect = aDraggedItem.parentNode.getBoundingClientRect();
+    size = {width: rect.width, height: rect.height};
+    // Cache the found value of size for this target.
+    itemMap.set(targetArea, size);
+
+    if (targetArea != currentArea) {
+      this.unwrapToolbarItem(aDraggedItem.parentNode);
+      // Put the item back into its previous position.
+      currentParent.insertBefore(aDraggedItem, currentSibling);
+      // restore the areaType
+      if (areaType) {
+        if (currentType === false)
+          aDraggedItem.removeAttribute(kAreaType);
+        else
+          aDraggedItem.setAttribute(kAreaType, currentType);
+      }
+      this.createOrUpdateWrapper(aDraggedItem, null, true);
+      CustomizableUI.onWidgetDrag(aDraggedItem.id);
+    } else {
+      aDraggedItem.parentNode.hidden = true;
+    }
+    return size;
+  },
+
+  _getCustomizableParent(aElement) {
+    if (aElement) {
+      // Deal with drag/drop on the padding of the panel.
+      let containingPanelHolder = aElement.closest("#customization-panelHolder");
+      if (containingPanelHolder) {
+        return containingPanelHolder.querySelector("#widget-overflow-fixed-list");
+      }
+    }
+
+    let areas = CustomizableUI.areas;
+    areas.push(kPaletteId);
+    return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(","));
+  },
+
+  _getDragOverNode(aEvent, aAreaElement, aAreaType, aDraggedItemId) {
+    let expectedParent =
+      CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement;
+    if (!expectedParent.contains(aEvent.target)) {
+      return expectedParent;
+    }
+    // Offset the drag event's position with the offset to the center of
+    // the thing we're dragging
+    let dragX = aEvent.clientX - this._dragOffset.x;
+    let dragY = aEvent.clientY - this._dragOffset.y;
+
+    // Ensure this is within the container
+    let boundsContainer = expectedParent;
+    let bounds = this._getBoundsWithoutFlushing(boundsContainer);
+    dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
+    dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
+
+    let targetNode;
+    if (aAreaType == "toolbar" || aAreaType == "menu-panel") {
+      targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
+      while (targetNode && targetNode.parentNode != expectedParent) {
+        targetNode = targetNode.parentNode;
+      }
+    } else {
+      let positionManager = DragPositionManager.getManagerForArea(aAreaElement);
+      // Make it relative to the container:
+      dragX -= bounds.left;
+      dragY -= bounds.top;
+      // Find the closest node:
+      targetNode = positionManager.find(aAreaElement, dragX, dragY);
+    }
+    return targetNode || aEvent.target;
+  },
+
+  _onMouseDown(aEvent) {
+    log.debug("_onMouseDown");
+    if (aEvent.button != 0) {
+      return;
+    }
+    let doc = aEvent.target.ownerDocument;
+    doc.documentElement.setAttribute("customizing-movingItem", true);
+    let item = this._getWrapper(aEvent.target);
+    if (item) {
+      item.setAttribute("mousedown", "true");
+    }
+  },
+
+  _onMouseUp(aEvent) {
+    log.debug("_onMouseUp");
+    if (aEvent.button != 0) {
+      return;
+    }
+    let doc = aEvent.target.ownerDocument;
+    doc.documentElement.removeAttribute("customizing-movingItem");
+    let item = this._getWrapper(aEvent.target);
+    if (item) {
+      item.removeAttribute("mousedown");
+    }
+  },
+
+  _getWrapper(aElement) {
+    while (aElement && aElement.localName != "toolbarpaletteitem") {
+      if (aElement.localName == "toolbar")
+        return null;
+      aElement = aElement.parentNode;
+    }
+    return aElement;
+  },
+
+  _findVisiblePreviousSiblingNode(aReferenceNode) {
+    while (aReferenceNode &&
+           aReferenceNode.localName == "toolbarpaletteitem" &&
+           aReferenceNode.firstElementChild.hidden) {
+      aReferenceNode = aReferenceNode.previousElementSibling;
+    }
+    return aReferenceNode;
+  },
+
+  onPaletteContextMenuShowing(event) {
+   let isFlexibleSpace = event.target.triggerNode.id.includes("wrapper-customizableui-special-spring");
+   event.target.querySelector(".customize-context-addToPanel").disabled = isFlexibleSpace;
+ },
+
+  onPanelContextMenuShowing(event) {
+    let inPermanentArea = !!event.target.triggerNode.closest("#widget-overflow-fixed-list");
+    let doc = event.target.ownerDocument;
+    doc.getElementById("customizationPanelItemContextMenuUnpin").hidden = !inPermanentArea;
+    doc.getElementById("customizationPanelItemContextMenuPin").hidden = inPermanentArea;
+  },
+
+  _checkForDownloadsClick(event) {
+    if (event.target.closest("#wrapper-downloads-button") && event.button == 0) {
+      event.view.gCustomizeMode._showDownloadsAutoHidePanel();
+    }
+  },
+
+  _setupDownloadAutoHideToggle() {
+    this.$(kDownloadAutohidePanelId).removeAttribute("hidden");
+    this.window.addEventListener("click", this._checkForDownloadsClick, true);
+  },
+
+  _teardownDownloadAutoHideToggle() {
+    this.window.removeEventListener("click", this._checkForDownloadsClick, true);
+    this.$(kDownloadAutohidePanelId).hidePopup();
+  },
+
+  _maybeMoveDownloadsButtonToNavBar() {
+    // If the user toggled the autohide checkbox while the item was in the
+    // palette, and hasn't moved it since, move the item to the default
+    // location in the navbar for them.
+    if (!CustomizableUI.getPlacementOfWidget("downloads-button") &&
+        this._moveDownloadsButtonToNavBar &&
+        this.window.DownloadsButton.autoHideDownloadsButton) {
+      let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar");
+      let insertionPoint = navbarPlacements.indexOf("urlbar-container");
+      while (++insertionPoint < navbarPlacements.length) {
+        let widget = navbarPlacements[insertionPoint];
+        // If we find a non-searchbar, non-spacer node, break out of the loop:
+        if (widget != "search-container" &&
+             !(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring"))) {
+          break;
+        }
+      }
+      CustomizableUI.addWidgetToArea("downloads-button", "nav-bar", insertionPoint);
+    }
+  },
+
+  async _showDownloadsAutoHidePanel() {
+    let doc = this.document;
+    let panel = doc.getElementById(kDownloadAutohidePanelId);
+    panel.hidePopup();
+    let button = doc.getElementById("downloads-button");
+    // We don't show the tooltip if the button is in the panel.
+    if (button.closest("#widget-overflow-fixed-list")) {
+      return;
+    }
+
+    let offsetX = 0, offsetY = 0;
+    let panelOnTheLeft = false;
+    let toolbarContainer = button.closest("toolbar");
+    if (toolbarContainer && toolbarContainer.id == "nav-bar") {
+      let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar");
+      if (navbarWidgets.indexOf("urlbar-container") <= navbarWidgets.indexOf("downloads-button")) {
+        panelOnTheLeft = true;
+      }
+    } else {
+      await this.window.promiseDocumentFlushed(() => {});
+
+      if (!this._customizing || !this._wantToBeInCustomizeMode) {
+        return;
+      }
+      let buttonBounds = this._getBoundsWithoutFlushing(button);
+      let windowBounds = this._getBoundsWithoutFlushing(doc.documentElement);
+      panelOnTheLeft = (buttonBounds.left + buttonBounds.width / 2) > windowBounds.width / 2;
+    }
+    let position;
+    if (panelOnTheLeft) {
+      // Tested in RTL, these get inverted automatically, so this does the
+      // right thing without taking RTL into account explicitly.
+      position = "leftcenter topright";
+      if (toolbarContainer) {
+        offsetX = 8;
+      }
+    } else {
+      position = "rightcenter topleft";
+      if (toolbarContainer) {
+        offsetX = -8;
+      }
+    }
+
+    let checkbox = doc.getElementById(kDownloadAutohideCheckboxId);
+    if (this.window.DownloadsButton.autoHideDownloadsButton) {
+      checkbox.setAttribute("checked", "true");
+    } else {
+      checkbox.removeAttribute("checked");
+    }
+
+    // We don't use the icon to anchor because it might be resizing because of
+    // the animations for drag/drop. Hence the use of offsets.
+    panel.openPopup(button, position, offsetX, offsetY);
+  },
+
+  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;
+  },
+
+  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) {
+      this.visiblePalette.setAttribute("whimsypong", "true");
+      this.pongArena.hidden = false;
+      if (!this.uninitWhimsy) {
+        this.uninitWhimsy = this.whimsypong();
+      }
+    } else {
+      this.visiblePalette.removeAttribute("whimsypong");
+      if (this.uninitWhimsy) {
+        this.uninitWhimsy();
+        this.uninitWhimsy = null;
+      }
+      this.pongArena.hidden = true;
+    }
+  },
+
+  whimsypong() {
+    function update() {
+      updateBall();
+      updatePlayers();
+    }
+
+    function updateBall() {
+      if (ball[1] <= 0 || ball[1] >= gameSide) {
+        if ((ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) ||
+            (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth))) {
+          updateScore(ball[1] <= 0 ? 0 : 1);
+        } else {
+          if ((ball[1] <= 0 && (ball[0] - p1 < paddleEdge || p1 + paddleWidth - ball[0] < paddleEdge)) ||
+              (ball[1] >= gameSide && (ball[0] - p2 < paddleEdge || p2 + paddleWidth - ball[0] < paddleEdge))) {
+            ballDxDy[0] *= Math.random() + 1.3;
+            ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6);
+            if (Math.abs(ballDxDy[0]) == 6) {
+              ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random();
+            }
+          } else {
+            ballDxDy[0] /= 1.1;
+          }
+          ballDxDy[1] *= -1;
+          ball[1] = ball[1] <= 0 ? 0 : gameSide;
+        }
+      }
+      ball = [Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0),
+              Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0)];
+      if (ball[0] <= 0 || ball[0] >= gameSide) {
+        ballDxDy[0] *= -1;
+      }
+    }
+
+    function updatePlayers() {
+      if (keydown) {
+        let p1Adj = 1;
+        if ((keydown == 37 && !window.RTL_UI) ||
+            (keydown == 39 && window.RTL_UI)) {
+          p1Adj = -1;
+        }
+        p1 += p1Adj * 10 * keydownAdj;
+      }
+
+      let sign = Math.sign(ballDxDy[0]);
+      if ((sign > 0 && ball[0] > p2 + paddleWidth / 2) ||
+          (sign < 0 && ball[0] < p2 + paddleWidth / 2)) {
+        p2 += sign * 3;
+      } else if ((sign > 0 && ball[0] > p2 + paddleWidth / 1.1) ||
+                 (sign < 0 && ball[0] < p2 + paddleWidth / 1.1)) {
+        p2 += sign * 9;
+      }
+
+      if (score >= winScore) {
+        p1 = ball[0];
+        p2 = ball[0];
+      }
+      p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0);
+      p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0);
+    }
+
+    function updateScore(adj) {
+      if (adj) {
+        score += adj;
+      } else if (--lives == 0) {
+        quit = true;
+      }
+      ball = ballDef.slice();
+      ballDxDy = ballDxDyDef.slice();
+      ballDxDy[1] *= score / winScore + 1;
+    }
+
+    function draw() {
+      let xAdj = window.RTL_UI ? -1 : 1;
+      elements["wp-player1"].style.transform = "translate(" + (xAdj * p1) + "px, -37px)";
+      elements["wp-player2"].style.transform = "translate(" + (xAdj * p2) + "px, " + gameSide + "px)";
+      elements["wp-ball"].style.transform = "translate(" + (xAdj * ball[0]) + "px, " + ball[1] + "px)";
+      elements["wp-score"].textContent = score;
+      elements["wp-lives"].setAttribute("lives", lives);
+      if (score >= winScore) {
+        let arena = elements.arena;
+        let image = "url(chrome://browser/skin/customizableui/whimsy.png)";
+        let position = `${(window.RTL_UI ? gameSide : 0) + (xAdj * ball[0]) - 10}px ${ball[1] - 10}px`;
+        let repeat = "no-repeat";
+        let size = "20px";
+        if (arena.style.backgroundImage) {
+          if (arena.style.backgroundImage.split(",").length >= 160) {
+            quit = true;
+          }
+
+          image += ", " + arena.style.backgroundImage;
+          position += ", " + arena.style.backgroundPosition;
+          repeat += ", " + arena.style.backgroundRepeat;
+          size += ", " + arena.style.backgroundSize;
+        }
+        arena.style.backgroundImage = image;
+        arena.style.backgroundPosition = position;
+        arena.style.backgroundRepeat = repeat;
+        arena.style.backgroundSize = size;
+      }
+    }
+
+    function onkeydown(event) {
+      keys.push(event.which);
+      if (keys.length > 10) {
+        keys.shift();
+        let codeEntered = true;
+        for (let i = 0; i < keys.length; i++) {
+          if (keys[i] != keysCode[i]) {
+            codeEntered = false;
+            break;
+          }
+        }
+        if (codeEntered) {
+          elements.arena.setAttribute("kcode", "true");
+          let spacer = document.querySelector("#customization-palette > toolbarpaletteitem");
+          spacer.setAttribute("kcode", "true");
+        }
+      }
+      if (event.which == 37 /* left */ ||
+          event.which == 39 /* right */) {
+        keydown = event.which;
+        keydownAdj *= 1.05;
+      }
+    }
+
+    function onkeyup(event) {
+      if (event.which == 37 || event.which == 39) {
+        keydownAdj = 1;
+        keydown = 0;
+      }
+    }
+
+    function uninit() {
+      document.removeEventListener("keydown", onkeydown);
+      document.removeEventListener("keyup", onkeyup);
+      if (rAFHandle) {
+        window.cancelAnimationFrame(rAFHandle);
+      }
+      let arena = elements.arena;
+      while (arena.firstChild) {
+        arena.firstChild.remove();
+      }
+      arena.removeAttribute("score");
+      arena.removeAttribute("lives");
+      arena.removeAttribute("kcode");
+      arena.style.removeProperty("background-image");
+      arena.style.removeProperty("background-position");
+      arena.style.removeProperty("background-repeat");
+      arena.style.removeProperty("background-size");
+      let spacer = document.querySelector("#customization-palette > toolbarpaletteitem");
+      spacer.removeAttribute("kcode");
+      elements = null;
+      document = null;
+      quit = true;
+    }
+
+    if (this.uninitWhimsy) {
+      return this.uninitWhimsy;
+    }
+
+    let ballDef = [10, 10];
+    let ball = [10, 10];
+    let ballDxDyDef = [2, 2];
+    let ballDxDy = [2, 2];
+    let score = 0;
+    let p1 = 0;
+    let p2 = 10;
+    let gameSide = 300;
+    let paddleEdge = 30;
+    let paddleWidth = 84;
+    let keydownAdj = 1;
+    let keydown = 0;
+    let keys = [];
+    let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
+    let lives = 5;
+    let winScore = 11;
+    let quit = false;
+    let document = this.document;
+    let rAFHandle = 0;
+    let elements = {
+      arena: document.getElementById("customization-pong-arena"),
+    };
+
+    document.addEventListener("keydown", onkeydown);
+    document.addEventListener("keyup", onkeyup);
+
+    for (let id of ["player1", "player2", "ball", "score", "lives"]) {
+      let el = document.createXULElement("box");
+      el.id = "wp-" + id;
+      elements[el.id] = elements.arena.appendChild(el);
+    }
+
+    let spacer = this.visiblePalette.querySelector("toolbarpaletteitem");
+    for (let player of ["#wp-player1", "#wp-player2"]) {
+      let val = "-moz-element(#" + spacer.id + ") no-repeat";
+      elements.arena.querySelector(player).style.background = val;
+    }
+
+    let window = this.window;
+    rAFHandle = window.requestAnimationFrame(function animate() {
+      update();
+      draw();
+      if (quit) {
+        elements["wp-score"].textContent = score;
+        elements["wp-lives"] && elements["wp-lives"].setAttribute("lives", lives);
+        elements.arena.setAttribute("score", score);
+        elements.arena.setAttribute("lives", lives);
+      } else {
+        rAFHandle = window.requestAnimationFrame(animate);
+      }
+    });
+
+    return uninit;
+  },
+};
+
+function __dumpDragData(aEvent, caller) {
+  if (!gDebug) {
+    return;
+  }
+  let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
+  str += "  type: " + aEvent.type + "\n";
+  for (let el of ["target", "currentTarget", "relatedTarget"]) {
+    if (aEvent[el]) {
+      str += "  " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n";
+    }
+  }
+  for (let prop in aEvent.dataTransfer) {
+    if (typeof aEvent.dataTransfer[prop] != "function") {
+      str += "  dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
+    }
+  }
+  str += "}";
+  log.debug(str);
+}
+
+function dispatchFunction(aFunc) {
+  Services.tm.dispatchToMainThread(aFunc);
+}
new file mode 100644
--- /dev/null
+++ b/mail/components/customizableui/DragPositionManager.jsm
@@ -0,0 +1,313 @@
+/* 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";
+
+var gManagers = new WeakMap();
+
+const kPaletteId = "customization-palette";
+
+var EXPORTED_SYMBOLS = ["DragPositionManager"];
+
+function AreaPositionManager(aContainer) {
+  // Caching the direction and bounds of the container for quick access later:
+  this._rtl = aContainer.ownerGlobal.RTL_UI;
+  let containerRect = aContainer.getBoundingClientRect();
+  this._containerInfo = {
+    left: containerRect.left,
+    right: containerRect.right,
+    top: containerRect.top,
+    width: containerRect.width,
+  };
+  this._horizontalDistance = null;
+  this.update(aContainer);
+}
+
+AreaPositionManager.prototype = {
+  _nodePositionStore: null,
+
+  update(aContainer) {
+    this._nodePositionStore = new WeakMap();
+    let last = null;
+    let singleItemHeight;
+    for (let child of aContainer.children) {
+      if (child.hidden) {
+        continue;
+      }
+      let coordinates = this._lazyStoreGet(child);
+      // We keep a baseline horizontal distance between nodes around
+      // for use when we can't compare with previous/next nodes
+      if (!this._horizontalDistance && last) {
+        this._horizontalDistance = coordinates.left - last.left;
+      }
+      // We also keep the basic height of items for use below:
+      if (!singleItemHeight) {
+        singleItemHeight = coordinates.height;
+      }
+      last = coordinates;
+    }
+    this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
+  },
+
+  /**
+   * Find the closest node in the container given the coordinates.
+   * "Closest" is defined in a somewhat strange manner: we prefer nodes
+   * which are in the same row over nodes that are in a different row.
+   * In order to implement this, we use a weighted cartesian distance
+   * where dy is more heavily weighted by a factor corresponding to the
+   * ratio between the container's width and the height of its elements.
+   */
+  find(aContainer, aX, aY) {
+    let closest = null;
+    let minCartesian = Number.MAX_VALUE;
+    let containerX = this._containerInfo.left;
+    let containerY = this._containerInfo.top;
+    for (let node of aContainer.children) {
+      let coordinates = this._lazyStoreGet(node);
+      let offsetX = coordinates.x - containerX;
+      let offsetY = coordinates.y - containerY;
+      let hDiff = offsetX - aX;
+      let vDiff = offsetY - aY;
+      // Then compensate for the height/width ratio so that we prefer items
+      // which are in the same row:
+      hDiff /= this._heightToWidthFactor;
+
+      let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
+      if (cartesianDiff < minCartesian) {
+        minCartesian = cartesianDiff;
+        closest = node;
+      }
+    }
+
+    // Now correct this node based on what we're dragging
+    if (closest) {
+      let targetBounds = this._lazyStoreGet(closest);
+      let farSide = this._rtl ? "left" : "right";
+      let outsideX = targetBounds[farSide];
+      // Check if we're closer to the next target than to this one:
+      // Only move if we're not targeting a node in a different row:
+      if (aY > targetBounds.top && aY < targetBounds.bottom) {
+        if ((!this._rtl && aX > outsideX) ||
+            (this._rtl && aX < outsideX)) {
+          return closest.nextElementSibling || aContainer;
+        }
+      }
+    }
+    return closest;
+  },
+
+  /**
+   * "Insert" a "placeholder" by shifting the subsequent children out of the
+   * way. We go through all the children, and shift them based on the position
+   * they would have if we had inserted something before aBefore. We use CSS
+   * transforms for this, which are CSS transitioned.
+   */
+  insertPlaceholder(aContainer, aBefore, aSize, aIsFromThisArea) {
+    let isShifted = false;
+    for (let child of aContainer.children) {
+      // Don't need to shift hidden nodes:
+      if (child.getAttribute("hidden") == "true") {
+        continue;
+      }
+      // If this is the node before which we're inserting, start shifting
+      // everything that comes after. One exception is inserting at the end
+      // of the menupanel, in which case we do not shift the placeholders:
+      if (child == aBefore) {
+        isShifted = true;
+      }
+      if (isShifted) {
+        if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
+          child.setAttribute("notransition", "true");
+        }
+        // Determine the CSS transform based on the next node:
+        child.style.transform = this._diffWithNext(child, aSize);
+      } else {
+        // If we're not shifting this node, reset the transform
+        child.style.transform = "";
+      }
+    }
+    if (aContainer.lastElementChild && aIsFromThisArea &&
+        !this._lastPlaceholderInsertion) {
+      // Flush layout:
+      aContainer.lastElementChild.getBoundingClientRect();
+      // then remove all the [notransition]
+      for (let child of aContainer.children) {
+        child.removeAttribute("notransition");
+      }
+    }
+    this._lastPlaceholderInsertion = aBefore;
+  },
+
+  /**
+   * Reset all the transforms in this container, optionally without
+   * transitioning them.
+   * @param aContainer    the container in which to reset transforms
+   * @param aNoTransition if truthy, adds a notransition attribute to the node
+   *                      while resetting the transform.
+   */
+  clearPlaceholders(aContainer, aNoTransition) {
+    for (let child of aContainer.children) {
+      if (aNoTransition) {
+        child.setAttribute("notransition", true);
+      }
+      child.style.transform = "";
+      if (aNoTransition) {
+        // Need to force a reflow otherwise this won't work.
+        child.getBoundingClientRect();
+        child.removeAttribute("notransition");
+      }
+    }
+    // We snapped back, so we can assume there's no more
+    // "last" placeholder insertion point to keep track of.
+    if (aNoTransition) {
+      this._lastPlaceholderInsertion = null;
+    }
+  },
+
+  _diffWithNext(aNode, aSize) {
+    let xDiff;
+    let yDiff = null;
+    let nodeBounds = this._lazyStoreGet(aNode);
+    let side = this._rtl ? "right" : "left";
+    let next = this._getVisibleSiblingForDirection(aNode, "next");
+    // First we determine the transform along the x axis.
+    // Usually, there will be a next node to base this on:
+    if (next) {
+      let otherBounds = this._lazyStoreGet(next);
+      xDiff = otherBounds[side] - nodeBounds[side];
+      // We set this explicitly because otherwise some strange difference
+      // between the height and the actual difference between line creeps in
+      // and messes with alignments
+      yDiff = otherBounds.top - nodeBounds.top;
+    } else {
+      // We don't have a sibling whose position we can use. First, let's see
+      // if we're also the first item (which complicates things):
+      let firstNode = this._firstInRow(aNode);
+      if (aNode == firstNode) {
+        // Maybe we stored the horizontal distance between nodes,
+        // if not, we'll use the width of the incoming node as a proxy:
+        xDiff = this._horizontalDistance || (this._rtl ? -1 : 1) * aSize.width;
+      } else {
+        // If not, we should be able to get the distance to the previous node
+        // and use the inverse, unless there's no room for another node (ie we
+        // are the last node and there's no room for another one)
+        xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
+      }
+    }
+
+    // If we've not determined the vertical difference yet, check it here
+    if (yDiff === null) {
+      // If the next node is behind rather than in front, we must have moved
+      // vertically:
+      if ((xDiff > 0 && this._rtl) || (xDiff < 0 && !this._rtl)) {
+        yDiff = aSize.height;
+      } else {
+        // Otherwise, we haven't
+        yDiff = 0;
+      }
+    }
+    return "translate(" + xDiff + "px, " + yDiff + "px)";
+  },
+
+  /**
+   * Helper function to find the transform a node if there isn't a next node
+   * to base that on.
+   * @param aNode           the node to transform
+   * @param aNodeBounds     the bounding rect info of this node
+   * @param aFirstNodeInRow the first node in aNode's row
+   */
+  _moveNextBasedOnPrevious(aNode, aNodeBounds, aFirstNodeInRow) {
+    let next = this._getVisibleSiblingForDirection(aNode, "previous");
+    let otherBounds = this._lazyStoreGet(next);
+    let side = this._rtl ? "right" : "left";
+    let xDiff = aNodeBounds[side] - otherBounds[side];
+    // If, however, this means we move outside the container's box
+    // (i.e. the row in which this item is placed is full)
+    // we should move it to align with the first item in the next row instead
+    let bound = this._containerInfo[this._rtl ? "left" : "right"];
+    if ((!this._rtl && xDiff + aNodeBounds.right > bound) ||
+        (this._rtl && xDiff + aNodeBounds.left < bound)) {
+      xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side];
+    }
+    return xDiff;
+  },
+
+  /**
+   * Get position details from our cache. If the node is not yet cached, get its position
+   * information and cache it now.
+   * @param aNode  the node whose position info we want
+   * @return the position info
+   */
+  _lazyStoreGet(aNode) {
+    let rect = this._nodePositionStore.get(aNode);
+    if (!rect) {
+      // getBoundingClientRect() returns a DOMRect that is live, meaning that
+      // as the element moves around, the rects values change. We don't want
+      // that - we want a snapshot of what the rect values are right at this
+      // moment, and nothing else. So we have to clone the values.
+      let clientRect = aNode.getBoundingClientRect();
+      rect = {
+        left: clientRect.left,
+        right: clientRect.right,
+        width: clientRect.width,
+        height: clientRect.height,
+        top: clientRect.top,
+        bottom: clientRect.bottom,
+      };
+      rect.x = rect.left + rect.width / 2;
+      rect.y = rect.top + rect.height / 2;
+      Object.freeze(rect);
+      this._nodePositionStore.set(aNode, rect);
+    }
+    return rect;
+  },
+
+  _firstInRow(aNode) {
+    // XXXmconley: I'm not entirely sure why we need to take the floor of these
+    // values - it looks like, periodically, we're getting fractional pixels back
+    // from lazyStoreGet. I've filed bug 994247 to investigate.
+    let bound = Math.floor(this._lazyStoreGet(aNode).top);
+    let rv = aNode;
+    let prev;
+    while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
+      if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
+        return rv;
+      }
+      rv = prev;
+    }
+    return rv;
+  },
+
+  _getVisibleSiblingForDirection(aNode, aDirection) {
+    let rv = aNode;
+    do {
+      rv = rv[aDirection + "ElementSibling"];
+    } while (rv && rv.getAttribute("hidden") == "true");
+    return rv;
+  },
+};
+
+var DragPositionManager = {
+  start(aWindow) {
+    let areas = [aWindow.document.getElementById(kPaletteId)];
+    for (let areaNode of areas) {
+      let positionManager = gManagers.get(areaNode);
+      if (positionManager) {
+        positionManager.update(areaNode);
+      } else {
+        gManagers.set(areaNode, new AreaPositionManager(areaNode));
+      }
+    }
+  },
+
+  stop() {
+    gManagers = new WeakMap();
+  },
+
+  getManagerForArea(aArea) {
+    return gManagers.get(aArea);
+  },
+};
+
+Object.freeze(DragPositionManager);
new file mode 100644
--- /dev/null
+++ b/mail/components/customizableui/PanelMultiView.jsm
@@ -0,0 +1,1728 @@
+/* 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/. */
+
+/**
+ * Allows a popup panel to host multiple subviews. The main view shown when the
+ * panel is opened may slide out to display a subview, which in turn may lead to
+ * other subviews in a cascade menu pattern.
+ *
+ * The <panel> element should contain a <panelmultiview> element. Views are
+ * declared using <panelview> elements that are usually children of the main
+ * <panelmultiview> element, although they don't need to be, as views can also
+ * be imported into the panel from other panels or popup sets.
+ *
+ * The panel should be opened asynchronously using the openPopup static method
+ * on the PanelMultiView object. This will display the view specified using the
+ * mainViewId attribute on the contained <panelmultiview> element.
+ *
+ * Specific subviews can slide in using the showSubView method, and backwards
+ * navigation can be done using the goBack method or through a button in the
+ * subview headers.
+ *
+ * The process of displaying the main view or a new subview requires multiple
+ * steps to be completed, hence at any given time the <panelview> element may
+ * be in different states:
+ *
+ * -- Open or closed
+ *
+ *    All the <panelview> elements start "closed", meaning that they are not
+ *    associated to a <panelmultiview> element and can be located anywhere in
+ *    the document. When the openPopup or showSubView methods are called, the
+ *    relevant view becomes "open" and the <panelview> element may be moved to
+ *    ensure it is a descendant of the <panelmultiview> element.
+ *
+ *    The "ViewShowing" event is fired at this point, when the view is not
+ *    visible yet. The event is allowed to cancel the operation, in which case
+ *    the view is closed immediately.
+ *
+ *    Closing the view does not move the node back to its original position.
+ *
+ * -- Visible or invisible
+ *
+ *    This indicates whether the view is visible in the document from a layout
+ *    perspective, regardless of whether it is currently scrolled into view. In
+ *    fact, all subviews are already visible before they start sliding in.
+ *
+ *    Before scrolling into view, a view may become visible but be placed in a
+ *    special off-screen area of the document where layout and measurements can
+ *    take place asyncronously.
+ *
+ *    When navigating forward, an open view may become invisible but stay open
+ *    after sliding out of view. The last known size of these views is still
+ *    taken into account for determining the overall panel size.
+ *
+ *    When navigating backwards, an open subview will first become invisible and
+ *    then will be closed.
+ *
+ * -- Active or inactive
+ *
+ *    This indicates whether the view is fully scrolled into the visible area
+ *    and ready to receive mouse and keyboard events. An active view is always
+ *    visible, but a visible view may be inactive. For example, during a scroll
+ *    transition, both views will be inactive.
+ *
+ *    When a view becomes active, the ViewShown event is fired synchronously,
+ *    and the showSubView and goBack methods can be called for navigation.
+ *
+ *    For the main view of the panel, the ViewShown event is dispatched during
+ *    the "popupshown" event, which means that other "popupshown" handlers may
+ *    be called before the view is active. Thus, code that needs to perform
+ *    further navigation automatically should either use the ViewShown event or
+ *    wait for an event loop tick, like BrowserTestUtils.waitForEvent does.
+ *
+ * -- Navigating with the keyboard
+ *
+ *    An open view may keep state related to keyboard navigation, even if it is
+ *    invisible. When a view is closed, keyboard navigation state is cleared.
+ *
+ * This diagram shows how <panelview> nodes move during navigation:
+ *
+ *   In this <panelmultiview>     In other panels    Action
+ *             ┌───┬───┬───┐        ┌───┬───┐
+ *             │(A)│ B │ C │        │ D │ E │          Open panel
+ *             └───┴───┴───┘        └───┴───┘
+ *         ┌───┬───┬───┐            ┌───┬───┐
+ *         │{A}│(C)│ B │            │ D │ E │          Show subview C
+ *         └───┴───┴───┘            └───┴───┘
+ *     ┌───┬───┬───┬───┐            ┌───┐
+ *     │{A}│{C}│(D)│ B │            │ E │              Show subview D
+ *     └───┴───┴───┴───┘            └───┘
+ *       │ ┌───┬───┬───┬───┐        ┌───┐
+ *       │ │{A}│(C)│ D │ B │        │ E │              Go back
+ *       │ └───┴───┴───┴───┘        └───┘
+ *       │   │   │
+ *       │   │   └── Currently visible view
+ *       │   │   │
+ *       └───┴───┴── Open views
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+  "PanelMultiView",
+  "PanelView",
+];
+
+const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.defineModuleGetter(this, "CustomizableUI",
+  "resource:///modules/CustomizableUI.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gBundle", function() {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/browser.properties");
+});
+
+/**
+ * Safety timeout after which asynchronous events will be canceled if any of the
+ * registered blockers does not return.
+ */
+const BLOCKERS_TIMEOUT_MS = 10000;
+
+const TRANSITION_PHASES = Object.freeze({
+  START: 1,
+  PREPARE: 2,
+  TRANSITION: 3,
+});
+
+let gNodeToObjectMap = new WeakMap();
+let gWindowsWithUnloadHandler = new WeakSet();
+let gMultiLineElementsMap = new WeakMap();
+
+/**
+ * Allows associating an object to a node lazily using a weak map.
+ *
+ * Classes deriving from this one may be easily converted to Custom Elements,
+ * although they would lose the ability of being associated lazily.
+ */
+var AssociatedToNode = class {
+  constructor(node) {
+    /**
+     * Node associated to this object.
+     */
+    this.node = node;
+
+    /**
+     * This promise is resolved when the current set of blockers set by event
+     * handlers have all been processed.
+     */
+    this._blockersPromise = Promise.resolve();
+  }
+
+  /**
+   * Retrieves the instance associated with the given node, constructing a new
+   * one if necessary. When the last reference to the node is released, the
+   * object instance will be garbage collected as well.
+   */
+  static forNode(node) {
+    let associatedToNode = gNodeToObjectMap.get(node);
+    if (!associatedToNode) {
+      associatedToNode = new this(node);
+      gNodeToObjectMap.set(node, associatedToNode);
+    }
+    return associatedToNode;
+  }
+
+  get document() {
+    return this.node.ownerDocument;
+  }
+
+  get window() {
+    return this.node.ownerGlobal;
+  }
+
+  _getBoundsWithoutFlushing(element) {
+    return this.window.windowUtils.getBoundsWithoutFlushing(element);
+  }
+
+  /**
+   * Dispatches a custom event on this element.
+   *
+   * @param  {String}    eventName Name of the event to dispatch.
+   * @param  {Object}    [detail]  Event detail object. Optional.
+   * @param  {Boolean}   cancelable If the event can be canceled.
+   * @return {Boolean} `true` if the event was canceled by an event handler, `false`
+   *                   otherwise.
+   */
+  dispatchCustomEvent(eventName, detail, cancelable = false) {
+    let event = new this.window.CustomEvent(eventName, {
+      detail,
+      bubbles: true,
+      cancelable,
+    });
+    this.node.dispatchEvent(event);
+    return event.defaultPrevented;
+  }
+
+  /**
+   * Dispatches a custom event on this element and waits for any blocking
+   * promises registered using the "addBlocker" function on the details object.
+   * If this function is called again, the event is only dispatched after all
+   * the previously registered blockers have returned.
+   *
+   * The event can be canceled either by resolving any blocking promise to the
+   * boolean value "false" or by calling preventDefault on the event. Rejections
+   * and exceptions will be reported and will cancel the event.
+   *
+   * Blocking should be used sporadically because it slows down the interface.
+   * Also, non-reentrancy is not strictly guaranteed because a safety timeout of
+   * BLOCKERS_TIMEOUT_MS is implemented, after which the event will be canceled.
+   * This helps to prevent deadlocks if any of the event handlers does not
+   * resolve a blocker promise.
+   *
+   * @note Since there is no use case for dispatching different asynchronous
+   *       events in parallel for the same element, this function will also wait
+   *       for previous blockers when the event name is different.
+   *
+   * @param eventName
+   *        Name of the custom event to dispatch.
+   *
+   * @resolves True if the event was canceled by a handler, false otherwise.
+   */
+  async dispatchAsyncEvent(eventName) {
+    // Wait for all the previous blockers before dispatching the event.
+    let blockersPromise = this._blockersPromise.catch(() => {});
+    return this._blockersPromise = blockersPromise.then(async () => {
+      let blockers = new Set();
+      let cancel = this.dispatchCustomEvent(eventName, {
+        addBlocker(promise) {
+          // Any exception in the blocker will cancel the operation.
+          blockers.add(promise.catch(ex => {
+            Cu.reportError(ex);
+            return true;
+          }));
+        },
+      }, true);
+      if (blockers.size) {
+        let timeoutPromise = new Promise((resolve, reject) => {
+          this.window.setTimeout(reject, BLOCKERS_TIMEOUT_MS);
+        });
+        try {
+          let results = await Promise.race([Promise.all(blockers),
+                                            timeoutPromise]);
+          cancel = cancel || results.some(result => result === false);
+        } catch (ex) {
+          Cu.reportError(new Error(
+            `One of the blockers for ${eventName} timed out.`));
+          return true;
+        }
+      }
+      return cancel;
+    });
+  }
+};
+
+/**
+ * This is associated to <panelmultiview> elements.
+ */
+var PanelMultiView = class extends AssociatedToNode {
+  /**
+   * Tries to open the specified <panel> and displays the main view specified
+   * with the "mainViewId" attribute on the <panelmultiview> node it contains.
+   *
+   * If the panel does not contain a <panelmultiview>, it is opened directly.
+   * This allows consumers like page actions to accept different panel types.
+   *
+   * @see The non-static openPopup method for details.
+   */
+  static async openPopup(panelNode, ...args) {
+    let panelMultiViewNode = panelNode.querySelector("panelmultiview");
+    if (panelMultiViewNode) {
+      return this.forNode(panelMultiViewNode).openPopup(...args);
+    }
+    panelNode.openPopup(...args);
+    return true;
+  }
+
+  /**
+   * Closes the specified <panel> which contains a <panelmultiview> node.
+   *
+   * If the panel does not contain a <panelmultiview>, it is closed directly.
+   * This allows consumers like page actions to accept different panel types.
+   *
+   * @see The non-static hidePopup method for details.
+   */
+  static hidePopup(panelNode) {
+    let panelMultiViewNode = panelNode.querySelector("panelmultiview");
+    if (panelMultiViewNode) {
+      this.forNode(panelMultiViewNode).hidePopup();
+    } else {
+      panelNode.hidePopup();
+    }
+  }
+
+  /**
+   * Removes the specified <panel> from the document, ensuring that any
+   * <panelmultiview> node it contains is destroyed properly.
+   *
+   * If the viewCacheId attribute is present on the <panelmultiview> element,
+   * imported subviews will be moved out again to the element it specifies, so
+   * that the panel element can be removed safely.
+   *
+   * If the panel does not contain a <panelmultiview>, it is removed directly.
+   * This allows consumers like page actions to accept different panel types.
+   */
+  static removePopup(panelNode) {
+    try {
+      let panelMultiViewNode = panelNode.querySelector("panelmultiview");
+      if (panelMultiViewNode) {
+        let panelMultiView = this.forNode(panelMultiViewNode);
+        panelMultiView._moveOutKids();
+        panelMultiView.disconnect();
+      }
+    } finally {
+      // Make sure to remove the panel element even if disconnecting fails.
+      panelNode.remove();
+    }
+  }
+
+  /**
+   * Ensures that when the specified window is closed all the <panelmultiview>
+   * node it contains are destroyed properly.
+   */
+  static ensureUnloadHandlerRegistered(window) {
+    if (gWindowsWithUnloadHandler.has(window)) {
+      return;
+    }
+
+    window.addEventListener("unload", () => {
+      for (let panelMultiViewNode of
+           window.document.querySelectorAll("panelmultiview")) {
+        this.forNode(panelMultiViewNode).disconnect();
+      }
+    }, { once: true });
+
+    gWindowsWithUnloadHandler.add(window);
+  }
+
+  get _panel() {
+    return this.node.parentNode;
+  }
+
+  set _transitioning(val) {
+    if (val) {
+      this.node.setAttribute("transitioning", "true");
+    } else {
+      this.node.removeAttribute("transitioning");
+    }
+  }
+
+  get _screenManager() {
+    if (this.__screenManager)
+      return this.__screenManager;
+    return this.__screenManager = Cc["@mozilla.org/gfx/screenmanager;1"]
+                                    .getService(Ci.nsIScreenManager);
+  }
+
+  constructor(node) {
+    super(node);
+    this._openPopupPromise = Promise.resolve(false);
+    this._openPopupCancelCallback = () => {};
+  }
+
+  connect() {
+    this.connected = true;
+
+    PanelMultiView.ensureUnloadHandlerRegistered(this.window);
+
+    let viewContainer = this._viewContainer =
+      this.document.createXULElement("box");
+    viewContainer.classList.add("panel-viewcontainer");
+
+    let viewStack = this._viewStack = this.document.createXULElement("box");
+    viewStack.classList.add("panel-viewstack");
+    viewContainer.append(viewStack);
+
+    let offscreenViewContainer = this.document.createXULElement("box");
+    offscreenViewContainer.classList.add("panel-viewcontainer", "offscreen");
+
+    let offscreenViewStack = this._offscreenViewStack =
+      this.document.createXULElement("box");
+    offscreenViewStack.classList.add("panel-viewstack");
+    offscreenViewContainer.append(offscreenViewStack);
+
+    this.node.prepend(offscreenViewContainer);
+    this.node.prepend(viewContainer);
+
+    this.openViews = [];
+
+    this._panel.addEventListener("popupshowing", this);
+    this._panel.addEventListener("popuppositioned", this);
+    this._panel.addEventListener("popuphidden", this);
+    this._panel.addEventListener("popupshown", this);
+
+    // Proxy these public properties and methods, as used elsewhere by various
+    // parts of the browser, to this instance.
+    ["goBack", "showSubView"].forEach(method => {
+      Object.defineProperty(this.node, method, {
+        enumerable: true,
+        value: (...args) => this[method](...args),
+      });
+    });
+  }
+
+  disconnect() {
+    // Guard against re-entrancy.
+    if (!this.node || !this.connected)
+      return;
+
+    this._panel.removeEventListener("mousemove", this);
+    this._panel.removeEventListener("popupshowing", this);
+    this._panel.removeEventListener("popuppositioned", this);
+    this._panel.removeEventListener("popupshown", this);
+    this._panel.removeEventListener("popuphidden", this);
+    this.window.removeEventListener("keydown", this, true);
+    this.node = this._openPopupPromise = this._openPopupCancelCallback =
+      this._viewContainer = this._viewStack = this._transitionDetails = null;
+  }
+
+  /**
+   * Tries to open the panel associated with this PanelMultiView, and displays
+   * the main view specified with the "mainViewId" attribute.
+   *
+   * The hidePopup method can be called while the operation is in progress to
+   * prevent the panel from being displayed. View events may also cancel the
+   * operation, so there is no guarantee that the panel will become visible.
+   *
+   * The "popuphidden" event will be fired either when the operation is canceled
+   * or when the popup is closed later. This event can be used for example to
+   * reset the "open" state of the anchor or tear down temporary panels.
+   *
+   * If this method is called again before the panel is shown, the result
+   * depends on the operation currently in progress. If the operation was not
+   * canceled, the panel is opened using the arguments from the previous call,
+   * and this call is ignored. If the operation was canceled, it will be
+   * retried again using the arguments from this call.
+   *
+   * It's not necessary for the <panelmultiview> binding to be connected when
+   * this method is called, but the containing panel must have its display
+   * turned on, for example it shouldn't have the "hidden" attribute.
+   *
+   * @param anchor
+   *        The node to anchor the popup to.
+   * @param options
+   *        Either options to use or a string position. This is forwarded to
+   *        the openPopup method of the panel.
+   * @param args
+   *        Additional arguments to be forwarded to the openPopup method of the
+   *        panel.
+   *
+   * @resolves With true as soon as the request to display the panel has been
+   *           sent, or with false if the operation was canceled. The state of
+   *           the panel at this point is not guaranteed. It may be still
+   *           showing, completely shown, or completely hidden.
+   * @rejects If an exception is thrown at any point in the process before the
+   *          request to display the panel is sent.
+   */
+  async openPopup(anchor, options, ...args) {
+    // Set up the function that allows hidePopup or a second call to showPopup
+    // to cancel the specific panel opening operation that we're starting below.
+    // This function must be synchronous, meaning we can't use Promise.race,
+    // because hidePopup wants to dispatch the "popuphidden" event synchronously
+    // even if the panel has not been opened yet.
+    let canCancel = true;
+    let cancelCallback = this._openPopupCancelCallback = () => {
+      // If the cancel callback is called and the panel hasn't been prepared
+      // yet, cancel showing it. Setting canCancel to false will prevent the
+      // popup from opening. If the panel has opened by the time the cancel
+      // callback is called, canCancel will be false already, and we will not
+      // fire the "popuphidden" event.
+      if (canCancel && this.node) {
+        canCancel = false;
+        this.dispatchCustomEvent("popuphidden");
+      }
+    };
+
+    // Create a promise that is resolved with the result of the last call to
+    // this method, where errors indicate that the panel was not opened.
+    let openPopupPromise = this._openPopupPromise.catch(() => {
+      return false;
+    });
+
+    // Make the preparation done before showing the panel non-reentrant. The
+    // promise created here will be resolved only after the panel preparation is
+    // completed, even if a cancellation request is received in the meantime.
+    return this._openPopupPromise = openPopupPromise.then(async wasShown => {
+      // The panel may have been destroyed in the meantime.
+      if (!this.node) {
+        return false;
+      }
+      // If the panel has been already opened there is nothing more to do. We
+      // check the actual state of the panel rather than setting some state in
+      // our handler of the "popuphidden" event because this has a lower chance
+      // of locking indefinitely if events aren't raised in the expected order.
+      if (wasShown && ["open", "showing"].includes(this._panel.state)) {
+        return true;
+      }
+      try {
+        if (!this.connected) {
+          this.connect();
+        }
+        // Allow any of the ViewShowing handlers to prevent showing the main view.
+        if (!(await this._showMainView())) {
+          cancelCallback();
+        }
+      } catch (ex) {
+        cancelCallback();
+        throw ex;
+      }
+      // If a cancellation request was received there is nothing more to do.
+      if (!canCancel || !this.node) {
+        return false;
+      }
+      // We have to set canCancel to false before opening the popup because the
+      // hidePopup method of PanelMultiView can be re-entered by event handlers.
+      // If the openPopup call fails, however, we still have to dispatch the
+      // "popuphidden" event even if canCancel was set to false.
+      try {
+        canCancel = false;
+        this._panel.openPopup(anchor, options, ...args);
+
+        // On Windows, if another popup is hiding while we call openPopup, the
+        // call won't fail but the popup won't open. In this case, we have to
+        // dispatch an artificial "popuphidden" event to reset our state.
+        if (this._panel.state == "closed" && this.openViews.length) {
+          this.dispatchCustomEvent("popuphidden");
+          return false;
+        }
+
+        if (options && typeof options == "object" && options.triggerEvent &&
+            options.triggerEvent.type == "keypress" &&
+            this.openViews.length) {
+          // This was opened via the keyboard, so focus the first item.
+          this.openViews[0].focusWhenActive = true;
+        }
+
+        return true;
+      } catch (ex) {
+        this.dispatchCustomEvent("popuphidden");
+        throw ex;
+      }
+    });
+  }
+
+  /**
+   * Closes the panel associated with this PanelMultiView.
+   *
+   * If the openPopup method was called but the panel has not been displayed
+   * yet, the operation is canceled and the panel will not be displayed, but the
+   * "popuphidden" event is fired synchronously anyways.
+   *
+   * This means that by the time this method returns all the operations handled
+   * by the "popuphidden" event are completed, for example resetting the "open"
+   * state of the anchor, and the panel is already invisible.
+   */
+  hidePopup() {
+    if (!this.node || !this.connected) {
+      return;
+    }
+
+    // If we have already reached the _panel.openPopup call in the openPopup
+    // method, we can call hidePopup. Otherwise, we have to cancel the latest
+    // request to open the panel, which will have no effect if the request has
+    // been canceled already.
+    if (["open", "showing"].includes(this._panel.state)) {
+      this._panel.hidePopup();
+    } else {
+      this._openPopupCancelCallback();
+    }
+
+    // We close all the views synchronously, so that they are ready to be opened
+    // in other PanelMultiView instances. The "popuphidden" handler may also
+    // call this function, but the second time openViews will be empty.
+    this.closeAllViews();
+  }
+
+  /**
+   * Move any child subviews into the element defined by "viewCacheId" to make
+   * sure they will not be removed together with the <panelmultiview> element.
+   */
+  _moveOutKids() {
+    let viewCacheId = this.node.getAttribute("viewCacheId");
+    if (!viewCacheId) {
+      return;
+    }
+
+    // Node.children and Node.children is live to DOM changes like the
+    // ones we're about to do, so iterate over a static copy:
+    let subviews = Array.from(this._viewStack.children);
+    let viewCache = this.document.getElementById(viewCacheId);
+    for (let subview of subviews) {
+      viewCache.appendChild(subview);
+    }
+  }
+
+  /**
+   * Slides in the specified view as a subview.
+   *
+   * @param viewIdOrNode
+   *        DOM element or string ID of the <panelview> to display.
+   * @param anchor
+   *        DOM element that triggered the subview, which will be highlighted
+   *        and whose "label" attribute will be used for the title of the
+   *        subview when a "title" attribute is not specified.
+   */
+  showSubView(viewIdOrNode, anchor) {
+    // When autoPosition is true, the popup window manager would attempt to re-position
+    // the panel as subviews are opened and it changes size. The resulting popoppositioned
+    // events triggers the binding's arrow position adjustment - and its reflow.
+    // This is not needed here, as we calculated and set maxHeight so it is known
+    // to fit the screen while open.
+    // We do need autoposition for cases where the panel's anchor moves, which can happen
+    // especially with the "page actions" button in the URL bar (see bug 1520607), so
+    // we only set this to false when showing a subview, and set it back to true after we
+    // activate the subview.
+    this._panel.autoPosition = false;
+
+    this._showSubView(viewIdOrNode, anchor).catch(Cu.reportError);
+  }
+  async _showSubView(viewIdOrNode, anchor) {
+    let viewNode = typeof viewIdOrNode == "string" ?
+                   this.document.getElementById(viewIdOrNode) : viewIdOrNode;
+    if (!viewNode) {
+      Cu.reportError(new Error(`Subview ${viewIdOrNode} doesn't exist.`));
+      return;
+    }
+
+    if (!this.openViews.length) {
+      Cu.reportError(new Error(`Cannot show a subview in a closed panel.`));
+      return;
+    }
+
+    let prevPanelView = this.openViews[this.openViews.length - 1];
+    let nextPanelView = PanelView.forNode(viewNode);
+    if (this.openViews.includes(nextPanelView)) {
+      Cu.reportError(new Error(`Subview ${viewNode.id} is already open.`));
+      return;
+    }
+
+    // Do not re-enter the process if navigation is already in progress. Since
+    // there is only one active view at any given time, we can do this check
+    // safely, even considering that during the navigation process the actual
+    // view to which prevPanelView refers will change.
+    if (!prevPanelView.active) {
+      return;
+    }
+    // If prevPanelView._doingKeyboardActivation is true, it will be reset to
+    // false synchronously. Therefore, we must capture it before we use any
+    // "await" statements.
+    let doingKeyboardActivation = prevPanelView._doingKeyboardActivation;
+    // Marking the view that is about to scrolled out of the visible area as
+    // inactive will prevent re-entrancy and also disable keyboard navigation.
+    // From this point onwards, "await" statements can be used safely.
+    prevPanelView.active = false;
+
+    // Provide visual feedback while navigation is in progress, starting before
+    // the transition starts and ending when the previous view is invisible.
+    if (anchor) {
+      anchor.setAttribute("open", "true");
+    }
+    try {
+      // If the ViewShowing event cancels the operation we have to re-enable
+      // keyboard navigation, but this must be avoided if the panel was closed.
+      if (!(await this._openView(nextPanelView))) {
+        if (prevPanelView.isOpenIn(this)) {
+          // We don't raise a ViewShown event because nothing actually changed.
+          // Technically we should use a different state flag just because there
+          // is code that could check the "active" property to determine whether
+          // to wait for a ViewShown event later, but this only happens in
+          // regression tests and is less likely to be a technique used in
+          // production code, where use of ViewShown is less common.
+          prevPanelView.active = true;
+        }
+        return;
+      }
+
+      prevPanelView.captureKnownSize();
+
+      // The main view of a panel can be a subview in another one. Make sure to
+      // reset all the properties that may be set on a subview.
+      nextPanelView.mainview = false;
+      // The header may change based on how the subview was opened.
+      nextPanelView.headerText = viewNode.getAttribute("title") ||
+                                 (anchor && anchor.getAttribute("label"));
+      // The constrained width of subviews may also vary between panels.
+      nextPanelView.minMaxWidth = prevPanelView.knownWidth;
+
+      if (anchor) {
+        viewNode.classList.add("PanelUI-subView");
+      }
+
+      await this._transitionViews(prevPanelView.node, viewNode, false, anchor);
+    } finally {
+      if (anchor) {
+        anchor.removeAttribute("open");
+      }
+    }
+
+    nextPanelView.focusWhenActive = doingKeyboardActivation;
+    this._activateView(nextPanelView);
+  }
+
+  /**
+   * Navigates backwards by sliding out the most recent subview.
+   */
+  goBack() {
+    this._goBack().catch(Cu.reportError);
+  }