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