1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4"use strict";
5
6var EXPORTED_SYMBOLS = ["CustomizableUI"];
7
8const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
9const { XPCOMUtils } = ChromeUtils.import(
10  "resource://gre/modules/XPCOMUtils.jsm"
11);
12const { AppConstants } = ChromeUtils.import(
13  "resource://gre/modules/AppConstants.jsm"
14);
15
16XPCOMUtils.defineLazyModuleGetters(this, {
17  AddonManager: "resource://gre/modules/AddonManager.jsm",
18  AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
19  SearchWidgetTracker: "resource:///modules/SearchWidgetTracker.jsm",
20  CustomizableWidgets: "resource:///modules/CustomizableWidgets.jsm",
21  DeferredTask: "resource://gre/modules/DeferredTask.jsm",
22  PanelMultiView: "resource:///modules/PanelMultiView.jsm",
23  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
24  ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
25});
26
27XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
28  const kUrl =
29    "chrome://browser/locale/customizableui/customizableWidgets.properties";
30  return Services.strings.createBundle(kUrl);
31});
32
33XPCOMUtils.defineLazyServiceGetter(
34  this,
35  "gELS",
36  "@mozilla.org/eventlistenerservice;1",
37  "nsIEventListenerService"
38);
39
40const kDefaultThemeID = "default-theme@mozilla.org";
41
42const kSpecialWidgetPfx = "customizableui-special-";
43
44const kPrefCustomizationState = "browser.uiCustomization.state";
45const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
46const kPrefCustomizationDebug = "browser.uiCustomization.debug";
47const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
48const kPrefExtraDragSpace = "browser.tabs.extraDragSpace";
49const kPrefUIDensity = "browser.uidensity";
50const kPrefAutoTouchMode = "browser.touchmode.auto";
51const kPrefAutoHideDownloadsButton = "browser.download.autohideButton";
52
53const kExpectedWindowURL = "chrome://messenger/content/messenger.xhtml";
54
55var gDefaultTheme;
56var gSelectedTheme;
57
58/**
59 * The keys are the handlers that are fired when the event type (the value)
60 * is fired on the subview. A widget that provides a subview has the option
61 * of providing onViewShowing and onViewHiding event handlers.
62 */
63const kSubviewEvents = ["ViewShowing", "ViewHiding"];
64
65/**
66 * The current version. We can use this to auto-add new default widgets as necessary.
67 * (would be const but isn't because of testing purposes)
68 */
69var kVersion = 16;
70
71/**
72 * Buttons removed from built-ins by version they were removed. kVersion must be
73 * bumped any time a new id is added to this. Use the button id as key, and
74 * version the button is removed in as the value.  e.g. "pocket-button": 5
75 */
76var ObsoleteBuiltinButtons = {
77  "feed-button": 15,
78};
79
80/**
81 * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
82 * on their IDs.
83 */
84var gPalette = new Map();
85
86/**
87 * gAreas maps area IDs to Sets of properties about those areas. An area is a
88 * place where a widget can be put.
89 */
90var gAreas = new Map();
91
92/**
93 * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
94 * are placed within that area (either directly in the area node, or in the
95 * customizationTarget of the node).
96 */
97var gPlacements = new Map();
98
99/**
100 * gFuturePlacements represent placements that will happen for areas that have
101 * not yet loaded (due to lazy-loading). This can occur when add-ons register
102 * widgets.
103 */
104var gFuturePlacements = new Map();
105
106// XXXunf Temporary. Need a nice way to abstract functions to build widgets
107//       of these types.
108var gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
109
110/**
111 * gPanelsForWindow is a list of known panels in a window which we may need to close
112 * should command events fire which target them.
113 */
114var gPanelsForWindow = new WeakMap();
115
116/**
117 * gSeenWidgets remembers which widgets the user has seen for the first time
118 * before. This way, if a new widget is created, and the user has not seen it
119 * before, it can be put in its default location. Otherwise, it remains in the
120 * palette.
121 */
122var gSeenWidgets = new Set();
123
124/**
125 * gDirtyAreaCache is a set of area IDs for areas where items have been added,
126 * moved or removed at least once. This set is persisted, and is used to
127 * optimize building of toolbars in the default case where no toolbars should
128 * be "dirty".
129 */
130var gDirtyAreaCache = new Set();
131
132/**
133 * gPendingBuildAreas is a map from area IDs to map from build nodes to their
134 * existing children at the time of node registration, that are waiting
135 * for the area to be registered
136 */
137var gPendingBuildAreas = new Map();
138
139var gSavedState = null;
140var gRestoring = false;
141var gDirty = false;
142var gInBatchStack = 0;
143var gResetting = false;
144var gUndoResetting = false;
145
146/**
147 * gBuildAreas maps area IDs to actual area nodes within browser windows.
148 */
149var gBuildAreas = new Map();
150
151/**
152 * gBuildWindows is a map of windows that have registered build areas, mapped
153 * to a Set of known toolboxes in that window.
154 */
155var gBuildWindows = new Map();
156
157var gNewElementCount = 0;
158var gGroupWrapperCache = new Map();
159var gSingleWrapperCache = new WeakMap();
160var gListeners = new Set();
161
162var gUIStateBeforeReset = {
163  uiCustomizationState: null,
164  drawInTitlebar: null,
165  extraDragSpace: null,
166  currentTheme: null,
167  uiDensity: null,
168  autoTouchMode: null,
169};
170
171XPCOMUtils.defineLazyPreferenceGetter(
172  this,
173  "gDebuggingEnabled",
174  kPrefCustomizationDebug,
175  false,
176  (pref, oldVal, newVal) => {
177    if (typeof log != "undefined") {
178      log.maxLogLevel = newVal ? "all" : "log";
179    }
180  }
181);
182
183XPCOMUtils.defineLazyGetter(this, "log", () => {
184  let scope = {};
185  ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
186  let consoleOptions = {
187    maxLogLevel: gDebuggingEnabled ? "all" : "log",
188    prefix: "CustomizableUI",
189  };
190  return new scope.ConsoleAPI(consoleOptions);
191});
192
193var CustomizableUIInternal = {
194  initialize() {
195    log.debug("Initializing");
196
197    AddonManagerPrivate.databaseReady.then(async () => {
198      AddonManager.addAddonListener(this);
199
200      let addons = await AddonManager.getAddonsByTypes(["theme"]);
201      gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID);
202      gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme;
203    });
204
205    this.addListener(this);
206    this._defineBuiltInWidgets();
207    this.loadSavedState();
208    try {
209      this._updateForNewVersion();
210    } catch (e) {
211      Cu.reportError(e);
212    }
213    this._markObsoleteBuiltinButtonsSeen();
214
215    this.registerArea(
216      CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
217      {
218        type: CustomizableUI.TYPE_MENU_PANEL,
219        defaultPlacements: [],
220        anchor: "nav-bar-overflow-button",
221      },
222      true
223    );
224
225    let navbarPlacements = [
226      "back-button",
227      "forward-button",
228      "stop-reload-button",
229      "home-button",
230      "spring",
231      "urlbar-container",
232      "spring",
233      "downloads-button",
234      "library-button",
235      "sidebar-button",
236      "fxa-toolbar-menu-button",
237    ];
238
239    if (AppConstants.MOZ_DEV_EDITION) {
240      navbarPlacements.splice(2, 0, "developer-button");
241    }
242
243    this.registerArea(
244      CustomizableUI.AREA_NAVBAR,
245      {
246        type: CustomizableUI.TYPE_TOOLBAR,
247        overflowable: true,
248        defaultPlacements: navbarPlacements,
249        defaultCollapsed: false,
250      },
251      true
252    );
253
254    if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
255      this.registerArea(
256        CustomizableUI.AREA_MENUBAR,
257        {
258          type: CustomizableUI.TYPE_TOOLBAR,
259          defaultPlacements: ["menubar-items"],
260          defaultCollapsed: true,
261        },
262        true
263      );
264    }
265
266    this.registerArea(
267      CustomizableUI.AREA_TABSTRIP,
268      {
269        type: CustomizableUI.TYPE_TOOLBAR,
270        defaultPlacements: ["tabmail-tabs", "new-tab-button", "alltabs-button"],
271        defaultCollapsed: null,
272      },
273      true
274    );
275    this.registerArea(
276      CustomizableUI.AREA_BOOKMARKS,
277      {
278        type: CustomizableUI.TYPE_TOOLBAR,
279        defaultPlacements: ["personal-bookmarks"],
280        defaultCollapsed: true,
281      },
282      true
283    );
284
285    SearchWidgetTracker.init();
286  },
287
288  onEnabled(addon) {
289    if (addon.type == "theme") {
290      gSelectedTheme = addon;
291    }
292  },
293
294  get _builtinAreas() {
295    return new Set([
296      ...this._builtinToolbars,
297      CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
298    ]);
299  },
300
301  get _builtinToolbars() {
302    let toolbars = new Set([
303      CustomizableUI.AREA_NAVBAR,
304      CustomizableUI.AREA_BOOKMARKS,
305      CustomizableUI.AREA_TABSTRIP,
306    ]);
307    if (AppConstants.platform != "macosx") {
308      toolbars.add(CustomizableUI.AREA_MENUBAR);
309    }
310    return toolbars;
311  },
312
313  _defineBuiltInWidgets() {
314    for (let widgetDefinition of CustomizableWidgets) {
315      this.createBuiltinWidget(widgetDefinition);
316    }
317  },
318
319  // eslint-disable-next-line complexity
320  _updateForNewVersion() {
321    // We should still enter even if gSavedState.currentVersion >= kVersion
322    // because the per-widget pref facility is independent of versioning.
323    if (!gSavedState) {
324      // Flip all the prefs so we don't try to re-introduce later:
325      for (let [, widget] of gPalette) {
326        if (widget.defaultArea && widget._introducedInVersion === "pref") {
327          let prefId = "browser.toolbarbuttons.introduced." + widget.id;
328          Services.prefs.setBoolPref(prefId, true);
329        }
330      }
331      return;
332    }
333
334    let currentVersion = gSavedState.currentVersion;
335    for (let [id, widget] of gPalette) {
336      if (widget.defaultArea) {
337        let shouldAdd = false;
338        let shouldSetPref = false;
339        let prefId = "browser.toolbarbuttons.introduced." + widget.id;
340        if (widget._introducedInVersion === "pref") {
341          try {
342            shouldAdd = !Services.prefs.getBoolPref(prefId);
343          } catch (ex) {
344            // Pref doesn't exist:
345            shouldAdd = true;
346          }
347          shouldSetPref = shouldAdd;
348        } else if (widget._introducedInVersion > currentVersion) {
349          shouldAdd = true;
350        }
351
352        if (shouldAdd) {
353          let futurePlacements = gFuturePlacements.get(widget.defaultArea);
354          if (futurePlacements) {
355            futurePlacements.add(id);
356          } else {
357            gFuturePlacements.set(widget.defaultArea, new Set([id]));
358          }
359          if (shouldSetPref) {
360            Services.prefs.setBoolPref(prefId, true);
361          }
362        }
363      }
364    }
365
366    if (
367      currentVersion < 7 &&
368      gSavedState.placements &&
369      gSavedState.placements[CustomizableUI.AREA_NAVBAR]
370    ) {
371      let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
372      let newPlacements = [
373        "back-button",
374        "forward-button",
375        "stop-reload-button",
376        "home-button",
377      ];
378      for (let button of placements) {
379        if (!newPlacements.includes(button)) {
380          newPlacements.push(button);
381        }
382      }
383
384      if (!newPlacements.includes("sidebar-button")) {
385        newPlacements.push("sidebar-button");
386      }
387
388      gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements;
389    }
390
391    if (
392      currentVersion < 8 &&
393      gSavedState.placements &&
394      gSavedState.placements["PanelUI-contents"]
395    ) {
396      let savedPanelPlacements = gSavedState.placements["PanelUI-contents"];
397      delete gSavedState.placements["PanelUI-contents"];
398      let defaultPlacements = [
399        "edit-controls",
400        "zoom-controls",
401        "new-window-button",
402        "privatebrowsing-button",
403        "save-page-button",
404        "print-button",
405        "history-panelmenu",
406        "fullscreen-button",
407        "find-button",
408        "preferences-button",
409        "add-ons-button",
410        "sync-button",
411      ];
412
413      if (!AppConstants.MOZ_DEV_EDITION) {
414        defaultPlacements.splice(-1, 0, "developer-button");
415      }
416
417      savedPanelPlacements = savedPanelPlacements.filter(
418        id => !defaultPlacements.includes(id)
419      );
420
421      if (savedPanelPlacements.length) {
422        gSavedState.placements[
423          CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
424        ] = savedPanelPlacements;
425      }
426    }
427
428    if (
429      currentVersion < 9 &&
430      gSavedState.placements &&
431      gSavedState.placements["nav-bar"]
432    ) {
433      let placements = gSavedState.placements["nav-bar"];
434      if (placements.includes("urlbar-container")) {
435        let urlbarIndex = placements.indexOf("urlbar-container");
436        let secondSpringIndex = urlbarIndex + 1;
437        // Insert if there isn't already a spring before the urlbar
438        if (
439          urlbarIndex == 0 ||
440          !placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring")
441        ) {
442          placements.splice(urlbarIndex, 0, "spring");
443          // The url bar is now 1 index later, so increment the insertion point for
444          // the second spring.
445          secondSpringIndex++;
446        }
447        // If the search container is present, insert after the search container
448        // instead of after the url bar
449        let searchContainerIndex = placements.indexOf("search-container");
450        if (searchContainerIndex != -1) {
451          secondSpringIndex = searchContainerIndex + 1;
452        }
453        if (
454          secondSpringIndex == placements.length ||
455          !placements[secondSpringIndex].startsWith(
456            kSpecialWidgetPfx + "spring"
457          )
458        ) {
459          placements.splice(secondSpringIndex, 0, "spring");
460        }
461      }
462
463      // Finally, replace the bookmarks menu button with the library one if present
464      if (placements.includes("bookmarks-menu-button")) {
465        let bmbIndex = placements.indexOf("bookmarks-menu-button");
466        placements.splice(bmbIndex, 1);
467        let downloadButtonIndex = placements.indexOf("downloads-button");
468        let libraryIndex =
469          downloadButtonIndex == -1 ? bmbIndex : downloadButtonIndex + 1;
470        placements.splice(libraryIndex, 0, "library-button");
471      }
472    }
473
474    if (currentVersion < 10 && gSavedState.placements) {
475      for (let placements of Object.values(gSavedState.placements)) {
476        if (placements.includes("webcompat-reporter-button")) {
477          placements.splice(placements.indexOf("webcompat-reporter-button"), 1);
478          break;
479        }
480      }
481    }
482
483    // Move the downloads button to the default position in the navbar if it's
484    // not there already.
485    if (currentVersion < 11 && gSavedState.placements) {
486      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
487      // First remove from wherever it currently lives, if anywhere:
488      for (let placements of Object.values(gSavedState.placements)) {
489        let existingIndex = placements.indexOf("downloads-button");
490        if (existingIndex != -1) {
491          placements.splice(existingIndex, 1);
492          break; // It can only be in 1 place, so no point looking elsewhere.
493        }
494      }
495
496      // Now put the button in the navbar in the correct spot:
497      if (navbarPlacements) {
498        let insertionPoint = navbarPlacements.indexOf("urlbar-container");
499        // Deliberately iterate to 1 past the end of the array to insert at the
500        // end if need be.
501        while (++insertionPoint < navbarPlacements.length) {
502          let widget = navbarPlacements[insertionPoint];
503          // If we find a non-searchbar, non-spacer node, break out of the loop:
504          if (
505            widget != "search-container" &&
506            !this.matchingSpecials(widget, "spring")
507          ) {
508            break;
509          }
510        }
511        // We either found the right spot, or reached the end of the
512        // placements, so insert here:
513        navbarPlacements.splice(insertionPoint, 0, "downloads-button");
514      }
515    }
516
517    if (currentVersion < 12 && gSavedState.placements) {
518      const removedButtons = [
519        "loop-call-button",
520        "loop-button-throttled",
521        "pocket-button",
522      ];
523      for (let placements of Object.values(gSavedState.placements)) {
524        for (let button of removedButtons) {
525          let buttonIndex = placements.indexOf(button);
526          if (buttonIndex != -1) {
527            placements.splice(buttonIndex, 1);
528          }
529        }
530      }
531    }
532
533    // Remove the old placements from the now-gone Nightly-only
534    // "New non-e10s window" button.
535    if (currentVersion < 13 && gSavedState.placements) {
536      for (let placements of Object.values(gSavedState.placements)) {
537        let buttonIndex = placements.indexOf("e10s-button");
538        if (buttonIndex != -1) {
539          placements.splice(buttonIndex, 1);
540        }
541      }
542    }
543
544    // Remove unsupported custom toolbar saved placements
545    if (currentVersion < 14 && gSavedState.placements) {
546      for (let area in gSavedState.placements) {
547        if (!this._builtinAreas.has(area)) {
548          delete gSavedState.placements[area];
549        }
550      }
551    }
552
553    // Add the FxA toolbar menu as the right most button item
554    if (currentVersion < 16 && gSavedState.placements) {
555      let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
556      // Place the menu item as the first item to the left of the hamburger menu
557      if (navbarPlacements) {
558        navbarPlacements.push("fxa-toolbar-menu-button");
559      }
560    }
561  },
562
563  /**
564   * _markObsoleteBuiltinButtonsSeen
565   * when upgrading, ensure obsoleted buttons are in seen state.
566   */
567  _markObsoleteBuiltinButtonsSeen() {
568    if (!gSavedState) {
569      return;
570    }
571    let currentVersion = gSavedState.currentVersion;
572    if (currentVersion >= kVersion) {
573      return;
574    }
575    // we're upgrading, update state if necessary
576    for (let id in ObsoleteBuiltinButtons) {
577      let version = ObsoleteBuiltinButtons[id];
578      if (version == kVersion) {
579        gSeenWidgets.add(id);
580        gDirty = true;
581      }
582    }
583  },
584
585  _placeNewDefaultWidgetsInArea(aArea) {
586    let futurePlacedWidgets = gFuturePlacements.get(aArea);
587    let savedPlacements =
588      gSavedState && gSavedState.placements && gSavedState.placements[aArea];
589    let defaultPlacements = gAreas.get(aArea).get("defaultPlacements");
590    if (
591      !savedPlacements ||
592      !savedPlacements.length ||
593      !futurePlacedWidgets ||
594      !defaultPlacements ||
595      !defaultPlacements.length
596    ) {
597      return;
598    }
599    let defaultWidgetIndex = -1;
600
601    for (let widgetId of futurePlacedWidgets) {
602      let widget = gPalette.get(widgetId);
603      if (
604        !widget ||
605        widget.source !== CustomizableUI.SOURCE_BUILTIN ||
606        !widget.defaultArea ||
607        !widget._introducedInVersion ||
608        savedPlacements.includes(widget.id)
609      ) {
610        continue;
611      }
612      defaultWidgetIndex = defaultPlacements.indexOf(widget.id);
613      if (defaultWidgetIndex === -1) {
614        continue;
615      }
616      // Now we know that this widget should be here by default, was newly introduced,
617      // and we have a saved state to insert into, and a default state to work off of.
618      // Try introducing after widgets that come before it in the default placements:
619      for (let i = defaultWidgetIndex; i >= 0; i--) {
620        // Special case: if the defaults list this widget as coming first, insert at the beginning:
621        if (i === 0 && i === defaultWidgetIndex) {
622          savedPlacements.splice(0, 0, widget.id);
623          // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
624          // safe, and we won't skip any items.
625          futurePlacedWidgets.delete(widget.id);
626          gDirty = true;
627          break;
628        }
629        // Otherwise, if we're somewhere other than the beginning, check if the previous
630        // widget is in the saved placements.
631        if (i) {
632          let previousWidget = defaultPlacements[i - 1];
633          let previousWidgetIndex = savedPlacements.indexOf(previousWidget);
634          if (previousWidgetIndex != -1) {
635            savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
636            futurePlacedWidgets.delete(widget.id);
637            gDirty = true;
638            break;
639          }
640        }
641      }
642      // The loop above either inserts the item or doesn't - either way, we can get away
643      // with doing nothing else now; if the item remains in gFuturePlacements, we'll
644      // add it at the end in restoreStateForArea.
645    }
646    this.saveState();
647  },
648
649  getCustomizationTarget(aElement) {
650    if (!aElement) {
651      return null;
652    }
653
654    if (
655      !aElement._customizationTarget &&
656      aElement.hasAttribute("customizable")
657    ) {
658      let id = aElement.getAttribute("customizationtarget");
659      if (id) {
660        aElement._customizationTarget = aElement.ownerDocument.getElementById(
661          id
662        );
663      }
664
665      if (!aElement._customizationTarget) {
666        aElement._customizationTarget = aElement;
667      }
668    }
669
670    return aElement._customizationTarget;
671  },
672
673  wrapWidget(aWidgetId) {
674    if (gGroupWrapperCache.has(aWidgetId)) {
675      return gGroupWrapperCache.get(aWidgetId);
676    }
677
678    let provider = this.getWidgetProvider(aWidgetId);
679    if (!provider) {
680      return null;
681    }
682
683    if (provider == CustomizableUI.PROVIDER_API) {
684      let widget = gPalette.get(aWidgetId);
685      if (!widget.wrapper) {
686        widget.wrapper = new WidgetGroupWrapper(widget);
687        gGroupWrapperCache.set(aWidgetId, widget.wrapper);
688      }
689      return widget.wrapper;
690    }
691
692    // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
693    // XXXgijs: this causes bugs in code that depends on widgetWrapper.provider
694    // giving an accurate answer... filed as bug 1379821
695    let wrapper = new XULWidgetGroupWrapper(aWidgetId);
696    gGroupWrapperCache.set(aWidgetId, wrapper);
697    return wrapper;
698  },
699
700  registerArea(aName, aProperties, aInternalCaller) {
701    if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
702      throw new Error("Invalid area name");
703    }
704
705    let areaIsKnown = gAreas.has(aName);
706    let props = areaIsKnown ? gAreas.get(aName) : new Map();
707    const kImmutableProperties = new Set(["type", "overflowable"]);
708    for (let key in aProperties) {
709      if (
710        areaIsKnown &&
711        kImmutableProperties.has(key) &&
712        props.get(key) != aProperties[key]
713      ) {
714        throw new Error("An area cannot change the property for '" + key + "'");
715      }
716      props.set(key, aProperties[key]);
717    }
718    // Default to a toolbar:
719    if (!props.has("type")) {
720      props.set("type", CustomizableUI.TYPE_TOOLBAR);
721    }
722    if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
723      // Check aProperties instead of props because this check is only interested
724      // in the passed arguments, not the state of a potentially pre-existing area.
725      if (!aInternalCaller && aProperties.defaultCollapsed) {
726        throw new Error(
727          "defaultCollapsed is only allowed for default toolbars."
728        );
729      }
730      if (!props.has("defaultCollapsed")) {
731        props.set("defaultCollapsed", true);
732      }
733    } else if (props.has("defaultCollapsed")) {
734      throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
735    }
736    // Sanity check type:
737    let allTypes = [
738      CustomizableUI.TYPE_TOOLBAR,
739      CustomizableUI.TYPE_MENU_PANEL,
740    ];
741    if (!allTypes.includes(props.get("type"))) {
742      throw new Error("Invalid area type " + props.get("type"));
743    }
744
745    // And to no placements:
746    if (!props.has("defaultPlacements")) {
747      props.set("defaultPlacements", []);
748    }
749    // Sanity check default placements array:
750    if (!Array.isArray(props.get("defaultPlacements"))) {
751      throw new Error("Should provide an array of default placements");
752    }
753
754    if (!areaIsKnown) {
755      gAreas.set(aName, props);
756
757      // Reconcile new default widgets. Have to do this before we start restoring things.
758      this._placeNewDefaultWidgetsInArea(aName);
759
760      if (
761        props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
762        !gPlacements.has(aName)
763      ) {
764        // Guarantee this area exists in gFuturePlacements, to avoid checking it in
765        // various places elsewhere.
766        if (!gFuturePlacements.has(aName)) {
767          gFuturePlacements.set(aName, new Set());
768        }
769      } else {
770        this.restoreStateForArea(aName);
771      }
772
773      // If we have pending build area nodes, register all of them
774      if (gPendingBuildAreas.has(aName)) {
775        let pendingNodes = gPendingBuildAreas.get(aName);
776        for (let pendingNode of pendingNodes) {
777          this.registerToolbarNode(pendingNode);
778        }
779        gPendingBuildAreas.delete(aName);
780      }
781    }
782  },
783
784  unregisterArea(aName, aDestroyPlacements) {
785    if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
786      throw new Error("Invalid area name");
787    }
788    if (!gAreas.has(aName) && !gPlacements.has(aName)) {
789      throw new Error("Area not registered");
790    }
791
792    // Move all the widgets out
793    this.beginBatchUpdate();
794    try {
795      let placements = gPlacements.get(aName);
796      if (placements) {
797        // Need to clone this array so removeWidgetFromArea doesn't modify it
798        placements = [...placements];
799        placements.forEach(this.removeWidgetFromArea, this);
800      }
801
802      // Delete all remaining traces.
803      gAreas.delete(aName);
804      // Only destroy placements when necessary:
805      if (aDestroyPlacements) {
806        gPlacements.delete(aName);
807      } else {
808        // Otherwise we need to re-set them, as removeFromArea will have emptied
809        // them out:
810        gPlacements.set(aName, placements);
811      }
812      gFuturePlacements.delete(aName);
813      let existingAreaNodes = gBuildAreas.get(aName);
814      if (existingAreaNodes) {
815        for (let areaNode of existingAreaNodes) {
816          this.notifyListeners(
817            "onAreaNodeUnregistered",
818            aName,
819            this.getCustomizationTarget(areaNode),
820            CustomizableUI.REASON_AREA_UNREGISTERED
821          );
822        }
823      }
824      gBuildAreas.delete(aName);
825    } finally {
826      this.endBatchUpdate(true);
827    }
828  },
829
830  registerToolbarNode(aToolbar) {
831    let area = aToolbar.id;
832    if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
833      return;
834    }
835    let areaProperties = gAreas.get(area);
836
837    // If this area is not registered, try to do it automatically:
838    if (!areaProperties) {
839      if (!gPendingBuildAreas.has(area)) {
840        gPendingBuildAreas.set(area, []);
841      }
842      gPendingBuildAreas.get(area).push(aToolbar);
843      return;
844    }
845
846    this.beginBatchUpdate();
847    try {
848      let placements = gPlacements.get(area);
849      if (
850        !placements &&
851        areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR
852      ) {
853        this.restoreStateForArea(area);
854        placements = gPlacements.get(area);
855      }
856
857      // For toolbars that need it, mark as dirty.
858      let defaultPlacements = areaProperties.get("defaultPlacements");
859      if (
860        !this._builtinToolbars.has(area) ||
861        placements.length != defaultPlacements.length ||
862        !placements.every((id, i) => id == defaultPlacements[i])
863      ) {
864        gDirtyAreaCache.add(area);
865      }
866
867      if (areaProperties.has("overflowable")) {
868        aToolbar.overflowable = new OverflowableToolbar(aToolbar);
869      }
870
871      this.registerBuildArea(area, aToolbar);
872
873      // We only build the toolbar if it's been marked as "dirty". Dirty means
874      // one of the following things:
875      // 1) Items have been added, moved or removed from this toolbar before.
876      // 2) The number of children of the toolbar does not match the length of
877      //    the placements array for that area.
878      //
879      // This notion of being "dirty" is stored in a cache which is persisted
880      // in the saved state.
881      if (gDirtyAreaCache.has(area)) {
882        this.buildArea(area, placements, aToolbar);
883      } else {
884        // We must have a builtin toolbar that's in the default state. We need
885        // to only make sure that all the special nodes are correct.
886        let specials = placements.filter(p => this.isSpecialWidget(p));
887        if (specials.length) {
888          this.updateSpecialsForBuiltinToolbar(aToolbar, specials);
889        }
890      }
891      this.notifyListeners(
892        "onAreaNodeRegistered",
893        area,
894        this.getCustomizationTarget(aToolbar)
895      );
896    } finally {
897      this.endBatchUpdate();
898    }
899  },
900
901  updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) {
902    // Nodes are going to be in the correct order, so we can do this straightforwardly:
903    let { children } = this.getCustomizationTarget(aToolbar);
904    for (let kid of children) {
905      if (
906        this.matchingSpecials(aSpecialIDs[0], kid) &&
907        kid.getAttribute("skipintoolbarset") != "true"
908      ) {
909        kid.id = aSpecialIDs.shift();
910      }
911      if (!aSpecialIDs.length) {
912        return;
913      }
914    }
915  },
916
917  // eslint-disable-next-line complexity
918  buildArea(aArea, aPlacements, aAreaNode) {
919    let document = aAreaNode.ownerDocument;
920    let window = document.defaultView;
921    let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
922    let container = this.getCustomizationTarget(aAreaNode);
923    let areaIsPanel =
924      gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
925
926    if (!container) {
927      throw new Error(
928        "Expected area " + aArea + " to have a customizationTarget attribute."
929      );
930    }
931
932    // Restore nav-bar visibility since it may have been hidden
933    // through a migration path (bug 938980) or an add-on.
934    if (aArea == CustomizableUI.AREA_NAVBAR) {
935      aAreaNode.collapsed = false;
936    }
937
938    this.beginBatchUpdate();
939
940    try {
941      let currentNode = container.firstElementChild;
942      let placementsToRemove = new Set();
943      for (let id of aPlacements) {
944        while (
945          currentNode &&
946          currentNode.getAttribute("skipintoolbarset") == "true"
947        ) {
948          currentNode = currentNode.nextElementSibling;
949        }
950
951        // Fix ids for specials and continue, for correctly placed specials.
952        if (
953          currentNode &&
954          (!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) &&
955          this.matchingSpecials(id, currentNode)
956        ) {
957          currentNode.id = id;
958        }
959        if (currentNode && currentNode.id == id) {
960          currentNode = currentNode.nextElementSibling;
961          continue;
962        }
963
964        if (this.isSpecialWidget(id) && areaIsPanel) {
965          placementsToRemove.add(id);
966          continue;
967        }
968
969        let [provider, node] = this.getWidgetNode(id, window);
970        if (!node) {
971          log.debug("Unknown widget: " + id);
972          continue;
973        }
974
975        let widget = null;
976        // If the placements have items in them which are (now) no longer removable,
977        // we shouldn't be moving them:
978        if (provider == CustomizableUI.PROVIDER_API) {
979          widget = gPalette.get(id);
980          if (!widget.removable && aArea != widget.defaultArea) {
981            placementsToRemove.add(id);
982            continue;
983          }
984        } else if (
985          provider == CustomizableUI.PROVIDER_XUL &&
986          node.parentNode != container &&
987          !this.isWidgetRemovable(node)
988        ) {
989          placementsToRemove.add(id);
990          continue;
991        } // Special widgets are always removable, so no need to check them
992
993        if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) {
994          continue;
995        }
996
997        this.ensureButtonContextMenu(node, aAreaNode);
998
999        // This needs updating in case we're resetting / undoing a reset.
1000        if (widget) {
1001          widget.currentArea = aArea;
1002        }
1003        this.insertWidgetBefore(node, currentNode, container, aArea);
1004        if (gResetting) {
1005          this.notifyListeners("onWidgetReset", node, container);
1006        } else if (gUndoResetting) {
1007          this.notifyListeners("onWidgetUndoMove", node, container);
1008        }
1009      }
1010
1011      if (currentNode) {
1012        let palette = window.gNavToolbox ? window.gNavToolbox.palette : null;
1013        let limit = currentNode.previousElementSibling;
1014        let node = container.lastElementChild;
1015        while (node && node != limit) {
1016          let previousElementSibling = node.previousElementSibling;
1017          // Nodes opt-in to removability. If they're removable, and we haven't
1018          // seen them in the placements array, then we toss them into the palette
1019          // if one exists. If no palette exists, we just remove the node. If the
1020          // node is not removable, we leave it where it is. However, we can only
1021          // safely touch elements that have an ID - both because we depend on
1022          // IDs (or are specials), and because such elements are not intended to
1023          // be widgets (eg, titlebar-spacer elements).
1024          if (
1025            (node.id || this.isSpecialWidget(node)) &&
1026            node.getAttribute("skipintoolbarset") != "true"
1027          ) {
1028            if (this.isWidgetRemovable(node)) {
1029              if (node.id && (gResetting || gUndoResetting)) {
1030                let widget = gPalette.get(node.id);
1031                if (widget) {
1032                  widget.currentArea = null;
1033                }
1034              }
1035              if (palette && !this.isSpecialWidget(node.id)) {
1036                palette.appendChild(node);
1037                this.removeLocationAttributes(node);
1038              } else {
1039                container.removeChild(node);
1040              }
1041            } else {
1042              node.setAttribute("removable", false);
1043              log.debug(
1044                "Adding non-removable widget to placements of " +
1045                  aArea +
1046                  ": " +
1047                  node.id
1048              );
1049              gPlacements.get(aArea).push(node.id);
1050              gDirty = true;
1051            }
1052          }
1053          node = previousElementSibling;
1054        }
1055      }
1056
1057      // If there are placements in here which aren't removable from their original area,
1058      // we remove them from this area's placement array. They will (have) be(en) added
1059      // to their original area's placements array in the block above this one.
1060      if (placementsToRemove.size) {
1061        let placementAry = gPlacements.get(aArea);
1062        for (let id of placementsToRemove) {
1063          let index = placementAry.indexOf(id);
1064          placementAry.splice(index, 1);
1065        }
1066      }
1067
1068      if (gResetting) {
1069        this.notifyListeners("onAreaReset", aArea, container);
1070      }
1071    } finally {
1072      this.endBatchUpdate();
1073    }
1074  },
1075
1076  addPanelCloseListeners(aPanel) {
1077    gELS.addSystemEventListener(aPanel, "click", this, false);
1078    gELS.addSystemEventListener(aPanel, "keypress", this, false);
1079    let win = aPanel.ownerGlobal;
1080    if (!gPanelsForWindow.has(win)) {
1081      gPanelsForWindow.set(win, new Set());
1082    }
1083    gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
1084  },
1085
1086  removePanelCloseListeners(aPanel) {
1087    gELS.removeSystemEventListener(aPanel, "click", this, false);
1088    gELS.removeSystemEventListener(aPanel, "keypress", this, false);
1089    let win = aPanel.ownerGlobal;
1090    let panels = gPanelsForWindow.get(win);
1091    if (panels) {
1092      panels.delete(this._getPanelForNode(aPanel));
1093    }
1094  },
1095
1096  ensureButtonContextMenu(aNode, aAreaNode, forcePanel) {
1097    const kPanelItemContextMenu = "customizationPanelItemContextMenu";
1098
1099    let currentContextMenu =
1100      aNode.getAttribute("context") || aNode.getAttribute("contextmenu");
1101    let contextMenuForPlace =
1102      forcePanel || "menu-panel" == CustomizableUI.getPlaceForItem(aAreaNode)
1103        ? kPanelItemContextMenu
1104        : null;
1105    if (contextMenuForPlace && !currentContextMenu) {
1106      aNode.setAttribute("context", contextMenuForPlace);
1107    } else if (
1108      currentContextMenu == kPanelItemContextMenu &&
1109      contextMenuForPlace != kPanelItemContextMenu
1110    ) {
1111      aNode.removeAttribute("context");
1112      aNode.removeAttribute("contextmenu");
1113    }
1114  },
1115
1116  getWidgetProvider(aWidgetId) {
1117    if (this.isSpecialWidget(aWidgetId)) {
1118      return CustomizableUI.PROVIDER_SPECIAL;
1119    }
1120    if (gPalette.has(aWidgetId)) {
1121      return CustomizableUI.PROVIDER_API;
1122    }
1123    // If this was an API widget that was destroyed, return null:
1124    if (gSeenWidgets.has(aWidgetId)) {
1125      return null;
1126    }
1127
1128    // We fall back to the XUL provider, but we don't know for sure (at this
1129    // point) whether it exists there either. So the API is technically lying.
1130    // Ideally, it would be able to return an error value (or throw an
1131    // exception) if it really didn't exist. Our code calling this function
1132    // handles that fine, but this is a public API.
1133    return CustomizableUI.PROVIDER_XUL;
1134  },
1135
1136  getWidgetNode(aWidgetId, aWindow) {
1137    let document = aWindow.document;
1138
1139    if (this.isSpecialWidget(aWidgetId)) {
1140      let widgetNode =
1141        document.getElementById(aWidgetId) ||
1142        this.createSpecialWidget(aWidgetId, document);
1143      return [CustomizableUI.PROVIDER_SPECIAL, widgetNode];
1144    }
1145
1146    let widget = gPalette.get(aWidgetId);
1147    if (widget) {
1148      // If we have an instance of this widget already, just use that.
1149      if (widget.instances.has(document)) {
1150        log.debug(
1151          "An instance of widget " +
1152            aWidgetId +
1153            " already exists in this " +
1154            "document. Reusing."
1155        );
1156        return [CustomizableUI.PROVIDER_API, widget.instances.get(document)];
1157      }
1158
1159      return [CustomizableUI.PROVIDER_API, this.buildWidget(document, widget)];
1160    }
1161
1162    log.debug("Searching for " + aWidgetId + " in toolbox.");
1163    let node = this.findWidgetInWindow(aWidgetId, aWindow);
1164    if (node) {
1165      return [CustomizableUI.PROVIDER_XUL, node];
1166    }
1167
1168    log.debug("No node for " + aWidgetId + " found.");
1169    return [null, null];
1170  },
1171
1172  registerMenuPanel(aPanelContents, aArea) {
1173    if (gBuildAreas.has(aArea) && gBuildAreas.get(aArea).has(aPanelContents)) {
1174      return;
1175    }
1176
1177    aPanelContents._customizationTarget = aPanelContents;
1178    this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
1179
1180    let placements = gPlacements.get(aArea);
1181    this.buildArea(aArea, placements, aPanelContents);
1182    this.notifyListeners("onAreaNodeRegistered", aArea, aPanelContents);
1183
1184    for (let child of aPanelContents.children) {
1185      if (child.localName != "toolbarbutton") {
1186        if (child.localName == "toolbaritem") {
1187          this.ensureButtonContextMenu(child, aPanelContents, true);
1188        }
1189        continue;
1190      }
1191      this.ensureButtonContextMenu(child, aPanelContents, true);
1192    }
1193
1194    this.registerBuildArea(aArea, aPanelContents);
1195  },
1196
1197  onWidgetAdded(aWidgetId, aArea, aPosition) {
1198    this.insertNode(aWidgetId, aArea, aPosition, true);
1199
1200    if (!gResetting) {
1201      this._clearPreviousUIState();
1202    }
1203  },
1204
1205  onWidgetRemoved(aWidgetId, aArea) {
1206    let areaNodes = gBuildAreas.get(aArea);
1207    if (!areaNodes) {
1208      return;
1209    }
1210
1211    let area = gAreas.get(aArea);
1212    let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
1213    let isOverflowable = isToolbar && area.get("overflowable");
1214    let showInPrivateBrowsing = gPalette.has(aWidgetId)
1215      ? gPalette.get(aWidgetId).showInPrivateBrowsing
1216      : true;
1217
1218    for (let areaNode of areaNodes) {
1219      let window = areaNode.ownerGlobal;
1220      if (
1221        !showInPrivateBrowsing &&
1222        PrivateBrowsingUtils.isWindowPrivate(window)
1223      ) {
1224        continue;
1225      }
1226
1227      let container = this.getCustomizationTarget(areaNode);
1228      let widgetNode = window.document.getElementById(aWidgetId);
1229      if (widgetNode && isOverflowable) {
1230        container = areaNode.overflowable.getContainerFor(widgetNode);
1231      }
1232
1233      if (!widgetNode || !container.contains(widgetNode)) {
1234        log.info(
1235          "Widget " + aWidgetId + " not found, unable to remove from " + aArea
1236        );
1237        continue;
1238      }
1239
1240      this.notifyListeners(
1241        "onWidgetBeforeDOMChange",
1242        widgetNode,
1243        null,
1244        container,
1245        true
1246      );
1247
1248      // We remove location attributes here to make sure they're gone too when a
1249      // widget is removed from a toolbar to the palette. See bug 930950.
1250      this.removeLocationAttributes(widgetNode);
1251      // We also need to remove the panel context menu if it's there:
1252      this.ensureButtonContextMenu(widgetNode);
1253      if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
1254        container.removeChild(widgetNode);
1255      } else {
1256        window.gNavToolbox.palette.appendChild(widgetNode);
1257      }
1258      this.notifyListeners(
1259        "onWidgetAfterDOMChange",
1260        widgetNode,
1261        null,
1262        container,
1263        true
1264      );
1265
1266      let windowCache = gSingleWrapperCache.get(window);
1267      if (windowCache) {
1268        windowCache.delete(aWidgetId);
1269      }
1270    }
1271    if (!gResetting) {
1272      this._clearPreviousUIState();
1273    }
1274  },
1275
1276  onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
1277    this.insertNode(aWidgetId, aArea, aNewPosition);
1278    if (!gResetting) {
1279      this._clearPreviousUIState();
1280    }
1281  },
1282
1283  onCustomizeEnd(aWindow) {
1284    this._clearPreviousUIState();
1285  },
1286
1287  registerBuildArea(aArea, aNode) {
1288    // We ensure that the window is registered to have its customization data
1289    // cleaned up when unloading.
1290    let window = aNode.ownerGlobal;
1291    if (window.closed) {
1292      return;
1293    }
1294    this.registerBuildWindow(window);
1295
1296    // Also register this build area's toolbox.
1297    if (window.gNavToolbox) {
1298      gBuildWindows.get(window).add(window.gNavToolbox);
1299    }
1300
1301    if (!gBuildAreas.has(aArea)) {
1302      gBuildAreas.set(aArea, new Set());
1303    }
1304
1305    gBuildAreas.get(aArea).add(aNode);
1306
1307    // Give a class to all customize targets to be used for styling in Customize Mode
1308    let customizableNode = this.getCustomizeTargetForArea(aArea, window);
1309    customizableNode.classList.add("customization-target");
1310  },
1311
1312  registerBuildWindow(aWindow) {
1313    if (!gBuildWindows.has(aWindow)) {
1314      gBuildWindows.set(aWindow, new Set());
1315
1316      aWindow.addEventListener("unload", this);
1317      aWindow.addEventListener("command", this, true);
1318
1319      this.notifyListeners("onWindowOpened", aWindow);
1320    }
1321  },
1322
1323  unregisterBuildWindow(aWindow) {
1324    aWindow.removeEventListener("unload", this);
1325    aWindow.removeEventListener("command", this, true);
1326    gPanelsForWindow.delete(aWindow);
1327    gBuildWindows.delete(aWindow);
1328    gSingleWrapperCache.delete(aWindow);
1329    let document = aWindow.document;
1330
1331    for (let [areaId, areaNodes] of gBuildAreas) {
1332      let areaProperties = gAreas.get(areaId);
1333      for (let node of areaNodes) {
1334        if (node.ownerDocument == document) {
1335          this.notifyListeners(
1336            "onAreaNodeUnregistered",
1337            areaId,
1338            this.getCustomizationTarget(node),
1339            CustomizableUI.REASON_WINDOW_CLOSED
1340          );
1341          if (areaProperties.has("overflowable")) {
1342            node.overflowable.uninit();
1343            node.overflowable = null;
1344          }
1345          areaNodes.delete(node);
1346        }
1347      }
1348    }
1349
1350    for (let [, widget] of gPalette) {
1351      widget.instances.delete(document);
1352      this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
1353    }
1354
1355    for (let [, pendingNodes] of gPendingBuildAreas) {
1356      for (let i = pendingNodes.length - 1; i >= 0; i--) {
1357        if (pendingNodes[i].ownerDocument == document) {
1358          pendingNodes.splice(i, 1);
1359        }
1360      }
1361    }
1362
1363    this.notifyListeners("onWindowClosed", aWindow);
1364  },
1365
1366  setLocationAttributes(aNode, aArea) {
1367    let props = gAreas.get(aArea);
1368    if (!props) {
1369      throw new Error(
1370        "Expected area " +
1371          aArea +
1372          " to have a properties Map " +
1373          "associated with it."
1374      );
1375    }
1376
1377    aNode.setAttribute("cui-areatype", props.get("type") || "");
1378    let anchor = props.get("anchor");
1379    if (anchor) {
1380      aNode.setAttribute("cui-anchorid", anchor);
1381    } else {
1382      aNode.removeAttribute("cui-anchorid");
1383    }
1384  },
1385
1386  removeLocationAttributes(aNode) {
1387    aNode.removeAttribute("cui-areatype");
1388    aNode.removeAttribute("cui-anchorid");
1389  },
1390
1391  insertNode(aWidgetId, aArea, aPosition, isNew) {
1392    let areaNodes = gBuildAreas.get(aArea);
1393    if (!areaNodes) {
1394      return;
1395    }
1396
1397    let placements = gPlacements.get(aArea);
1398    if (!placements) {
1399      log.error(
1400        "Could not find any placements for " + aArea + " when moving a widget."
1401      );
1402      return;
1403    }
1404
1405    // Go through each of the nodes associated with this area and move the
1406    // widget to the requested location.
1407    for (let areaNode of areaNodes) {
1408      this.insertNodeInWindow(aWidgetId, areaNode, isNew);
1409    }
1410  },
1411
1412  insertNodeInWindow(aWidgetId, aAreaNode, isNew) {
1413    let window = aAreaNode.ownerGlobal;
1414    let showInPrivateBrowsing = gPalette.has(aWidgetId)
1415      ? gPalette.get(aWidgetId).showInPrivateBrowsing
1416      : true;
1417
1418    if (
1419      !showInPrivateBrowsing &&
1420      PrivateBrowsingUtils.isWindowPrivate(window)
1421    ) {
1422      return;
1423    }
1424
1425    let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
1426    if (!widgetNode) {
1427      log.error("Widget '" + aWidgetId + "' not found, unable to move");
1428      return;
1429    }
1430
1431    let areaId = aAreaNode.id;
1432    if (isNew) {
1433      this.ensureButtonContextMenu(widgetNode, aAreaNode);
1434    }
1435
1436    let [insertionContainer, nextNode] = this.findInsertionPoints(
1437      widgetNode,
1438      aAreaNode
1439    );
1440    this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
1441  },
1442
1443  findInsertionPoints(aNode, aAreaNode) {
1444    let areaId = aAreaNode.id;
1445    let props = gAreas.get(areaId);
1446
1447    // For overflowable toolbars, rely on them (because the work is more complicated):
1448    if (
1449      props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
1450      props.get("overflowable")
1451    ) {
1452      return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
1453    }
1454
1455    let container = this.getCustomizationTarget(aAreaNode);
1456    let placements = gPlacements.get(areaId);
1457    let nodeIndex = placements.indexOf(aNode.id);
1458
1459    while (++nodeIndex < placements.length) {
1460      let nextNodeId = placements[nodeIndex];
1461      let nextNode = aNode.ownerDocument.getElementById(nextNodeId);
1462      // If the next placed widget exists, and is a direct child of the
1463      // container, or wrapped in a customize mode wrapper (toolbarpaletteitem)
1464      // inside the container, insert beside it.
1465      // We have to check the parent to avoid errors when the placement ids
1466      // are for nodes that are no longer customizable.
1467      if (
1468        nextNode &&
1469        (nextNode.parentNode == container ||
1470          (nextNode.parentNode.localName == "toolbarpaletteitem" &&
1471            nextNode.parentNode.parentNode == container))
1472      ) {
1473        return [container, nextNode];
1474      }
1475    }
1476
1477    return [container, null];
1478  },
1479
1480  insertWidgetBefore(aNode, aNextNode, aContainer, aArea) {
1481    this.notifyListeners(
1482      "onWidgetBeforeDOMChange",
1483      aNode,
1484      aNextNode,
1485      aContainer
1486    );
1487    this.setLocationAttributes(aNode, aArea);
1488    aContainer.insertBefore(aNode, aNextNode);
1489    this.notifyListeners(
1490      "onWidgetAfterDOMChange",
1491      aNode,
1492      aNextNode,
1493      aContainer
1494    );
1495  },
1496
1497  handleEvent(aEvent) {
1498    switch (aEvent.type) {
1499      case "command":
1500        if (!this._originalEventInPanel(aEvent)) {
1501          break;
1502        }
1503        aEvent = aEvent.sourceEvent;
1504      // Fall through
1505      case "click":
1506      case "keypress":
1507        this.maybeAutoHidePanel(aEvent);
1508        break;
1509      case "unload":
1510        this.unregisterBuildWindow(aEvent.currentTarget);
1511        break;
1512    }
1513  },
1514
1515  _originalEventInPanel(aEvent) {
1516    let e = aEvent.sourceEvent;
1517    if (!e) {
1518      return false;
1519    }
1520    let node = this._getPanelForNode(e.target);
1521    if (!node) {
1522      return false;
1523    }
1524    let win = e.view;
1525    let panels = gPanelsForWindow.get(win);
1526    return !!panels && panels.has(node);
1527  },
1528
1529  _getSpecialIdForNode(aNode) {
1530    if (typeof aNode == "object" && aNode.localName) {
1531      if (aNode.id) {
1532        return aNode.id;
1533      }
1534      if (aNode.localName.startsWith("toolbar")) {
1535        return aNode.localName.substring(7);
1536      }
1537      return "";
1538    }
1539    return aNode;
1540  },
1541
1542  isSpecialWidget(aId) {
1543    aId = this._getSpecialIdForNode(aId);
1544    return (
1545      aId.startsWith(kSpecialWidgetPfx) ||
1546      aId.startsWith("separator") ||
1547      aId.startsWith("spring") ||
1548      aId.startsWith("spacer")
1549    );
1550  },
1551
1552  matchingSpecials(aId1, aId2) {
1553    aId1 = this._getSpecialIdForNode(aId1);
1554    aId2 = this._getSpecialIdForNode(aId2);
1555
1556    return (
1557      this.isSpecialWidget(aId1) &&
1558      this.isSpecialWidget(aId2) &&
1559      aId1.match(/spring|spacer|separator/)[0] ==
1560        aId2.match(/spring|spacer|separator/)[0]
1561    );
1562  },
1563
1564  ensureSpecialWidgetId(aId) {
1565    let nodeType = aId.match(/spring|spacer|separator/)[0];
1566    // If the ID we were passed isn't a generated one, generate one now:
1567    if (nodeType == aId) {
1568      // Ids are differentiated through a unique count suffix.
1569      return kSpecialWidgetPfx + aId + ++gNewElementCount;
1570    }
1571    return aId;
1572  },
1573
1574  createSpecialWidget(aId, aDocument) {
1575    let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
1576    let node = aDocument.createXULElement(nodeName);
1577    node.className = "chromeclass-toolbar-additional";
1578    node.id = this.ensureSpecialWidgetId(aId);
1579    return node;
1580  },
1581
1582  /* Find a XUL-provided widget in a window. Don't try to use this
1583   * for an API-provided widget or a special widget.
1584   */
1585  findWidgetInWindow(aId, aWindow) {
1586    if (!gBuildWindows.has(aWindow)) {
1587      throw new Error("Build window not registered");
1588    }
1589
1590    if (!aId) {
1591      log.error("findWidgetInWindow was passed an empty string.");
1592      return null;
1593    }
1594
1595    let document = aWindow.document;
1596
1597    // look for a node with the same id, as the node may be
1598    // in a different toolbar.
1599    let node = document.getElementById(aId);
1600    if (node) {
1601      let parent = node.parentNode;
1602      while (
1603        parent &&
1604        !(
1605          this.getCustomizationTarget(parent) ||
1606          parent == aWindow.gNavToolbox.palette
1607        )
1608      ) {
1609        parent = parent.parentNode;
1610      }
1611
1612      if (parent) {
1613        let nodeInArea =
1614          node.parentNode.localName == "toolbarpaletteitem"
1615            ? node.parentNode
1616            : node;
1617        // Check if we're in a customization target, or in the palette:
1618        if (
1619          (this.getCustomizationTarget(parent) == nodeInArea.parentNode &&
1620            gBuildWindows.get(aWindow).has(aWindow.gNavToolbox)) ||
1621          aWindow.gNavToolbox.palette == nodeInArea.parentNode
1622        ) {
1623          // Normalize the removable attribute. For backwards compat, if
1624          // the widget is not located in a toolbox palette then absence
1625          // of the "removable" attribute means it is not removable.
1626          if (!node.hasAttribute("removable")) {
1627            // If we first see this in customization mode, it may be in the
1628            // customization palette instead of the toolbox palette.
1629            node.setAttribute(
1630              "removable",
1631              !this.getCustomizationTarget(parent)
1632            );
1633          }
1634          return node;
1635        }
1636      }
1637    }
1638
1639    let toolboxes = gBuildWindows.get(aWindow);
1640    for (let toolbox of toolboxes) {
1641      if (toolbox.palette) {
1642        // Attempt to locate an element with a matching ID within
1643        // the palette.
1644        let element = toolbox.palette.getElementsByAttribute("id", aId)[0];
1645        if (element) {
1646          // Normalize the removable attribute. For backwards compat, this
1647          // is optional if the widget is located in the toolbox palette,
1648          // and defaults to *true*, unlike if it was located elsewhere.
1649          if (!element.hasAttribute("removable")) {
1650            element.setAttribute("removable", true);
1651          }
1652          return element;
1653        }
1654      }
1655    }
1656    return null;
1657  },
1658
1659  buildWidget(aDocument, aWidget) {
1660    if (aDocument.documentURI != kExpectedWindowURL) {
1661      throw new Error("buildWidget was called for a non-browser window!");
1662    }
1663    if (typeof aWidget == "string") {
1664      aWidget = gPalette.get(aWidget);
1665    }
1666    if (!aWidget) {
1667      throw new Error("buildWidget was passed a non-widget to build.");
1668    }
1669    if (
1670      !aWidget.showInPrivateBrowsing &&
1671      PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView)
1672    ) {
1673      return null;
1674    }
1675
1676    log.debug("Building " + aWidget.id + " of type " + aWidget.type);
1677
1678    let node;
1679    if (aWidget.type == "custom") {
1680      if (aWidget.onBuild) {
1681        node = aWidget.onBuild(aDocument);
1682      }
1683      if (!node || !(node instanceof aDocument.defaultView.XULElement)) {
1684        log.error(
1685          "Custom widget with id " +
1686            aWidget.id +
1687            " does not return a valid node"
1688        );
1689      }
1690    } else {
1691      if (aWidget.onBeforeCreated) {
1692        aWidget.onBeforeCreated(aDocument);
1693      }
1694      node = aDocument.createXULElement("toolbarbutton");
1695
1696      node.setAttribute("id", aWidget.id);
1697      node.setAttribute("widget-id", aWidget.id);
1698      node.setAttribute("widget-type", aWidget.type);
1699      if (aWidget.disabled) {
1700        node.setAttribute("disabled", true);
1701      }
1702      node.setAttribute("removable", aWidget.removable);
1703      node.setAttribute("overflows", aWidget.overflows);
1704      if (aWidget.tabSpecific) {
1705        node.setAttribute("tabspecific", aWidget.tabSpecific);
1706      }
1707      node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
1708      let additionalTooltipArguments = [];
1709      if (aWidget.shortcutId) {
1710        let keyEl = aDocument.getElementById(aWidget.shortcutId);
1711        if (keyEl) {
1712          additionalTooltipArguments.push(
1713            ShortcutUtils.prettifyShortcut(keyEl)
1714          );
1715        } else {
1716          log.error(
1717            "Key element with id '" +
1718              aWidget.shortcutId +
1719              "' for widget '" +
1720              aWidget.id +
1721              "' not found!"
1722          );
1723        }
1724      }
1725
1726      let tooltip = this.getLocalizedProperty(
1727        aWidget,
1728        "tooltiptext",
1729        additionalTooltipArguments
1730      );
1731      if (tooltip) {
1732        node.setAttribute("tooltiptext", tooltip);
1733      }
1734
1735      let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
1736      node.addEventListener("command", commandHandler);
1737      let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
1738      node.addEventListener("click", clickHandler);
1739
1740      let nodeClasses = ["toolbarbutton-1", "chromeclass-toolbar-additional"];
1741
1742      // If the widget has a view, and has view showing / hiding listeners,
1743      // hook those up to this widget.
1744      if (aWidget.type == "view") {
1745        log.debug(
1746          "Widget " +
1747            aWidget.id +
1748            " has a view. Auto-registering event handlers."
1749        );
1750        let viewNode = aDocument.getElementById(aWidget.viewId);
1751
1752        if (viewNode) {
1753          // PanelUI relies on the .PanelUI-subView class to be able to show only
1754          // one sub-view at a time.
1755          viewNode.classList.add("PanelUI-subView");
1756          if (aWidget.source == CustomizableUI.SOURCE_BUILTIN) {
1757            nodeClasses.push("subviewbutton-nav");
1758          }
1759          this.ensureSubviewListeners(viewNode);
1760        } else {
1761          log.error(
1762            "Could not find the view node with id: " +
1763              aWidget.viewId +
1764              ", for widget: " +
1765              aWidget.id +
1766              "."
1767          );
1768        }
1769
1770        let keyPressHandler = this.handleWidgetKeyPress.bind(
1771          this,
1772          aWidget,
1773          node
1774        );
1775        node.addEventListener("keypress", keyPressHandler);
1776      }
1777      node.setAttribute("class", nodeClasses.join(" "));
1778
1779      if (aWidget.onCreated) {
1780        aWidget.onCreated(node);
1781      }
1782    }
1783
1784    aWidget.instances.set(aDocument, node);
1785    return node;
1786  },
1787
1788  ensureSubviewListeners(viewNode) {
1789    if (viewNode._addedEventListeners) {
1790      return;
1791    }
1792    let viewId = viewNode.id;
1793    let widget = [...gPalette.values()].find(w => w.viewId == viewId);
1794    if (!widget) {
1795      return;
1796    }
1797    for (let eventName of kSubviewEvents) {
1798      let handler = "on" + eventName;
1799      if (typeof widget[handler] == "function") {
1800        viewNode.addEventListener(eventName, widget[handler]);
1801      }
1802    }
1803    viewNode._addedEventListeners = true;
1804    log.debug(
1805      "Widget " + widget.id + " showing and hiding event handlers set."
1806    );
1807  },
1808
1809  getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
1810    const kReqStringProps = ["label"];
1811
1812    if (typeof aWidget == "string") {
1813      aWidget = gPalette.get(aWidget);
1814    }
1815    if (!aWidget) {
1816      throw new Error(
1817        "getLocalizedProperty was passed a non-widget to work with."
1818      );
1819    }
1820    let def, name;
1821    // Let widgets pass their own string identifiers or strings, so that
1822    // we can use strings which aren't the default (in case string ids change)
1823    // and so that non-builtin-widgets can also provide labels, tooltips, etc.
1824    if (aWidget[aProp] != null) {
1825      name = aWidget[aProp];
1826      // By using this as the default, if a widget provides a full string rather
1827      // than a string ID for localization, we will fall back to that string
1828      // and return that.
1829      def = aDef || name;
1830    } else {
1831      name = aWidget.id + "." + aProp;
1832      def = aDef || "";
1833    }
1834    if (aWidget.localized === false) {
1835      return def;
1836    }
1837    try {
1838      if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
1839        return (
1840          gWidgetsBundle.formatStringFromName(
1841            name,
1842            aFormatArgs,
1843            aFormatArgs.length
1844          ) || def
1845        );
1846      }
1847      return gWidgetsBundle.GetStringFromName(name) || def;
1848    } catch (ex) {
1849      // If an empty string was explicitly passed, treat it as an actual
1850      // value rather than a missing property.
1851      if (!def && (name != "" || kReqStringProps.includes(aProp))) {
1852        log.error("Could not localize property '" + name + "'.");
1853      }
1854    }
1855    return def;
1856  },
1857
1858  addShortcut(aShortcutNode, aTargetNode = aShortcutNode) {
1859    // Detect if we've already been here before.
1860    if (aTargetNode.hasAttribute("shortcut")) {
1861      return;
1862    }
1863
1864    let document = aShortcutNode.ownerDocument;
1865    let shortcutId = aShortcutNode.getAttribute("key");
1866    let shortcut;
1867    if (shortcutId) {
1868      shortcut = document.getElementById(shortcutId);
1869    } else {
1870      let commandId = aShortcutNode.getAttribute("command");
1871      if (commandId) {
1872        shortcut = ShortcutUtils.findShortcut(
1873          document.getElementById(commandId)
1874        );
1875      }
1876    }
1877    if (!shortcut) {
1878      return;
1879    }
1880
1881    aTargetNode.setAttribute(
1882      "shortcut",
1883      ShortcutUtils.prettifyShortcut(shortcut)
1884    );
1885  },
1886
1887  handleWidgetCommand(aWidget, aNode, aEvent) {
1888    // Note that aEvent can be a keypress event for widgets of type "view".
1889    log.debug("handleWidgetCommand");
1890
1891    if (aWidget.onBeforeCommand) {
1892      try {
1893        aWidget.onBeforeCommand.call(null, aEvent);
1894      } catch (e) {
1895        log.error(e);
1896      }
1897    }
1898
1899    if (aWidget.type == "button") {
1900      if (aWidget.onCommand) {
1901        try {
1902          aWidget.onCommand.call(null, aEvent);
1903        } catch (e) {
1904          log.error(e);
1905        }
1906      } else {
1907        // XXXunf Need to think this through more, and formalize.
1908        Services.obs.notifyObservers(
1909          aNode,
1910          "customizedui-widget-command",
1911          aWidget.id
1912        );
1913      }
1914    } else if (aWidget.type == "view") {
1915      let ownerWindow = aNode.ownerGlobal;
1916      let area = this.getPlacementOfWidget(aNode.id).area;
1917      let areaType = CustomizableUI.getAreaType(area);
1918      let anchor = aNode;
1919      if (areaType != CustomizableUI.TYPE_MENU_PANEL) {
1920        let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
1921
1922        let hasMultiView = !!aNode.closest("panelmultiview");
1923        if (wrapper && !hasMultiView && wrapper.anchor) {
1924          this.hidePanelForNode(aNode);
1925          anchor = wrapper.anchor;
1926        }
1927      }
1928
1929      ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, aEvent);
1930    }
1931  },
1932
1933  handleWidgetClick(aWidget, aNode, aEvent) {
1934    log.debug("handleWidgetClick");
1935    if (aWidget.onClick) {
1936      try {
1937        aWidget.onClick.call(null, aEvent);
1938      } catch (e) {
1939        Cu.reportError(e);
1940      }
1941    } else {
1942      // XXXunf Need to think this through more, and formalize.
1943      Services.obs.notifyObservers(
1944        aNode,
1945        "customizedui-widget-click",
1946        aWidget.id
1947      );
1948    }
1949  },
1950
1951  handleWidgetKeyPress(aWidget, aNode, aEvent) {
1952    if (aEvent.key != " " && aEvent.key != "Enter") {
1953      return;
1954    }
1955    aEvent.stopPropagation();
1956    aEvent.preventDefault();
1957    this.handleWidgetCommand(aWidget, aNode, aEvent);
1958  },
1959
1960  _getPanelForNode(aNode) {
1961    return aNode.closest("panel");
1962  },
1963
1964  /*
1965   * If people put things in the panel which need more than single-click interaction,
1966   * we don't want to close it. Right now we check for text inputs and menu buttons.
1967   * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
1968   * part of the menu.
1969   */
1970  _isOnInteractiveElement(aEvent) {
1971    function getMenuPopupForDescendant(aNode) {
1972      let lastPopup = null;
1973      while (
1974        aNode &&
1975        aNode.parentNode &&
1976        aNode.parentNode.localName.startsWith("menu")
1977      ) {
1978        lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
1979        aNode = aNode.parentNode;
1980      }
1981      return lastPopup;
1982    }
1983
1984    let target = aEvent.target;
1985    let panel = this._getPanelForNode(aEvent.currentTarget);
1986    // This can happen in e.g. customize mode. If there's no panel,
1987    // there's clearly nothing for us to close; pretend we're interactive.
1988    if (!panel) {
1989      return true;
1990    }
1991    // We keep track of:
1992    // whether we're in an input container (text field)
1993    let inInput = false;
1994    // whether we're in a popup/context menu
1995    let inMenu = false;
1996    // whether we're in a toolbarbutton/toolbaritem
1997    let inItem = false;
1998    // whether the current menuitem has a valid closemenu attribute
1999    let menuitemCloseMenu = "auto";
2000
2001    // While keeping track of that, we go from the original target back up,
2002    // to the panel if we have to. We bail as soon as we find an input,
2003    // a toolbarbutton/item, or the panel:
2004    while (true && target) {
2005      // Skip out of iframes etc:
2006      if (target.nodeType == target.DOCUMENT_NODE) {
2007        if (!target.defaultView) {
2008          // Err, we're done.
2009          break;
2010        }
2011        // Find containing browser or iframe element in the parent doc.
2012        target = target.defaultView.docShell.chromeEventHandler;
2013        if (!target) {
2014          break;
2015        }
2016      }
2017      let tagName = target.localName;
2018      inInput = tagName == "input";
2019      inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
2020      let isMenuItem = tagName == "menuitem";
2021      inMenu = inMenu || isMenuItem;
2022
2023      if (isMenuItem && target.hasAttribute("closemenu")) {
2024        let closemenuVal = target.getAttribute("closemenu");
2025        menuitemCloseMenu =
2026          closemenuVal == "single" || closemenuVal == "none"
2027            ? closemenuVal
2028            : "auto";
2029      }
2030
2031      // Keep the menu open and break out of the loop if the click happened on
2032      // the ShadowRoot or a disabled menu item.
2033      if (
2034        target.nodeType == target.DOCUMENT_FRAGMENT_NODE ||
2035        target.getAttribute("disabled") == "true"
2036      ) {
2037        return true;
2038      }
2039
2040      // This isn't in the loop condition because we want to break before
2041      // changing |target| if any of these conditions are true
2042      if (inInput || inItem || target == panel) {
2043        break;
2044      }
2045      // We need specific code for popups: the item on which they were invoked
2046      // isn't necessarily in their parentNode chain:
2047      if (isMenuItem) {
2048        let topmostMenuPopup = getMenuPopupForDescendant(target);
2049        target =
2050          (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
2051          target.parentNode;
2052      } else {
2053        target = target.parentNode;
2054      }
2055    }
2056
2057    // If the user clicked a menu item...
2058    if (inMenu) {
2059      // We care if we're in an input also,
2060      // or if the user specified closemenu!="auto":
2061      if (inInput || menuitemCloseMenu != "auto") {
2062        return true;
2063      }
2064      // Otherwise, we're probably fine to close the panel
2065      return false;
2066    }
2067    // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
2068    // we'll now interact with the menu
2069    if (inItem && target.getAttribute("type") == "menu") {
2070      return true;
2071    }
2072    return inInput || !inItem;
2073  },
2074
2075  hidePanelForNode(aNode) {
2076    let panel = this._getPanelForNode(aNode);
2077    if (panel) {
2078      PanelMultiView.hidePopup(panel);
2079    }
2080  },
2081
2082  maybeAutoHidePanel(aEvent) {
2083    if (aEvent.type == "keypress") {
2084      if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
2085        return;
2086      }
2087      // If the user hit enter/return, we don't check preventDefault - it makes sense
2088      // that this was prevented, but we probably still want to close the panel.
2089      // If consumers don't want this to happen, they should specify the closemenu
2090      // attribute.
2091    } else if (aEvent.type != "command") {
2092      // mouse events:
2093      if (aEvent.defaultPrevented || aEvent.button != 0) {
2094        return;
2095      }
2096      let isInteractive = this._isOnInteractiveElement(aEvent);
2097      log.debug("maybeAutoHidePanel: interactive ? " + isInteractive);
2098      if (isInteractive) {
2099        return;
2100      }
2101    }
2102
2103    // We can't use event.target because we might have passed an anonymous
2104    // content boundary as well, and so target points to the outer element in
2105    // that case. Unfortunately, this means we get anonymous child nodes instead
2106    // of the real ones, so looking for the 'stoooop, don't close me' attributes
2107    // is more involved.
2108    let target = aEvent.target;
2109    while (target.parentNode && target.localName != "panel") {
2110      if (
2111        target.getAttribute("closemenu") == "none" ||
2112        target.getAttribute("widget-type") == "view"
2113      ) {
2114        return;
2115      }
2116      target = target.parentNode;
2117    }
2118
2119    // If we get here, we can actually hide the popup:
2120    this.hidePanelForNode(aEvent.target);
2121  },
2122
2123  getUnusedWidgets(aWindowPalette) {
2124    let window = aWindowPalette.ownerGlobal;
2125    let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
2126    // We use a Set because there can be overlap between the widgets in
2127    // gPalette and the items in the palette, especially after the first
2128    // customization, since programmatically generated widgets will remain
2129    // in the toolbox palette.
2130    let widgets = new Set();
2131
2132    // It's possible that some widgets have been defined programmatically and
2133    // have not been overlaid into the palette. We can find those inside
2134    // gPalette.
2135    for (let [id, widget] of gPalette) {
2136      if (!widget.currentArea) {
2137        if (widget.showInPrivateBrowsing || !isWindowPrivate) {
2138          widgets.add(id);
2139        }
2140      }
2141    }
2142
2143    log.debug("Iterating the actual nodes of the window palette");
2144    for (let node of aWindowPalette.children) {
2145      log.debug("In palette children: " + node.id);
2146      if (node.id && !this.getPlacementOfWidget(node.id)) {
2147        widgets.add(node.id);
2148      }
2149    }
2150
2151    return [...widgets];
2152  },
2153
2154  getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas) {
2155    if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
2156      return null;
2157    }
2158
2159    for (let [area, placements] of gPlacements) {
2160      if (!gAreas.has(area) && !aDeadAreas) {
2161        continue;
2162      }
2163      let index = placements.indexOf(aWidgetId);
2164      if (index != -1) {
2165        return { area, position: index };
2166      }
2167    }
2168
2169    return null;
2170  },
2171
2172  widgetExists(aWidgetId) {
2173    if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
2174      return true;
2175    }
2176
2177    // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
2178    if (gSeenWidgets.has(aWidgetId)) {
2179      return false;
2180    }
2181
2182    // We're assuming XUL widgets always exist, as it's much harder to check,
2183    // and checking would be much more error prone.
2184    return true;
2185  },
2186
2187  addWidgetToArea(aWidgetId, aArea, aPosition, aInitialAdd) {
2188    if (!gAreas.has(aArea)) {
2189      throw new Error("Unknown customization area: " + aArea);
2190    }
2191
2192    // Hack: don't want special widgets in the panel (need to check here as well
2193    // as in canWidgetMoveToArea because the menu panel is lazy):
2194    if (
2195      gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL &&
2196      this.isSpecialWidget(aWidgetId)
2197    ) {
2198      return;
2199    }
2200
2201    // If this is a lazy area that hasn't been restored yet, we can't yet modify
2202    // it - would would at least like to add to it. So we keep track of it in
2203    // gFuturePlacements,  and use that to add it when restoring the area. We
2204    // throw away aPosition though, as that can only be bogus if the area hasn't
2205    // yet been restorted (caller can't possibly know where its putting the
2206    // widget in relation to other widgets).
2207    if (this.isAreaLazy(aArea)) {
2208      gFuturePlacements.get(aArea).add(aWidgetId);
2209      return;
2210    }
2211
2212    if (this.isSpecialWidget(aWidgetId)) {
2213      aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
2214    }
2215
2216    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
2217    if (oldPlacement && oldPlacement.area == aArea) {
2218      this.moveWidgetWithinArea(aWidgetId, aPosition);
2219      return;
2220    }
2221
2222    // Do nothing if the widget is not allowed to move to the target area.
2223    if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
2224      return;
2225    }
2226
2227    if (oldPlacement) {
2228      this.removeWidgetFromArea(aWidgetId);
2229    }
2230
2231    if (!gPlacements.has(aArea)) {
2232      gPlacements.set(aArea, [aWidgetId]);
2233      aPosition = 0;
2234    } else {
2235      let placements = gPlacements.get(aArea);
2236      if (typeof aPosition != "number") {
2237        aPosition = placements.length;
2238      }
2239      if (aPosition < 0) {
2240        aPosition = 0;
2241      }
2242      placements.splice(aPosition, 0, aWidgetId);
2243    }
2244
2245    let widget = gPalette.get(aWidgetId);
2246    if (widget) {
2247      widget.currentArea = aArea;
2248      widget.currentPosition = aPosition;
2249    }
2250
2251    // We initially set placements with addWidgetToArea, so in that case
2252    // we don't consider the area "dirtied".
2253    if (!aInitialAdd) {
2254      gDirtyAreaCache.add(aArea);
2255    }
2256
2257    gDirty = true;
2258    this.saveState();
2259
2260    this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
2261  },
2262
2263  removeWidgetFromArea(aWidgetId) {
2264    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
2265    if (!oldPlacement) {
2266      return;
2267    }
2268
2269    if (!this.isWidgetRemovable(aWidgetId)) {
2270      return;
2271    }
2272
2273    let placements = gPlacements.get(oldPlacement.area);
2274    let position = placements.indexOf(aWidgetId);
2275    if (position != -1) {
2276      placements.splice(position, 1);
2277    }
2278
2279    let widget = gPalette.get(aWidgetId);
2280    if (widget) {
2281      widget.currentArea = null;
2282      widget.currentPosition = null;
2283    }
2284
2285    gDirty = true;
2286    this.saveState();
2287    gDirtyAreaCache.add(oldPlacement.area);
2288
2289    this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
2290  },
2291
2292  moveWidgetWithinArea(aWidgetId, aPosition) {
2293    let oldPlacement = this.getPlacementOfWidget(aWidgetId);
2294    if (!oldPlacement) {
2295      return;
2296    }
2297
2298    let placements = gPlacements.get(oldPlacement.area);
2299    if (typeof aPosition != "number") {
2300      aPosition = placements.length;
2301    } else if (aPosition < 0) {
2302      aPosition = 0;
2303    } else if (aPosition > placements.length) {
2304      aPosition = placements.length;
2305    }
2306
2307    let widget = gPalette.get(aWidgetId);
2308    if (widget) {
2309      widget.currentPosition = aPosition;
2310      widget.currentArea = oldPlacement.area;
2311    }
2312
2313    if (aPosition == oldPlacement.position) {
2314      return;
2315    }
2316
2317    placements.splice(oldPlacement.position, 1);
2318    // If we just removed the item from *before* where it is now added,
2319    // we need to compensate the position offset for that:
2320    if (oldPlacement.position < aPosition) {
2321      aPosition--;
2322    }
2323    placements.splice(aPosition, 0, aWidgetId);
2324
2325    gDirty = true;
2326    gDirtyAreaCache.add(oldPlacement.area);
2327
2328    this.saveState();
2329
2330    this.notifyListeners(
2331      "onWidgetMoved",
2332      aWidgetId,
2333      oldPlacement.area,
2334      oldPlacement.position,
2335      aPosition
2336    );
2337  },
2338
2339  // Note that this does not populate gPlacements, which is done lazily.
2340  // The panel area is an exception here.
2341  loadSavedState() {
2342    let state = Services.prefs.getCharPref(kPrefCustomizationState, "");
2343    if (!state) {
2344      log.debug("No saved state found");
2345      // Nothing has been customized, so silently fall back to the defaults.
2346      return;
2347    }
2348    try {
2349      gSavedState = JSON.parse(state);
2350      if (typeof gSavedState != "object" || gSavedState === null) {
2351        throw new Error("Invalid saved state");
2352      }
2353    } catch (e) {
2354      Services.prefs.clearUserPref(kPrefCustomizationState);
2355      gSavedState = {};
2356      log.debug(
2357        "Error loading saved UI customization state, falling back to defaults."
2358      );
2359    }
2360
2361    if (!("placements" in gSavedState)) {
2362      gSavedState.placements = {};
2363    }
2364
2365    if (!("currentVersion" in gSavedState)) {
2366      gSavedState.currentVersion = 0;
2367    }
2368
2369    gSeenWidgets = new Set(gSavedState.seen || []);
2370    gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
2371    gNewElementCount = gSavedState.newElementCount || 0;
2372  },
2373
2374  restoreStateForArea(aArea) {
2375    let placementsPreexisted = gPlacements.has(aArea);
2376
2377    this.beginBatchUpdate();
2378    try {
2379      gRestoring = true;
2380
2381      let restored = false;
2382      if (placementsPreexisted) {
2383        log.debug("Restoring " + aArea + " from pre-existing placements");
2384        for (let [position, id] of gPlacements.get(aArea).entries()) {
2385          this.moveWidgetWithinArea(id, position);
2386        }
2387        gDirty = false;
2388        restored = true;
2389      } else {
2390        gPlacements.set(aArea, []);
2391      }
2392
2393      if (!restored && gSavedState && aArea in gSavedState.placements) {
2394        log.debug("Restoring " + aArea + " from saved state");
2395        let placements = gSavedState.placements[aArea];
2396        for (let id of placements) {
2397          this.addWidgetToArea(id, aArea);
2398        }
2399        gDirty = false;
2400        restored = true;
2401      }
2402
2403      if (!restored) {
2404        log.debug("Restoring " + aArea + " from default state");
2405        let defaults = gAreas.get(aArea).get("defaultPlacements");
2406        if (defaults) {
2407          for (let id of defaults) {
2408            this.addWidgetToArea(id, aArea, null, true);
2409          }
2410        }
2411        gDirty = false;
2412      }
2413
2414      // Finally, add widgets to the area that were added before the it was able
2415      // to be restored. This can occur when add-ons register widgets for a
2416      // lazily-restored area before it's been restored.
2417      if (gFuturePlacements.has(aArea)) {
2418        for (let id of gFuturePlacements.get(aArea)) {
2419          this.addWidgetToArea(id, aArea);
2420        }
2421        gFuturePlacements.delete(aArea);
2422      }
2423
2424      log.debug(
2425        "Placements for " +
2426          aArea +
2427          ":\n\t" +
2428          gPlacements.get(aArea).join("\n\t")
2429      );
2430
2431      gRestoring = false;
2432    } finally {
2433      this.endBatchUpdate();
2434    }
2435  },
2436
2437  saveState() {
2438    if (gInBatchStack || !gDirty) {
2439      return;
2440    }
2441    // Clone because we want to modify this map:
2442    let state = {
2443      placements: new Map(gPlacements),
2444      seen: gSeenWidgets,
2445      dirtyAreaCache: gDirtyAreaCache,
2446      currentVersion: kVersion,
2447      newElementCount: gNewElementCount,
2448    };
2449
2450    // Merge in previously saved areas if not present in gPlacements.
2451    // This way, state is still persisted for e.g. temporarily disabled
2452    // add-ons - see bug 989338.
2453    if (gSavedState && gSavedState.placements) {
2454      for (let area of Object.keys(gSavedState.placements)) {
2455        if (!state.placements.has(area)) {
2456          let placements = gSavedState.placements[area];
2457          state.placements.set(area, placements);
2458        }
2459      }
2460    }
2461
2462    log.debug("Saving state.");
2463    let serialized = JSON.stringify(state, this.serializerHelper);
2464    log.debug("State saved as: " + serialized);
2465    Services.prefs.setCharPref(kPrefCustomizationState, serialized);
2466    gDirty = false;
2467  },
2468
2469  serializerHelper(aKey, aValue) {
2470    if (typeof aValue == "object" && aValue.constructor.name == "Map") {
2471      let result = {};
2472      for (let [mapKey, mapValue] of aValue) {
2473        result[mapKey] = mapValue;
2474      }
2475      return result;
2476    }
2477
2478    if (typeof aValue == "object" && aValue.constructor.name == "Set") {
2479      return [...aValue];
2480    }
2481
2482    return aValue;
2483  },
2484
2485  beginBatchUpdate() {
2486    gInBatchStack++;
2487  },
2488
2489  endBatchUpdate(aForceDirty) {
2490    gInBatchStack--;
2491    if (aForceDirty === true) {
2492      gDirty = true;
2493    }
2494    if (gInBatchStack == 0) {
2495      this.saveState();
2496    } else if (gInBatchStack < 0) {
2497      throw new Error(
2498        "The batch editing stack should never reach a negative number."
2499      );
2500    }
2501  },
2502
2503  addListener(aListener) {
2504    gListeners.add(aListener);
2505  },
2506
2507  removeListener(aListener) {
2508    if (aListener == this) {
2509      return;
2510    }
2511
2512    gListeners.delete(aListener);
2513  },
2514
2515  notifyListeners(aEvent, ...aArgs) {
2516    if (gRestoring) {
2517      return;
2518    }
2519
2520    for (let listener of gListeners) {
2521      try {
2522        if (typeof listener[aEvent] == "function") {
2523          listener[aEvent].apply(listener, aArgs);
2524        }
2525      } catch (e) {
2526        log.error(e + " -- " + e.fileName + ":" + e.lineNumber);
2527      }
2528    }
2529  },
2530
2531  _dispatchToolboxEventToWindow(aEventType, aDetails, aWindow) {
2532    let evt = new aWindow.CustomEvent(aEventType, {
2533      bubbles: true,
2534      cancelable: true,
2535      detail: aDetails,
2536    });
2537    aWindow.gNavToolbox.dispatchEvent(evt);
2538  },
2539
2540  dispatchToolboxEvent(aEventType, aDetails = {}, aWindow = null) {
2541    if (aWindow) {
2542      this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
2543      return;
2544    }
2545    for (let [win] of gBuildWindows) {
2546      this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
2547    }
2548  },
2549
2550  createWidget(aProperties) {
2551    let widget = this.normalizeWidget(
2552      aProperties,
2553      CustomizableUI.SOURCE_EXTERNAL
2554    );
2555    // XXXunf This should probably throw.
2556    if (!widget) {
2557      log.error("unable to normalize widget");
2558      return undefined;
2559    }
2560
2561    gPalette.set(widget.id, widget);
2562
2563    // Clear our caches:
2564    gGroupWrapperCache.delete(widget.id);
2565    for (let [win] of gBuildWindows) {
2566      let cache = gSingleWrapperCache.get(win);
2567      if (cache) {
2568        cache.delete(widget.id);
2569      }
2570    }
2571
2572    this.notifyListeners("onWidgetCreated", widget.id);
2573
2574    if (widget.defaultArea) {
2575      let addToDefaultPlacements = false;
2576      let area = gAreas.get(widget.defaultArea);
2577      if (
2578        !CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
2579        widget.defaultArea != CustomizableUI.AREA_FIXED_OVERFLOW_PANEL
2580      ) {
2581        addToDefaultPlacements = true;
2582      }
2583
2584      if (addToDefaultPlacements) {
2585        if (area.has("defaultPlacements")) {
2586          area.get("defaultPlacements").push(widget.id);
2587        } else {
2588          area.set("defaultPlacements", [widget.id]);
2589        }
2590      }
2591    }
2592
2593    // Look through previously saved state to see if we're restoring a widget.
2594    let seenAreas = new Set();
2595    let widgetMightNeedAutoAdding = true;
2596    for (let [area] of gPlacements) {
2597      seenAreas.add(area);
2598      let areaIsRegistered = gAreas.has(area);
2599      let index = gPlacements.get(area).indexOf(widget.id);
2600      if (index != -1) {
2601        widgetMightNeedAutoAdding = false;
2602        if (areaIsRegistered) {
2603          widget.currentArea = area;
2604          widget.currentPosition = index;
2605        }
2606        break;
2607      }
2608    }
2609
2610    // Also look at saved state data directly in areas that haven't yet been
2611    // restored. Can't rely on this for restored areas, as they may have
2612    // changed.
2613    if (widgetMightNeedAutoAdding && gSavedState) {
2614      for (let area of Object.keys(gSavedState.placements)) {
2615        if (seenAreas.has(area)) {
2616          continue;
2617        }
2618
2619        let areaIsRegistered = gAreas.has(area);
2620        let index = gSavedState.placements[area].indexOf(widget.id);
2621        if (index != -1) {
2622          widgetMightNeedAutoAdding = false;
2623          if (areaIsRegistered) {
2624            widget.currentArea = area;
2625            widget.currentPosition = index;
2626          }
2627          break;
2628        }
2629      }
2630    }
2631
2632    // If we're restoring the widget to it's old placement, fire off the
2633    // onWidgetAdded event - our own handler will take care of adding it to
2634    // any build areas.
2635    this.beginBatchUpdate();
2636    try {
2637      if (widget.currentArea) {
2638        this.notifyListeners(
2639          "onWidgetAdded",
2640          widget.id,
2641          widget.currentArea,
2642          widget.currentPosition
2643        );
2644      } else if (widgetMightNeedAutoAdding) {
2645        let autoAdd = Services.prefs.getBoolPref(
2646          kPrefCustomizationAutoAdd,
2647          true
2648        );
2649
2650        // If the widget doesn't have an existing placement, and it hasn't been
2651        // seen before, then add it to its default area so it can be used.
2652        // If the widget is not removable, we *have* to add it to its default
2653        // area here.
2654        let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
2655        if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
2656          if (widget.defaultArea) {
2657            if (this.isAreaLazy(widget.defaultArea)) {
2658              gFuturePlacements.get(widget.defaultArea).add(widget.id);
2659            } else {
2660              this.addWidgetToArea(widget.id, widget.defaultArea);
2661            }
2662          }
2663        }
2664      }
2665    } finally {
2666      // Ensure we always have this widget in gSeenWidgets, and save
2667      // state in case this needs to be done here.
2668      gSeenWidgets.add(widget.id);
2669      this.endBatchUpdate(true);
2670    }
2671
2672    this.notifyListeners(
2673      "onWidgetAfterCreation",
2674      widget.id,
2675      widget.currentArea
2676    );
2677    return widget.id;
2678  },
2679
2680  createBuiltinWidget(aData) {
2681    // This should only ever be called on startup, before any windows are
2682    // opened - so we know there's no build areas to handle. Also, builtin
2683    // widgets are expected to be (mostly) static, so shouldn't affect the
2684    // current placement settings.
2685
2686    // This allows a widget to be both built-in by default but also able to be
2687    // destroyed and removed from the area based on criteria that may not be
2688    // available when the widget is created -- for example, because some other
2689    // feature in the browser supersedes the widget.
2690    let conditionalDestroyPromise = aData.conditionalDestroyPromise || null;
2691    delete aData.conditionalDestroyPromise;
2692
2693    let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
2694    if (!widget) {
2695      log.error("Error creating builtin widget: " + aData.id);
2696      return;
2697    }
2698
2699    log.debug("Creating built-in widget with id: " + widget.id);
2700    gPalette.set(widget.id, widget);
2701
2702    if (conditionalDestroyPromise) {
2703      conditionalDestroyPromise.then(
2704        shouldDestroy => {
2705          if (shouldDestroy) {
2706            this.destroyWidget(widget.id);
2707            this.removeWidgetFromArea(widget.id);
2708          }
2709        },
2710        err => {
2711          Cu.reportError(err);
2712        }
2713      );
2714    }
2715  },
2716
2717  // Returns true if the area will eventually lazily restore (but hasn't yet).
2718  isAreaLazy(aArea) {
2719    if (gPlacements.has(aArea)) {
2720      return false;
2721    }
2722    return gAreas.get(aArea).get("type") == CustomizableUI.TYPE_TOOLBAR;
2723  },
2724
2725  // XXXunf Log some warnings here, when the data provided isn't up to scratch.
2726  normalizeWidget(aData, aSource) {
2727    let widget = {
2728      implementation: aData,
2729      source: aSource || CustomizableUI.SOURCE_EXTERNAL,
2730      instances: new Map(),
2731      currentArea: null,
2732      localized: true,
2733      removable: true,
2734      overflows: true,
2735      defaultArea: null,
2736      shortcutId: null,
2737      tabSpecific: false,
2738      tooltiptext: null,
2739      showInPrivateBrowsing: true,
2740      _introducedInVersion: -1,
2741    };
2742
2743    if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
2744      log.error("Given an illegal id in normalizeWidget: " + aData.id);
2745      return null;
2746    }
2747
2748    delete widget.implementation.currentArea;
2749    widget.implementation.__defineGetter__(
2750      "currentArea",
2751      () => widget.currentArea
2752    );
2753
2754    const kReqStringProps = ["id"];
2755    for (let prop of kReqStringProps) {
2756      if (typeof aData[prop] != "string") {
2757        log.error(
2758          "Missing required property '" +
2759            prop +
2760            "' in normalizeWidget: " +
2761            aData.id
2762        );
2763        return null;
2764      }
2765      widget[prop] = aData[prop];
2766    }
2767
2768    const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
2769    for (let prop of kOptStringProps) {
2770      if (typeof aData[prop] == "string") {
2771        widget[prop] = aData[prop];
2772      }
2773    }
2774
2775    const kOptBoolProps = [
2776      "removable",
2777      "showInPrivateBrowsing",
2778      "overflows",
2779      "tabSpecific",
2780      "localized",
2781    ];
2782    for (let prop of kOptBoolProps) {
2783      if (typeof aData[prop] == "boolean") {
2784        widget[prop] = aData[prop];
2785      }
2786    }
2787
2788    // When we normalize builtin widgets, areas have not yet been registered:
2789    if (
2790      aData.defaultArea &&
2791      (aSource == CustomizableUI.SOURCE_BUILTIN ||
2792        gAreas.has(aData.defaultArea))
2793    ) {
2794      widget.defaultArea = aData.defaultArea;
2795    } else if (!widget.removable) {
2796      log.error(
2797        "Widget '" +
2798          widget.id +
2799          "' is not removable but does not specify " +
2800          "a valid defaultArea. That's not possible; it must specify a " +
2801          "valid defaultArea as well."
2802      );
2803      return null;
2804    }
2805
2806    if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
2807      widget.type = aData.type;
2808    } else {
2809      widget.type = "button";
2810    }
2811
2812    widget.disabled = aData.disabled === true;
2813
2814    if (aSource == CustomizableUI.SOURCE_BUILTIN) {
2815      widget._introducedInVersion = aData.introducedInVersion || 0;
2816    }
2817
2818    this.wrapWidgetEventHandler("onBeforeCreated", widget);
2819    this.wrapWidgetEventHandler("onClick", widget);
2820    this.wrapWidgetEventHandler("onCreated", widget);
2821    this.wrapWidgetEventHandler("onDestroyed", widget);
2822
2823    if (typeof aData.onBeforeCommand == "function") {
2824      widget.onBeforeCommand = aData.onBeforeCommand;
2825    }
2826
2827    if (widget.type == "button") {
2828      widget.onCommand =
2829        typeof aData.onCommand == "function" ? aData.onCommand : null;
2830    } else if (widget.type == "view") {
2831      if (typeof aData.viewId != "string") {
2832        log.error(
2833          "Expected a string for widget " +
2834            widget.id +
2835            " viewId, but got " +
2836            aData.viewId
2837        );
2838        return null;
2839      }
2840      widget.viewId = aData.viewId;
2841
2842      this.wrapWidgetEventHandler("onViewShowing", widget);
2843      this.wrapWidgetEventHandler("onViewHiding", widget);
2844    } else if (widget.type == "custom") {
2845      this.wrapWidgetEventHandler("onBuild", widget);
2846    }
2847
2848    if (gPalette.has(widget.id)) {
2849      return null;
2850    }
2851
2852    return widget;
2853  },
2854
2855  wrapWidgetEventHandler(aEventName, aWidget) {
2856    if (typeof aWidget.implementation[aEventName] != "function") {
2857      aWidget[aEventName] = null;
2858      return;
2859    }
2860    aWidget[aEventName] = function(...aArgs) {
2861      // Wrap inside a try...catch to properly log errors, until bug 862627 is
2862      // fixed, which in turn might help bug 503244.
2863      try {
2864        // Don't copy the function to the normalized widget object, instead
2865        // keep it on the original object provided to the API so that
2866        // additional methods can be implemented and used by the event
2867        // handlers.
2868        return aWidget.implementation[aEventName].apply(
2869          aWidget.implementation,
2870          aArgs
2871        );
2872      } catch (e) {
2873        Cu.reportError(e);
2874        return undefined;
2875      }
2876    };
2877  },
2878
2879  destroyWidget(aWidgetId) {
2880    let widget = gPalette.get(aWidgetId);
2881    if (!widget) {
2882      gGroupWrapperCache.delete(aWidgetId);
2883      for (let [window] of gBuildWindows) {
2884        let windowCache = gSingleWrapperCache.get(window);
2885        if (windowCache) {
2886          windowCache.delete(aWidgetId);
2887        }
2888      }
2889      return;
2890    }
2891
2892    // Remove it from the default placements of an area if it was added there:
2893    if (widget.defaultArea) {
2894      let area = gAreas.get(widget.defaultArea);
2895      if (area) {
2896        let defaultPlacements = area.get("defaultPlacements");
2897        // We can assume this is present because if a widget has a defaultArea,
2898        // we automatically create a defaultPlacements array for that area.
2899        let widgetIndex = defaultPlacements.indexOf(aWidgetId);
2900        if (widgetIndex != -1) {
2901          defaultPlacements.splice(widgetIndex, 1);
2902        }
2903      }
2904    }
2905
2906    // This will not remove the widget from gPlacements - we want to keep the
2907    // setting so the widget gets put back in it's old position if/when it
2908    // returns.
2909    for (let [window] of gBuildWindows) {
2910      let windowCache = gSingleWrapperCache.get(window);
2911      if (windowCache) {
2912        windowCache.delete(aWidgetId);
2913      }
2914      let widgetNode =
2915        window.document.getElementById(aWidgetId) ||
2916        window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
2917      if (widgetNode) {
2918        let container = widgetNode.parentNode;
2919        this.notifyListeners(
2920          "onWidgetBeforeDOMChange",
2921          widgetNode,
2922          null,
2923          container,
2924          true
2925        );
2926        widgetNode.remove();
2927        this.notifyListeners(
2928          "onWidgetAfterDOMChange",
2929          widgetNode,
2930          null,
2931          container,
2932          true
2933        );
2934      }
2935      if (widget.type == "view") {
2936        let viewNode = window.document.getElementById(widget.viewId);
2937        if (viewNode) {
2938          for (let eventName of kSubviewEvents) {
2939            let handler = "on" + eventName;
2940            if (typeof widget[handler] == "function") {
2941              viewNode.removeEventListener(eventName, widget[handler]);
2942            }
2943          }
2944        }
2945      }
2946      if (widgetNode && widget.onDestroyed) {
2947        widget.onDestroyed(window.document);
2948      }
2949    }
2950
2951    gPalette.delete(aWidgetId);
2952    gGroupWrapperCache.delete(aWidgetId);
2953
2954    this.notifyListeners("onWidgetDestroyed", aWidgetId);
2955  },
2956
2957  getCustomizeTargetForArea(aArea, aWindow) {
2958    let buildAreaNodes = gBuildAreas.get(aArea);
2959    if (!buildAreaNodes) {
2960      return null;
2961    }
2962
2963    for (let node of buildAreaNodes) {
2964      if (node.ownerGlobal == aWindow) {
2965        return this.getCustomizationTarget(node) || node;
2966      }
2967    }
2968
2969    return null;
2970  },
2971
2972  reset() {
2973    gResetting = true;
2974    this._resetUIState();
2975
2976    // Rebuild each registered area (across windows) to reflect the state that
2977    // was reset above.
2978    this._rebuildRegisteredAreas();
2979
2980    for (let [widgetId, widget] of gPalette) {
2981      if (widget.source == CustomizableUI.SOURCE_EXTERNAL) {
2982        gSeenWidgets.add(widgetId);
2983      }
2984    }
2985    if (gSeenWidgets.size || gNewElementCount) {
2986      gDirty = true;
2987      this.saveState();
2988    }
2989
2990    gResetting = false;
2991  },
2992
2993  _resetUIState() {
2994    try {
2995      // kPrefDrawInTitlebar may not be defined on Linux/Gtk+ which throws an exception
2996      // and leads to whole test failure. Let's set a fallback default value to avoid that,
2997      // both titlebar states are tested anyway and it's not important which state is tested first.
2998      gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(
2999        kPrefDrawInTitlebar,
3000        false
3001      );
3002      gUIStateBeforeReset.extraDragSpace = Services.prefs.getBoolPref(
3003        kPrefExtraDragSpace
3004      );
3005      gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(
3006        kPrefCustomizationState
3007      );
3008      gUIStateBeforeReset.uiDensity = Services.prefs.getIntPref(kPrefUIDensity);
3009      gUIStateBeforeReset.autoTouchMode = Services.prefs.getBoolPref(
3010        kPrefAutoTouchMode
3011      );
3012      gUIStateBeforeReset.currentTheme = gSelectedTheme;
3013      gUIStateBeforeReset.autoHideDownloadsButton = Services.prefs.getBoolPref(
3014        kPrefAutoHideDownloadsButton
3015      );
3016      gUIStateBeforeReset.newElementCount = gNewElementCount;
3017    } catch (e) {}
3018
3019    Services.prefs.clearUserPref(kPrefCustomizationState);
3020    Services.prefs.clearUserPref(kPrefDrawInTitlebar);
3021    Services.prefs.clearUserPref(kPrefExtraDragSpace);
3022    Services.prefs.clearUserPref(kPrefUIDensity);
3023    Services.prefs.clearUserPref(kPrefAutoTouchMode);
3024    Services.prefs.clearUserPref(kPrefAutoHideDownloadsButton);
3025    gDefaultTheme.enable();
3026    gNewElementCount = 0;
3027    log.debug("State reset");
3028
3029    // Reset placements to make restoring default placements possible.
3030    gPlacements = new Map();
3031    gDirtyAreaCache = new Set();
3032    gSeenWidgets = new Set();
3033    // Clear the saved state to ensure that defaults will be used.
3034    gSavedState = null;
3035    // Restore the state for each area to its defaults
3036    for (let [areaId] of gAreas) {
3037      this.restoreStateForArea(areaId);
3038    }
3039  },
3040
3041  _rebuildRegisteredAreas() {
3042    for (let [areaId, areaNodes] of gBuildAreas) {
3043      let placements = gPlacements.get(areaId);
3044      let isFirstChangedToolbar = true;
3045      for (let areaNode of areaNodes) {
3046        this.buildArea(areaId, placements, areaNode);
3047
3048        let area = gAreas.get(areaId);
3049        if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
3050          let defaultCollapsed = area.get("defaultCollapsed");
3051          let win = areaNode.ownerGlobal;
3052          if (defaultCollapsed !== null) {
3053            win.setToolbarVisibility(
3054              areaNode,
3055              !defaultCollapsed,
3056              isFirstChangedToolbar
3057            );
3058          }
3059        }
3060        isFirstChangedToolbar = false;
3061      }
3062    }
3063  },
3064
3065  /**
3066   * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
3067   */
3068  undoReset() {
3069    if (
3070      gUIStateBeforeReset.uiCustomizationState == null ||
3071      gUIStateBeforeReset.drawInTitlebar == null
3072    ) {
3073      return;
3074    }
3075    gUndoResetting = true;
3076
3077    const {
3078      uiCustomizationState,
3079      drawInTitlebar,
3080      currentTheme,
3081      uiDensity,
3082      autoTouchMode,
3083      autoHideDownloadsButton,
3084      extraDragSpace,
3085    } = gUIStateBeforeReset;
3086    gNewElementCount = gUIStateBeforeReset.newElementCount;
3087
3088    // Need to clear the previous state before setting the prefs
3089    // because pref observers may check if there is a previous UI state.
3090    this._clearPreviousUIState();
3091
3092    Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
3093    Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
3094    Services.prefs.setBoolPref(kPrefExtraDragSpace, extraDragSpace);
3095    Services.prefs.setIntPref(kPrefUIDensity, uiDensity);
3096    Services.prefs.setBoolPref(kPrefAutoTouchMode, autoTouchMode);
3097    Services.prefs.setBoolPref(
3098      kPrefAutoHideDownloadsButton,
3099      autoHideDownloadsButton
3100    );
3101    currentTheme.enable();
3102    this.loadSavedState();
3103    // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
3104    // and we don't need to do anything else here:
3105    if (gSavedState) {
3106      for (let areaId of Object.keys(gSavedState.placements)) {
3107        let placements = gSavedState.placements[areaId];
3108        gPlacements.set(areaId, placements);
3109      }
3110      this._rebuildRegisteredAreas();
3111    }
3112
3113    gUndoResetting = false;
3114  },
3115
3116  _clearPreviousUIState() {
3117    Object.getOwnPropertyNames(gUIStateBeforeReset).forEach(prop => {
3118      gUIStateBeforeReset[prop] = null;
3119    });
3120  },
3121
3122  /**
3123   * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
3124   * @return {Boolean} whether the widget is removable
3125   */
3126  isWidgetRemovable(aWidget) {
3127    let widgetId;
3128    let widgetNode;
3129    if (typeof aWidget == "string") {
3130      widgetId = aWidget;
3131    } else {
3132      // Skipped items could just not have ids.
3133      if (!aWidget.id && aWidget.getAttribute("skipintoolbarset") == "true") {
3134        return false;
3135      }
3136      if (
3137        !aWidget.id &&
3138        !["toolbarspring", "toolbarspacer", "toolbarseparator"].includes(
3139          aWidget.nodeName
3140        )
3141      ) {
3142        throw new Error(
3143          "No nodes without ids that aren't special widgets should ever come into contact with CUI"
3144        );
3145      }
3146      // Use "spring" / "spacer" / "separator" for special widgets without ids
3147      widgetId =
3148        aWidget.id || aWidget.nodeName.substring(7 /* "toolbar".length */);
3149      widgetNode = aWidget;
3150    }
3151    let provider = this.getWidgetProvider(widgetId);
3152
3153    if (provider == CustomizableUI.PROVIDER_API) {
3154      return gPalette.get(widgetId).removable;
3155    }
3156
3157    if (provider == CustomizableUI.PROVIDER_XUL) {
3158      if (gBuildWindows.size == 0) {
3159        // We don't have any build windows to look at, so just assume for now
3160        // that its removable.
3161        return true;
3162      }
3163
3164      if (!widgetNode) {
3165        // Pick any of the build windows to look at.
3166        let [window] = [...gBuildWindows][0];
3167        [, widgetNode] = this.getWidgetNode(widgetId, window);
3168      }
3169      // If we don't have a node, we assume it's removable. This can happen because
3170      // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen
3171      // for API-provided widgets which have been destroyed.
3172      if (!widgetNode) {
3173        return true;
3174      }
3175      return widgetNode.getAttribute("removable") == "true";
3176    }
3177
3178    // Otherwise this is either a special widget, which is always removable, or
3179    // an API widget which has already been removed from gPalette. Returning true
3180    // here allows us to then remove its ID from any placements where it might
3181    // still occur.
3182    return true;
3183  },
3184
3185  canWidgetMoveToArea(aWidgetId, aArea) {
3186    // Special widgets can't move to the menu panel.
3187    if (
3188      this.isSpecialWidget(aWidgetId) &&
3189      gAreas.has(aArea) &&
3190      gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL
3191    ) {
3192      return false;
3193    }
3194    let placement = this.getPlacementOfWidget(aWidgetId);
3195    // Items in the palette can move, and items can move within their area:
3196    if (!placement || placement.area == aArea) {
3197      return true;
3198    }
3199    // For everything else, just return whether the widget is removable.
3200    return this.isWidgetRemovable(aWidgetId);
3201  },
3202
3203  ensureWidgetPlacedInWindow(aWidgetId, aWindow) {
3204    let placement = this.getPlacementOfWidget(aWidgetId);
3205    if (!placement) {
3206      return false;
3207    }
3208    let areaNodes = gBuildAreas.get(placement.area);
3209    if (!areaNodes) {
3210      return false;
3211    }
3212    let container = [...areaNodes].filter(n => n.ownerGlobal == aWindow);
3213    if (!container.length) {
3214      return false;
3215    }
3216    let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
3217    if (existingNode) {
3218      return true;
3219    }
3220
3221    this.insertNodeInWindow(aWidgetId, container[0], true);
3222    return true;
3223  },
3224
3225  _getCurrentWidgetsInContainer(container) {
3226    // Get a list of all the widget IDs in this container, including any that
3227    // are overflown.
3228    let currentWidgets = new Set();
3229    function addUnskippedChildren(parent) {
3230      for (let node of parent.children) {
3231        let realNode =
3232          node.localName == "toolbarpaletteitem"
3233            ? node.firstElementChild
3234            : node;
3235        if (realNode.getAttribute("skipintoolbarset") != "true") {
3236          currentWidgets.add(realNode.id);
3237        }
3238      }
3239    }
3240    addUnskippedChildren(this.getCustomizationTarget(container));
3241    if (container.getAttribute("overflowing") == "true") {
3242      let overflowTarget = container.getAttribute("overflowtarget");
3243      addUnskippedChildren(
3244        container.ownerDocument.getElementById(overflowTarget)
3245      );
3246    }
3247    // Then get the sorted list of placements, and filter based on the nodes
3248    // that are present. This avoids including items that don't exist (e.g. ids
3249    // of add-on items that the user has uninstalled).
3250    let orderedPlacements = CustomizableUI.getWidgetIdsInArea(container.id);
3251    return orderedPlacements.filter(w => currentWidgets.has(w));
3252  },
3253
3254  get inDefaultState() {
3255    for (let [areaId, props] of gAreas) {
3256      let defaultPlacements = props.get("defaultPlacements");
3257      let currentPlacements = gPlacements.get(areaId);
3258      // We're excluding all of the placement IDs for items that do not exist,
3259      // and items that have removable="false",
3260      // because we don't want to consider them when determining if we're
3261      // in the default state. This way, if an add-on introduces a widget
3262      // and is then uninstalled, the leftover placement doesn't cause us to
3263      // automatically assume that the buttons are not in the default state.
3264      let buildAreaNodes = gBuildAreas.get(areaId);
3265      if (buildAreaNodes && buildAreaNodes.size) {
3266        let container = [...buildAreaNodes][0];
3267        let removableOrDefault = itemNodeOrItem => {
3268          let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
3269          let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
3270          let isInDefault = defaultPlacements.includes(item);
3271          return isRemovable || isInDefault;
3272        };
3273        // Toolbars need to deal with overflown widgets (if any) - so
3274        // specialcase them:
3275        if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
3276          currentPlacements = this._getCurrentWidgetsInContainer(
3277            container
3278          ).filter(removableOrDefault);
3279        } else {
3280          currentPlacements = currentPlacements.filter(item => {
3281            let itemNode = container.getElementsByAttribute("id", item)[0];
3282            return itemNode && removableOrDefault(itemNode || item);
3283          });
3284        }
3285
3286        if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
3287          let attribute =
3288            container.getAttribute("type") == "menubar"
3289              ? "autohide"
3290              : "collapsed";
3291          let collapsed = container.getAttribute(attribute) == "true";
3292          let defaultCollapsed = props.get("defaultCollapsed");
3293          if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
3294            log.debug(
3295              "Found " +
3296                areaId +
3297                " had non-default toolbar visibility" +
3298                "(expected " +
3299                defaultCollapsed +
3300                ", was " +
3301                collapsed +
3302                ")"
3303            );
3304            return false;
3305          }
3306        }
3307      }
3308      log.debug(
3309        "Checking default state for " +
3310          areaId +
3311          ":\n" +
3312          currentPlacements.join(",") +
3313          "\nvs.\n" +
3314          defaultPlacements.join(",")
3315      );
3316
3317      if (currentPlacements.length != defaultPlacements.length) {
3318        return false;
3319      }
3320
3321      for (let i = 0; i < currentPlacements.length; ++i) {
3322        if (
3323          currentPlacements[i] != defaultPlacements[i] &&
3324          !this.matchingSpecials(currentPlacements[i], defaultPlacements[i])
3325        ) {
3326          log.debug(
3327            "Found " +
3328              currentPlacements[i] +
3329              " in " +
3330              areaId +
3331              " where " +
3332              defaultPlacements[i] +
3333              " was expected!"
3334          );
3335          return false;
3336        }
3337      }
3338    }
3339
3340    if (Services.prefs.prefHasUserValue(kPrefUIDensity)) {
3341      log.debug(kPrefUIDensity + " pref is non-default");
3342      return false;
3343    }
3344
3345    if (Services.prefs.prefHasUserValue(kPrefAutoTouchMode)) {
3346      log.debug(kPrefAutoTouchMode + " pref is non-default");
3347      return false;
3348    }
3349
3350    if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
3351      log.debug(kPrefDrawInTitlebar + " pref is non-default");
3352      return false;
3353    }
3354
3355    if (Services.prefs.prefHasUserValue(kPrefExtraDragSpace)) {
3356      log.debug(kPrefExtraDragSpace + " pref is non-default");
3357      return false;
3358    }
3359
3360    // This should just be `gDefaultTheme.isActive`, but bugs...
3361    if (gDefaultTheme && gDefaultTheme.id != gSelectedTheme.id) {
3362      log.debug(gSelectedTheme.id + " theme is non-default");
3363      return false;
3364    }
3365
3366    return true;
3367  },
3368
3369  setToolbarVisibility(aToolbarId, aIsVisible) {
3370    // We only persist the attribute the first time.
3371    let isFirstChangedToolbar = true;
3372    for (let window of CustomizableUI.windows) {
3373      let toolbar = window.document.getElementById(aToolbarId);
3374      if (toolbar) {
3375        window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
3376        isFirstChangedToolbar = false;
3377      }
3378    }
3379  },
3380};
3381Object.freeze(CustomizableUIInternal);
3382
3383var CustomizableUI = {
3384  /**
3385   * Constant reference to the ID of the navigation toolbar.
3386   */
3387  AREA_NAVBAR: "nav-bar",
3388  /**
3389   * Constant reference to the ID of the menubar's toolbar.
3390   */
3391  AREA_MENUBAR: "toolbar-menubar",
3392  /**
3393   * Constant reference to the ID of the tabstrip toolbar.
3394   */
3395  AREA_TABSTRIP: "TabsToolbar",
3396  /**
3397   * Constant reference to the ID of the bookmarks toolbar.
3398   */
3399  AREA_BOOKMARKS: "PersonalToolbar",
3400  /**
3401   * Constant reference to the ID of the non-dymanic (fixed) list in the overflow panel.
3402   */
3403  AREA_FIXED_OVERFLOW_PANEL: "widget-overflow-fixed-list",
3404
3405  /**
3406   * Constant indicating the area is a menu panel.
3407   */
3408  TYPE_MENU_PANEL: "menu-panel",
3409  /**
3410   * Constant indicating the area is a toolbar.
3411   */
3412  TYPE_TOOLBAR: "toolbar",
3413
3414  /**
3415   * Constant indicating a XUL-type provider.
3416   */
3417  PROVIDER_XUL: "xul",
3418  /**
3419   * Constant indicating an API-type provider.
3420   */
3421  PROVIDER_API: "api",
3422  /**
3423   * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
3424   */
3425  PROVIDER_SPECIAL: "special",
3426
3427  /**
3428   * Constant indicating the widget is built-in
3429   */
3430  SOURCE_BUILTIN: "builtin",
3431  /**
3432   * Constant indicating the widget is externally provided
3433   * (e.g. by add-ons or other items not part of the builtin widget set).
3434   */
3435  SOURCE_EXTERNAL: "external",
3436
3437  /**
3438   * Constant indicating the reason the event was fired was a window closing
3439   */
3440  REASON_WINDOW_CLOSED: "window-closed",
3441  /**
3442   * Constant indicating the reason the event was fired was an area being
3443   * unregistered separately from window closing mechanics.
3444   */
3445  REASON_AREA_UNREGISTERED: "area-unregistered",
3446
3447  /**
3448   * An iteratable property of windows managed by CustomizableUI.
3449   * Note that this can *only* be used as an iterator. ie:
3450   *     for (let window of CustomizableUI.windows) { ... }
3451   */
3452  windows: {
3453    *[Symbol.iterator]() {
3454      for (let [window] of gBuildWindows) {
3455        yield window;
3456      }
3457    },
3458  },
3459
3460  /**
3461   * Add a listener object that will get fired for various events regarding
3462   * customization.
3463   *
3464   * @param aListener the listener object to add
3465   *
3466   * Not all event handler methods need to be defined.
3467   * CustomizableUI will catch exceptions. Events are dispatched
3468   * synchronously on the UI thread, so if you can delay any/some of your
3469   * processing, that is advisable. The following event handlers are supported:
3470   *   - onWidgetAdded(aWidgetId, aArea, aPosition)
3471   *     Fired when a widget is added to an area. aWidgetId is the widget that
3472   *     was added, aArea the area it was added to, and aPosition the position
3473   *     in which it was added.
3474   *   - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition)
3475   *     Fired when a widget is moved within its area. aWidgetId is the widget
3476   *     that was moved, aArea the area it was moved in, aOldPosition its old
3477   *     position, and aNewPosition its new position.
3478   *   - onWidgetRemoved(aWidgetId, aArea)
3479   *     Fired when a widget is removed from its area. aWidgetId is the widget
3480   *     that was removed, aArea the area it was removed from.
3481   *
3482   *   - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval)
3483   *     Fired *before* a widget's DOM node is acted upon by CustomizableUI
3484   *     (to add, move or remove it). aNode is the DOM node changed, aNextNode
3485   *     the DOM node (if any) before which a widget will be inserted,
3486   *     aContainer the *actual* DOM container (could be an overflow panel in
3487   *     case of an overflowable toolbar), and aWasRemoval is true iff the
3488   *     action about to happen is the removal of the DOM node.
3489   *   - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval)
3490   *     Like onWidgetBeforeDOMChange, but fired after the change to the DOM
3491   *     node of the widget.
3492   *
3493   *   - onWidgetReset(aNode, aContainer)
3494   *     Fired after a reset to default placements moves a widget's node to a
3495   *     different location. aNode is the widget's node, aContainer is the
3496   *     area it was moved into (NB: it might already have been there and been
3497   *     moved to a different position!)
3498   *   - onWidgetUndoMove(aNode, aContainer)
3499   *     Fired after undoing a reset to default placements moves a widget's
3500   *     node to a different location. aNode is the widget's node, aContainer
3501   *     is the area it was moved into (NB: it might already have been there
3502   *     and been moved to a different position!)
3503   *   - onAreaReset(aArea, aContainer)
3504   *     Fired after a reset to default placements is complete on an area's
3505   *     DOM node. Note that this is fired for each DOM node. aArea is the area
3506   *     that was reset, aContainer the DOM node that was reset.
3507   *
3508   *   - onWidgetCreated(aWidgetId)
3509   *     Fired when a widget with id aWidgetId has been created, but before it
3510   *     is added to any placements or any DOM nodes have been constructed.
3511   *     Only fired for API-based widgets.
3512   *   - onWidgetAfterCreation(aWidgetId, aArea)
3513   *     Fired after a widget with id aWidgetId has been created, and has been
3514   *     added to either its default area or the area in which it was placed
3515   *     previously. If the widget has no default area and/or it has never
3516   *     been placed anywhere, aArea may be null. Only fired for API-based
3517   *     widgets.
3518   *   - onWidgetDestroyed(aWidgetId)
3519   *     Fired when widgets are destroyed. aWidgetId is the widget that is
3520   *     being destroyed. Only fired for API-based widgets.
3521   *   - onWidgetInstanceRemoved(aWidgetId, aDocument)
3522   *     Fired when a window is unloaded and a widget's instance is destroyed
3523   *     because of this. Only fired for API-based widgets.
3524   *
3525   *   - onWidgetDrag(aWidgetId, aArea)
3526   *     Fired both when and after customize mode drag handling system tries
3527   *     to determine the width and height of widget aWidgetId when dragged to a
3528   *     different area. aArea will be the area the item is dragged to, or
3529   *     undefined after the measurements have been done and the node has been
3530   *     moved back to its 'regular' area.
3531   *
3532   *   - onCustomizeStart(aWindow)
3533   *     Fired when opening customize mode in aWindow.
3534   *   - onCustomizeEnd(aWindow)
3535   *     Fired when exiting customize mode in aWindow.
3536   *
3537   *   - onWidgetOverflow(aNode, aContainer)
3538   *     Fired when a widget's DOM node is overflowing its container, a toolbar,
3539   *     and will be displayed in the overflow panel.
3540   *   - onWidgetUnderflow(aNode, aContainer)
3541   *     Fired when a widget's DOM node is *not* overflowing its container, a
3542   *     toolbar, anymore.
3543   *   - onWindowOpened(aWindow)
3544   *     Fired when a window has been opened that is managed by CustomizableUI,
3545   *     once all of the prerequisite setup has been done.
3546   *   - onWindowClosed(aWindow)
3547   *     Fired when a window that has been managed by CustomizableUI has been
3548   *     closed.
3549   *   - onAreaNodeRegistered(aArea, aContainer)
3550   *     Fired after an area node is first built when it is registered. This
3551   *     is often when the window has opened, but in the case of add-ons,
3552   *     could fire when the node has just been registered with CustomizableUI
3553   *     after an add-on update or disable/enable sequence.
3554   *   - onAreaNodeUnregistered(aArea, aContainer, aReason)
3555   *     Fired when an area node is explicitly unregistered by an API caller,
3556   *     or by a window closing. The aReason parameter indicates which of
3557   *     these is the case.
3558   */
3559  addListener(aListener) {
3560    CustomizableUIInternal.addListener(aListener);
3561  },
3562  /**
3563   * Remove a listener added with addListener
3564   * @param aListener the listener object to remove
3565   */
3566  removeListener(aListener) {
3567    CustomizableUIInternal.removeListener(aListener);
3568  },
3569
3570  /**
3571   * Register a customizable area with CustomizableUI.
3572   * @param aName   the name of the area to register. Can only contain
3573   *                alphanumeric characters, dashes (-) and underscores (_).
3574   * @param aProps  the properties of the area. The following properties are
3575   *                recognized:
3576   *                - type:   the type of area. Either TYPE_TOOLBAR (default) or
3577   *                          TYPE_MENU_PANEL;
3578   *                - anchor: for a menu panel or overflowable toolbar, the
3579   *                          anchoring node for the panel.
3580   *                - overflowable: set to true if your toolbar is overflowable.
3581   *                                This requires an anchor, and only has an
3582   *                                effect for toolbars.
3583   *                - defaultPlacements: an array of widget IDs making up the
3584   *                                     default contents of the area
3585   *                - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
3586   *                                    if toolbar is collapsed by default (default to true).
3587   *                                    Specify null to ensure that reset/inDefaultArea don't care
3588   *                                    about a toolbar's collapsed state
3589   */
3590  registerArea(aName, aProperties) {
3591    CustomizableUIInternal.registerArea(aName, aProperties);
3592  },
3593  /**
3594   * Register a concrete node for a registered area. This method needs to be called
3595   * with any toolbar in the main browser window that has its "customizable" attribute
3596   * set to true.
3597   *
3598   * Note that ideally, you should register your toolbar using registerArea
3599   * before calling this. If you don't, the node will be saved for processing when
3600   * you call registerArea. Note that CustomizableUI won't restore state in the area,
3601   * allow the user to customize it in customize mode, or otherwise deal
3602   * with it, until the area has been registered.
3603   */
3604  registerToolbarNode(aToolbar) {
3605    CustomizableUIInternal.registerToolbarNode(aToolbar);
3606  },
3607  /**
3608   * Register the menu panel node. This method should not be called by anyone
3609   * apart from the built-in PanelUI.
3610   * @param aPanelContents the panel contents DOM node being registered.
3611   * @param aArea the area for which to register this node.
3612   */
3613  registerMenuPanel(aPanelContents, aArea) {
3614    CustomizableUIInternal.registerMenuPanel(aPanelContents, aArea);
3615  },
3616  /**
3617   * Unregister a customizable area. The inverse of registerArea.
3618   *
3619   * Unregistering an area will remove all the (removable) widgets in the
3620   * area, which will return to the panel, and destroy all other traces
3621   * of the area within CustomizableUI. Note that this means the *contents*
3622   * of the area's DOM nodes will be moved to the panel or removed, but
3623   * the area's DOM nodes *themselves* will stay.
3624   *
3625   * Furthermore, by default the placements of the area will be kept in the
3626   * saved state (!) and restored if you re-register the area at a later
3627   * point. This is useful for e.g. add-ons that get disabled and then
3628   * re-enabled (e.g. when they update).
3629   *
3630   * You can override this last behaviour (and destroy the placements
3631   * information in the saved state) by passing true for aDestroyPlacements.
3632   *
3633   * @param aName              the name of the area to unregister
3634   * @param aDestroyPlacements whether to destroy the placements information
3635   *                           for the area, too.
3636   */
3637  unregisterArea(aName, aDestroyPlacements) {
3638    CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
3639  },
3640  /**
3641   * Add a widget to an area.
3642   * If the area to which you try to add is not known to CustomizableUI,
3643   * this will throw.
3644   * If the area to which you try to add is the same as the area in which
3645   * the widget is currently placed, this will do the same as
3646   * moveWidgetWithinArea.
3647   * If the widget cannot be removed from its original location, this will
3648   * no-op.
3649   *
3650   * This will fire an onWidgetAdded notification,
3651   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
3652   * for each window CustomizableUI knows about.
3653   *
3654   * @param aWidgetId the ID of the widget to add
3655   * @param aArea     the ID of the area to add the widget to
3656   * @param aPosition the position at which to add the widget. If you do not
3657   *                  pass a position, the widget will be added to the end
3658   *                  of the area.
3659   */
3660  addWidgetToArea(aWidgetId, aArea, aPosition) {
3661    CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
3662  },
3663  /**
3664   * Remove a widget from its area. If the widget cannot be removed from its
3665   * area, or is not in any area, this will no-op. Otherwise, this will fire an
3666   * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
3667   * onWidgetAfterDOMChange notification for each window CustomizableUI knows
3668   * about.
3669   *
3670   * @param aWidgetId the ID of the widget to remove
3671   */
3672  removeWidgetFromArea(aWidgetId) {
3673    CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
3674  },
3675  /**
3676   * Move a widget within an area.
3677   * If the widget is not in any area, this will no-op.
3678   * If the widget is already at the indicated position, this will no-op.
3679   *
3680   * Otherwise, this will move the widget and fire an onWidgetMoved notification,
3681   * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
3682   * each window CustomizableUI knows about.
3683   *
3684   * @param aWidgetId the ID of the widget to move
3685   * @param aPosition the position to move the widget to.
3686   *                  Negative values or values greater than the number of
3687   *                  widgets will be interpreted to mean moving the widget to
3688   *                  respectively the first or last position.
3689   */
3690  moveWidgetWithinArea(aWidgetId, aPosition) {
3691    CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
3692  },
3693  /**
3694   * Ensure a XUL-based widget created in a window after areas were
3695   * initialized moves to its correct position.
3696   * Always prefer this over moving items in the DOM yourself.
3697   *
3698   * @param aWidgetId the ID of the widget that was just created
3699   * @param aWindow the window in which you want to ensure it was added.
3700   *
3701   * NB: why is this API per-window, you wonder? Because if you need this,
3702   * presumably you yourself need to create the widget in all the windows
3703   * and need to loop through them anyway.
3704   */
3705  ensureWidgetPlacedInWindow(aWidgetId, aWindow) {
3706    return CustomizableUIInternal.ensureWidgetPlacedInWindow(
3707      aWidgetId,
3708      aWindow
3709    );
3710  },
3711  /**
3712   * Start a batch update of items.
3713   * During a batch update, the customization state is not saved to the user's
3714   * preferences file, in order to reduce (possibly sync) IO.
3715   * Calls to begin/endBatchUpdate may be nested.
3716   *
3717   * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
3718   * for each call to beginBatchUpdate, even if there are exceptions in the
3719   * code in the batch update. Otherwise, for the duration of the
3720   * Firefox session, customization state is never saved. Typically, you
3721   * would do this using a try...finally block.
3722   */
3723  beginBatchUpdate() {
3724    CustomizableUIInternal.beginBatchUpdate();
3725  },
3726  /**
3727   * End a batch update. See the documentation for beginBatchUpdate above.
3728   *
3729   * State is not saved if we believe it is identical to the last known
3730   * saved state. State is only ever saved when all batch updates have
3731   * finished (ie there has been 1 endBatchUpdate call for each
3732   * beginBatchUpdate call). If any of the endBatchUpdate calls pass
3733   * aForceDirty=true, we will flush to the prefs file.
3734   *
3735   * @param aForceDirty force CustomizableUI to flush to the prefs file when
3736   *                    all batch updates have finished.
3737   */
3738  endBatchUpdate(aForceDirty) {
3739    CustomizableUIInternal.endBatchUpdate(aForceDirty);
3740  },
3741  /**
3742   * Create a widget.
3743   *
3744   * To create a widget, you should pass an object with its desired
3745   * properties. The following properties are supported:
3746   *
3747   * - id:            the ID of the widget (required).
3748   * - type:          a string indicating the type of widget. Possible types
3749   *                  are:
3750   *                  'button' - for simple button widgets (the default)
3751   *                  'view'   - for buttons that open a panel or subview,
3752   *                             depending on where they are placed.
3753   *                  'custom' - for fine-grained control over the creation
3754   *                             of the widget.
3755   * - viewId:        Only useful for views (and required there): the id of the
3756   *                  <panelview> that should be shown when clicking the widget.
3757   * - onBuild(aDoc): Only useful for custom widgets (and required there); a
3758   *                  function that will be invoked with the document in which
3759   *                  to build a widget. Should return the DOM node that has
3760   *                  been constructed.
3761   * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function
3762   *                  that will be invoked before the widget gets a DOM node
3763   *                  constructed, passing the document in which that will happen.
3764   *                  This is useful especially for 'view' type widgets that need
3765   *                  to construct their views on the fly (e.g. from bootstrapped
3766   *                  add-ons)
3767   * - onCreated(aNode): Attached to all widgets; a function that will be invoked
3768   *                  whenever the widget has a DOM node constructed, passing the
3769   *                  constructed node as an argument.
3770   * - onDestroyed(aDoc): Attached to all non-custom widgets; a function that
3771   *                  will be invoked after the widget has a DOM node destroyed,
3772   *                  passing the document from which it was removed. This is
3773   *                  useful especially for 'view' type widgets that need to
3774   *                  cleanup after views that were constructed on the fly.
3775   * - onBeforeCommand(aEvt): A function that will be invoked when the user
3776   *                          activates the button but before the command
3777   *                          is evaluated. Useful if code needs to run to
3778   *                          change the button's icon in preparation to the
3779   *                          pending command action. Called for both type=button
3780   *                          and type=view.
3781   * - onCommand(aEvt): Only useful for button widgets; a function that will be
3782   *                    invoked when the user activates the button.
3783   * - onClick(aEvt): Attached to all widgets; a function that will be invoked
3784   *                  when the user clicks the widget.
3785   * - onViewShowing(aEvt): Only useful for views; a function that will be
3786   *                  invoked when a user shows your view. If any event
3787   *                  handler calls aEvt.preventDefault(), the view will
3788   *                  not be shown.
3789   *
3790   *                  The event's `detail` property is an object with an
3791   *                  `addBlocker` method. Handlers which need to
3792   *                  perform asynchronous operations before the view is
3793   *                  shown may pass this method a Promise, which will
3794   *                  prevent the view from showing until it resolves.
3795   *                  Additionally, if the promise resolves to the exact
3796   *                  value `false`, the view will not be shown.
3797   * - onViewHiding(aEvt): Only useful for views; a function that will be
3798   *                  invoked when a user hides your view.
3799   * - tooltiptext:   string to use for the tooltip of the widget
3800   * - label:         string to use for the label of the widget
3801   * - localized:     If true, or undefined, attempt to retrieve the
3802   *                  widget's string properties from the customizable
3803   *                  widgets string bundle.
3804   * - removable:     whether the widget is removable (optional, default: true)
3805   *                  NB: if you specify false here, you must provide a
3806   *                  defaultArea, too.
3807   * - overflows:     whether widget can overflow when in an overflowable
3808   *                  toolbar (optional, default: true)
3809   * - defaultArea:   default area to add the widget to
3810   *                  (optional, default: none; required if non-removable)
3811   * - shortcutId:    id of an element that has a shortcut for this widget
3812   *                  (optional, default: null). This is only used to display
3813   *                  the shortcut as part of the tooltip for builtin widgets
3814   *                  (which have strings inside
3815   *                  customizableWidgets.properties). If you're in an add-on,
3816   *                  you should not set this property.
3817   * - showInPrivateBrowsing: whether to show the widget in private browsing
3818   *                          mode (optional, default: true)
3819   *
3820   * @param aProperties the specifications for the widget.
3821   * @return a wrapper around the created widget (see getWidget)
3822   */
3823  createWidget(aProperties) {
3824    return CustomizableUIInternal.wrapWidget(
3825      CustomizableUIInternal.createWidget(aProperties)
3826    );
3827  },
3828  /**
3829   * Destroy a widget
3830   *
3831   * If the widget is part of the default placements in an area, this will
3832   * remove it from there. It will also remove any DOM instances. However,
3833   * it will keep the widget in the placements for whatever area it was
3834   * in at the time. You can remove it from there yourself by calling
3835   * CustomizableUI.removeWidgetFromArea(aWidgetId).
3836   *
3837   * @param aWidgetId the ID of the widget to destroy
3838   */
3839  destroyWidget(aWidgetId) {
3840    CustomizableUIInternal.destroyWidget(aWidgetId);
3841  },
3842  /**
3843   * Get a wrapper object with information about the widget.
3844   * The object provides the following properties
3845   * (all read-only unless otherwise indicated):
3846   *
3847   * - id:            the widget's ID;
3848   * - type:          the type of widget (button, view, custom). For
3849   *                  XUL-provided widgets, this is always 'custom';
3850   * - provider:      the provider type of the widget, id est one of
3851   *                  PROVIDER_API or PROVIDER_XUL;
3852   * - forWindow(w):  a method to obtain a single window wrapper for a widget,
3853   *                  in the window w passed as the only argument;
3854   * - instances:     an array of all instances (single window wrappers)
3855   *                  of the widget. This array is NOT live;
3856   * - areaType:      the type of the widget's current area
3857   * - isGroup:       true; will be false for wrappers around single widget nodes;
3858   * - source:        for API-provided widgets, whether they are built-in to
3859   *                  Firefox or add-on-provided;
3860   * - disabled:      for API-provided widgets, whether the widget is currently
3861   *                  disabled. NB: this property is writable, and will toggle
3862   *                  all the widgets' nodes' disabled states;
3863   * - label:         for API-provied widgets, the label of the widget;
3864   * - tooltiptext:   for API-provided widgets, the tooltip of the widget;
3865   * - showInPrivateBrowsing: for API-provided widgets, whether the widget is
3866   *                          visible in private browsing;
3867   *
3868   * Single window wrappers obtained through forWindow(someWindow) or from the
3869   * instances array have the following properties
3870   * (all read-only unless otherwise indicated):
3871   *
3872   * - id:            the widget's ID;
3873   * - type:          the type of widget (button, view, custom). For
3874   *                  XUL-provided widgets, this is always 'custom';
3875   * - provider:      the provider type of the widget, id est one of
3876   *                  PROVIDER_API or PROVIDER_XUL;
3877   * - node:          reference to the corresponding DOM node;
3878   * - anchor:        the anchor on which to anchor panels opened from this
3879   *                  node. This will point to the overflow chevron on
3880   *                  overflowable toolbars if and only if your widget node
3881   *                  is overflowed, to the anchor for the panel menu
3882   *                  if your widget is inside the panel menu, and to the
3883   *                  node itself in all other cases;
3884   * - overflowed:    boolean indicating whether the node is currently in the
3885   *                  overflow panel of the toolbar;
3886   * - isGroup:       false; will be true for the group widget;
3887   * - label:         for API-provided widgets, convenience getter for the
3888   *                  label attribute of the DOM node;
3889   * - tooltiptext:   for API-provided widgets, convenience getter for the
3890   *                  tooltiptext attribute of the DOM node;
3891   * - disabled:      for API-provided widgets, convenience getter *and setter*
3892   *                  for the disabled state of this single widget. Note that
3893   *                  you may prefer to use the group wrapper's getter/setter
3894   *                  instead.
3895   *
3896   * @param aWidgetId the ID of the widget whose information you need
3897   * @return a wrapper around the widget as described above, or null if the
3898   *         widget is known not to exist (anymore). NB: non-null return
3899   *         is no guarantee the widget exists because we cannot know in
3900   *         advance if a XUL widget exists or not.
3901   */
3902  getWidget(aWidgetId) {
3903    return CustomizableUIInternal.wrapWidget(aWidgetId);
3904  },
3905  /**
3906   * Get an array of widget wrappers (see getWidget) for all the widgets
3907   * which are currently not in any area (so which are in the palette).
3908   *
3909   * @param aWindowPalette the palette (and by extension, the window) in which
3910   *                       CustomizableUI should look. This matters because of
3911   *                       course XUL-provided widgets could be available in
3912   *                       some windows but not others, and likewise
3913   *                       API-provided widgets might not exist in a private
3914   *                       window (because of the showInPrivateBrowsing
3915   *                       property).
3916   *
3917   * @return an array of widget wrappers (see getWidget)
3918   */
3919  getUnusedWidgets(aWindowPalette) {
3920    return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
3921      CustomizableUIInternal.wrapWidget,
3922      CustomizableUIInternal
3923    );
3924  },
3925  /**
3926   * Get an array of all the widget IDs placed in an area.
3927   * Modifying the array will not affect CustomizableUI.
3928   *
3929   * @param aArea the ID of the area whose placements you want to obtain.
3930   * @return an array containing the widget IDs that are in the area.
3931   *
3932   * NB: will throw if called too early (before placements have been fetched)
3933   *     or if the area is not currently known to CustomizableUI.
3934   */
3935  getWidgetIdsInArea(aArea) {
3936    if (!gAreas.has(aArea)) {
3937      throw new Error("Unknown customization area: " + aArea);
3938    }
3939    if (!gPlacements.has(aArea)) {
3940      throw new Error("Area not yet restored");
3941    }
3942
3943    // We need to clone this, as we don't want to let consumers muck with placements
3944    return [...gPlacements.get(aArea)];
3945  },
3946  /**
3947   * Get an array of widget wrappers for all the widgets in an area. This is
3948   * the same as calling getWidgetIdsInArea and .map() ing the result through
3949   * CustomizableUI.getWidget. Careful: this means that if there are IDs in there
3950   * which don't have corresponding DOM nodes, there might be nulls in this array,
3951   * or items for which wrapper.forWindow(win) will return null.
3952   *
3953   * @param aArea the ID of the area whose widgets you want to obtain.
3954   * @return an array of widget wrappers and/or null values for the widget IDs
3955   *         placed in an area.
3956   *
3957   * NB: will throw if called too early (before placements have been fetched)
3958   *     or if the area is not currently known to CustomizableUI.
3959   */
3960  getWidgetsInArea(aArea) {
3961    return this.getWidgetIdsInArea(aArea).map(
3962      CustomizableUIInternal.wrapWidget,
3963      CustomizableUIInternal
3964    );
3965  },
3966
3967  /**
3968   * Ensure the customizable widget that matches up with this view node
3969   * will get the right subview showing/shown/hiding/hidden events when
3970   * they fire.
3971   * @param aViewNode the view node to add listeners to if they haven't
3972   *                  been added already.
3973   */
3974  ensureSubviewListeners(aViewNode) {
3975    return CustomizableUIInternal.ensureSubviewListeners(aViewNode);
3976  },
3977  /**
3978   * Obtain an array of all the area IDs known to CustomizableUI.
3979   * This array is created for you, so is modifiable without CustomizableUI
3980   * being affected.
3981   */
3982  get areas() {
3983    return [...gAreas.keys()];
3984  },
3985  /**
3986   * Check what kind of area (toolbar or menu panel) an area is. This is
3987   * useful if you have a widget that needs to behave differently depending
3988   * on its location. Note that widget wrappers have a convenience getter
3989   * property (areaType) for this purpose.
3990   *
3991   * @param aArea the ID of the area whose type you want to know
3992   * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if
3993   *         the area is unknown.
3994   */
3995  getAreaType(aArea) {
3996    let area = gAreas.get(aArea);
3997    return area ? area.get("type") : null;
3998  },
3999  /**
4000   * Check if a toolbar is collapsed by default.
4001   *
4002   * @param aArea the ID of the area whose default-collapsed state you want to know.
4003   * @return `true` or `false` depending on the area, null if the area is unknown,
4004   *         or its collapsed state cannot normally be controlled by the user
4005   */
4006  isToolbarDefaultCollapsed(aArea) {
4007    let area = gAreas.get(aArea);
4008    return area ? area.get("defaultCollapsed") : null;
4009  },
4010  /**
4011   * Obtain the DOM node that is the customize target for an area in a
4012   * specific window.
4013   *
4014   * Areas can have a customization target that does not correspond to the
4015   * node itself. In particular, toolbars that have a customizationtarget
4016   * attribute set will have their customization target set to that node.
4017   * This means widgets will end up in the customization target, not in the
4018   * DOM node with the ID that corresponds to the area ID. This is useful
4019   * because it lets you have fixed content in a toolbar (e.g. the panel
4020   * menu item in the navbar) and have all the customizable widgets use
4021   * the customization target.
4022   *
4023   * Using this API yourself is discouraged; you should generally not need
4024   * to be asking for the DOM container node used for a particular area.
4025   * In particular, if you're wanting to check it in relation to a widget's
4026   * node, your DOM node might not be a direct child of the customize target
4027   * in a window if, for instance, the window is in customization mode, or if
4028   * this is an overflowable toolbar and the widget has been overflowed.
4029   *
4030   * @param aArea   the ID of the area whose customize target you want to have
4031   * @param aWindow the window where you want to fetch the DOM node.
4032   * @return the customize target DOM node for aArea in aWindow
4033   */
4034  getCustomizeTargetForArea(aArea, aWindow) {
4035    return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
4036  },
4037  /**
4038   * Reset the customization state back to its default.
4039   *
4040   * This is the nuclear option. You should never call this except if the user
4041   * explicitly requests it. Firefox does this when the user clicks the
4042   * "Restore Defaults" button in customize mode.
4043   */
4044  reset() {
4045    CustomizableUIInternal.reset();
4046  },
4047
4048  /**
4049   * Undo the previous reset, can only be called immediately after a reset.
4050   * @return a promise that will be resolved when the operation is complete.
4051   */
4052  undoReset() {
4053    CustomizableUIInternal.undoReset();
4054  },
4055
4056  /**
4057   * Remove a custom toolbar added in a previous version of Firefox or using
4058   * an add-on. NB: only works on the customizable toolbars generated by
4059   * the toolbox itself. Intended for use from CustomizeMode, not by
4060   * other consumers.
4061   * @param aToolbarId the ID of the toolbar to remove
4062   */
4063  removeExtraToolbar(aToolbarId) {
4064    CustomizableUIInternal.removeExtraToolbar(aToolbarId);
4065  },
4066
4067  /**
4068   * Can the last Restore Defaults operation be undone.
4069   *
4070   * @return A boolean stating whether an undo of the
4071   *         Restore Defaults can be performed.
4072   */
4073  get canUndoReset() {
4074    return (
4075      gUIStateBeforeReset.uiCustomizationState != null ||
4076      gUIStateBeforeReset.drawInTitlebar != null ||
4077      gUIStateBeforeReset.extraDragSpace != null ||
4078      gUIStateBeforeReset.currentTheme != null ||
4079      gUIStateBeforeReset.autoTouchMode != null ||
4080      gUIStateBeforeReset.uiDensity != null
4081    );
4082  },
4083
4084  /**
4085   * Get the placement of a widget. This is by far the best way to obtain
4086   * information about what the state of your widget is. The internals of
4087   * this call are cheap (no DOM necessary) and you will know where the user
4088   * has put your widget.
4089   *
4090   * @param aWidgetId the ID of the widget whose placement you want to know
4091   * @return
4092   *   {
4093   *     area: "somearea", // The ID of the area where the widget is placed
4094   *     position: 42 // the index in the placements array corresponding to
4095   *                  // your widget.
4096   *   }
4097   *
4098   *   OR
4099   *
4100   *   null // if the widget is not placed anywhere (ie in the palette)
4101   */
4102  getPlacementOfWidget(aWidgetId, aOnlyRegistered = true, aDeadAreas = false) {
4103    return CustomizableUIInternal.getPlacementOfWidget(
4104      aWidgetId,
4105      aOnlyRegistered,
4106      aDeadAreas
4107    );
4108  },
4109  /**
4110   * Check if a widget can be removed from the area it's in.
4111   *
4112   * Note that if you're wanting to move the widget somewhere, you should
4113   * generally be checking canWidgetMoveToArea, because that will return
4114   * true if the widget is already in the area where you want to move it (!).
4115   *
4116   * NB: oh, also, this method might lie if the widget in question is a
4117   *     XUL-provided widget and there are no windows open, because it
4118   *     can obviously not check anything in this case. It will return
4119   *     true. You will be able to move the widget elsewhere. However,
4120   *     once the user reopens a window, the widget will move back to its
4121   *     'proper' area automagically.
4122   *
4123   * @param aWidgetId a widget ID or DOM node to check
4124   * @return true if the widget can be removed from its area,
4125   *          false otherwise.
4126   */
4127  isWidgetRemovable(aWidgetId) {
4128    return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
4129  },
4130  /**
4131   * Check if a widget can be moved to a particular area. Like
4132   * isWidgetRemovable but better, because it'll return true if the widget
4133   * is already in the right area.
4134   *
4135   * @param aWidgetId the widget ID or DOM node you want to move somewhere
4136   * @param aArea     the area ID you want to move it to.
4137   * @return true if this is possible, false if it is not. The same caveats as
4138   *              for isWidgetRemovable apply, however, if no windows are open.
4139   */
4140  canWidgetMoveToArea(aWidgetId, aArea) {
4141    return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
4142  },
4143  /**
4144   * Whether we're in a default state. Note that non-removable non-default
4145   * widgets and non-existing widgets are not taken into account in determining
4146   * whether we're in the default state.
4147   *
4148   * NB: this is a property with a getter. The getter is NOT cheap, because
4149   * it does smart things with non-removable non-default items, non-existent
4150   * items, and so forth. Please don't call unless necessary.
4151   */
4152  get inDefaultState() {
4153    return CustomizableUIInternal.inDefaultState;
4154  },
4155
4156  /**
4157   * Set a toolbar's visibility state in all windows.
4158   * @param aToolbarId    the toolbar whose visibility should be adjusted
4159   * @param aIsVisible    whether the toolbar should be visible
4160   */
4161  setToolbarVisibility(aToolbarId, aIsVisible) {
4162    CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
4163  },
4164
4165  /**
4166   * Get a localized property off a (widget?) object.
4167   *
4168   * NB: this is unlikely to be useful unless you're in Firefox code, because
4169   *     this code uses the builtin widget stringbundle, and can't be told
4170   *     to use add-on-provided strings. It's mainly here as convenience for
4171   *     custom builtin widgets that build their own DOM but use the same
4172   *     stringbundle as the other builtin widgets.
4173   *
4174   * @param aWidget     the object whose property we should use to fetch a
4175   *                    localizable string;
4176   * @param aProp       the property on the object to use for the fetching;
4177   * @param aFormatArgs (optional) any extra arguments to use for a formatted
4178   *                    string;
4179   * @param aDef        (optional) the default to return if we don't find the
4180   *                    string in the stringbundle;
4181   *
4182   * @return the localized string, or aDef if the string isn't in the bundle.
4183   *         If no default is provided,
4184   *           if aProp exists on aWidget, we'll return that,
4185   *           otherwise we'll return the empty string
4186   *
4187   */
4188  getLocalizedProperty(aWidget, aProp, aFormatArgs, aDef) {
4189    return CustomizableUIInternal.getLocalizedProperty(
4190      aWidget,
4191      aProp,
4192      aFormatArgs,
4193      aDef
4194    );
4195  },
4196  /**
4197   * Utility function to detect, find and set a keyboard shortcut for a menuitem
4198   * or (toolbar)button.
4199   *
4200   * @param aShortcutNode the XUL node where the shortcut will be derived from;
4201   * @param aTargetNode   (optional) the XUL node on which the `shortcut`
4202   *                      attribute will be set. If NULL, the shortcut will be
4203   *                      set on aShortcutNode;
4204   */
4205  addShortcut(aShortcutNode, aTargetNode) {
4206    return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode);
4207  },
4208  /**
4209   * Given a node, walk up to the first panel in its ancestor chain, and
4210   * close it.
4211   *
4212   * @param aNode a node whose panel should be closed;
4213   */
4214  hidePanelForNode(aNode) {
4215    CustomizableUIInternal.hidePanelForNode(aNode);
4216  },
4217  /**
4218   * Check if a widget is a "special" widget: a spring, spacer or separator.
4219   *
4220   * @param aWidgetId the widget ID to check.
4221   * @return true if the widget is 'special', false otherwise.
4222   */
4223  isSpecialWidget(aWidgetId) {
4224    return CustomizableUIInternal.isSpecialWidget(aWidgetId);
4225  },
4226  /**
4227   * Add listeners to a panel that will close it. For use from the menu panel
4228   * and overflowable toolbar implementations, unlikely to be useful for
4229   * consumers.
4230   *
4231   * @param aPanel the panel to which listeners should be attached.
4232   */
4233  addPanelCloseListeners(aPanel) {
4234    CustomizableUIInternal.addPanelCloseListeners(aPanel);
4235  },
4236  /**
4237   * Remove close listeners that have been added to a panel with
4238   * addPanelCloseListeners. For use from the menu panel and overflowable
4239   * toolbar implementations, unlikely to be useful for consumers.
4240   *
4241   * @param aPanel the panel from which listeners should be removed.
4242   */
4243  removePanelCloseListeners(aPanel) {
4244    CustomizableUIInternal.removePanelCloseListeners(aPanel);
4245  },
4246  /**
4247   * Notify listeners a widget is about to be dragged to an area. For use from
4248   * Customize Mode only, do not use otherwise.
4249   *
4250   * @param aWidgetId the ID of the widget that is being dragged to an area.
4251   * @param aArea     the ID of the area to which the widget is being dragged.
4252   */
4253  onWidgetDrag(aWidgetId, aArea) {
4254    CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
4255  },
4256  /**
4257   * Notify listeners that a window is entering customize mode. For use from
4258   * Customize Mode only, do not use otherwise.
4259   * @param aWindow the window entering customize mode
4260   */
4261  notifyStartCustomizing(aWindow) {
4262    CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
4263  },
4264  /**
4265   * Notify listeners that a window is exiting customize mode. For use from
4266   * Customize Mode only, do not use otherwise.
4267   * @param aWindow the window exiting customize mode
4268   */
4269  notifyEndCustomizing(aWindow) {
4270    CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
4271  },
4272
4273  /**
4274   * Notify toolbox(es) of a particular event. If you don't pass aWindow,
4275   * all toolboxes will be notified. For use from Customize Mode only,
4276   * do not use otherwise.
4277   * @param aEvent the name of the event to send.
4278   * @param aDetails optional, the details of the event.
4279   * @param aWindow optional, the window in which to send the event.
4280   */
4281  dispatchToolboxEvent(aEvent, aDetails = {}, aWindow = null) {
4282    CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
4283  },
4284
4285  /**
4286   * Check whether an area is overflowable.
4287   *
4288   * @param aAreaId the ID of an area to check for overflowable-ness
4289   * @return true if the area is overflowable, false otherwise.
4290   */
4291  isAreaOverflowable(aAreaId) {
4292    let area = gAreas.get(aAreaId);
4293    return area
4294      ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
4295      : false;
4296  },
4297  /**
4298   * Obtain a string indicating the place of an element. This is intended
4299   * for use from customize mode; You should generally use getPlacementOfWidget
4300   * instead, which is cheaper because it does not use the DOM.
4301   *
4302   * @param aElement the DOM node whose place we need to check
4303   * @return "toolbar" if the node is in a toolbar, "panel" if it is in the
4304   *         menu panel, "palette" if it is in the (visible!) customization
4305   *         palette, undefined otherwise.
4306   */
4307  getPlaceForItem(aElement) {
4308    let place;
4309    let node = aElement;
4310    while (node && !place) {
4311      if (node.localName == "toolbar") {
4312        place = "toolbar";
4313      } else if (node.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
4314        place = "menu-panel";
4315      } else if (node.id == "customization-palette") {
4316        place = "palette";
4317      }
4318
4319      node = node.parentNode;
4320    }
4321    return place;
4322  },
4323
4324  /**
4325   * Check if a toolbar is builtin or not.
4326   * @param aToolbarId the ID of the toolbar you want to check
4327   */
4328  isBuiltinToolbar(aToolbarId) {
4329    return CustomizableUIInternal._builtinToolbars.has(aToolbarId);
4330  },
4331
4332  /**
4333   * Create an instance of a spring, spacer or separator.
4334   * @param aId       the type of special widget (spring, spacer or separator)
4335   * @param aDocument the document in which to create it.
4336   */
4337  createSpecialWidget(aId, aDocument) {
4338    return CustomizableUIInternal.createSpecialWidget(aId, aDocument);
4339  },
4340
4341  /**
4342   * Fills a submenu with menu items.
4343   * @param aMenuItems the menu items to display.
4344   * @param aSubview   the subview to fill.
4345   */
4346  fillSubviewFromMenuItems(aMenuItems, aSubview) {
4347    let attrs = [
4348      "oncommand",
4349      "onclick",
4350      "label",
4351      "key",
4352      "disabled",
4353      "command",
4354      "observes",
4355      "hidden",
4356      "class",
4357      "origin",
4358      "image",
4359      "checked",
4360      "style",
4361    ];
4362
4363    let doc = aSubview.ownerDocument;
4364    let fragment = doc.createDocumentFragment();
4365    for (let menuChild of aMenuItems) {
4366      if (menuChild.hidden) {
4367        continue;
4368      }
4369
4370      let subviewItem;
4371      if (menuChild.localName == "menuseparator") {
4372        // Don't insert duplicate or leading separators. This can happen if there are
4373        // menus (which we don't copy) above the separator.
4374        if (
4375          !fragment.lastElementChild ||
4376          fragment.lastElementChild.localName == "menuseparator"
4377        ) {
4378          continue;
4379        }
4380        subviewItem = doc.createXULElement("menuseparator");
4381      } else if (menuChild.localName == "menuitem") {
4382        subviewItem = doc.createXULElement("toolbarbutton");
4383        CustomizableUI.addShortcut(menuChild, subviewItem);
4384
4385        let item = menuChild;
4386        if (!item.hasAttribute("onclick")) {
4387          subviewItem.addEventListener("click", event => {
4388            let newEvent = new doc.defaultView.MouseEvent(event.type, event);
4389            item.dispatchEvent(newEvent);
4390          });
4391        }
4392
4393        if (!item.hasAttribute("oncommand")) {
4394          subviewItem.addEventListener("command", event => {
4395            let newEvent = doc.createEvent("XULCommandEvent");
4396            newEvent.initCommandEvent(
4397              event.type,
4398              event.bubbles,
4399              event.cancelable,
4400              event.view,
4401              event.detail,
4402              event.ctrlKey,
4403              event.altKey,
4404              event.shiftKey,
4405              event.metaKey,
4406              event.sourceEvent,
4407              0
4408            );
4409            item.dispatchEvent(newEvent);
4410          });
4411        }
4412      } else {
4413        continue;
4414      }
4415      for (let attr of attrs) {
4416        let attrVal = menuChild.getAttribute(attr);
4417        if (attrVal) {
4418          subviewItem.setAttribute(attr, attrVal);
4419        }
4420      }
4421      // We do this after so the .subviewbutton class doesn't get overridden.
4422      if (menuChild.localName == "menuitem") {
4423        subviewItem.classList.add("subviewbutton");
4424      }
4425      fragment.appendChild(subviewItem);
4426    }
4427    aSubview.appendChild(fragment);
4428  },
4429
4430  /**
4431   * A helper function for clearing subviews.
4432   * @param aSubview the subview to clear.
4433   */
4434  clearSubview(aSubview) {
4435    let parent = aSubview.parentNode;
4436    // We'll take the container out of the document before cleaning it out
4437    // to avoid reflowing each time we remove something.
4438    parent.removeChild(aSubview);
4439
4440    while (aSubview.firstElementChild) {
4441      aSubview.firstElementChild.remove();
4442    }
4443
4444    parent.appendChild(aSubview);
4445  },
4446
4447  getCustomizationTarget(aElement) {
4448    return CustomizableUIInternal.getCustomizationTarget(aElement);
4449  },
4450};
4451Object.freeze(CustomizableUI);
4452Object.freeze(CustomizableUI.windows);
4453
4454/**
4455 * All external consumers of widgets are really interacting with these wrappers
4456 * which provide a common interface.
4457 */
4458
4459/**
4460 * WidgetGroupWrapper is the common interface for interacting with an entire
4461 * widget group - AKA, all instances of a widget across a series of windows.
4462 * This particular wrapper is only used for widgets created via the provider
4463 * API.
4464 */
4465function WidgetGroupWrapper(aWidget) {
4466  this.isGroup = true;
4467
4468  const kBareProps = [
4469    "id",
4470    "source",
4471    "type",
4472    "disabled",
4473    "label",
4474    "tooltiptext",
4475    "showInPrivateBrowsing",
4476    "viewId",
4477  ];
4478  for (let prop of kBareProps) {
4479    let propertyName = prop;
4480    this.__defineGetter__(propertyName, () => aWidget[propertyName]);
4481  }
4482
4483  this.__defineGetter__("provider", () => CustomizableUI.PROVIDER_API);
4484
4485  this.__defineSetter__("disabled", function(aValue) {
4486    aValue = !!aValue;
4487    aWidget.disabled = aValue;
4488    for (let [, instance] of aWidget.instances) {
4489      instance.disabled = aValue;
4490    }
4491  });
4492
4493  // eslint-disable-next-line func-names
4494  this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
4495    let wrapperMap;
4496    if (!gSingleWrapperCache.has(aWindow)) {
4497      wrapperMap = new Map();
4498      gSingleWrapperCache.set(aWindow, wrapperMap);
4499    } else {
4500      wrapperMap = gSingleWrapperCache.get(aWindow);
4501    }
4502    if (wrapperMap.has(aWidget.id)) {
4503      return wrapperMap.get(aWidget.id);
4504    }
4505
4506    let instance = aWidget.instances.get(aWindow.document);
4507    if (!instance) {
4508      instance = CustomizableUIInternal.buildWidget(aWindow.document, aWidget);
4509    }
4510
4511    let wrapper = new WidgetSingleWrapper(aWidget, instance);
4512    wrapperMap.set(aWidget.id, wrapper);
4513    return wrapper;
4514  };
4515
4516  this.__defineGetter__("instances", function() {
4517    // Can't use gBuildWindows here because some areas load lazily:
4518    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
4519    if (!placement) {
4520      return [];
4521    }
4522    let area = placement.area;
4523    let buildAreas = gBuildAreas.get(area);
4524    if (!buildAreas) {
4525      return [];
4526    }
4527    return Array.from(buildAreas, node => this.forWindow(node.ownerGlobal));
4528  });
4529
4530  this.__defineGetter__("areaType", function() {
4531    let areaProps = gAreas.get(aWidget.currentArea);
4532    return areaProps && areaProps.get("type");
4533  });
4534
4535  Object.freeze(this);
4536}
4537
4538/**
4539 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
4540 * a particular window.
4541 */
4542function WidgetSingleWrapper(aWidget, aNode) {
4543  this.isGroup = false;
4544
4545  this.node = aNode;
4546  this.provider = CustomizableUI.PROVIDER_API;
4547
4548  const kGlobalProps = ["id", "type"];
4549  for (let prop of kGlobalProps) {
4550    this[prop] = aWidget[prop];
4551  }
4552
4553  const kNodeProps = ["label", "tooltiptext"];
4554  for (let prop of kNodeProps) {
4555    let propertyName = prop;
4556    // Look at the node for these, instead of the widget data, to ensure the
4557    // wrapper always reflects this live instance.
4558    this.__defineGetter__(propertyName, () => aNode.getAttribute(propertyName));
4559  }
4560
4561  this.__defineGetter__("disabled", () => aNode.disabled);
4562  this.__defineSetter__("disabled", function(aValue) {
4563    aNode.disabled = !!aValue;
4564  });
4565
4566  this.__defineGetter__("anchor", function() {
4567    let anchorId;
4568    // First check for an anchor for the area:
4569    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
4570    if (placement) {
4571      anchorId = gAreas.get(placement.area).get("anchor");
4572    }
4573    if (!anchorId) {
4574      anchorId = aNode.getAttribute("cui-anchorid");
4575    }
4576
4577    return anchorId ? aNode.ownerDocument.getElementById(anchorId) : aNode;
4578  });
4579
4580  this.__defineGetter__("overflowed", function() {
4581    return aNode.getAttribute("overflowedItem") == "true";
4582  });
4583
4584  Object.freeze(this);
4585}
4586
4587/**
4588 * XULWidgetGroupWrapper is the common interface for interacting with an entire
4589 * widget group - AKA, all instances of a widget across a series of windows.
4590 * This particular wrapper is only used for widgets created via the old-school
4591 * XUL method (overlays, or programmatically injecting toolbaritems, or other
4592 * such things).
4593 */
4594// XXXunf Going to need to hook this up to some events to keep it all live.
4595function XULWidgetGroupWrapper(aWidgetId) {
4596  this.isGroup = true;
4597  this.id = aWidgetId;
4598  this.type = "custom";
4599  this.provider = CustomizableUI.PROVIDER_XUL;
4600
4601  // eslint-disable-next-line func-names
4602  this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) {
4603    let wrapperMap;
4604    if (!gSingleWrapperCache.has(aWindow)) {
4605      wrapperMap = new Map();
4606      gSingleWrapperCache.set(aWindow, wrapperMap);
4607    } else {
4608      wrapperMap = gSingleWrapperCache.get(aWindow);
4609    }
4610    if (wrapperMap.has(aWidgetId)) {
4611      return wrapperMap.get(aWidgetId);
4612    }
4613
4614    let instance = aWindow.document.getElementById(aWidgetId);
4615    if (!instance) {
4616      // Toolbar palettes aren't part of the document, so elements in there
4617      // won't be found via document.getElementById().
4618      instance = aWindow.gNavToolbox.palette.getElementsByAttribute(
4619        "id",
4620        aWidgetId
4621      )[0];
4622    }
4623
4624    let wrapper = new XULWidgetSingleWrapper(
4625      aWidgetId,
4626      instance,
4627      aWindow.document
4628    );
4629    wrapperMap.set(aWidgetId, wrapper);
4630    return wrapper;
4631  };
4632
4633  this.__defineGetter__("areaType", function() {
4634    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
4635    if (!placement) {
4636      return null;
4637    }
4638
4639    let areaProps = gAreas.get(placement.area);
4640    return areaProps && areaProps.get("type");
4641  });
4642
4643  this.__defineGetter__("instances", function() {
4644    return Array.from(gBuildWindows, wins => this.forWindow(wins[0]));
4645  });
4646
4647  Object.freeze(this);
4648}
4649
4650/**
4651 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL
4652 * widget in a particular window.
4653 */
4654function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) {
4655  this.isGroup = false;
4656
4657  this.id = aWidgetId;
4658  this.type = "custom";
4659  this.provider = CustomizableUI.PROVIDER_XUL;
4660
4661  let weakDoc = Cu.getWeakReference(aDocument);
4662  // If we keep a strong ref, the weak ref will never die, so null it out:
4663  aDocument = null;
4664
4665  this.__defineGetter__("node", function() {
4666    // If we've set this to null (further down), we're sure there's nothing to
4667    // be gotten here, so bail out early:
4668    if (!weakDoc) {
4669      return null;
4670    }
4671    if (aNode) {
4672      // Return the last known node if it's still in the DOM...
4673      if (aNode.ownerDocument.contains(aNode)) {
4674        return aNode;
4675      }
4676      // ... or the toolbox
4677      let toolbox = aNode.ownerGlobal.gNavToolbox;
4678      if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
4679        return aNode;
4680      }
4681      // If it isn't, clear the cached value and fall through to the "slow" case:
4682      aNode = null;
4683    }
4684
4685    let doc = weakDoc.get();
4686    if (doc) {
4687      // Store locally so we can cache the result:
4688      aNode = CustomizableUIInternal.findWidgetInWindow(
4689        aWidgetId,
4690        doc.defaultView
4691      );
4692      return aNode;
4693    }
4694    // The weakref to the document is dead, we're done here forever more:
4695    weakDoc = null;
4696    return null;
4697  });
4698
4699  this.__defineGetter__("anchor", function() {
4700    let anchorId;
4701    // First check for an anchor for the area:
4702    let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
4703    if (placement) {
4704      anchorId = gAreas.get(placement.area).get("anchor");
4705    }
4706
4707    let node = this.node;
4708    if (!anchorId && node) {
4709      anchorId = node.getAttribute("cui-anchorid");
4710    }
4711
4712    return anchorId && node
4713      ? node.ownerDocument.getElementById(anchorId)
4714      : node;
4715  });
4716
4717  this.__defineGetter__("overflowed", function() {
4718    let node = this.node;
4719    if (!node) {
4720      return false;
4721    }
4722    return node.getAttribute("overflowedItem") == "true";
4723  });
4724
4725  Object.freeze(this);
4726}
4727
4728const LAZY_RESIZE_INTERVAL_MS = 200;
4729const OVERFLOW_PANEL_HIDE_DELAY_MS = 500;
4730
4731function OverflowableToolbar(aToolbarNode) {
4732  this._toolbar = aToolbarNode;
4733  this._collapsed = new Map();
4734  this._enabled = true;
4735  this._toolbar.addEventListener("overflow", this);
4736  this._toolbar.addEventListener("underflow", this);
4737
4738  this._toolbar.setAttribute("overflowable", "true");
4739  let doc = this._toolbar.ownerDocument;
4740  this._target = CustomizableUI.getCustomizationTarget(this._toolbar);
4741  this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
4742  this._list._customizationTarget = this._list;
4743
4744  let window = this._toolbar.ownerGlobal;
4745  if (window.gBrowserInit.delayedStartupFinished) {
4746    this.init();
4747  } else {
4748    Services.obs.addObserver(this, "browser-delayed-startup-finished");
4749  }
4750}
4751
4752OverflowableToolbar.prototype = {
4753  initialized: false,
4754  _forceOnOverflow: false,
4755  _addedListener: false,
4756
4757  observe(aSubject, aTopic, aData) {
4758    if (
4759      aTopic == "browser-delayed-startup-finished" &&
4760      aSubject == this._toolbar.ownerGlobal
4761    ) {
4762      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
4763      this.init();
4764    }
4765  },
4766
4767  init() {
4768    let doc = this._toolbar.ownerDocument;
4769    let window = doc.defaultView;
4770    window.addEventListener("resize", this);
4771    window.gNavToolbox.addEventListener("customizationstarting", this);
4772    window.gNavToolbox.addEventListener("aftercustomization", this);
4773
4774    let chevronId = this._toolbar.getAttribute("overflowbutton");
4775    this._chevron = doc.getElementById(chevronId);
4776    this._chevron.addEventListener("mousedown", this);
4777    this._chevron.addEventListener("keypress", this);
4778    this._chevron.addEventListener("dragover", this);
4779    this._chevron.addEventListener("dragend", this);
4780
4781    let panelId = this._toolbar.getAttribute("overflowpanel");
4782    this._panel = doc.getElementById(panelId);
4783    this._panel.addEventListener("popuphiding", this);
4784    CustomizableUIInternal.addPanelCloseListeners(this._panel);
4785
4786    CustomizableUI.addListener(this);
4787    this._addedListener = true;
4788
4789    // The 'overflow' event may have been fired before init was called.
4790    if (this.overflowedDuringConstruction) {
4791      this.onOverflow(this.overflowedDuringConstruction);
4792      this.overflowedDuringConstruction = null;
4793    }
4794
4795    this.initialized = true;
4796  },
4797
4798  uninit() {
4799    this._toolbar.removeEventListener("overflow", this._toolbar);
4800    this._toolbar.removeEventListener("underflow", this._toolbar);
4801    this._toolbar.removeAttribute("overflowable");
4802
4803    if (!this.initialized) {
4804      Services.obs.removeObserver(this, "browser-delayed-startup-finished");
4805      return;
4806    }
4807
4808    this._disable();
4809
4810    let window = this._toolbar.ownerGlobal;
4811    window.removeEventListener("resize", this);
4812    window.gNavToolbox.removeEventListener("customizationstarting", this);
4813    window.gNavToolbox.removeEventListener("aftercustomization", this);
4814    this._chevron.removeEventListener("mousedown", this);
4815    this._chevron.removeEventListener("keypress", this);
4816    this._chevron.removeEventListener("dragover", this);
4817    this._chevron.removeEventListener("dragend", this);
4818    this._panel.removeEventListener("popuphiding", this);
4819    CustomizableUI.removeListener(this);
4820    this._addedListener = false;
4821    CustomizableUIInternal.removePanelCloseListeners(this._panel);
4822  },
4823
4824  handleEvent(aEvent) {
4825    switch (aEvent.type) {
4826      case "overflow":
4827        // Ignore vertical overflow and events from from nodes inside the toolbar.
4828        if (aEvent.detail > 0 && aEvent.target == this._target) {
4829          if (this.initialized) {
4830            this.onOverflow(aEvent);
4831          } else {
4832            this.overflowedDuringConstruction = aEvent;
4833          }
4834        }
4835        break;
4836      case "underflow":
4837        // Ignore vertical underflow and events from from nodes inside the toolbar.
4838        if (aEvent.detail > 0 && aEvent.target == this._target) {
4839          this.overflowedDuringConstruction = null;
4840        }
4841        break;
4842      case "aftercustomization":
4843        this._enable();
4844        break;
4845      case "mousedown":
4846        if (aEvent.button != 0) {
4847          break;
4848        }
4849        if (aEvent.target == this._chevron) {
4850          this._onClickChevron(aEvent);
4851        } else {
4852          PanelMultiView.hidePopup(this._panel);
4853        }
4854        break;
4855      case "keypress":
4856        if (
4857          aEvent.target == this._chevron &&
4858          (aEvent.key == " " || aEvent.key == "Enter")
4859        ) {
4860          this._onClickChevron(aEvent);
4861        }
4862        break;
4863      case "customizationstarting":
4864        this._disable();
4865        break;
4866      case "dragover":
4867        if (this._enabled) {
4868          this._showWithTimeout();
4869        }
4870        break;
4871      case "dragend":
4872        PanelMultiView.hidePopup(this._panel);
4873        break;
4874      case "popuphiding":
4875        this._onPanelHiding(aEvent);
4876        break;
4877      case "resize":
4878        this._onResize(aEvent);
4879    }
4880  },
4881
4882  show(aEvent) {
4883    if (this._panel.state == "open") {
4884      return Promise.resolve();
4885    }
4886    return new Promise(resolve => {
4887      let doc = this._panel.ownerDocument;
4888      this._panel.hidden = false;
4889      let multiview = this._panel.querySelector("panelmultiview");
4890      let mainViewId = multiview.getAttribute("mainViewId");
4891      let mainView = doc.getElementById(mainViewId);
4892      let contextMenu = doc.getElementById(mainView.getAttribute("context"));
4893      gELS.addSystemEventListener(contextMenu, "command", this, true);
4894      let anchor = this.chevron.icon;
4895      // Ensure we update the gEditUIVisible flag when opening the popup, in
4896      // case the edit controls are in it.
4897      this._panel.addEventListener(
4898        "popupshowing",
4899        () => doc.defaultView.updateEditUIVisibility(),
4900        { once: true }
4901      );
4902      PanelMultiView.openPopup(this._panel, anchor || this._chevron, {
4903        triggerEvent: aEvent,
4904      }).catch(Cu.reportError);
4905      this._chevron.open = true;
4906
4907      this._panel.addEventListener(
4908        "popupshown",
4909        () => {
4910          this._panel.addEventListener("dragover", this);
4911          this._panel.addEventListener("dragend", this);
4912          // Wait until the next tick to resolve so all popupshown
4913          // handlers have a chance to run before our promise resolution
4914          // handlers do.
4915          Services.tm.dispatchToMainThread(resolve);
4916        },
4917        { once: true }
4918      );
4919    });
4920  },
4921
4922  _onClickChevron(aEvent) {
4923    if (this._chevron.open) {
4924      this._chevron.open = false;
4925      PanelMultiView.hidePopup(this._panel);
4926    } else if (this._panel.state != "hiding" && !this._chevron.disabled) {
4927      this.show(aEvent);
4928    }
4929  },
4930
4931  _onPanelHiding(aEvent) {
4932    if (aEvent.target != this._panel) {
4933      // Ignore context menus, <select> popups, etc.
4934      return;
4935    }
4936    this._chevron.open = false;
4937    this._panel.removeEventListener("dragover", this);
4938    this._panel.removeEventListener("dragend", this);
4939    let doc = aEvent.target.ownerDocument;
4940    doc.defaultView.updateEditUIVisibility();
4941    let contextMenuId = this._panel.getAttribute("context");
4942    if (contextMenuId) {
4943      let contextMenu = doc.getElementById(contextMenuId);
4944      gELS.removeSystemEventListener(contextMenu, "command", this, true);
4945    }
4946  },
4947
4948  /**
4949   * Avoid re-entrancy in the overflow handling by keeping track of invocations:
4950   */
4951  _lastOverflowCounter: 0,
4952
4953  /**
4954   * Handle overflow in the toolbar by moving items to the overflow menu.
4955   * @param {Event} aEvent
4956   *        The overflow event that triggered handling overflow. May be omitted
4957   *        in some cases (e.g. when we run this method after overflow handling
4958   *        is re-enabled from customize mode, to ensure correct handling of
4959   *        initial overflow).
4960   */
4961  async onOverflow(aEvent) {
4962    if (!this._enabled) {
4963      return;
4964    }
4965
4966    let child = this._target.lastElementChild;
4967
4968    let thisOverflowResponse = ++this._lastOverflowCounter;
4969
4970    let win = this._target.ownerGlobal;
4971    let [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(
4972      () => {
4973        return [this._target.scrollLeftMin, this._target.scrollLeftMax];
4974      }
4975    );
4976    if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
4977      return;
4978    }
4979
4980    while (child && scrollLeftMin != scrollLeftMax) {
4981      let prevChild = child.previousElementSibling;
4982
4983      if (child.getAttribute("overflows") != "false") {
4984        this._collapsed.set(child.id, this._target.clientWidth);
4985        child.setAttribute("overflowedItem", true);
4986        child.setAttribute("cui-anchorid", this._chevron.id);
4987        CustomizableUIInternal.ensureButtonContextMenu(
4988          child,
4989          this._toolbar,
4990          true
4991        );
4992        CustomizableUIInternal.notifyListeners(
4993          "onWidgetOverflow",
4994          child,
4995          this._target
4996        );
4997
4998        this._list.insertBefore(child, this._list.firstElementChild);
4999        if (!this._addedListener) {
5000          CustomizableUI.addListener(this);
5001        }
5002        if (!CustomizableUI.isSpecialWidget(child.id)) {
5003          this._toolbar.setAttribute("overflowing", "true");
5004        }
5005      }
5006      child = prevChild;
5007      [scrollLeftMin, scrollLeftMax] = await win.promiseDocumentFlushed(() => {
5008        return [this._target.scrollLeftMin, this._target.scrollLeftMax];
5009      });
5010      // If the window has closed or if we re-enter because we were waiting
5011      // for layout, stop.
5012      if (win.closed || this._lastOverflowCounter != thisOverflowResponse) {
5013        return;
5014      }
5015    }
5016
5017    win.UpdateUrlbarSearchSplitterState();
5018    // Reset the counter because we finished handling overflow.
5019    this._lastOverflowCounter = 0;
5020  },
5021
5022  _onResize(aEvent) {
5023    // Ignore bubbled-up resize events.
5024    if (aEvent.target != aEvent.target.ownerGlobal.top) {
5025      return;
5026    }
5027    if (!this._lazyResizeHandler) {
5028      this._lazyResizeHandler = new DeferredTask(
5029        this._onLazyResize.bind(this),
5030        LAZY_RESIZE_INTERVAL_MS,
5031        0
5032      );
5033    }
5034    this._lazyResizeHandler.arm();
5035  },
5036
5037  /**
5038   * Try to move toolbar items back to the toolbar from the overflow menu.
5039   * @param {boolean} shouldMoveAllItems
5040   *        Whether we should move everything (e.g. because we're being disabled)
5041   * @param {number} targetWidth
5042   *        Optional; the width of the toolbar in which we can put things.
5043   *        Some consumers pass this to avoid reflows.
5044   *        While there are items in the list, this width won't change, and so
5045   *        we can avoid flushing layout by providing it and/or caching it.
5046   *        Note that if `shouldMoveAllItems` is true, we never need the width
5047   *        anyway.
5048   */
5049  _moveItemsBackToTheirOrigin(shouldMoveAllItems, targetWidth) {
5050    let placements = gPlacements.get(this._toolbar.id);
5051    let win = this._target.ownerGlobal;
5052    while (this._list.firstElementChild) {
5053      let child = this._list.firstElementChild;
5054      let minSize = this._collapsed.get(child.id);
5055
5056      if (!shouldMoveAllItems && minSize) {
5057        if (!targetWidth) {
5058          let dwu = win.windowUtils;
5059          targetWidth = Math.floor(
5060            dwu.getBoundsWithoutFlushing(this._target).width
5061          );
5062        }
5063        if (targetWidth <= minSize) {
5064          break;
5065        }
5066      }
5067
5068      this._collapsed.delete(child.id);
5069      let beforeNodeIndex = placements.indexOf(child.id) + 1;
5070      // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
5071      // we're inserting it at the end. This will mean first-in, first-out (more or less)
5072      // leading to as little change in order as possible.
5073      if (beforeNodeIndex == 0) {
5074        beforeNodeIndex = placements.length;
5075      }
5076      let inserted = false;
5077      for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
5078        let beforeNode = this._target.getElementsByAttribute(
5079          "id",
5080          placements[beforeNodeIndex]
5081        )[0];
5082        // Unfortunately, XUL add-ons can mess with nodes after they are inserted,
5083        // and this breaks the following code if the button isn't where we expect
5084        // it to be (ie not a child of the target). In this case, ignore the node.
5085        if (beforeNode && this._target == beforeNode.parentElement) {
5086          this._target.insertBefore(child, beforeNode);
5087          inserted = true;
5088          break;
5089        }
5090      }
5091      if (!inserted) {
5092        this._target.appendChild(child);
5093      }
5094      child.removeAttribute("cui-anchorid");
5095      child.removeAttribute("overflowedItem");
5096      CustomizableUIInternal.ensureButtonContextMenu(child, this._target);
5097      CustomizableUIInternal.notifyListeners(
5098        "onWidgetUnderflow",
5099        child,
5100        this._target
5101      );
5102    }
5103
5104    win.UpdateUrlbarSearchSplitterState();
5105
5106    let collapsedWidgetIds = Array.from(this._collapsed.keys());
5107    if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
5108      this._toolbar.removeAttribute("overflowing");
5109    }
5110    if (this._addedListener && !this._collapsed.size) {
5111      CustomizableUI.removeListener(this);
5112      this._addedListener = false;
5113    }
5114  },
5115
5116  async _onLazyResize() {
5117    if (!this._enabled) {
5118      return;
5119    }
5120
5121    let win = this._target.ownerGlobal;
5122    let [min, max, targetWidth] = await win.promiseDocumentFlushed(() => {
5123      return [
5124        this._target.scrollLeftMin,
5125        this._target.scrollLeftMax,
5126        this._target.clientWidth,
5127      ];
5128    });
5129    if (win.closed) {
5130      return;
5131    }
5132    if (min != max) {
5133      this.onOverflow();
5134    } else {
5135      this._moveItemsBackToTheirOrigin(false, targetWidth);
5136    }
5137  },
5138
5139  _disable() {
5140    this._enabled = false;
5141    this._moveItemsBackToTheirOrigin(true);
5142    if (this._lazyResizeHandler) {
5143      this._lazyResizeHandler.disarm();
5144    }
5145  },
5146
5147  _enable() {
5148    this._enabled = true;
5149    this.onOverflow();
5150  },
5151
5152  onWidgetBeforeDOMChange(aNode, aNextNode, aContainer) {
5153    if (aContainer != this._target && aContainer != this._list) {
5154      return;
5155    }
5156    // When we (re)move an item, update all the items that come after it in the list
5157    // with the minsize *of the item before the to-be-removed node*. This way, we
5158    // ensure that we try to move items back as soon as that's possible.
5159    if (aNode.parentNode == this._list) {
5160      let updatedMinSize;
5161      if (aNode.previousElementSibling) {
5162        updatedMinSize = this._collapsed.get(aNode.previousElementSibling.id);
5163      } else {
5164        // Force (these) items to try to flow back into the bar:
5165        updatedMinSize = 1;
5166      }
5167      let nextItem = aNode.nextElementSibling;
5168      while (nextItem) {
5169        this._collapsed.set(nextItem.id, updatedMinSize);
5170        nextItem = nextItem.nextElementSibling;
5171      }
5172    }
5173  },
5174
5175  onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
5176    if (aContainer != this._target && aContainer != this._list) {
5177      return;
5178    }
5179
5180    let nowInBar = aNode.parentNode == aContainer;
5181    let nowOverflowed = aNode.parentNode == this._list;
5182    let wasOverflowed = this._collapsed.has(aNode.id);
5183
5184    // If this wasn't overflowed before...
5185    if (!wasOverflowed) {
5186      // ... but it is now, then we added to the overflow panel.
5187      if (nowOverflowed) {
5188        // We could be the first item in the overflow panel if we're being inserted
5189        // before the previous first item in it. We can't assume the minimum
5190        // size is the same (because the other item might be much wider), so if
5191        // there is no previous item, just allow this item to be put back in the
5192        // toolbar immediately by specifying a very low minimum size.
5193        let sourceOfMinSize = aNode.previousElementSibling;
5194        let minSize = sourceOfMinSize
5195          ? this._collapsed.get(sourceOfMinSize.id)
5196          : 1;
5197        this._collapsed.set(aNode.id, minSize);
5198        aNode.setAttribute("cui-anchorid", this._chevron.id);
5199        aNode.setAttribute("overflowedItem", true);
5200        CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer, true);
5201        CustomizableUIInternal.notifyListeners(
5202          "onWidgetOverflow",
5203          aNode,
5204          this._target
5205        );
5206      } else if (!nowInBar) {
5207        // If it is not overflowed and not in the toolbar, and was not overflowed
5208        // either, it moved out of the toolbar. That means there's now space in there!
5209        // Let's try to move stuff back:
5210        this._moveItemsBackToTheirOrigin(true);
5211      }
5212      // If it's in the toolbar now, then we don't care. An overflow event may
5213      // fire afterwards; that's ok!
5214    } else if (!nowOverflowed) {
5215      // If it used to be overflowed...
5216      // ... and isn't anymore, let's remove our bookkeeping:
5217      this._collapsed.delete(aNode.id);
5218      aNode.removeAttribute("cui-anchorid");
5219      aNode.removeAttribute("overflowedItem");
5220      CustomizableUIInternal.ensureButtonContextMenu(aNode, aContainer);
5221      CustomizableUIInternal.notifyListeners(
5222        "onWidgetUnderflow",
5223        aNode,
5224        this._target
5225      );
5226
5227      let collapsedWidgetIds = Array.from(this._collapsed.keys());
5228      if (collapsedWidgetIds.every(w => CustomizableUI.isSpecialWidget(w))) {
5229        this._toolbar.removeAttribute("overflowing");
5230      }
5231      if (this._addedListener && !this._collapsed.size) {
5232        CustomizableUI.removeListener(this);
5233        this._addedListener = false;
5234      }
5235    } else if (aNode.previousElementSibling) {
5236      // but if it still is, it must have changed places. Bookkeep:
5237      let prevId = aNode.previousElementSibling.id;
5238      let minSize = this._collapsed.get(prevId);
5239      this._collapsed.set(aNode.id, minSize);
5240    } else {
5241      // If it's now the first item in the overflow list,
5242      // maybe we can return it:
5243      this._moveItemsBackToTheirOrigin(false);
5244    }
5245  },
5246
5247  findOverflowedInsertionPoints(aNode) {
5248    let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
5249    let areaId = this._toolbar.id;
5250    let placements = gPlacements.get(areaId);
5251    let nodeIndex = placements.indexOf(aNode.id);
5252    let nodeBeforeNewNodeIsOverflown = false;
5253
5254    let loopIndex = -1;
5255    // Loop through placements to find where to insert this item.
5256    // As soon as we find an overflown widget, we will only
5257    // insert in the overflow panel (this is why we check placements
5258    // before the desired location for the new node). Once we pass
5259    // the desired location of the widget, we look for placement ids
5260    // that actually have DOM equivalents to insert before. If all
5261    // else fails, we insert at the end of either the overflow list
5262    // or the toolbar target.
5263    while (++loopIndex < placements.length) {
5264      let nextNodeId = placements[loopIndex];
5265      if (loopIndex > nodeIndex) {
5266        let nextNode = aNode.ownerDocument.getElementById(nextNodeId);
5267        // If the node we're inserting can overflow, and the next node
5268        // in the toolbar is overflown, we should insert this node
5269        // in the overflow panel before it.
5270        if (
5271          newNodeCanOverflow &&
5272          this._collapsed.has(nextNodeId) &&
5273          nextNode &&
5274          nextNode.parentNode == this._list
5275        ) {
5276          return [this._list, nextNode];
5277        }
5278        // Otherwise (if either we can't overflow, or the previous node
5279        // wasn't overflown), and the next node is in the toolbar itself,
5280        // insert the node in the toolbar.
5281        if (
5282          (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) &&
5283          nextNode &&
5284          (nextNode.parentNode == this._target ||
5285            // Also check if the next node is in a customization wrapper
5286            // (toolbarpaletteitem). We don't need to do this for the
5287            // overflow case because overflow is disabled in customize mode.
5288            (nextNode.parentNode.localName == "toolbarpaletteitem" &&
5289              nextNode.parentNode.parentNode == this._target))
5290        ) {
5291          return [this._target, nextNode];
5292        }
5293      } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) {
5294        nodeBeforeNewNodeIsOverflown = true;
5295      }
5296    }
5297
5298    let containerForAppending =
5299      this._collapsed.size && newNodeCanOverflow ? this._list : this._target;
5300    return [containerForAppending, null];
5301  },
5302
5303  getContainerFor(aNode) {
5304    if (aNode.getAttribute("overflowedItem") == "true") {
5305      return this._list;
5306    }
5307    return this._target;
5308  },
5309
5310  _hideTimeoutId: null,
5311  _showWithTimeout() {
5312    this.show().then(() => {
5313      let window = this._toolbar.ownerGlobal;
5314      if (this._hideTimeoutId) {
5315        window.clearTimeout(this._hideTimeoutId);
5316      }
5317      this._hideTimeoutId = window.setTimeout(() => {
5318        if (!this._panel.firstElementChild.matches(":hover")) {
5319          PanelMultiView.hidePopup(this._panel);
5320        }
5321      }, OVERFLOW_PANEL_HIDE_DELAY_MS);
5322    });
5323  },
5324};
5325
5326CustomizableUIInternal.initialize();
5327