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