Bug 1842809 - Don't resize web content when Entering/Exiting Customize Mode r=emilio
[gecko.git] / browser / components / customizableui / CustomizeMode.sys.mjs
blob8e8b7e1793793aee1573c930e84c4771ee57aadf
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/. */
5 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
6 const kPaletteId = "customization-palette";
7 const kDragDataTypePrefix = "text/toolbarwrapper-id/";
8 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
9 const kDrawInTitlebarPref = "browser.tabs.inTitlebar";
10 const kCompactModeShowPref = "browser.compactmode.show";
11 const kBookmarksToolbarPref = "browser.toolbars.bookmarks.visibility";
12 const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";
14 const kPanelItemContextMenu = "customizationPanelItemContextMenu";
15 const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
17 const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
18 const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
19 const kDownloadAutoHidePref = "browser.download.autohideButton";
21 import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs";
22 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
23 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
25 const lazy = {};
27 ChromeUtils.defineESModuleGetters(lazy, {
28   AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
29   BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
30   DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
31   SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
32   URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
33 });
34 XPCOMUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
35   const kUrl =
36     "chrome://browser/locale/customizableui/customizableWidgets.properties";
37   return Services.strings.createBundle(kUrl);
38 });
39 XPCOMUtils.defineLazyServiceGetter(
40   lazy,
41   "gTouchBarUpdater",
42   "@mozilla.org/widget/touchbarupdater;1",
43   "nsITouchBarUpdater"
46 let gDebug;
47 XPCOMUtils.defineLazyGetter(lazy, "log", () => {
48   let { ConsoleAPI } = ChromeUtils.importESModule(
49     "resource://gre/modules/Console.sys.mjs"
50   );
51   gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
52   let consoleOptions = {
53     maxLogLevel: gDebug ? "all" : "log",
54     prefix: "CustomizeMode",
55   };
56   return new ConsoleAPI(consoleOptions);
57 });
59 var gDraggingInToolbars;
61 var gTab;
63 function closeGlobalTab() {
64   let win = gTab.ownerGlobal;
65   if (win.gBrowser.browsers.length == 1) {
66     win.BrowserOpenTab();
67   }
68   win.gBrowser.removeTab(gTab, { animate: true });
69   gTab = null;
72 var gTabsProgressListener = {
73   onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) {
74     // Tear down customize mode when the customize mode tab loads some other page.
75     // Customize mode will be re-entered if "about:blank" is loaded again, so
76     // don't tear down in this case.
77     if (
78       !gTab ||
79       gTab.linkedBrowser != aBrowser ||
80       aLocation.spec == "about:blank"
81     ) {
82       return;
83     }
85     unregisterGlobalTab();
86   },
89 function unregisterGlobalTab() {
90   gTab.removeEventListener("TabClose", unregisterGlobalTab);
91   let win = gTab.ownerGlobal;
92   win.removeEventListener("unload", unregisterGlobalTab);
93   win.gBrowser.removeTabsProgressListener(gTabsProgressListener);
95   gTab.removeAttribute("customizemode");
97   gTab = null;
100 export function CustomizeMode(aWindow) {
101   this.window = aWindow;
102   this.document = aWindow.document;
103   this.browser = aWindow.gBrowser;
104   this.areas = new Set();
106   this._translationObserver = new aWindow.MutationObserver(mutations =>
107     this._onTranslations(mutations)
108   );
109   this._ensureCustomizationPanels();
111   let content = this.$("customization-content-container");
112   if (!content) {
113     this.window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl");
114     let container = this.$("customization-container");
115     container.replaceChild(
116       this.window.MozXULElement.parseXULToFragment(container.firstChild.data),
117       container.lastChild
118     );
119   }
120   // There are two palettes - there's the palette that can be overlayed with
121   // toolbar items in browser.xhtml. This is invisible, and never seen by the
122   // user. Then there's the visible palette, which gets populated and displayed
123   // to the user when in customizing mode.
124   this.visiblePalette = this.$(kPaletteId);
125   this.pongArena = this.$("customization-pong-arena");
127   if (this._canDrawInTitlebar()) {
128     this._updateTitlebarCheckbox();
129     Services.prefs.addObserver(kDrawInTitlebarPref, this);
130   } else {
131     this.$("customization-titlebar-visibility-checkbox").hidden = true;
132   }
134   // Observe pref changes to the bookmarks toolbar visibility,
135   // since we won't get a toolbarvisibilitychange event if the
136   // toolbar is changing from 'newtab' to 'always' in Customize mode
137   // since the toolbar is shown with the 'newtab' setting.
138   Services.prefs.addObserver(kBookmarksToolbarPref, this);
140   this.window.addEventListener("unload", this);
143 CustomizeMode.prototype = {
144   _changed: false,
145   _transitioning: false,
146   window: null,
147   document: null,
148   // areas is used to cache the customizable areas when in customization mode.
149   areas: null,
150   // When in customizing mode, we swap out the reference to the invisible
151   // palette in gNavToolbox.palette for our visiblePalette. This way, for the
152   // customizing browser window, when widgets are removed from customizable
153   // areas and added to the palette, they're added to the visible palette.
154   // _stowedPalette is a reference to the old invisible palette so we can
155   // restore gNavToolbox.palette to its original state after exiting
156   // customization mode.
157   _stowedPalette: null,
158   _dragOverItem: null,
159   _customizing: false,
160   _skipSourceNodeCheck: null,
161   _mainViewContext: null,
163   // These are the commands we continue to leave enabled while in customize mode.
164   // All other commands are disabled, and we remove the disabled attribute when
165   // leaving customize mode.
166   _enabledCommands: new Set([
167     "cmd_newNavigator",
168     "cmd_newNavigatorTab",
169     "cmd_newNavigatorTabNoEvent",
170     "cmd_close",
171     "cmd_closeWindow",
172     "cmd_quitApplication",
173     "View:FullScreen",
174     "Browser:NextTab",
175     "Browser:PrevTab",
176     "Browser:NewUserContextTab",
177     "Tools:PrivateBrowsing",
178     "minimizeWindow",
179     "zoomWindow",
180   ]),
182   get _handler() {
183     return this.window.CustomizationHandler;
184   },
186   uninit() {
187     if (this._canDrawInTitlebar()) {
188       Services.prefs.removeObserver(kDrawInTitlebarPref, this);
189     }
190     Services.prefs.removeObserver(kBookmarksToolbarPref, this);
191   },
193   $(id) {
194     return this.document.getElementById(id);
195   },
197   toggle() {
198     if (
199       this._handler.isEnteringCustomizeMode ||
200       this._handler.isExitingCustomizeMode
201     ) {
202       this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
203       return;
204     }
205     if (this._customizing) {
206       this.exit();
207     } else {
208       this.enter();
209     }
210   },
212   setTab(aTab) {
213     if (gTab == aTab) {
214       return;
215     }
217     if (gTab) {
218       closeGlobalTab();
219     }
221     gTab = aTab;
223     gTab.setAttribute("customizemode", "true");
224     lazy.SessionStore.persistTabAttribute("customizemode");
226     if (gTab.linkedPanel) {
227       gTab.linkedBrowser.stop();
228     }
230     let win = gTab.ownerGlobal;
232     win.gBrowser.setTabTitle(gTab);
233     win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg");
235     gTab.addEventListener("TabClose", unregisterGlobalTab);
237     win.gBrowser.addTabsProgressListener(gTabsProgressListener);
239     win.addEventListener("unload", unregisterGlobalTab);
241     if (gTab.selected) {
242       win.gCustomizeMode.enter();
243     }
244   },
246   enter() {
247     if (!this.window.toolbar.visible) {
248       let w = lazy.URILoadingHelper.getTargetWindow(this.window, {
249         skipPopups: true,
250       });
251       if (w) {
252         w.gCustomizeMode.enter();
253         return;
254       }
255       let obs = () => {
256         Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
257         w = lazy.URILoadingHelper.getTargetWindow(this.window, {
258           skipPopups: true,
259         });
260         w.gCustomizeMode.enter();
261       };
262       Services.obs.addObserver(obs, "browser-delayed-startup-finished");
263       this.window.openTrustedLinkIn("about:newtab", "window");
264       return;
265     }
266     this._wantToBeInCustomizeMode = true;
268     if (this._customizing || this._handler.isEnteringCustomizeMode) {
269       return;
270     }
272     // Exiting; want to re-enter once we've done that.
273     if (this._handler.isExitingCustomizeMode) {
274       lazy.log.debug(
275         "Attempted to enter while we're in the middle of exiting. " +
276           "We'll exit after we've entered"
277       );
278       return;
279     }
281     if (!gTab) {
282       this.setTab(
283         this.browser.addTab("about:blank", {
284           inBackground: false,
285           forceNotRemote: true,
286           skipAnimation: true,
287           triggeringPrincipal:
288             Services.scriptSecurityManager.getSystemPrincipal(),
289         })
290       );
291       return;
292     }
293     if (!gTab.selected) {
294       // This will force another .enter() to be called via the
295       // onlocationchange handler of the tabbrowser, so we return early.
296       gTab.ownerGlobal.gBrowser.selectedTab = gTab;
297       return;
298     }
299     gTab.ownerGlobal.focus();
300     if (gTab.ownerDocument != this.document) {
301       return;
302     }
304     let window = this.window;
305     let document = this.document;
307     this._handler.isEnteringCustomizeMode = true;
309     // Always disable the reset button at the start of customize mode, it'll be re-enabled
310     // if necessary when we finish entering:
311     let resetButton = this.$("customization-reset-button");
312     resetButton.setAttribute("disabled", "true");
314     (async () => {
315       // We shouldn't start customize mode until after browser-delayed-startup has finished:
316       if (!this.window.gBrowserInit.delayedStartupFinished) {
317         await new Promise(resolve => {
318           let delayedStartupObserver = aSubject => {
319             if (aSubject == this.window) {
320               Services.obs.removeObserver(
321                 delayedStartupObserver,
322                 "browser-delayed-startup-finished"
323               );
324               resolve();
325             }
326           };
328           Services.obs.addObserver(
329             delayedStartupObserver,
330             "browser-delayed-startup-finished"
331           );
332         });
333       }
335       CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
336       CustomizableUI.notifyStartCustomizing(this.window);
338       // Add a keypress listener to the document so that we can quickly exit
339       // customization mode when pressing ESC.
340       document.addEventListener("keypress", this);
342       // Same goes for the menu button - if we're customizing, a click on the
343       // menu button means a quick exit from customization mode.
344       window.PanelUI.hide();
346       let panelHolder = document.getElementById("customization-panelHolder");
347       let panelContextMenu = document.getElementById(kPanelItemContextMenu);
348       this._previousPanelContextMenuParent = panelContextMenu.parentNode;
349       document.getElementById("mainPopupSet").appendChild(panelContextMenu);
350       panelHolder.appendChild(window.PanelUI.overflowFixedList);
352       window.PanelUI.overflowFixedList.setAttribute("customizing", true);
353       window.PanelUI.menuButton.disabled = true;
354       document.getElementById("nav-bar-overflow-button").disabled = true;
356       this._transitioning = true;
358       let customizer = document.getElementById("customization-container");
359       let browser = document.getElementById("browser");
360       browser.hidden = true;
361       customizer.hidden = false;
363       this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP);
365       this.document.documentElement.setAttribute("customizing", true);
367       let customizableToolbars = document.querySelectorAll(
368         "toolbar[customizable=true]:not([autohide=true], [collapsed=true])"
369       );
370       for (let toolbar of customizableToolbars) {
371         toolbar.setAttribute("customizing", true);
372       }
374       this._updateOverflowPanelArrowOffset();
376       // Let everybody in this window know that we're about to customize.
377       CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
379       await this._wrapToolbarItems();
380       this.populatePalette();
382       this._setupPaletteDragging();
384       window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
386       this._updateResetButton();
387       this._updateUndoResetButton();
388       this._updateTouchBarButton();
389       this._updateDensityMenu();
391       this._skipSourceNodeCheck =
392         Services.prefs.getPrefType(kSkipSourceNodePref) ==
393           Ci.nsIPrefBranch.PREF_BOOL &&
394         Services.prefs.getBoolPref(kSkipSourceNodePref);
396       CustomizableUI.addListener(this);
397       this._customizing = true;
398       this._transitioning = false;
400       // Show the palette now that the transition has finished.
401       this.visiblePalette.hidden = false;
402       window.setTimeout(() => {
403         // Force layout reflow to ensure the animation runs,
404         // and make it async so it doesn't affect the timing.
405         this.visiblePalette.clientTop;
406         this.visiblePalette.setAttribute("showing", "true");
407       }, 0);
408       this._updateEmptyPaletteNotice();
410       lazy.AddonManager.addAddonListener(this);
412       this._setupDownloadAutoHideToggle();
414       this._handler.isEnteringCustomizeMode = false;
416       CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
418       if (!this._wantToBeInCustomizeMode) {
419         this.exit();
420       }
421     })().catch(e => {
422       lazy.log.error("Error entering customize mode", e);
423       this._handler.isEnteringCustomizeMode = false;
424       // Exit customize mode to ensure proper clean-up when entering failed.
425       this.exit();
426     });
427   },
429   exit() {
430     this._wantToBeInCustomizeMode = false;
432     if (!this._customizing || this._handler.isExitingCustomizeMode) {
433       return;
434     }
436     // Entering; want to exit once we've done that.
437     if (this._handler.isEnteringCustomizeMode) {
438       lazy.log.debug(
439         "Attempted to exit while we're in the middle of entering. " +
440           "We'll exit after we've entered"
441       );
442       return;
443     }
445     if (this.resetting) {
446       lazy.log.debug(
447         "Attempted to exit while we're resetting. " +
448           "We'll exit after resetting has finished."
449       );
450       return;
451     }
453     this._handler.isExitingCustomizeMode = true;
455     this._translationObserver.disconnect();
457     this._teardownDownloadAutoHideToggle();
459     lazy.AddonManager.removeAddonListener(this);
460     CustomizableUI.removeListener(this);
462     let window = this.window;
463     let document = this.document;
465     document.removeEventListener("keypress", this);
467     this.togglePong(false);
469     // Disable the reset and undo reset buttons while transitioning:
470     let resetButton = this.$("customization-reset-button");
471     let undoResetButton = this.$("customization-undo-reset-button");
472     undoResetButton.hidden = resetButton.disabled = true;
474     this._transitioning = true;
476     this._depopulatePalette();
478     // We need to set this._customizing to false and remove the `customizing`
479     // attribute before removing the tab or else
480     // XULBrowserWindow.onLocationChange might think that we're still in
481     // customization mode and need to exit it for a second time.
482     this._customizing = false;
483     document.documentElement.removeAttribute("customizing");
485     if (this.browser.selectedTab == gTab) {
486       closeGlobalTab();
487     }
489     let customizer = document.getElementById("customization-container");
490     let browser = document.getElementById("browser");
491     customizer.hidden = true;
492     browser.hidden = false;
494     window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
496     this._teardownPaletteDragging();
498     (async () => {
499       await this._unwrapToolbarItems();
501       // And drop all area references.
502       this.areas.clear();
504       // Let everybody in this window know that we're starting to
505       // exit customization mode.
506       CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
508       window.PanelUI.menuButton.disabled = false;
509       let overflowContainer = document.getElementById(
510         "widget-overflow-mainView"
511       ).firstElementChild;
512       overflowContainer.appendChild(window.PanelUI.overflowFixedList);
513       document.getElementById("nav-bar-overflow-button").disabled = false;
514       let panelContextMenu = document.getElementById(kPanelItemContextMenu);
515       this._previousPanelContextMenuParent.appendChild(panelContextMenu);
517       let customizableToolbars = document.querySelectorAll(
518         "toolbar[customizable=true]:not([autohide=true])"
519       );
520       for (let toolbar of customizableToolbars) {
521         toolbar.removeAttribute("customizing");
522       }
524       this._maybeMoveDownloadsButtonToNavBar();
526       delete this._lastLightweightTheme;
527       this._changed = false;
528       this._transitioning = false;
529       this._handler.isExitingCustomizeMode = false;
530       CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
531       CustomizableUI.notifyEndCustomizing(window);
533       if (this._wantToBeInCustomizeMode) {
534         this.enter();
535       }
536     })().catch(e => {
537       lazy.log.error("Error exiting customize mode", e);
538       this._handler.isExitingCustomizeMode = false;
539     });
540   },
542   /**
543    * The overflow panel in customize mode should have its arrow pointing
544    * at the overflow button. In order to do this correctly, we pass the
545    * distance between the inside of window and the middle of the button
546    * to the customize mode markup in which the arrow and panel are placed.
547    */
548   async _updateOverflowPanelArrowOffset() {
549     let currentDensity =
550       this.document.documentElement.getAttribute("uidensity");
551     let offset = await this.window.promiseDocumentFlushed(() => {
552       let overflowButton = this.$("nav-bar-overflow-button");
553       let buttonRect = overflowButton.getBoundingClientRect();
554       let endDistance;
555       if (this.window.RTL_UI) {
556         endDistance = buttonRect.left;
557       } else {
558         endDistance = this.window.innerWidth - buttonRect.right;
559       }
560       return endDistance + buttonRect.width / 2;
561     });
562     if (
563       !this.document ||
564       currentDensity != this.document.documentElement.getAttribute("uidensity")
565     ) {
566       return;
567     }
568     this.$("customization-panelWrapper").style.setProperty(
569       "--panel-arrow-offset",
570       offset + "px"
571     );
572   },
574   _getCustomizableChildForNode(aNode) {
575     // NB: adjusted from _getCustomizableParent to keep that method fast
576     // (it's used during drags), and avoid multiple DOM loops
577     let areas = CustomizableUI.areas;
578     // Caching this length is important because otherwise we'll also iterate
579     // over items we add to the end from within the loop.
580     let numberOfAreas = areas.length;
581     for (let i = 0; i < numberOfAreas; i++) {
582       let area = areas[i];
583       let areaNode = aNode.ownerDocument.getElementById(area);
584       let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode);
585       if (customizationTarget && customizationTarget != areaNode) {
586         areas.push(customizationTarget.id);
587       }
588       let overflowTarget =
589         areaNode && areaNode.getAttribute("default-overflowtarget");
590       if (overflowTarget) {
591         areas.push(overflowTarget);
592       }
593     }
594     areas.push(kPaletteId);
596     while (aNode && aNode.parentNode) {
597       let parent = aNode.parentNode;
598       if (areas.includes(parent.id)) {
599         return aNode;
600       }
601       aNode = parent;
602     }
603     return null;
604   },
606   _promiseWidgetAnimationOut(aNode) {
607     if (
608       this.window.gReduceMotion ||
609       aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" ||
610       (aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") ||
611       (aNode.id == "downloads-button" && aNode.hidden)
612     ) {
613       return null;
614     }
616     let animationNode;
617     if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
618       animationNode = aNode.parentNode;
619     } else {
620       animationNode = aNode;
621     }
622     return new Promise(resolve => {
623       function cleanupCustomizationExit() {
624         resolveAnimationPromise();
625       }
627       function cleanupWidgetAnimationEnd(e) {
628         if (
629           e.animationName == "widget-animate-out" &&
630           e.target.id == animationNode.id
631         ) {
632           resolveAnimationPromise();
633         }
634       }
636       function resolveAnimationPromise() {
637         animationNode.removeEventListener(
638           "animationend",
639           cleanupWidgetAnimationEnd
640         );
641         animationNode.removeEventListener(
642           "customizationending",
643           cleanupCustomizationExit
644         );
645         resolve(animationNode);
646       }
648       // Wait until the next frame before setting the class to ensure
649       // we do start the animation.
650       this.window.requestAnimationFrame(() => {
651         this.window.requestAnimationFrame(() => {
652           animationNode.classList.add("animate-out");
653           animationNode.ownerGlobal.gNavToolbox.addEventListener(
654             "customizationending",
655             cleanupCustomizationExit
656           );
657           animationNode.addEventListener(
658             "animationend",
659             cleanupWidgetAnimationEnd
660           );
661         });
662       });
663     });
664   },
666   async addToToolbar(aNode, aReason) {
667     aNode = this._getCustomizableChildForNode(aNode);
668     if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
669       aNode = aNode.firstElementChild;
670     }
671     let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
672     let animationNode;
673     if (widgetAnimationPromise) {
674       animationNode = await widgetAnimationPromise;
675     }
677     let widgetToAdd = aNode.id;
678     if (
679       CustomizableUI.isSpecialWidget(widgetToAdd) &&
680       aNode.closest("#customization-palette")
681     ) {
682       widgetToAdd = widgetToAdd.match(
683         /^customizableui-special-(spring|spacer|separator)/
684       )[1];
685     }
687     CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
688     lazy.BrowserUsageTelemetry.recordWidgetChange(
689       widgetToAdd,
690       CustomizableUI.AREA_NAVBAR
691     );
692     if (!this._customizing) {
693       CustomizableUI.dispatchToolboxEvent("customizationchange");
694     }
696     // If the user explicitly moves this item, turn off autohide.
697     if (aNode.id == "downloads-button") {
698       Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
699       if (this._customizing) {
700         this._showDownloadsAutoHidePanel();
701       }
702     }
704     if (animationNode) {
705       animationNode.classList.remove("animate-out");
706     }
707   },
709   async addToPanel(aNode, aReason) {
710     aNode = this._getCustomizableChildForNode(aNode);
711     if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
712       aNode = aNode.firstElementChild;
713     }
714     let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
715     let animationNode;
716     if (widgetAnimationPromise) {
717       animationNode = await widgetAnimationPromise;
718     }
720     let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
721     CustomizableUI.addWidgetToArea(aNode.id, panel);
722     lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, panel, aReason);
723     if (!this._customizing) {
724       CustomizableUI.dispatchToolboxEvent("customizationchange");
725     }
727     // If the user explicitly moves this item, turn off autohide.
728     if (aNode.id == "downloads-button") {
729       Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
730       if (this._customizing) {
731         this._showDownloadsAutoHidePanel();
732       }
733     }
735     if (animationNode) {
736       animationNode.classList.remove("animate-out");
737     }
738     if (!this.window.gReduceMotion) {
739       let overflowButton = this.$("nav-bar-overflow-button");
740       overflowButton.setAttribute("animate", "true");
741       overflowButton.addEventListener(
742         "animationend",
743         function onAnimationEnd(event) {
744           if (event.animationName.startsWith("overflow-animation")) {
745             this.removeEventListener("animationend", onAnimationEnd);
746             this.removeAttribute("animate");
747           }
748         }
749       );
750     }
751   },
753   async removeFromArea(aNode, aReason) {
754     aNode = this._getCustomizableChildForNode(aNode);
755     if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
756       aNode = aNode.firstElementChild;
757     }
758     let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
759     let animationNode;
760     if (widgetAnimationPromise) {
761       animationNode = await widgetAnimationPromise;
762     }
764     CustomizableUI.removeWidgetFromArea(aNode.id);
765     lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason);
766     if (!this._customizing) {
767       CustomizableUI.dispatchToolboxEvent("customizationchange");
768     }
770     // If the user explicitly removes this item, turn off autohide.
771     if (aNode.id == "downloads-button") {
772       Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
773       if (this._customizing) {
774         this._showDownloadsAutoHidePanel();
775       }
776     }
777     if (animationNode) {
778       animationNode.classList.remove("animate-out");
779     }
780   },
782   populatePalette() {
783     let fragment = this.document.createDocumentFragment();
784     let toolboxPalette = this.window.gNavToolbox.palette;
786     try {
787       let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
788       for (let widget of unusedWidgets) {
789         let paletteItem = this.makePaletteItem(widget, "palette");
790         if (!paletteItem) {
791           continue;
792         }
793         fragment.appendChild(paletteItem);
794       }
796       let flexSpace = CustomizableUI.createSpecialWidget(
797         "spring",
798         this.document
799       );
800       fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette"));
802       this.visiblePalette.appendChild(fragment);
803       this._stowedPalette = this.window.gNavToolbox.palette;
804       this.window.gNavToolbox.palette = this.visiblePalette;
806       // Now that the palette items are all here, disable all commands.
807       // We do this here rather than directly in `enter` because we
808       // need to do/undo this when we're called from reset(), too.
809       this._updateCommandsDisabledState(true);
810     } catch (ex) {
811       lazy.log.error(ex);
812     }
813   },
815   // XXXunf Maybe this should use -moz-element instead of wrapping the node?
816   //       Would ensure no weird interactions/event handling from original node,
817   //       and makes it possible to put this in a lazy-loaded iframe/real tab
818   //       while still getting rid of the need for overlays.
819   makePaletteItem(aWidget, aPlace) {
820     let widgetNode = aWidget.forWindow(this.window).node;
821     if (!widgetNode) {
822       lazy.log.error(
823         "Widget with id " + aWidget.id + " does not return a valid node"
824       );
825       return null;
826     }
827     // Do not build a palette item for hidden widgets; there's not much to show.
828     if (widgetNode.hidden) {
829       return null;
830     }
832     let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
833     wrapper.appendChild(widgetNode);
834     return wrapper;
835   },
837   _depopulatePalette() {
838     // Quick, undo the command disabling before we depopulate completely:
839     this._updateCommandsDisabledState(false);
841     this.visiblePalette.hidden = true;
842     let paletteChild = this.visiblePalette.firstElementChild;
843     let nextChild;
844     while (paletteChild) {
845       nextChild = paletteChild.nextElementSibling;
846       let itemId = paletteChild.firstElementChild.id;
847       if (CustomizableUI.isSpecialWidget(itemId)) {
848         this.visiblePalette.removeChild(paletteChild);
849       } else {
850         // XXXunf Currently this doesn't destroy the (now unused) node in the
851         //       API provider case. It would be good to do so, but we need to
852         //       keep strong refs to it in CustomizableUI (can't iterate of
853         //       WeakMaps), and there's the question of what behavior
854         //       wrappers should have if consumers keep hold of them.
855         let unwrappedPaletteItem = this.unwrapToolbarItem(paletteChild);
856         this._stowedPalette.appendChild(unwrappedPaletteItem);
857       }
859       paletteChild = nextChild;
860     }
861     this.visiblePalette.hidden = false;
862     this.window.gNavToolbox.palette = this._stowedPalette;
863   },
865   _updateCommandsDisabledState(shouldBeDisabled) {
866     for (let command of this.document.querySelectorAll("command")) {
867       if (!command.id || !this._enabledCommands.has(command.id)) {
868         if (shouldBeDisabled) {
869           if (command.getAttribute("disabled") != "true") {
870             command.setAttribute("disabled", true);
871           } else {
872             command.setAttribute("wasdisabled", true);
873           }
874         } else if (command.getAttribute("wasdisabled") != "true") {
875           command.removeAttribute("disabled");
876         } else {
877           command.removeAttribute("wasdisabled");
878         }
879       }
880     }
881   },
883   isCustomizableItem(aNode) {
884     return (
885       aNode.localName == "toolbarbutton" ||
886       aNode.localName == "toolbaritem" ||
887       aNode.localName == "toolbarseparator" ||
888       aNode.localName == "toolbarspring" ||
889       aNode.localName == "toolbarspacer"
890     );
891   },
893   isWrappedToolbarItem(aNode) {
894     return aNode.localName == "toolbarpaletteitem";
895   },
897   deferredWrapToolbarItem(aNode, aPlace) {
898     return new Promise(resolve => {
899       dispatchFunction(() => {
900         let wrapper = this.wrapToolbarItem(aNode, aPlace);
901         resolve(wrapper);
902       });
903     });
904   },
906   wrapToolbarItem(aNode, aPlace) {
907     if (!this.isCustomizableItem(aNode)) {
908       return aNode;
909     }
910     let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
912     // It's possible that this toolbar node is "mid-flight" and doesn't have
913     // a parent, in which case we skip replacing it. This can happen if a
914     // toolbar item has been dragged into the palette. In that case, we tell
915     // CustomizableUI to remove the widget from its area before putting the
916     // widget in the palette - so the node will have no parent.
917     if (aNode.parentNode) {
918       aNode = aNode.parentNode.replaceChild(wrapper, aNode);
919     }
920     wrapper.appendChild(aNode);
921     return wrapper;
922   },
924   /**
925    * Helper to set the label, either directly or to set up the translation
926    * observer so we can set the label once it's available.
927    */
928   _updateWrapperLabel(aNode, aIsUpdate, aWrapper = aNode.parentElement) {
929     if (aNode.hasAttribute("label")) {
930       aWrapper.setAttribute("title", aNode.getAttribute("label"));
931       aWrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
932     } else if (aNode.hasAttribute("title")) {
933       aWrapper.setAttribute("title", aNode.getAttribute("title"));
934       aWrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
935     } else if (aNode.hasAttribute("data-l10n-id") && !aIsUpdate) {
936       this._translationObserver.observe(aNode, {
937         attributes: true,
938         attributeFilter: ["label", "title"],
939       });
940     }
941   },
943   /**
944    * Called when a node without a label or title is updated.
945    */
946   _onTranslations(aMutations) {
947     for (let mut of aMutations) {
948       let { target } = mut;
949       if (
950         target.parentElement?.localName == "toolbarpaletteitem" &&
951         (target.hasAttribute("label") || mut.target.hasAttribute("title"))
952       ) {
953         this._updateWrapperLabel(target, true);
954       }
955     }
956   },
958   createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
959     let wrapper;
960     if (
961       aIsUpdate &&
962       aNode.parentNode &&
963       aNode.parentNode.localName == "toolbarpaletteitem"
964     ) {
965       wrapper = aNode.parentNode;
966       aPlace = wrapper.getAttribute("place");
967     } else {
968       wrapper = this.document.createXULElement("toolbarpaletteitem");
969       // "place" is used to show the label when it's sitting in the palette.
970       wrapper.setAttribute("place", aPlace);
971     }
973     // Ensure the wrapped item doesn't look like it's in any special state, and
974     // can't be interactved with when in the customization palette.
975     // Note that some buttons opt out of this with the
976     // keepbroadcastattributeswhencustomizing attribute.
977     if (
978       aNode.hasAttribute("command") &&
979       aNode.getAttribute(kKeepBroadcastAttributes) != "true"
980     ) {
981       wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
982       aNode.removeAttribute("command");
983     }
985     if (
986       aNode.hasAttribute("observes") &&
987       aNode.getAttribute(kKeepBroadcastAttributes) != "true"
988     ) {
989       wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
990       aNode.removeAttribute("observes");
991     }
993     if (aNode.getAttribute("checked") == "true") {
994       wrapper.setAttribute("itemchecked", "true");
995       aNode.removeAttribute("checked");
996     }
998     if (aNode.hasAttribute("id")) {
999       wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
1000     }
1002     this._updateWrapperLabel(aNode, aIsUpdate, wrapper);
1004     if (aNode.hasAttribute("flex")) {
1005       wrapper.setAttribute("flex", aNode.getAttribute("flex"));
1006     }
1008     let removable =
1009       aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
1010     wrapper.setAttribute("removable", removable);
1012     // Allow touch events to initiate dragging in customize mode.
1013     // This is only supported on Windows for now.
1014     wrapper.setAttribute("touchdownstartsdrag", "true");
1016     let contextMenuAttrName = "";
1017     if (aNode.getAttribute("context")) {
1018       contextMenuAttrName = "context";
1019     } else if (aNode.getAttribute("contextmenu")) {
1020       contextMenuAttrName = "contextmenu";
1021     }
1022     let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
1023     let contextMenuForPlace =
1024       aPlace == "panel" ? kPanelItemContextMenu : kPaletteItemContextMenu;
1025     if (aPlace != "toolbar") {
1026       wrapper.setAttribute("context", contextMenuForPlace);
1027     }
1028     // Only keep track of the menu if it is non-default.
1029     if (currentContextMenu && currentContextMenu != contextMenuForPlace) {
1030       aNode.setAttribute("wrapped-context", currentContextMenu);
1031       aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName);
1032       aNode.removeAttribute(contextMenuAttrName);
1033     } else if (currentContextMenu == contextMenuForPlace) {
1034       aNode.removeAttribute(contextMenuAttrName);
1035     }
1037     // Only add listeners for newly created wrappers:
1038     if (!aIsUpdate) {
1039       wrapper.addEventListener("mousedown", this);
1040       wrapper.addEventListener("mouseup", this);
1041     }
1043     if (CustomizableUI.isSpecialWidget(aNode.id)) {
1044       wrapper.setAttribute(
1045         "title",
1046         lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
1047       );
1048     }
1050     return wrapper;
1051   },
1053   deferredUnwrapToolbarItem(aWrapper) {
1054     return new Promise(resolve => {
1055       dispatchFunction(() => {
1056         let item = null;
1057         try {
1058           item = this.unwrapToolbarItem(aWrapper);
1059         } catch (ex) {
1060           console.error(ex);
1061         }
1062         resolve(item);
1063       });
1064     });
1065   },
1067   unwrapToolbarItem(aWrapper) {
1068     if (aWrapper.nodeName != "toolbarpaletteitem") {
1069       return aWrapper;
1070     }
1071     aWrapper.removeEventListener("mousedown", this);
1072     aWrapper.removeEventListener("mouseup", this);
1074     let place = aWrapper.getAttribute("place");
1076     let toolbarItem = aWrapper.firstElementChild;
1077     if (!toolbarItem) {
1078       lazy.log.error(
1079         "no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id
1080       );
1081       aWrapper.remove();
1082       return null;
1083     }
1085     if (aWrapper.hasAttribute("itemobserves")) {
1086       toolbarItem.setAttribute(
1087         "observes",
1088         aWrapper.getAttribute("itemobserves")
1089       );
1090     }
1092     if (aWrapper.hasAttribute("itemchecked")) {
1093       toolbarItem.checked = true;
1094     }
1096     if (aWrapper.hasAttribute("itemcommand")) {
1097       let commandID = aWrapper.getAttribute("itemcommand");
1098       toolbarItem.setAttribute("command", commandID);
1100       // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
1101       let command = this.$(commandID);
1102       if (command && command.hasAttribute("disabled")) {
1103         toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
1104       }
1105     }
1107     let wrappedContext = toolbarItem.getAttribute("wrapped-context");
1108     if (wrappedContext) {
1109       let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
1110       toolbarItem.setAttribute(contextAttrName, wrappedContext);
1111       toolbarItem.removeAttribute("wrapped-contextAttrName");
1112       toolbarItem.removeAttribute("wrapped-context");
1113     } else if (place == "panel") {
1114       toolbarItem.setAttribute("context", kPanelItemContextMenu);
1115     }
1117     if (aWrapper.parentNode) {
1118       aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
1119     }
1120     return toolbarItem;
1121   },
1123   async _wrapToolbarItem(aArea) {
1124     let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1125     if (!target || this.areas.has(target)) {
1126       return null;
1127     }
1129     this._addDragHandlers(target);
1130     for (let child of target.children) {
1131       if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
1132         await this.deferredWrapToolbarItem(
1133           child,
1134           CustomizableUI.getPlaceForItem(child)
1135         ).catch(lazy.log.error);
1136       }
1137     }
1138     this.areas.add(target);
1139     return target;
1140   },
1142   _wrapToolbarItemSync(aArea) {
1143     let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1144     if (!target || this.areas.has(target)) {
1145       return null;
1146     }
1148     this._addDragHandlers(target);
1149     try {
1150       for (let child of target.children) {
1151         if (
1152           this.isCustomizableItem(child) &&
1153           !this.isWrappedToolbarItem(child)
1154         ) {
1155           this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1156         }
1157       }
1158     } catch (ex) {
1159       lazy.log.error(ex, ex.stack);
1160     }
1162     this.areas.add(target);
1163     return target;
1164   },
1166   async _wrapToolbarItems() {
1167     for (let area of CustomizableUI.areas) {
1168       await this._wrapToolbarItem(area);
1169     }
1170   },
1172   _addDragHandlers(aTarget) {
1173     // Allow dropping on the padding of the arrow panel.
1174     if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
1175       aTarget = this.$("customization-panelHolder");
1176     }
1177     aTarget.addEventListener("dragstart", this, true);
1178     aTarget.addEventListener("dragover", this, true);
1179     aTarget.addEventListener("dragleave", this, true);
1180     aTarget.addEventListener("drop", this, true);
1181     aTarget.addEventListener("dragend", this, true);
1182   },
1184   _wrapItemsInArea(target) {
1185     for (let child of target.children) {
1186       if (this.isCustomizableItem(child)) {
1187         this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1188       }
1189     }
1190   },
1192   _removeDragHandlers(aTarget) {
1193     // Remove handler from different target if it was added to
1194     // allow dropping on the padding of the arrow panel.
1195     if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
1196       aTarget = this.$("customization-panelHolder");
1197     }
1198     aTarget.removeEventListener("dragstart", this, true);
1199     aTarget.removeEventListener("dragover", this, true);
1200     aTarget.removeEventListener("dragleave", this, true);
1201     aTarget.removeEventListener("drop", this, true);
1202     aTarget.removeEventListener("dragend", this, true);
1203   },
1205   _unwrapItemsInArea(target) {
1206     for (let toolbarItem of target.children) {
1207       if (this.isWrappedToolbarItem(toolbarItem)) {
1208         this.unwrapToolbarItem(toolbarItem);
1209       }
1210     }
1211   },
1213   _unwrapToolbarItems() {
1214     return (async () => {
1215       for (let target of this.areas) {
1216         for (let toolbarItem of target.children) {
1217           if (this.isWrappedToolbarItem(toolbarItem)) {
1218             await this.deferredUnwrapToolbarItem(toolbarItem);
1219           }
1220         }
1221         this._removeDragHandlers(target);
1222       }
1223       this.areas.clear();
1224     })().catch(lazy.log.error);
1225   },
1227   reset() {
1228     this.resetting = true;
1229     // Disable the reset button temporarily while resetting:
1230     let btn = this.$("customization-reset-button");
1231     btn.disabled = true;
1232     return (async () => {
1233       this._depopulatePalette();
1234       await this._unwrapToolbarItems();
1236       CustomizableUI.reset();
1238       await this._wrapToolbarItems();
1239       this.populatePalette();
1241       this._updateResetButton();
1242       this._updateUndoResetButton();
1243       this._updateEmptyPaletteNotice();
1244       this._moveDownloadsButtonToNavBar = false;
1245       this.resetting = false;
1246       if (!this._wantToBeInCustomizeMode) {
1247         this.exit();
1248       }
1249     })().catch(lazy.log.error);
1250   },
1252   undoReset() {
1253     this.resetting = true;
1255     return (async () => {
1256       this._depopulatePalette();
1257       await this._unwrapToolbarItems();
1259       CustomizableUI.undoReset();
1261       await this._wrapToolbarItems();
1262       this.populatePalette();
1264       this._updateResetButton();
1265       this._updateUndoResetButton();
1266       this._updateEmptyPaletteNotice();
1267       this._moveDownloadsButtonToNavBar = false;
1268       this.resetting = false;
1269     })().catch(lazy.log.error);
1270   },
1272   _onToolbarVisibilityChange(aEvent) {
1273     let toolbar = aEvent.target;
1274     if (
1275       aEvent.detail.visible &&
1276       toolbar.getAttribute("customizable") == "true"
1277     ) {
1278       toolbar.setAttribute("customizing", "true");
1279     } else {
1280       toolbar.removeAttribute("customizing");
1281     }
1282     this._onUIChange();
1283   },
1285   onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
1286     this._onUIChange();
1287   },
1289   onWidgetAdded(aWidgetId, aArea, aPosition) {
1290     this._onUIChange();
1291   },
1293   onWidgetRemoved(aWidgetId, aArea) {
1294     this._onUIChange();
1295   },
1297   onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
1298     if (aContainer.ownerGlobal != this.window || this.resetting) {
1299       return;
1300     }
1301     // If we get called for widgets that aren't in the window yet, they might not have
1302     // a parentNode at all.
1303     if (aNodeToChange.parentNode) {
1304       this.unwrapToolbarItem(aNodeToChange.parentNode);
1305     }
1306     if (aSecondaryNode) {
1307       this.unwrapToolbarItem(aSecondaryNode.parentNode);
1308     }
1309   },
1311   onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
1312     if (aContainer.ownerGlobal != this.window || this.resetting) {
1313       return;
1314     }
1315     // If the node is still attached to the container, wrap it again:
1316     if (aNodeToChange.parentNode) {
1317       let place = CustomizableUI.getPlaceForItem(aNodeToChange);
1318       this.wrapToolbarItem(aNodeToChange, place);
1319       if (aSecondaryNode) {
1320         this.wrapToolbarItem(aSecondaryNode, place);
1321       }
1322     } else {
1323       // If not, it got removed.
1325       // If an API-based widget is removed while customizing, append it to the palette.
1326       // The _applyDrop code itself will take care of positioning it correctly, if
1327       // applicable. We need the code to be here so removing widgets using CustomizableUI's
1328       // API also does the right thing (and adds it to the palette)
1329       let widgetId = aNodeToChange.id;
1330       let widget = CustomizableUI.getWidget(widgetId);
1331       if (widget.provider == CustomizableUI.PROVIDER_API) {
1332         let paletteItem = this.makePaletteItem(widget, "palette");
1333         this.visiblePalette.appendChild(paletteItem);
1334       }
1335     }
1336   },
1338   onWidgetDestroyed(aWidgetId) {
1339     let wrapper = this.$("wrapper-" + aWidgetId);
1340     if (wrapper) {
1341       wrapper.remove();
1342     }
1343   },
1345   onWidgetAfterCreation(aWidgetId, aArea) {
1346     // If the node was added to an area, we would have gotten an onWidgetAdded notification,
1347     // plus associated DOM change notifications, so only do stuff for the palette:
1348     if (!aArea) {
1349       let widgetNode = this.$(aWidgetId);
1350       if (widgetNode) {
1351         this.wrapToolbarItem(widgetNode, "palette");
1352       } else {
1353         let widget = CustomizableUI.getWidget(aWidgetId);
1354         this.visiblePalette.appendChild(
1355           this.makePaletteItem(widget, "palette")
1356         );
1357       }
1358     }
1359   },
1361   onAreaNodeRegistered(aArea, aContainer) {
1362     if (aContainer.ownerDocument == this.document) {
1363       this._wrapItemsInArea(aContainer);
1364       this._addDragHandlers(aContainer);
1365       this.areas.add(aContainer);
1366     }
1367   },
1369   onAreaNodeUnregistered(aArea, aContainer, aReason) {
1370     if (
1371       aContainer.ownerDocument == this.document &&
1372       aReason == CustomizableUI.REASON_AREA_UNREGISTERED
1373     ) {
1374       this._unwrapItemsInArea(aContainer);
1375       this._removeDragHandlers(aContainer);
1376       this.areas.delete(aContainer);
1377     }
1378   },
1380   openAddonsManagerThemes() {
1381     this.window.BrowserOpenAddonsMgr("addons://list/theme");
1382   },
1384   getMoreThemes(aEvent) {
1385     aEvent.target.parentNode.parentNode.hidePopup();
1386     let getMoreURL = Services.urlFormatter.formatURLPref(
1387       "lightweightThemes.getMoreURL"
1388     );
1389     this.window.openTrustedLinkIn(getMoreURL, "tab");
1390   },
1392   updateUIDensity(mode) {
1393     this.window.gUIDensity.update(mode);
1394     this._updateOverflowPanelArrowOffset();
1395   },
1397   setUIDensity(mode) {
1398     let win = this.window;
1399     let gUIDensity = win.gUIDensity;
1400     let currentDensity = gUIDensity.getCurrentDensity();
1401     let panel = win.document.getElementById("customization-uidensity-menu");
1403     Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode);
1405     // If the user is choosing a different UI density mode while
1406     // the mode is overriden to Touch, remove the override.
1407     if (currentDensity.overridden) {
1408       Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false);
1409     }
1411     this._onUIChange();
1412     panel.hidePopup();
1413     this._updateOverflowPanelArrowOffset();
1414   },
1416   resetUIDensity() {
1417     this.window.gUIDensity.update();
1418     this._updateOverflowPanelArrowOffset();
1419   },
1421   onUIDensityMenuShowing() {
1422     let win = this.window;
1423     let doc = win.document;
1424     let gUIDensity = win.gUIDensity;
1425     let currentDensity = gUIDensity.getCurrentDensity();
1427     let normalItem = doc.getElementById(
1428       "customization-uidensity-menuitem-normal"
1429     );
1430     normalItem.mode = gUIDensity.MODE_NORMAL;
1432     let items = [normalItem];
1434     let compactItem = doc.getElementById(
1435       "customization-uidensity-menuitem-compact"
1436     );
1437     compactItem.mode = gUIDensity.MODE_COMPACT;
1439     if (Services.prefs.getBoolPref(kCompactModeShowPref)) {
1440       compactItem.hidden = false;
1441       items.push(compactItem);
1442     } else {
1443       compactItem.hidden = true;
1444     }
1446     let touchItem = doc.getElementById(
1447       "customization-uidensity-menuitem-touch"
1448     );
1449     // Touch mode can not be enabled in OSX right now.
1450     if (touchItem) {
1451       touchItem.mode = gUIDensity.MODE_TOUCH;
1452       items.push(touchItem);
1453     }
1455     // Mark the active mode menuitem.
1456     for (let item of items) {
1457       if (item.mode == currentDensity.mode) {
1458         item.setAttribute("aria-checked", "true");
1459         item.setAttribute("active", "true");
1460       } else {
1461         item.removeAttribute("aria-checked");
1462         item.removeAttribute("active");
1463       }
1464     }
1466     // Add menu items for automatically switching to Touch mode in Windows Tablet Mode,
1467     // which is only available in Windows 10.
1468     if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
1469       let spacer = doc.getElementById("customization-uidensity-touch-spacer");
1470       let checkbox = doc.getElementById(
1471         "customization-uidensity-autotouchmode-checkbox"
1472       );
1473       spacer.removeAttribute("hidden");
1474       checkbox.removeAttribute("hidden");
1476       // Show a hint that the UI density was overridden automatically.
1477       if (currentDensity.overridden) {
1478         let sb = Services.strings.createBundle(
1479           "chrome://browser/locale/uiDensity.properties"
1480         );
1481         touchItem.setAttribute(
1482           "acceltext",
1483           sb.GetStringFromName("uiDensity.menuitem-touch.acceltext")
1484         );
1485       } else {
1486         touchItem.removeAttribute("acceltext");
1487       }
1489       let autoTouchMode = Services.prefs.getBoolPref(
1490         win.gUIDensity.autoTouchModePref
1491       );
1492       if (autoTouchMode) {
1493         checkbox.setAttribute("checked", "true");
1494       } else {
1495         checkbox.removeAttribute("checked");
1496       }
1497     }
1498   },
1500   updateAutoTouchMode(checked) {
1501     Services.prefs.setBoolPref("browser.touchmode.auto", checked);
1502     // Re-render the menu items since the active mode might have
1503     // change because of this.
1504     this.onUIDensityMenuShowing();
1505     this._onUIChange();
1506   },
1508   _onUIChange() {
1509     this._changed = true;
1510     if (!this.resetting) {
1511       this._updateResetButton();
1512       this._updateUndoResetButton();
1513       this._updateEmptyPaletteNotice();
1514     }
1515     CustomizableUI.dispatchToolboxEvent("customizationchange");
1516   },
1518   _updateEmptyPaletteNotice() {
1519     let paletteItems =
1520       this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
1521     let whimsyButton = this.$("whimsy-button");
1523     if (
1524       paletteItems.length == 1 &&
1525       paletteItems[0].id.includes("wrapper-customizableui-special-spring")
1526     ) {
1527       whimsyButton.hidden = false;
1528     } else {
1529       this.togglePong(false);
1530       whimsyButton.hidden = true;
1531     }
1532   },
1534   _updateResetButton() {
1535     let btn = this.$("customization-reset-button");
1536     btn.disabled = CustomizableUI.inDefaultState;
1537   },
1539   _updateUndoResetButton() {
1540     let undoResetButton = this.$("customization-undo-reset-button");
1541     undoResetButton.hidden = !CustomizableUI.canUndoReset;
1542   },
1544   _updateTouchBarButton() {
1545     if (AppConstants.platform != "macosx") {
1546       return;
1547     }
1548     let touchBarButton = this.$("customization-touchbar-button");
1549     let touchBarSpacer = this.$("customization-touchbar-spacer");
1551     let isTouchBarInitialized = lazy.gTouchBarUpdater.isTouchBarInitialized();
1552     touchBarButton.hidden = !isTouchBarInitialized;
1553     touchBarSpacer.hidden = !isTouchBarInitialized;
1554   },
1556   _updateDensityMenu() {
1557     // If we're entering Customize Mode, and we're using compact mode,
1558     // then show the button after that.
1559     let gUIDensity = this.window.gUIDensity;
1560     if (gUIDensity.getCurrentDensity().mode == gUIDensity.MODE_COMPACT) {
1561       Services.prefs.setBoolPref(kCompactModeShowPref, true);
1562     }
1564     let button = this.document.getElementById("customization-uidensity-button");
1565     button.hidden =
1566       !Services.prefs.getBoolPref(kCompactModeShowPref) &&
1567       !button.querySelector("#customization-uidensity-menuitem-touch");
1568   },
1570   handleEvent(aEvent) {
1571     switch (aEvent.type) {
1572       case "toolbarvisibilitychange":
1573         this._onToolbarVisibilityChange(aEvent);
1574         break;
1575       case "dragstart":
1576         this._onDragStart(aEvent);
1577         break;
1578       case "dragover":
1579         this._onDragOver(aEvent);
1580         break;
1581       case "drop":
1582         this._onDragDrop(aEvent);
1583         break;
1584       case "dragleave":
1585         this._onDragLeave(aEvent);
1586         break;
1587       case "dragend":
1588         this._onDragEnd(aEvent);
1589         break;
1590       case "mousedown":
1591         this._onMouseDown(aEvent);
1592         break;
1593       case "mouseup":
1594         this._onMouseUp(aEvent);
1595         break;
1596       case "keypress":
1597         if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
1598           this.exit();
1599         }
1600         break;
1601       case "unload":
1602         this.uninit();
1603         break;
1604     }
1605   },
1607   /**
1608    * We handle dragover/drop on the outer palette separately
1609    * to avoid overlap with other drag/drop handlers.
1610    */
1611   _setupPaletteDragging() {
1612     this._addDragHandlers(this.visiblePalette);
1614     this.paletteDragHandler = aEvent => {
1615       let originalTarget = aEvent.originalTarget;
1616       if (
1617         this._isUnwantedDragDrop(aEvent) ||
1618         this.visiblePalette.contains(originalTarget) ||
1619         this.$("customization-panelHolder").contains(originalTarget)
1620       ) {
1621         return;
1622       }
1623       // We have a dragover/drop on the palette.
1624       if (aEvent.type == "dragover") {
1625         this._onDragOver(aEvent, this.visiblePalette);
1626       } else {
1627         this._onDragDrop(aEvent, this.visiblePalette);
1628       }
1629     };
1630     let contentContainer = this.$("customization-content-container");
1631     contentContainer.addEventListener(
1632       "dragover",
1633       this.paletteDragHandler,
1634       true
1635     );
1636     contentContainer.addEventListener("drop", this.paletteDragHandler, true);
1637   },
1639   _teardownPaletteDragging() {
1640     lazy.DragPositionManager.stop();
1641     this._removeDragHandlers(this.visiblePalette);
1643     let contentContainer = this.$("customization-content-container");
1644     contentContainer.removeEventListener(
1645       "dragover",
1646       this.paletteDragHandler,
1647       true
1648     );
1649     contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
1650     delete this.paletteDragHandler;
1651   },
1653   observe(aSubject, aTopic, aData) {
1654     switch (aTopic) {
1655       case "nsPref:changed":
1656         this._updateResetButton();
1657         this._updateUndoResetButton();
1658         if (this._canDrawInTitlebar()) {
1659           this._updateTitlebarCheckbox();
1660         }
1661         break;
1662     }
1663   },
1665   async onInstalled(addon) {
1666     await this.onEnabled(addon);
1667   },
1669   async onEnabled(addon) {
1670     if (addon.type != "theme") {
1671       return;
1672     }
1674     if (this._nextThemeChangeUserTriggered) {
1675       this._onUIChange();
1676     }
1677     this._nextThemeChangeUserTriggered = false;
1678   },
1680   _canDrawInTitlebar() {
1681     return this.window.TabsInTitlebar.systemSupported;
1682   },
1684   _ensureCustomizationPanels() {
1685     let template = this.$("customizationPanel");
1686     template.replaceWith(template.content);
1688     let wrapper = this.$("customModeWrapper");
1689     wrapper.replaceWith(wrapper.content);
1690   },
1692   _updateTitlebarCheckbox() {
1693     let drawInTitlebar = Services.appinfo.drawInTitlebar;
1694     let checkbox = this.$("customization-titlebar-visibility-checkbox");
1695     // Drawing in the titlebar means 'hiding' the titlebar.
1696     // We use the attribute rather than a property because if we're not in
1697     // customize mode the button is hidden and properties don't work.
1698     if (drawInTitlebar) {
1699       checkbox.removeAttribute("checked");
1700     } else {
1701       checkbox.setAttribute("checked", "true");
1702     }
1703   },
1705   toggleTitlebar(aShouldShowTitlebar) {
1706     // Drawing in the titlebar means not showing the titlebar, hence the negation:
1707     Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
1708   },
1710   _getBoundsWithoutFlushing(element) {
1711     return this.window.windowUtils.getBoundsWithoutFlushing(element);
1712   },
1714   _onDragStart(aEvent) {
1715     __dumpDragData(aEvent);
1716     let item = aEvent.target;
1717     while (item && item.localName != "toolbarpaletteitem") {
1718       if (
1719         item.localName == "toolbar" ||
1720         item.id == kPaletteId ||
1721         item.id == "customization-panelHolder"
1722       ) {
1723         return;
1724       }
1725       item = item.parentNode;
1726     }
1728     let draggedItem = item.firstElementChild;
1729     let placeForItem = CustomizableUI.getPlaceForItem(item);
1731     let dt = aEvent.dataTransfer;
1732     let documentId = aEvent.target.ownerDocument.documentElement.id;
1734     dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
1735     dt.effectAllowed = "move";
1737     let itemRect = this._getBoundsWithoutFlushing(draggedItem);
1738     let itemCenter = {
1739       x: itemRect.left + itemRect.width / 2,
1740       y: itemRect.top + itemRect.height / 2,
1741     };
1742     this._dragOffset = {
1743       x: aEvent.clientX - itemCenter.x,
1744       y: aEvent.clientY - itemCenter.y,
1745     };
1747     let toolbarParent = draggedItem.closest("toolbar");
1748     if (toolbarParent) {
1749       let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent);
1750       toolbarParent.style.minHeight = toolbarRect.height + "px";
1751     }
1753     gDraggingInToolbars = new Set();
1755     // Hack needed so that the dragimage will still show the
1756     // item as it appeared before it was hidden.
1757     this._initializeDragAfterMove = () => {
1758       // For automated tests, we sometimes start exiting customization mode
1759       // before this fires, which leaves us with placeholders inserted after
1760       // we've exited. So we need to check that we are indeed customizing.
1761       if (this._customizing && !this._transitioning) {
1762         item.hidden = true;
1763         lazy.DragPositionManager.start(this.window);
1764         let canUsePrevSibling =
1765           placeForItem == "toolbar" || placeForItem == "panel";
1766         if (item.nextElementSibling) {
1767           this._setDragActive(
1768             item.nextElementSibling,
1769             "before",
1770             draggedItem.id,
1771             placeForItem
1772           );
1773           this._dragOverItem = item.nextElementSibling;
1774         } else if (canUsePrevSibling && item.previousElementSibling) {
1775           this._setDragActive(
1776             item.previousElementSibling,
1777             "after",
1778             draggedItem.id,
1779             placeForItem
1780           );
1781           this._dragOverItem = item.previousElementSibling;
1782         }
1783         let currentArea = this._getCustomizableParent(item);
1784         currentArea.setAttribute("draggingover", "true");
1785       }
1786       this._initializeDragAfterMove = null;
1787       this.window.clearTimeout(this._dragInitializeTimeout);
1788     };
1789     this._dragInitializeTimeout = this.window.setTimeout(
1790       this._initializeDragAfterMove,
1791       0
1792     );
1793   },
1795   _onDragOver(aEvent, aOverrideTarget) {
1796     if (this._isUnwantedDragDrop(aEvent)) {
1797       return;
1798     }
1799     if (this._initializeDragAfterMove) {
1800       this._initializeDragAfterMove();
1801     }
1803     __dumpDragData(aEvent);
1805     let document = aEvent.target.ownerDocument;
1806     let documentId = document.documentElement.id;
1807     if (!aEvent.dataTransfer.mozTypesAt(0).length) {
1808       return;
1809     }
1811     let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
1812       kDragDataTypePrefix + documentId,
1813       0
1814     );
1815     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1816     let targetArea = this._getCustomizableParent(
1817       aOverrideTarget || aEvent.currentTarget
1818     );
1819     let originArea = this._getCustomizableParent(draggedWrapper);
1821     // Do nothing if the target or origin are not customizable.
1822     if (!targetArea || !originArea) {
1823       return;
1824     }
1826     // Do nothing if the widget is not allowed to be removed.
1827     if (
1828       targetArea.id == kPaletteId &&
1829       !CustomizableUI.isWidgetRemovable(draggedItemId)
1830     ) {
1831       return;
1832     }
1834     // Do nothing if the widget is not allowed to move to the target area.
1835     if (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
1836       return;
1837     }
1839     let targetAreaType = CustomizableUI.getPlaceForItem(targetArea);
1840     let targetNode = this._getDragOverNode(
1841       aEvent,
1842       targetArea,
1843       targetAreaType,
1844       draggedItemId
1845     );
1847     // We need to determine the place that the widget is being dropped in
1848     // the target.
1849     let dragOverItem, dragValue;
1850     if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) {
1851       // We'll assume if the user is dragging directly over the target, that
1852       // they're attempting to append a child to that target.
1853       dragOverItem =
1854         (targetAreaType == "toolbar"
1855           ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
1856           : targetNode.lastElementChild) || targetNode;
1857       dragValue = "after";
1858     } else {
1859       let targetParent = targetNode.parentNode;
1860       let position = Array.prototype.indexOf.call(
1861         targetParent.children,
1862         targetNode
1863       );
1864       if (position == -1) {
1865         dragOverItem =
1866           targetAreaType == "toolbar"
1867             ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
1868             : targetNode.lastElementChild;
1869         dragValue = "after";
1870       } else {
1871         dragOverItem = targetParent.children[position];
1872         if (targetAreaType == "toolbar") {
1873           // Check if the aDraggedItem is hovered past the first half of dragOverItem
1874           let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
1875           let dropTargetCenter = itemRect.left + itemRect.width / 2;
1876           let existingDir = dragOverItem.getAttribute("dragover");
1877           let dirFactor = this.window.RTL_UI ? -1 : 1;
1878           if (existingDir == "before") {
1879             dropTargetCenter +=
1880               ((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) *
1881               dirFactor;
1882           } else {
1883             dropTargetCenter -=
1884               ((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) *
1885               dirFactor;
1886           }
1887           let before = this.window.RTL_UI
1888             ? aEvent.clientX > dropTargetCenter
1889             : aEvent.clientX < dropTargetCenter;
1890           dragValue = before ? "before" : "after";
1891         } else if (targetAreaType == "panel") {
1892           let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
1893           let dropTargetCenter = itemRect.top + itemRect.height / 2;
1894           let existingDir = dragOverItem.getAttribute("dragover");
1895           if (existingDir == "before") {
1896             dropTargetCenter +=
1897               (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2;
1898           } else {
1899             dropTargetCenter -=
1900               (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2;
1901           }
1902           dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after";
1903         } else {
1904           dragValue = "before";
1905         }
1906       }
1907     }
1909     if (this._dragOverItem && dragOverItem != this._dragOverItem) {
1910       this._cancelDragActive(this._dragOverItem, dragOverItem);
1911     }
1913     if (
1914       dragOverItem != this._dragOverItem ||
1915       dragValue != dragOverItem.getAttribute("dragover")
1916     ) {
1917       if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) {
1918         this._setDragActive(
1919           dragOverItem,
1920           dragValue,
1921           draggedItemId,
1922           targetAreaType
1923         );
1924       }
1925       this._dragOverItem = dragOverItem;
1926       targetArea.setAttribute("draggingover", "true");
1927     }
1929     aEvent.preventDefault();
1930     aEvent.stopPropagation();
1931   },
1933   _onDragDrop(aEvent, aOverrideTarget) {
1934     if (this._isUnwantedDragDrop(aEvent)) {
1935       return;
1936     }
1938     __dumpDragData(aEvent);
1939     this._initializeDragAfterMove = null;
1940     this.window.clearTimeout(this._dragInitializeTimeout);
1942     let targetArea = this._getCustomizableParent(
1943       aOverrideTarget || aEvent.currentTarget
1944     );
1945     let document = aEvent.target.ownerDocument;
1946     let documentId = document.documentElement.id;
1947     let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
1948       kDragDataTypePrefix + documentId,
1949       0
1950     );
1951     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1952     let originArea = this._getCustomizableParent(draggedWrapper);
1953     if (this._dragSizeMap) {
1954       this._dragSizeMap = new WeakMap();
1955     }
1956     // Do nothing if the target area or origin area are not customizable.
1957     if (!targetArea || !originArea) {
1958       return;
1959     }
1960     let targetNode = this._dragOverItem;
1961     let dropDir = targetNode.getAttribute("dragover");
1962     // Need to insert *after* this node if we promised the user that:
1963     if (targetNode != targetArea && dropDir == "after") {
1964       if (targetNode.nextElementSibling) {
1965         targetNode = targetNode.nextElementSibling;
1966       } else {
1967         targetNode = targetArea;
1968       }
1969     }
1970     if (targetNode.tagName == "toolbarpaletteitem") {
1971       targetNode = targetNode.firstElementChild;
1972     }
1974     this._cancelDragActive(this._dragOverItem, null, true);
1976     try {
1977       this._applyDrop(
1978         aEvent,
1979         targetArea,
1980         originArea,
1981         draggedItemId,
1982         targetNode
1983       );
1984     } catch (ex) {
1985       lazy.log.error(ex, ex.stack);
1986     }
1988     // If the user explicitly moves this item, turn off autohide.
1989     if (draggedItemId == "downloads-button") {
1990       Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
1991       this._showDownloadsAutoHidePanel();
1992     }
1993   },
1995   _applyDrop(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
1996     let document = aEvent.target.ownerDocument;
1997     let draggedItem = document.getElementById(aDraggedItemId);
1998     draggedItem.hidden = false;
1999     draggedItem.removeAttribute("mousedown");
2001     let toolbarParent = draggedItem.closest("toolbar");
2002     if (toolbarParent) {
2003       toolbarParent.style.removeProperty("min-height");
2004     }
2006     // Do nothing if the target was dropped onto itself (ie, no change in area
2007     // or position).
2008     if (draggedItem == aTargetNode) {
2009       return;
2010     }
2012     if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
2013       return;
2014     }
2016     // Is the target area the customization palette?
2017     if (aTargetArea.id == kPaletteId) {
2018       // Did we drag from outside the palette?
2019       if (aOriginArea.id !== kPaletteId) {
2020         if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) {
2021           return;
2022         }
2024         CustomizableUI.removeWidgetFromArea(aDraggedItemId, "drag");
2025         lazy.BrowserUsageTelemetry.recordWidgetChange(
2026           aDraggedItemId,
2027           null,
2028           "drag"
2029         );
2030         // Special widgets are removed outright, we can return here:
2031         if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
2032           return;
2033         }
2034       }
2035       draggedItem = draggedItem.parentNode;
2037       // If the target node is the palette itself, just append
2038       if (aTargetNode == this.visiblePalette) {
2039         this.visiblePalette.appendChild(draggedItem);
2040       } else {
2041         // The items in the palette are wrapped, so we need the target node's parent here:
2042         this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
2043       }
2044       this._onDragEnd(aEvent);
2045       return;
2046     }
2048     // Skipintoolbarset items won't really be moved:
2049     let areaCustomizationTarget =
2050       CustomizableUI.getCustomizationTarget(aTargetArea);
2051     if (draggedItem.getAttribute("skipintoolbarset") == "true") {
2052       // These items should never leave their area:
2053       if (aTargetArea != aOriginArea) {
2054         return;
2055       }
2056       let place = draggedItem.parentNode.getAttribute("place");
2057       this.unwrapToolbarItem(draggedItem.parentNode);
2058       if (aTargetNode == areaCustomizationTarget) {
2059         areaCustomizationTarget.appendChild(draggedItem);
2060       } else {
2061         this.unwrapToolbarItem(aTargetNode.parentNode);
2062         areaCustomizationTarget.insertBefore(draggedItem, aTargetNode);
2063         this.wrapToolbarItem(aTargetNode, place);
2064       }
2065       this.wrapToolbarItem(draggedItem, place);
2066       return;
2067     }
2069     // Force creating a new spacer/spring/separator if dragging from the palette
2070     if (
2071       CustomizableUI.isSpecialWidget(aDraggedItemId) &&
2072       aOriginArea.id == kPaletteId
2073     ) {
2074       aDraggedItemId = aDraggedItemId.match(
2075         /^customizableui-special-(spring|spacer|separator)/
2076       )[1];
2077     }
2079     // Is the target the customization area itself? If so, we just add the
2080     // widget to the end of the area.
2081     if (aTargetNode == areaCustomizationTarget) {
2082       CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
2083       lazy.BrowserUsageTelemetry.recordWidgetChange(
2084         aDraggedItemId,
2085         aTargetArea.id,
2086         "drag"
2087       );
2088       this._onDragEnd(aEvent);
2089       return;
2090     }
2092     // We need to determine the place that the widget is being dropped in
2093     // the target.
2094     let placement;
2095     let itemForPlacement = aTargetNode;
2096     // Skip the skipintoolbarset items when determining the place of the item:
2097     while (
2098       itemForPlacement &&
2099       itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
2100       itemForPlacement.parentNode &&
2101       itemForPlacement.parentNode.nodeName == "toolbarpaletteitem"
2102     ) {
2103       itemForPlacement = itemForPlacement.parentNode.nextElementSibling;
2104       if (
2105         itemForPlacement &&
2106         itemForPlacement.nodeName == "toolbarpaletteitem"
2107       ) {
2108         itemForPlacement = itemForPlacement.firstElementChild;
2109       }
2110     }
2111     if (itemForPlacement) {
2112       let targetNodeId =
2113         itemForPlacement.nodeName == "toolbarpaletteitem"
2114           ? itemForPlacement.firstElementChild &&
2115             itemForPlacement.firstElementChild.id
2116           : itemForPlacement.id;
2117       placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
2118     }
2119     if (!placement) {
2120       lazy.log.debug(
2121         "Could not get a position for " +
2122           aTargetNode.nodeName +
2123           "#" +
2124           aTargetNode.id +
2125           "." +
2126           aTargetNode.className
2127       );
2128     }
2129     let position = placement ? placement.position : null;
2131     // Is the target area the same as the origin? Since we've already handled
2132     // the possibility that the target is the customization palette, we know
2133     // that the widget is moving within a customizable area.
2134     if (aTargetArea == aOriginArea) {
2135       CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position);
2136       lazy.BrowserUsageTelemetry.recordWidgetChange(
2137         aDraggedItemId,
2138         aTargetArea.id,
2139         "drag"
2140       );
2141     } else {
2142       CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
2143       lazy.BrowserUsageTelemetry.recordWidgetChange(
2144         aDraggedItemId,
2145         aTargetArea.id,
2146         "drag"
2147       );
2148     }
2150     this._onDragEnd(aEvent);
2152     // If we dropped onto a skipintoolbarset item, manually correct the drop location:
2153     if (aTargetNode != itemForPlacement) {
2154       let draggedWrapper = draggedItem.parentNode;
2155       let container = draggedWrapper.parentNode;
2156       container.insertBefore(draggedWrapper, aTargetNode.parentNode);
2157     }
2158   },
2160   _onDragLeave(aEvent) {
2161     if (this._isUnwantedDragDrop(aEvent)) {
2162       return;
2163     }
2165     __dumpDragData(aEvent);
2167     // When leaving customization areas, cancel the drag on the last dragover item
2168     // We've attached the listener to areas, so aEvent.currentTarget will be the area.
2169     // We don't care about dragleave events fired on descendants of the area,
2170     // so we check that the event's target is the same as the area to which the listener
2171     // was attached.
2172     if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
2173       this._cancelDragActive(this._dragOverItem);
2174       this._dragOverItem = null;
2175     }
2176   },
2178   /**
2179    * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
2180    *
2181    * Note that that means that this function may be called multiple times by a single drag operation.
2182    */
2183   _onDragEnd(aEvent) {
2184     if (this._isUnwantedDragDrop(aEvent)) {
2185       return;
2186     }
2187     this._initializeDragAfterMove = null;
2188     this.window.clearTimeout(this._dragInitializeTimeout);
2189     __dumpDragData(aEvent, "_onDragEnd");
2191     let document = aEvent.target.ownerDocument;
2192     document.documentElement.removeAttribute("customizing-movingItem");
2194     let documentId = document.documentElement.id;
2195     if (!aEvent.dataTransfer.mozTypesAt(0)) {
2196       return;
2197     }
2199     let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
2200       kDragDataTypePrefix + documentId,
2201       0
2202     );
2204     let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
2206     // DraggedWrapper might no longer available if a widget node is
2207     // destroyed after starting (but before stopping) a drag.
2208     if (draggedWrapper) {
2209       draggedWrapper.hidden = false;
2210       draggedWrapper.removeAttribute("mousedown");
2212       let toolbarParent = draggedWrapper.closest("toolbar");
2213       if (toolbarParent) {
2214         toolbarParent.style.removeProperty("min-height");
2215       }
2216     }
2218     if (this._dragOverItem) {
2219       this._cancelDragActive(this._dragOverItem);
2220       this._dragOverItem = null;
2221     }
2222     lazy.DragPositionManager.stop();
2223   },
2225   _isUnwantedDragDrop(aEvent) {
2226     // The synthesized events for tests generated by synthesizePlainDragAndDrop
2227     // and synthesizeDrop in mochitests are used only for testing whether the
2228     // right data is being put into the dataTransfer. Neither cause a real drop
2229     // to occur, so they don't set the source node. There isn't a means of
2230     // testing real drag and drops, so this pref skips the check but it should
2231     // only be set by test code.
2232     if (this._skipSourceNodeCheck) {
2233       return false;
2234     }
2236     /* Discard drag events that originated from a separate window to
2237        prevent content->chrome privilege escalations. */
2238     let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
2239     // mozSourceNode is null in the dragStart event handler or if
2240     // the drag event originated in an external application.
2241     return !mozSourceNode || mozSourceNode.ownerGlobal != this.window;
2242   },
2244   _setDragActive(aItem, aValue, aDraggedItemId, aAreaType) {
2245     if (!aItem) {
2246       return;
2247     }
2249     if (aItem.getAttribute("dragover") != aValue) {
2250       aItem.setAttribute("dragover", aValue);
2252       let window = aItem.ownerGlobal;
2253       let draggedItem = window.document.getElementById(aDraggedItemId);
2254       if (aAreaType == "palette") {
2255         this._setGridDragActive(aItem, draggedItem, aValue);
2256       } else {
2257         let targetArea = this._getCustomizableParent(aItem);
2258         let makeSpaceImmediately = false;
2259         if (!gDraggingInToolbars.has(targetArea.id)) {
2260           gDraggingInToolbars.add(targetArea.id);
2261           let draggedWrapper = this.$("wrapper-" + aDraggedItemId);
2262           let originArea = this._getCustomizableParent(draggedWrapper);
2263           makeSpaceImmediately = originArea == targetArea;
2264         }
2265         let propertyToMeasure = aAreaType == "toolbar" ? "width" : "height";
2266         // Calculate width/height of the item when it'd be dropped in this position.
2267         let borderWidth = this._getDragItemSize(aItem, draggedItem)[
2268           propertyToMeasure
2269         ];
2270         let layoutSide = aAreaType == "toolbar" ? "Inline" : "Block";
2271         let prop, otherProp;
2272         if (aValue == "before") {
2273           prop = "border" + layoutSide + "StartWidth";
2274           otherProp = "border-" + layoutSide.toLowerCase() + "-end-width";
2275         } else {
2276           prop = "border" + layoutSide + "EndWidth";
2277           otherProp = "border-" + layoutSide.toLowerCase() + "-start-width";
2278         }
2279         if (makeSpaceImmediately) {
2280           aItem.setAttribute("notransition", "true");
2281         }
2282         aItem.style[prop] = borderWidth + "px";
2283         aItem.style.removeProperty(otherProp);
2284         if (makeSpaceImmediately) {
2285           // Force a layout flush:
2286           aItem.getBoundingClientRect();
2287           aItem.removeAttribute("notransition");
2288         }
2289       }
2290     }
2291   },
2292   _cancelDragActive(aItem, aNextItem, aNoTransition) {
2293     let currentArea = this._getCustomizableParent(aItem);
2294     if (!currentArea) {
2295       return;
2296     }
2297     let nextArea = aNextItem ? this._getCustomizableParent(aNextItem) : null;
2298     if (currentArea != nextArea) {
2299       currentArea.removeAttribute("draggingover");
2300     }
2301     let areaType = CustomizableUI.getAreaType(currentArea.id);
2302     if (areaType) {
2303       if (aNoTransition) {
2304         aItem.setAttribute("notransition", "true");
2305       }
2306       aItem.removeAttribute("dragover");
2307       // Remove all property values in the case that the end padding
2308       // had been set.
2309       aItem.style.removeProperty("border-inline-start-width");
2310       aItem.style.removeProperty("border-inline-end-width");
2311       aItem.style.removeProperty("border-block-start-width");
2312       aItem.style.removeProperty("border-block-end-width");
2313       if (aNoTransition) {
2314         // Force a layout flush:
2315         aItem.getBoundingClientRect();
2316         aItem.removeAttribute("notransition");
2317       }
2318     } else {
2319       aItem.removeAttribute("dragover");
2320       if (aNextItem) {
2321         if (nextArea == currentArea) {
2322           // No need to do anything if we're still dragging in this area:
2323           return;
2324         }
2325       }
2326       // Otherwise, clear everything out:
2327       let positionManager =
2328         lazy.DragPositionManager.getManagerForArea(currentArea);
2329       positionManager.clearPlaceholders(currentArea, aNoTransition);
2330     }
2331   },
2333   _setGridDragActive(aDragOverNode, aDraggedItem, aValue) {
2334     let targetArea = this._getCustomizableParent(aDragOverNode);
2335     let draggedWrapper = this.$("wrapper-" + aDraggedItem.id);
2336     let originArea = this._getCustomizableParent(draggedWrapper);
2337     let positionManager =
2338       lazy.DragPositionManager.getManagerForArea(targetArea);
2339     let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
2340     positionManager.insertPlaceholder(
2341       targetArea,
2342       aDragOverNode,
2343       draggedSize,
2344       originArea == targetArea
2345     );
2346   },
2348   _getDragItemSize(aDragOverNode, aDraggedItem) {
2349     // Cache it good, cache it real good.
2350     if (!this._dragSizeMap) {
2351       this._dragSizeMap = new WeakMap();
2352     }
2353     if (!this._dragSizeMap.has(aDraggedItem)) {
2354       this._dragSizeMap.set(aDraggedItem, new WeakMap());
2355     }
2356     let itemMap = this._dragSizeMap.get(aDraggedItem);
2357     let targetArea = this._getCustomizableParent(aDragOverNode);
2358     let currentArea = this._getCustomizableParent(aDraggedItem);
2359     // Return the size for this target from cache, if it exists.
2360     let size = itemMap.get(targetArea);
2361     if (size) {
2362       return size;
2363     }
2365     // Calculate size of the item when it'd be dropped in this position.
2366     let currentParent = aDraggedItem.parentNode;
2367     let currentSibling = aDraggedItem.nextElementSibling;
2368     const kAreaType = "cui-areatype";
2369     let areaType, currentType;
2371     if (targetArea != currentArea) {
2372       // Move the widget temporarily next to the placeholder.
2373       aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
2374       // Update the node's areaType.
2375       areaType = CustomizableUI.getAreaType(targetArea.id);
2376       currentType =
2377         aDraggedItem.hasAttribute(kAreaType) &&
2378         aDraggedItem.getAttribute(kAreaType);
2379       if (areaType) {
2380         aDraggedItem.setAttribute(kAreaType, areaType);
2381       }
2382       this.wrapToolbarItem(aDraggedItem, areaType || "palette");
2383       CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
2384     } else {
2385       aDraggedItem.parentNode.hidden = false;
2386     }
2388     // Fetch the new size.
2389     let rect = aDraggedItem.parentNode.getBoundingClientRect();
2390     size = { width: rect.width, height: rect.height };
2391     // Cache the found value of size for this target.
2392     itemMap.set(targetArea, size);
2394     if (targetArea != currentArea) {
2395       this.unwrapToolbarItem(aDraggedItem.parentNode);
2396       // Put the item back into its previous position.
2397       currentParent.insertBefore(aDraggedItem, currentSibling);
2398       // restore the areaType
2399       if (areaType) {
2400         if (currentType === false) {
2401           aDraggedItem.removeAttribute(kAreaType);
2402         } else {
2403           aDraggedItem.setAttribute(kAreaType, currentType);
2404         }
2405       }
2406       this.createOrUpdateWrapper(aDraggedItem, null, true);
2407       CustomizableUI.onWidgetDrag(aDraggedItem.id);
2408     } else {
2409       aDraggedItem.parentNode.hidden = true;
2410     }
2411     return size;
2412   },
2414   _getCustomizableParent(aElement) {
2415     if (aElement) {
2416       // Deal with drag/drop on the padding of the panel.
2417       let containingPanelHolder = aElement.closest(
2418         "#customization-panelHolder"
2419       );
2420       if (containingPanelHolder) {
2421         return containingPanelHolder.querySelector(
2422           "#widget-overflow-fixed-list"
2423         );
2424       }
2425     }
2427     let areas = CustomizableUI.areas;
2428     areas.push(kPaletteId);
2429     return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(","));
2430   },
2432   _getDragOverNode(aEvent, aAreaElement, aAreaType, aDraggedItemId) {
2433     let expectedParent =
2434       CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement;
2435     if (!expectedParent.contains(aEvent.target)) {
2436       return expectedParent;
2437     }
2438     // Offset the drag event's position with the offset to the center of
2439     // the thing we're dragging
2440     let dragX = aEvent.clientX - this._dragOffset.x;
2441     let dragY = aEvent.clientY - this._dragOffset.y;
2443     // Ensure this is within the container
2444     let boundsContainer = expectedParent;
2445     let bounds = this._getBoundsWithoutFlushing(boundsContainer);
2446     dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
2447     dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
2449     let targetNode;
2450     if (aAreaType == "toolbar" || aAreaType == "panel") {
2451       targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
2452       while (targetNode && targetNode.parentNode != expectedParent) {
2453         targetNode = targetNode.parentNode;
2454       }
2455     } else {
2456       let positionManager =
2457         lazy.DragPositionManager.getManagerForArea(aAreaElement);
2458       // Make it relative to the container:
2459       dragX -= bounds.left;
2460       dragY -= bounds.top;
2461       // Find the closest node:
2462       targetNode = positionManager.find(aAreaElement, dragX, dragY);
2463     }
2464     return targetNode || aEvent.target;
2465   },
2467   _onMouseDown(aEvent) {
2468     lazy.log.debug("_onMouseDown");
2469     if (aEvent.button != 0) {
2470       return;
2471     }
2472     let doc = aEvent.target.ownerDocument;
2473     doc.documentElement.setAttribute("customizing-movingItem", true);
2474     let item = this._getWrapper(aEvent.target);
2475     if (item) {
2476       item.setAttribute("mousedown", "true");
2477     }
2478   },
2480   _onMouseUp(aEvent) {
2481     lazy.log.debug("_onMouseUp");
2482     if (aEvent.button != 0) {
2483       return;
2484     }
2485     let doc = aEvent.target.ownerDocument;
2486     doc.documentElement.removeAttribute("customizing-movingItem");
2487     let item = this._getWrapper(aEvent.target);
2488     if (item) {
2489       item.removeAttribute("mousedown");
2490     }
2491   },
2493   _getWrapper(aElement) {
2494     while (aElement && aElement.localName != "toolbarpaletteitem") {
2495       if (aElement.localName == "toolbar") {
2496         return null;
2497       }
2498       aElement = aElement.parentNode;
2499     }
2500     return aElement;
2501   },
2503   _findVisiblePreviousSiblingNode(aReferenceNode) {
2504     while (
2505       aReferenceNode &&
2506       aReferenceNode.localName == "toolbarpaletteitem" &&
2507       aReferenceNode.firstElementChild.hidden
2508     ) {
2509       aReferenceNode = aReferenceNode.previousElementSibling;
2510     }
2511     return aReferenceNode;
2512   },
2514   onPaletteContextMenuShowing(event) {
2515     let isFlexibleSpace = event.target.triggerNode.id.includes(
2516       "wrapper-customizableui-special-spring"
2517     );
2518     event.target.querySelector(".customize-context-addToPanel").disabled =
2519       isFlexibleSpace;
2520   },
2522   onPanelContextMenuShowing(event) {
2523     let inPermanentArea = !!event.target.triggerNode.closest(
2524       "#widget-overflow-fixed-list"
2525     );
2526     let doc = event.target.ownerDocument;
2527     doc.getElementById("customizationPanelItemContextMenuUnpin").hidden =
2528       !inPermanentArea;
2529     doc.getElementById("customizationPanelItemContextMenuPin").hidden =
2530       inPermanentArea;
2532     doc.ownerGlobal.MozXULElement.insertFTLIfNeeded(
2533       "browser/toolbarContextMenu.ftl"
2534     );
2535     event.target.querySelectorAll("[data-lazy-l10n-id]").forEach(el => {
2536       el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
2537       el.removeAttribute("data-lazy-l10n-id");
2538     });
2539   },
2541   _checkForDownloadsClick(event) {
2542     if (
2543       event.target.closest("#wrapper-downloads-button") &&
2544       event.button == 0
2545     ) {
2546       event.view.gCustomizeMode._showDownloadsAutoHidePanel();
2547     }
2548   },
2550   _setupDownloadAutoHideToggle() {
2551     this.window.addEventListener("click", this._checkForDownloadsClick, true);
2552   },
2554   _teardownDownloadAutoHideToggle() {
2555     this.window.removeEventListener(
2556       "click",
2557       this._checkForDownloadsClick,
2558       true
2559     );
2560     this.$(kDownloadAutohidePanelId).hidePopup();
2561   },
2563   _maybeMoveDownloadsButtonToNavBar() {
2564     // If the user toggled the autohide checkbox while the item was in the
2565     // palette, and hasn't moved it since, move the item to the default
2566     // location in the navbar for them.
2567     if (
2568       !CustomizableUI.getPlacementOfWidget("downloads-button") &&
2569       this._moveDownloadsButtonToNavBar &&
2570       this.window.DownloadsButton.autoHideDownloadsButton
2571     ) {
2572       let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar");
2573       let insertionPoint = navbarPlacements.indexOf("urlbar-container");
2574       while (++insertionPoint < navbarPlacements.length) {
2575         let widget = navbarPlacements[insertionPoint];
2576         // If we find a non-searchbar, non-spacer node, break out of the loop:
2577         if (
2578           widget != "search-container" &&
2579           !(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring"))
2580         ) {
2581           break;
2582         }
2583       }
2584       CustomizableUI.addWidgetToArea(
2585         "downloads-button",
2586         "nav-bar",
2587         insertionPoint
2588       );
2589       lazy.BrowserUsageTelemetry.recordWidgetChange(
2590         "downloads-button",
2591         "nav-bar",
2592         "move-downloads"
2593       );
2594     }
2595   },
2597   async _showDownloadsAutoHidePanel() {
2598     let doc = this.document;
2599     let panel = doc.getElementById(kDownloadAutohidePanelId);
2600     panel.hidePopup();
2601     let button = doc.getElementById("downloads-button");
2602     // We don't show the tooltip if the button is in the panel.
2603     if (button.closest("#widget-overflow-fixed-list")) {
2604       return;
2605     }
2607     let offsetX = 0,
2608       offsetY = 0;
2609     let panelOnTheLeft = false;
2610     let toolbarContainer = button.closest("toolbar");
2611     if (toolbarContainer && toolbarContainer.id == "nav-bar") {
2612       let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar");
2613       if (
2614         navbarWidgets.indexOf("urlbar-container") <=
2615         navbarWidgets.indexOf("downloads-button")
2616       ) {
2617         panelOnTheLeft = true;
2618       }
2619     } else {
2620       await this.window.promiseDocumentFlushed(() => {});
2622       if (!this._customizing || !this._wantToBeInCustomizeMode) {
2623         return;
2624       }
2625       let buttonBounds = this._getBoundsWithoutFlushing(button);
2626       let windowBounds = this._getBoundsWithoutFlushing(doc.documentElement);
2627       panelOnTheLeft =
2628         buttonBounds.left + buttonBounds.width / 2 > windowBounds.width / 2;
2629     }
2630     let position;
2631     if (panelOnTheLeft) {
2632       // Tested in RTL, these get inverted automatically, so this does the
2633       // right thing without taking RTL into account explicitly.
2634       position = "topleft topright";
2635       if (toolbarContainer) {
2636         offsetX = 8;
2637       }
2638     } else {
2639       position = "topright topleft";
2640       if (toolbarContainer) {
2641         offsetX = -8;
2642       }
2643     }
2645     let checkbox = doc.getElementById(kDownloadAutohideCheckboxId);
2646     if (this.window.DownloadsButton.autoHideDownloadsButton) {
2647       checkbox.setAttribute("checked", "true");
2648     } else {
2649       checkbox.removeAttribute("checked");
2650     }
2652     // We don't use the icon to anchor because it might be resizing because of
2653     // the animations for drag/drop. Hence the use of offsets.
2654     panel.openPopup(button, position, offsetX, offsetY);
2655   },
2657   onDownloadsAutoHideChange(event) {
2658     let checkbox = event.target.ownerDocument.getElementById(
2659       kDownloadAutohideCheckboxId
2660     );
2661     Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked);
2662     // Ensure we move the button (back) after the user leaves customize mode.
2663     event.view.gCustomizeMode._moveDownloadsButtonToNavBar = checkbox.checked;
2664   },
2666   customizeTouchBar() {
2667     let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService(
2668       Ci.nsITouchBarUpdater
2669     );
2670     updater.enterCustomizeMode();
2671   },
2673   togglePong(enabled) {
2674     // It's possible we're toggling for a reason other than hitting
2675     // the button (we might be exiting, for example), so make sure that
2676     // the state and checkbox are in sync.
2677     let whimsyButton = this.$("whimsy-button");
2678     whimsyButton.checked = enabled;
2680     if (enabled) {
2681       this.visiblePalette.setAttribute("whimsypong", "true");
2682       this.pongArena.hidden = false;
2683       if (!this.uninitWhimsy) {
2684         this.uninitWhimsy = this.whimsypong();
2685       }
2686     } else {
2687       this.visiblePalette.removeAttribute("whimsypong");
2688       if (this.uninitWhimsy) {
2689         this.uninitWhimsy();
2690         this.uninitWhimsy = null;
2691       }
2692       this.pongArena.hidden = true;
2693     }
2694   },
2696   whimsypong() {
2697     function update() {
2698       updateBall();
2699       updatePlayers();
2700     }
2702     function updateBall() {
2703       if (ball[1] <= 0 || ball[1] >= gameSide) {
2704         if (
2705           (ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) ||
2706           (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth))
2707         ) {
2708           updateScore(ball[1] <= 0 ? 0 : 1);
2709         } else {
2710           if (
2711             (ball[1] <= 0 &&
2712               (ball[0] - p1 < paddleEdge ||
2713                 p1 + paddleWidth - ball[0] < paddleEdge)) ||
2714             (ball[1] >= gameSide &&
2715               (ball[0] - p2 < paddleEdge ||
2716                 p2 + paddleWidth - ball[0] < paddleEdge))
2717           ) {
2718             ballDxDy[0] *= Math.random() + 1.3;
2719             ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6);
2720             if (Math.abs(ballDxDy[0]) == 6) {
2721               ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random();
2722             }
2723           } else {
2724             ballDxDy[0] /= 1.1;
2725           }
2726           ballDxDy[1] *= -1;
2727           ball[1] = ball[1] <= 0 ? 0 : gameSide;
2728         }
2729       }
2730       ball = [
2731         Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0),
2732         Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0),
2733       ];
2734       if (ball[0] <= 0 || ball[0] >= gameSide) {
2735         ballDxDy[0] *= -1;
2736       }
2737     }
2739     function updatePlayers() {
2740       if (keydown) {
2741         let p1Adj = 1;
2742         if (
2743           (keydown == 37 && !window.RTL_UI) ||
2744           (keydown == 39 && window.RTL_UI)
2745         ) {
2746           p1Adj = -1;
2747         }
2748         p1 += p1Adj * 10 * keydownAdj;
2749       }
2751       let sign = Math.sign(ballDxDy[0]);
2752       if (
2753         (sign > 0 && ball[0] > p2 + paddleWidth / 2) ||
2754         (sign < 0 && ball[0] < p2 + paddleWidth / 2)
2755       ) {
2756         p2 += sign * 3;
2757       } else if (
2758         (sign > 0 && ball[0] > p2 + paddleWidth / 1.1) ||
2759         (sign < 0 && ball[0] < p2 + paddleWidth / 1.1)
2760       ) {
2761         p2 += sign * 9;
2762       }
2764       if (score >= winScore) {
2765         p1 = ball[0];
2766         p2 = ball[0];
2767       }
2768       p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0);
2769       p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0);
2770     }
2772     function updateScore(adj) {
2773       if (adj) {
2774         score += adj;
2775       } else if (--lives == 0) {
2776         quit = true;
2777       }
2778       ball = ballDef.slice();
2779       ballDxDy = ballDxDyDef.slice();
2780       ballDxDy[1] *= score / winScore + 1;
2781     }
2783     function draw() {
2784       let xAdj = window.RTL_UI ? -1 : 1;
2785       elements["wp-player1"].style.transform =
2786         "translate(" + xAdj * p1 + "px, -37px)";
2787       elements["wp-player2"].style.transform =
2788         "translate(" + xAdj * p2 + "px, " + gameSide + "px)";
2789       elements["wp-ball"].style.transform =
2790         "translate(" + xAdj * ball[0] + "px, " + ball[1] + "px)";
2791       elements["wp-score"].textContent = score;
2792       elements["wp-lives"].setAttribute("lives", lives);
2793       if (score >= winScore) {
2794         let arena = elements.arena;
2795         let image = "url(chrome://browser/skin/customizableui/whimsy.png)";
2796         let position = `${
2797           (window.RTL_UI ? gameSide : 0) + xAdj * ball[0] - 10
2798         }px ${ball[1] - 10}px`;
2799         let repeat = "no-repeat";
2800         let size = "20px";
2801         if (arena.style.backgroundImage) {
2802           if (arena.style.backgroundImage.split(",").length >= 160) {
2803             quit = true;
2804           }
2806           image += ", " + arena.style.backgroundImage;
2807           position += ", " + arena.style.backgroundPosition;
2808           repeat += ", " + arena.style.backgroundRepeat;
2809           size += ", " + arena.style.backgroundSize;
2810         }
2811         arena.style.backgroundImage = image;
2812         arena.style.backgroundPosition = position;
2813         arena.style.backgroundRepeat = repeat;
2814         arena.style.backgroundSize = size;
2815       }
2816     }
2818     function onkeydown(event) {
2819       keys.push(event.which);
2820       if (keys.length > 10) {
2821         keys.shift();
2822         let codeEntered = true;
2823         for (let i = 0; i < keys.length; i++) {
2824           if (keys[i] != keysCode[i]) {
2825             codeEntered = false;
2826             break;
2827           }
2828         }
2829         if (codeEntered) {
2830           elements.arena.setAttribute("kcode", "true");
2831           let spacer = document.querySelector(
2832             "#customization-palette > toolbarpaletteitem"
2833           );
2834           spacer.setAttribute("kcode", "true");
2835         }
2836       }
2837       if (event.which == 37 /* left */ || event.which == 39 /* right */) {
2838         keydown = event.which;
2839         keydownAdj *= 1.05;
2840       }
2841     }
2843     function onkeyup(event) {
2844       if (event.which == 37 || event.which == 39) {
2845         keydownAdj = 1;
2846         keydown = 0;
2847       }
2848     }
2850     function uninit() {
2851       document.removeEventListener("keydown", onkeydown);
2852       document.removeEventListener("keyup", onkeyup);
2853       if (rAFHandle) {
2854         window.cancelAnimationFrame(rAFHandle);
2855       }
2856       let arena = elements.arena;
2857       while (arena.firstChild) {
2858         arena.firstChild.remove();
2859       }
2860       arena.removeAttribute("score");
2861       arena.removeAttribute("lives");
2862       arena.removeAttribute("kcode");
2863       arena.style.removeProperty("background-image");
2864       arena.style.removeProperty("background-position");
2865       arena.style.removeProperty("background-repeat");
2866       arena.style.removeProperty("background-size");
2867       let spacer = document.querySelector(
2868         "#customization-palette > toolbarpaletteitem"
2869       );
2870       spacer.removeAttribute("kcode");
2871       elements = null;
2872       document = null;
2873       quit = true;
2874     }
2876     if (this.uninitWhimsy) {
2877       return this.uninitWhimsy;
2878     }
2880     let ballDef = [10, 10];
2881     let ball = [10, 10];
2882     let ballDxDyDef = [2, 2];
2883     let ballDxDy = [2, 2];
2884     let score = 0;
2885     let p1 = 0;
2886     let p2 = 10;
2887     let gameSide = 300;
2888     let paddleEdge = 30;
2889     let paddleWidth = 84;
2890     let keydownAdj = 1;
2891     let keydown = 0;
2892     let keys = [];
2893     let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
2894     let lives = 5;
2895     let winScore = 11;
2896     let quit = false;
2897     let document = this.document;
2898     let rAFHandle = 0;
2899     let elements = {
2900       arena: document.getElementById("customization-pong-arena"),
2901     };
2903     document.addEventListener("keydown", onkeydown);
2904     document.addEventListener("keyup", onkeyup);
2906     for (let id of ["player1", "player2", "ball", "score", "lives"]) {
2907       let el = document.createXULElement("box");
2908       el.id = "wp-" + id;
2909       elements[el.id] = elements.arena.appendChild(el);
2910     }
2912     let spacer = this.visiblePalette.querySelector("toolbarpaletteitem");
2913     for (let player of ["#wp-player1", "#wp-player2"]) {
2914       let val = "-moz-element(#" + spacer.id + ") no-repeat";
2915       elements.arena.querySelector(player).style.background = val;
2916     }
2918     let window = this.window;
2919     rAFHandle = window.requestAnimationFrame(function animate() {
2920       update();
2921       draw();
2922       if (quit) {
2923         elements["wp-score"].textContent = score;
2924         elements["wp-lives"] &&
2925           elements["wp-lives"].setAttribute("lives", lives);
2926         elements.arena.setAttribute("score", score);
2927         elements.arena.setAttribute("lives", lives);
2928       } else {
2929         rAFHandle = window.requestAnimationFrame(animate);
2930       }
2931     });
2933     return uninit;
2934   },
2937 function __dumpDragData(aEvent, caller) {
2938   if (!gDebug) {
2939     return;
2940   }
2941   let str =
2942     "Dumping drag data (" +
2943     (caller ? caller + " in " : "") +
2944     "CustomizeMode.sys.mjs) {\n";
2945   str += "  type: " + aEvent.type + "\n";
2946   for (let el of ["target", "currentTarget", "relatedTarget"]) {
2947     if (aEvent[el]) {
2948       str +=
2949         "  " +
2950         el +
2951         ": " +
2952         aEvent[el] +
2953         "(localName=" +
2954         aEvent[el].localName +
2955         "; id=" +
2956         aEvent[el].id +
2957         ")\n";
2958     }
2959   }
2960   for (let prop in aEvent.dataTransfer) {
2961     if (typeof aEvent.dataTransfer[prop] != "function") {
2962       str +=
2963         "  dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
2964     }
2965   }
2966   str += "}";
2967   lazy.log.debug(str);
2970 function dispatchFunction(aFunc) {
2971   Services.tm.dispatchToMainThread(aFunc);