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";
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",
34 XPCOMUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
36 "chrome://browser/locale/customizableui/customizableWidgets.properties";
37 return Services.strings.createBundle(kUrl);
39 XPCOMUtils.defineLazyServiceGetter(
42 "@mozilla.org/widget/touchbarupdater;1",
47 XPCOMUtils.defineLazyGetter(lazy, "log", () => {
48 let { ConsoleAPI } = ChromeUtils.importESModule(
49 "resource://gre/modules/Console.sys.mjs"
51 gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
52 let consoleOptions = {
53 maxLogLevel: gDebug ? "all" : "log",
54 prefix: "CustomizeMode",
56 return new ConsoleAPI(consoleOptions);
59 var gDraggingInToolbars;
63 function closeGlobalTab() {
64 let win = gTab.ownerGlobal;
65 if (win.gBrowser.browsers.length == 1) {
68 win.gBrowser.removeTab(gTab, { animate: true });
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.
79 gTab.linkedBrowser != aBrowser ||
80 aLocation.spec == "about:blank"
85 unregisterGlobalTab();
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");
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)
109 this._ensureCustomizationPanels();
111 let content = this.$("customization-content-container");
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),
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);
131 this.$("customization-titlebar-visibility-checkbox").hidden = true;
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 = {
145 _transitioning: false,
148 // areas is used to cache the customizable areas when in customization mode.
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,
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([
168 "cmd_newNavigatorTab",
169 "cmd_newNavigatorTabNoEvent",
172 "cmd_quitApplication",
176 "Browser:NewUserContextTab",
177 "Tools:PrivateBrowsing",
183 return this.window.CustomizationHandler;
187 if (this._canDrawInTitlebar()) {
188 Services.prefs.removeObserver(kDrawInTitlebarPref, this);
190 Services.prefs.removeObserver(kBookmarksToolbarPref, this);
194 return this.document.getElementById(id);
199 this._handler.isEnteringCustomizeMode ||
200 this._handler.isExitingCustomizeMode
202 this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
205 if (this._customizing) {
223 gTab.setAttribute("customizemode", "true");
224 lazy.SessionStore.persistTabAttribute("customizemode");
226 if (gTab.linkedPanel) {
227 gTab.linkedBrowser.stop();
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);
242 win.gCustomizeMode.enter();
247 if (!this.window.toolbar.visible) {
248 let w = lazy.URILoadingHelper.getTargetWindow(this.window, {
252 w.gCustomizeMode.enter();
256 Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
257 w = lazy.URILoadingHelper.getTargetWindow(this.window, {
260 w.gCustomizeMode.enter();
262 Services.obs.addObserver(obs, "browser-delayed-startup-finished");
263 this.window.openTrustedLinkIn("about:newtab", "window");
266 this._wantToBeInCustomizeMode = true;
268 if (this._customizing || this._handler.isEnteringCustomizeMode) {
272 // Exiting; want to re-enter once we've done that.
273 if (this._handler.isExitingCustomizeMode) {
275 "Attempted to enter while we're in the middle of exiting. " +
276 "We'll exit after we've entered"
283 this.browser.addTab("about:blank", {
285 forceNotRemote: true,
288 Services.scriptSecurityManager.getSystemPrincipal(),
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;
299 gTab.ownerGlobal.focus();
300 if (gTab.ownerDocument != this.document) {
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");
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"
328 Services.obs.addObserver(
329 delayedStartupObserver,
330 "browser-delayed-startup-finished"
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])"
370 for (let toolbar of customizableToolbars) {
371 toolbar.setAttribute("customizing", true);
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");
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) {
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.
430 this._wantToBeInCustomizeMode = false;
432 if (!this._customizing || this._handler.isExitingCustomizeMode) {
436 // Entering; want to exit once we've done that.
437 if (this._handler.isEnteringCustomizeMode) {
439 "Attempted to exit while we're in the middle of entering. " +
440 "We'll exit after we've entered"
445 if (this.resetting) {
447 "Attempted to exit while we're resetting. " +
448 "We'll exit after resetting has finished."
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) {
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();
499 await this._unwrapToolbarItems();
501 // And drop all area references.
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"
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])"
520 for (let toolbar of customizableToolbars) {
521 toolbar.removeAttribute("customizing");
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) {
537 lazy.log.error("Error exiting customize mode", e);
538 this._handler.isExitingCustomizeMode = false;
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.
548 async _updateOverflowPanelArrowOffset() {
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();
555 if (this.window.RTL_UI) {
556 endDistance = buttonRect.left;
558 endDistance = this.window.innerWidth - buttonRect.right;
560 return endDistance + buttonRect.width / 2;
564 currentDensity != this.document.documentElement.getAttribute("uidensity")
568 this.$("customization-panelWrapper").style.setProperty(
569 "--panel-arrow-offset",
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++) {
583 let areaNode = aNode.ownerDocument.getElementById(area);
584 let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode);
585 if (customizationTarget && customizationTarget != areaNode) {
586 areas.push(customizationTarget.id);
589 areaNode && areaNode.getAttribute("default-overflowtarget");
590 if (overflowTarget) {
591 areas.push(overflowTarget);
594 areas.push(kPaletteId);
596 while (aNode && aNode.parentNode) {
597 let parent = aNode.parentNode;
598 if (areas.includes(parent.id)) {
606 _promiseWidgetAnimationOut(aNode) {
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)
617 if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
618 animationNode = aNode.parentNode;
620 animationNode = aNode;
622 return new Promise(resolve => {
623 function cleanupCustomizationExit() {
624 resolveAnimationPromise();
627 function cleanupWidgetAnimationEnd(e) {
629 e.animationName == "widget-animate-out" &&
630 e.target.id == animationNode.id
632 resolveAnimationPromise();
636 function resolveAnimationPromise() {
637 animationNode.removeEventListener(
639 cleanupWidgetAnimationEnd
641 animationNode.removeEventListener(
642 "customizationending",
643 cleanupCustomizationExit
645 resolve(animationNode);
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
657 animationNode.addEventListener(
659 cleanupWidgetAnimationEnd
666 async addToToolbar(aNode, aReason) {
667 aNode = this._getCustomizableChildForNode(aNode);
668 if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
669 aNode = aNode.firstElementChild;
671 let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
673 if (widgetAnimationPromise) {
674 animationNode = await widgetAnimationPromise;
677 let widgetToAdd = aNode.id;
679 CustomizableUI.isSpecialWidget(widgetToAdd) &&
680 aNode.closest("#customization-palette")
682 widgetToAdd = widgetToAdd.match(
683 /^customizableui-special-(spring|spacer|separator)/
687 CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
688 lazy.BrowserUsageTelemetry.recordWidgetChange(
690 CustomizableUI.AREA_NAVBAR
692 if (!this._customizing) {
693 CustomizableUI.dispatchToolboxEvent("customizationchange");
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();
705 animationNode.classList.remove("animate-out");
709 async addToPanel(aNode, aReason) {
710 aNode = this._getCustomizableChildForNode(aNode);
711 if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
712 aNode = aNode.firstElementChild;
714 let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
716 if (widgetAnimationPromise) {
717 animationNode = await widgetAnimationPromise;
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");
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();
736 animationNode.classList.remove("animate-out");
738 if (!this.window.gReduceMotion) {
739 let overflowButton = this.$("nav-bar-overflow-button");
740 overflowButton.setAttribute("animate", "true");
741 overflowButton.addEventListener(
743 function onAnimationEnd(event) {
744 if (event.animationName.startsWith("overflow-animation")) {
745 this.removeEventListener("animationend", onAnimationEnd);
746 this.removeAttribute("animate");
753 async removeFromArea(aNode, aReason) {
754 aNode = this._getCustomizableChildForNode(aNode);
755 if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
756 aNode = aNode.firstElementChild;
758 let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
760 if (widgetAnimationPromise) {
761 animationNode = await widgetAnimationPromise;
764 CustomizableUI.removeWidgetFromArea(aNode.id);
765 lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason);
766 if (!this._customizing) {
767 CustomizableUI.dispatchToolboxEvent("customizationchange");
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();
778 animationNode.classList.remove("animate-out");
783 let fragment = this.document.createDocumentFragment();
784 let toolboxPalette = this.window.gNavToolbox.palette;
787 let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
788 for (let widget of unusedWidgets) {
789 let paletteItem = this.makePaletteItem(widget, "palette");
793 fragment.appendChild(paletteItem);
796 let flexSpace = CustomizableUI.createSpecialWidget(
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);
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;
823 "Widget with id " + aWidget.id + " does not return a valid node"
827 // Do not build a palette item for hidden widgets; there's not much to show.
828 if (widgetNode.hidden) {
832 let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
833 wrapper.appendChild(widgetNode);
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;
844 while (paletteChild) {
845 nextChild = paletteChild.nextElementSibling;
846 let itemId = paletteChild.firstElementChild.id;
847 if (CustomizableUI.isSpecialWidget(itemId)) {
848 this.visiblePalette.removeChild(paletteChild);
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);
859 paletteChild = nextChild;
861 this.visiblePalette.hidden = false;
862 this.window.gNavToolbox.palette = this._stowedPalette;
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);
872 command.setAttribute("wasdisabled", true);
874 } else if (command.getAttribute("wasdisabled") != "true") {
875 command.removeAttribute("disabled");
877 command.removeAttribute("wasdisabled");
883 isCustomizableItem(aNode) {
885 aNode.localName == "toolbarbutton" ||
886 aNode.localName == "toolbaritem" ||
887 aNode.localName == "toolbarseparator" ||
888 aNode.localName == "toolbarspring" ||
889 aNode.localName == "toolbarspacer"
893 isWrappedToolbarItem(aNode) {
894 return aNode.localName == "toolbarpaletteitem";
897 deferredWrapToolbarItem(aNode, aPlace) {
898 return new Promise(resolve => {
899 dispatchFunction(() => {
900 let wrapper = this.wrapToolbarItem(aNode, aPlace);
906 wrapToolbarItem(aNode, aPlace) {
907 if (!this.isCustomizableItem(aNode)) {
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);
920 wrapper.appendChild(aNode);
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.
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, {
938 attributeFilter: ["label", "title"],
944 * Called when a node without a label or title is updated.
946 _onTranslations(aMutations) {
947 for (let mut of aMutations) {
948 let { target } = mut;
950 target.parentElement?.localName == "toolbarpaletteitem" &&
951 (target.hasAttribute("label") || mut.target.hasAttribute("title"))
953 this._updateWrapperLabel(target, true);
958 createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
963 aNode.parentNode.localName == "toolbarpaletteitem"
965 wrapper = aNode.parentNode;
966 aPlace = wrapper.getAttribute("place");
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);
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.
978 aNode.hasAttribute("command") &&
979 aNode.getAttribute(kKeepBroadcastAttributes) != "true"
981 wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
982 aNode.removeAttribute("command");
986 aNode.hasAttribute("observes") &&
987 aNode.getAttribute(kKeepBroadcastAttributes) != "true"
989 wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
990 aNode.removeAttribute("observes");
993 if (aNode.getAttribute("checked") == "true") {
994 wrapper.setAttribute("itemchecked", "true");
995 aNode.removeAttribute("checked");
998 if (aNode.hasAttribute("id")) {
999 wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
1002 this._updateWrapperLabel(aNode, aIsUpdate, wrapper);
1004 if (aNode.hasAttribute("flex")) {
1005 wrapper.setAttribute("flex", aNode.getAttribute("flex"));
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";
1022 let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
1023 let contextMenuForPlace =
1024 aPlace == "panel" ? kPanelItemContextMenu : kPaletteItemContextMenu;
1025 if (aPlace != "toolbar") {
1026 wrapper.setAttribute("context", contextMenuForPlace);
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);
1037 // Only add listeners for newly created wrappers:
1039 wrapper.addEventListener("mousedown", this);
1040 wrapper.addEventListener("mouseup", this);
1043 if (CustomizableUI.isSpecialWidget(aNode.id)) {
1044 wrapper.setAttribute(
1046 lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
1053 deferredUnwrapToolbarItem(aWrapper) {
1054 return new Promise(resolve => {
1055 dispatchFunction(() => {
1058 item = this.unwrapToolbarItem(aWrapper);
1067 unwrapToolbarItem(aWrapper) {
1068 if (aWrapper.nodeName != "toolbarpaletteitem") {
1071 aWrapper.removeEventListener("mousedown", this);
1072 aWrapper.removeEventListener("mouseup", this);
1074 let place = aWrapper.getAttribute("place");
1076 let toolbarItem = aWrapper.firstElementChild;
1079 "no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id
1085 if (aWrapper.hasAttribute("itemobserves")) {
1086 toolbarItem.setAttribute(
1088 aWrapper.getAttribute("itemobserves")
1092 if (aWrapper.hasAttribute("itemchecked")) {
1093 toolbarItem.checked = true;
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"));
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);
1117 if (aWrapper.parentNode) {
1118 aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
1123 async _wrapToolbarItem(aArea) {
1124 let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1125 if (!target || this.areas.has(target)) {
1129 this._addDragHandlers(target);
1130 for (let child of target.children) {
1131 if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) {
1132 await this.deferredWrapToolbarItem(
1134 CustomizableUI.getPlaceForItem(child)
1135 ).catch(lazy.log.error);
1138 this.areas.add(target);
1142 _wrapToolbarItemSync(aArea) {
1143 let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window);
1144 if (!target || this.areas.has(target)) {
1148 this._addDragHandlers(target);
1150 for (let child of target.children) {
1152 this.isCustomizableItem(child) &&
1153 !this.isWrappedToolbarItem(child)
1155 this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
1159 lazy.log.error(ex, ex.stack);
1162 this.areas.add(target);
1166 async _wrapToolbarItems() {
1167 for (let area of CustomizableUI.areas) {
1168 await this._wrapToolbarItem(area);
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");
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);
1184 _wrapItemsInArea(target) {
1185 for (let child of target.children) {
1186 if (this.isCustomizableItem(child)) {
1187 this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
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");
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);
1205 _unwrapItemsInArea(target) {
1206 for (let toolbarItem of target.children) {
1207 if (this.isWrappedToolbarItem(toolbarItem)) {
1208 this.unwrapToolbarItem(toolbarItem);
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);
1221 this._removeDragHandlers(target);
1224 })().catch(lazy.log.error);
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) {
1249 })().catch(lazy.log.error);
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);
1272 _onToolbarVisibilityChange(aEvent) {
1273 let toolbar = aEvent.target;
1275 aEvent.detail.visible &&
1276 toolbar.getAttribute("customizable") == "true"
1278 toolbar.setAttribute("customizing", "true");
1280 toolbar.removeAttribute("customizing");
1285 onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) {
1289 onWidgetAdded(aWidgetId, aArea, aPosition) {
1293 onWidgetRemoved(aWidgetId, aArea) {
1297 onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
1298 if (aContainer.ownerGlobal != this.window || this.resetting) {
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);
1306 if (aSecondaryNode) {
1307 this.unwrapToolbarItem(aSecondaryNode.parentNode);
1311 onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
1312 if (aContainer.ownerGlobal != this.window || this.resetting) {
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);
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);
1338 onWidgetDestroyed(aWidgetId) {
1339 let wrapper = this.$("wrapper-" + aWidgetId);
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:
1349 let widgetNode = this.$(aWidgetId);
1351 this.wrapToolbarItem(widgetNode, "palette");
1353 let widget = CustomizableUI.getWidget(aWidgetId);
1354 this.visiblePalette.appendChild(
1355 this.makePaletteItem(widget, "palette")
1361 onAreaNodeRegistered(aArea, aContainer) {
1362 if (aContainer.ownerDocument == this.document) {
1363 this._wrapItemsInArea(aContainer);
1364 this._addDragHandlers(aContainer);
1365 this.areas.add(aContainer);
1369 onAreaNodeUnregistered(aArea, aContainer, aReason) {
1371 aContainer.ownerDocument == this.document &&
1372 aReason == CustomizableUI.REASON_AREA_UNREGISTERED
1374 this._unwrapItemsInArea(aContainer);
1375 this._removeDragHandlers(aContainer);
1376 this.areas.delete(aContainer);
1380 openAddonsManagerThemes() {
1381 this.window.BrowserOpenAddonsMgr("addons://list/theme");
1384 getMoreThemes(aEvent) {
1385 aEvent.target.parentNode.parentNode.hidePopup();
1386 let getMoreURL = Services.urlFormatter.formatURLPref(
1387 "lightweightThemes.getMoreURL"
1389 this.window.openTrustedLinkIn(getMoreURL, "tab");
1392 updateUIDensity(mode) {
1393 this.window.gUIDensity.update(mode);
1394 this._updateOverflowPanelArrowOffset();
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);
1413 this._updateOverflowPanelArrowOffset();
1417 this.window.gUIDensity.update();
1418 this._updateOverflowPanelArrowOffset();
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"
1430 normalItem.mode = gUIDensity.MODE_NORMAL;
1432 let items = [normalItem];
1434 let compactItem = doc.getElementById(
1435 "customization-uidensity-menuitem-compact"
1437 compactItem.mode = gUIDensity.MODE_COMPACT;
1439 if (Services.prefs.getBoolPref(kCompactModeShowPref)) {
1440 compactItem.hidden = false;
1441 items.push(compactItem);
1443 compactItem.hidden = true;
1446 let touchItem = doc.getElementById(
1447 "customization-uidensity-menuitem-touch"
1449 // Touch mode can not be enabled in OSX right now.
1451 touchItem.mode = gUIDensity.MODE_TOUCH;
1452 items.push(touchItem);
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");
1461 item.removeAttribute("aria-checked");
1462 item.removeAttribute("active");
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"
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"
1481 touchItem.setAttribute(
1483 sb.GetStringFromName("uiDensity.menuitem-touch.acceltext")
1486 touchItem.removeAttribute("acceltext");
1489 let autoTouchMode = Services.prefs.getBoolPref(
1490 win.gUIDensity.autoTouchModePref
1492 if (autoTouchMode) {
1493 checkbox.setAttribute("checked", "true");
1495 checkbox.removeAttribute("checked");
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();
1509 this._changed = true;
1510 if (!this.resetting) {
1511 this._updateResetButton();
1512 this._updateUndoResetButton();
1513 this._updateEmptyPaletteNotice();
1515 CustomizableUI.dispatchToolboxEvent("customizationchange");
1518 _updateEmptyPaletteNotice() {
1520 this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
1521 let whimsyButton = this.$("whimsy-button");
1524 paletteItems.length == 1 &&
1525 paletteItems[0].id.includes("wrapper-customizableui-special-spring")
1527 whimsyButton.hidden = false;
1529 this.togglePong(false);
1530 whimsyButton.hidden = true;
1534 _updateResetButton() {
1535 let btn = this.$("customization-reset-button");
1536 btn.disabled = CustomizableUI.inDefaultState;
1539 _updateUndoResetButton() {
1540 let undoResetButton = this.$("customization-undo-reset-button");
1541 undoResetButton.hidden = !CustomizableUI.canUndoReset;
1544 _updateTouchBarButton() {
1545 if (AppConstants.platform != "macosx") {
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;
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);
1564 let button = this.document.getElementById("customization-uidensity-button");
1566 !Services.prefs.getBoolPref(kCompactModeShowPref) &&
1567 !button.querySelector("#customization-uidensity-menuitem-touch");
1570 handleEvent(aEvent) {
1571 switch (aEvent.type) {
1572 case "toolbarvisibilitychange":
1573 this._onToolbarVisibilityChange(aEvent);
1576 this._onDragStart(aEvent);
1579 this._onDragOver(aEvent);
1582 this._onDragDrop(aEvent);
1585 this._onDragLeave(aEvent);
1588 this._onDragEnd(aEvent);
1591 this._onMouseDown(aEvent);
1594 this._onMouseUp(aEvent);
1597 if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
1608 * We handle dragover/drop on the outer palette separately
1609 * to avoid overlap with other drag/drop handlers.
1611 _setupPaletteDragging() {
1612 this._addDragHandlers(this.visiblePalette);
1614 this.paletteDragHandler = aEvent => {
1615 let originalTarget = aEvent.originalTarget;
1617 this._isUnwantedDragDrop(aEvent) ||
1618 this.visiblePalette.contains(originalTarget) ||
1619 this.$("customization-panelHolder").contains(originalTarget)
1623 // We have a dragover/drop on the palette.
1624 if (aEvent.type == "dragover") {
1625 this._onDragOver(aEvent, this.visiblePalette);
1627 this._onDragDrop(aEvent, this.visiblePalette);
1630 let contentContainer = this.$("customization-content-container");
1631 contentContainer.addEventListener(
1633 this.paletteDragHandler,
1636 contentContainer.addEventListener("drop", this.paletteDragHandler, true);
1639 _teardownPaletteDragging() {
1640 lazy.DragPositionManager.stop();
1641 this._removeDragHandlers(this.visiblePalette);
1643 let contentContainer = this.$("customization-content-container");
1644 contentContainer.removeEventListener(
1646 this.paletteDragHandler,
1649 contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
1650 delete this.paletteDragHandler;
1653 observe(aSubject, aTopic, aData) {
1655 case "nsPref:changed":
1656 this._updateResetButton();
1657 this._updateUndoResetButton();
1658 if (this._canDrawInTitlebar()) {
1659 this._updateTitlebarCheckbox();
1665 async onInstalled(addon) {
1666 await this.onEnabled(addon);
1669 async onEnabled(addon) {
1670 if (addon.type != "theme") {
1674 if (this._nextThemeChangeUserTriggered) {
1677 this._nextThemeChangeUserTriggered = false;
1680 _canDrawInTitlebar() {
1681 return this.window.TabsInTitlebar.systemSupported;
1684 _ensureCustomizationPanels() {
1685 let template = this.$("customizationPanel");
1686 template.replaceWith(template.content);
1688 let wrapper = this.$("customModeWrapper");
1689 wrapper.replaceWith(wrapper.content);
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");
1701 checkbox.setAttribute("checked", "true");
1705 toggleTitlebar(aShouldShowTitlebar) {
1706 // Drawing in the titlebar means not showing the titlebar, hence the negation:
1707 Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
1710 _getBoundsWithoutFlushing(element) {
1711 return this.window.windowUtils.getBoundsWithoutFlushing(element);
1714 _onDragStart(aEvent) {
1715 __dumpDragData(aEvent);
1716 let item = aEvent.target;
1717 while (item && item.localName != "toolbarpaletteitem") {
1719 item.localName == "toolbar" ||
1720 item.id == kPaletteId ||
1721 item.id == "customization-panelHolder"
1725 item = item.parentNode;
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);
1739 x: itemRect.left + itemRect.width / 2,
1740 y: itemRect.top + itemRect.height / 2,
1742 this._dragOffset = {
1743 x: aEvent.clientX - itemCenter.x,
1744 y: aEvent.clientY - itemCenter.y,
1747 let toolbarParent = draggedItem.closest("toolbar");
1748 if (toolbarParent) {
1749 let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent);
1750 toolbarParent.style.minHeight = toolbarRect.height + "px";
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) {
1763 lazy.DragPositionManager.start(this.window);
1764 let canUsePrevSibling =
1765 placeForItem == "toolbar" || placeForItem == "panel";
1766 if (item.nextElementSibling) {
1767 this._setDragActive(
1768 item.nextElementSibling,
1773 this._dragOverItem = item.nextElementSibling;
1774 } else if (canUsePrevSibling && item.previousElementSibling) {
1775 this._setDragActive(
1776 item.previousElementSibling,
1781 this._dragOverItem = item.previousElementSibling;
1783 let currentArea = this._getCustomizableParent(item);
1784 currentArea.setAttribute("draggingover", "true");
1786 this._initializeDragAfterMove = null;
1787 this.window.clearTimeout(this._dragInitializeTimeout);
1789 this._dragInitializeTimeout = this.window.setTimeout(
1790 this._initializeDragAfterMove,
1795 _onDragOver(aEvent, aOverrideTarget) {
1796 if (this._isUnwantedDragDrop(aEvent)) {
1799 if (this._initializeDragAfterMove) {
1800 this._initializeDragAfterMove();
1803 __dumpDragData(aEvent);
1805 let document = aEvent.target.ownerDocument;
1806 let documentId = document.documentElement.id;
1807 if (!aEvent.dataTransfer.mozTypesAt(0).length) {
1811 let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
1812 kDragDataTypePrefix + documentId,
1815 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1816 let targetArea = this._getCustomizableParent(
1817 aOverrideTarget || aEvent.currentTarget
1819 let originArea = this._getCustomizableParent(draggedWrapper);
1821 // Do nothing if the target or origin are not customizable.
1822 if (!targetArea || !originArea) {
1826 // Do nothing if the widget is not allowed to be removed.
1828 targetArea.id == kPaletteId &&
1829 !CustomizableUI.isWidgetRemovable(draggedItemId)
1834 // Do nothing if the widget is not allowed to move to the target area.
1835 if (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
1839 let targetAreaType = CustomizableUI.getPlaceForItem(targetArea);
1840 let targetNode = this._getDragOverNode(
1847 // We need to determine the place that the widget is being dropped in
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.
1854 (targetAreaType == "toolbar"
1855 ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
1856 : targetNode.lastElementChild) || targetNode;
1857 dragValue = "after";
1859 let targetParent = targetNode.parentNode;
1860 let position = Array.prototype.indexOf.call(
1861 targetParent.children,
1864 if (position == -1) {
1866 targetAreaType == "toolbar"
1867 ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
1868 : targetNode.lastElementChild;
1869 dragValue = "after";
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") {
1880 ((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) *
1884 ((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) *
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") {
1897 (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2;
1900 (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2;
1902 dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after";
1904 dragValue = "before";
1909 if (this._dragOverItem && dragOverItem != this._dragOverItem) {
1910 this._cancelDragActive(this._dragOverItem, dragOverItem);
1914 dragOverItem != this._dragOverItem ||
1915 dragValue != dragOverItem.getAttribute("dragover")
1917 if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) {
1918 this._setDragActive(
1925 this._dragOverItem = dragOverItem;
1926 targetArea.setAttribute("draggingover", "true");
1929 aEvent.preventDefault();
1930 aEvent.stopPropagation();
1933 _onDragDrop(aEvent, aOverrideTarget) {
1934 if (this._isUnwantedDragDrop(aEvent)) {
1938 __dumpDragData(aEvent);
1939 this._initializeDragAfterMove = null;
1940 this.window.clearTimeout(this._dragInitializeTimeout);
1942 let targetArea = this._getCustomizableParent(
1943 aOverrideTarget || aEvent.currentTarget
1945 let document = aEvent.target.ownerDocument;
1946 let documentId = document.documentElement.id;
1947 let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
1948 kDragDataTypePrefix + documentId,
1951 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1952 let originArea = this._getCustomizableParent(draggedWrapper);
1953 if (this._dragSizeMap) {
1954 this._dragSizeMap = new WeakMap();
1956 // Do nothing if the target area or origin area are not customizable.
1957 if (!targetArea || !originArea) {
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;
1967 targetNode = targetArea;
1970 if (targetNode.tagName == "toolbarpaletteitem") {
1971 targetNode = targetNode.firstElementChild;
1974 this._cancelDragActive(this._dragOverItem, null, true);
1985 lazy.log.error(ex, ex.stack);
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();
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");
2006 // Do nothing if the target was dropped onto itself (ie, no change in area
2008 if (draggedItem == aTargetNode) {
2012 if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
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)) {
2024 CustomizableUI.removeWidgetFromArea(aDraggedItemId, "drag");
2025 lazy.BrowserUsageTelemetry.recordWidgetChange(
2030 // Special widgets are removed outright, we can return here:
2031 if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
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);
2041 // The items in the palette are wrapped, so we need the target node's parent here:
2042 this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
2044 this._onDragEnd(aEvent);
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) {
2056 let place = draggedItem.parentNode.getAttribute("place");
2057 this.unwrapToolbarItem(draggedItem.parentNode);
2058 if (aTargetNode == areaCustomizationTarget) {
2059 areaCustomizationTarget.appendChild(draggedItem);
2061 this.unwrapToolbarItem(aTargetNode.parentNode);
2062 areaCustomizationTarget.insertBefore(draggedItem, aTargetNode);
2063 this.wrapToolbarItem(aTargetNode, place);
2065 this.wrapToolbarItem(draggedItem, place);
2069 // Force creating a new spacer/spring/separator if dragging from the palette
2071 CustomizableUI.isSpecialWidget(aDraggedItemId) &&
2072 aOriginArea.id == kPaletteId
2074 aDraggedItemId = aDraggedItemId.match(
2075 /^customizableui-special-(spring|spacer|separator)/
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(
2088 this._onDragEnd(aEvent);
2092 // We need to determine the place that the widget is being dropped in
2095 let itemForPlacement = aTargetNode;
2096 // Skip the skipintoolbarset items when determining the place of the item:
2099 itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
2100 itemForPlacement.parentNode &&
2101 itemForPlacement.parentNode.nodeName == "toolbarpaletteitem"
2103 itemForPlacement = itemForPlacement.parentNode.nextElementSibling;
2106 itemForPlacement.nodeName == "toolbarpaletteitem"
2108 itemForPlacement = itemForPlacement.firstElementChild;
2111 if (itemForPlacement) {
2113 itemForPlacement.nodeName == "toolbarpaletteitem"
2114 ? itemForPlacement.firstElementChild &&
2115 itemForPlacement.firstElementChild.id
2116 : itemForPlacement.id;
2117 placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
2121 "Could not get a position for " +
2122 aTargetNode.nodeName +
2126 aTargetNode.className
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(
2142 CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
2143 lazy.BrowserUsageTelemetry.recordWidgetChange(
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);
2160 _onDragLeave(aEvent) {
2161 if (this._isUnwantedDragDrop(aEvent)) {
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
2172 if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
2173 this._cancelDragActive(this._dragOverItem);
2174 this._dragOverItem = null;
2179 * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
2181 * Note that that means that this function may be called multiple times by a single drag operation.
2183 _onDragEnd(aEvent) {
2184 if (this._isUnwantedDragDrop(aEvent)) {
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)) {
2199 let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
2200 kDragDataTypePrefix + documentId,
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");
2218 if (this._dragOverItem) {
2219 this._cancelDragActive(this._dragOverItem);
2220 this._dragOverItem = null;
2222 lazy.DragPositionManager.stop();
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) {
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;
2244 _setDragActive(aItem, aValue, aDraggedItemId, aAreaType) {
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);
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;
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)[
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";
2276 prop = "border" + layoutSide + "EndWidth";
2277 otherProp = "border-" + layoutSide.toLowerCase() + "-start-width";
2279 if (makeSpaceImmediately) {
2280 aItem.setAttribute("notransition", "true");
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");
2292 _cancelDragActive(aItem, aNextItem, aNoTransition) {
2293 let currentArea = this._getCustomizableParent(aItem);
2297 let nextArea = aNextItem ? this._getCustomizableParent(aNextItem) : null;
2298 if (currentArea != nextArea) {
2299 currentArea.removeAttribute("draggingover");
2301 let areaType = CustomizableUI.getAreaType(currentArea.id);
2303 if (aNoTransition) {
2304 aItem.setAttribute("notransition", "true");
2306 aItem.removeAttribute("dragover");
2307 // Remove all property values in the case that the end padding
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");
2319 aItem.removeAttribute("dragover");
2321 if (nextArea == currentArea) {
2322 // No need to do anything if we're still dragging in this area:
2326 // Otherwise, clear everything out:
2327 let positionManager =
2328 lazy.DragPositionManager.getManagerForArea(currentArea);
2329 positionManager.clearPlaceholders(currentArea, aNoTransition);
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(
2344 originArea == targetArea
2348 _getDragItemSize(aDragOverNode, aDraggedItem) {
2349 // Cache it good, cache it real good.
2350 if (!this._dragSizeMap) {
2351 this._dragSizeMap = new WeakMap();
2353 if (!this._dragSizeMap.has(aDraggedItem)) {
2354 this._dragSizeMap.set(aDraggedItem, new WeakMap());
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);
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);
2377 aDraggedItem.hasAttribute(kAreaType) &&
2378 aDraggedItem.getAttribute(kAreaType);
2380 aDraggedItem.setAttribute(kAreaType, areaType);
2382 this.wrapToolbarItem(aDraggedItem, areaType || "palette");
2383 CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
2385 aDraggedItem.parentNode.hidden = false;
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
2400 if (currentType === false) {
2401 aDraggedItem.removeAttribute(kAreaType);
2403 aDraggedItem.setAttribute(kAreaType, currentType);
2406 this.createOrUpdateWrapper(aDraggedItem, null, true);
2407 CustomizableUI.onWidgetDrag(aDraggedItem.id);
2409 aDraggedItem.parentNode.hidden = true;
2414 _getCustomizableParent(aElement) {
2416 // Deal with drag/drop on the padding of the panel.
2417 let containingPanelHolder = aElement.closest(
2418 "#customization-panelHolder"
2420 if (containingPanelHolder) {
2421 return containingPanelHolder.querySelector(
2422 "#widget-overflow-fixed-list"
2427 let areas = CustomizableUI.areas;
2428 areas.push(kPaletteId);
2429 return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(","));
2432 _getDragOverNode(aEvent, aAreaElement, aAreaType, aDraggedItemId) {
2433 let expectedParent =
2434 CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement;
2435 if (!expectedParent.contains(aEvent.target)) {
2436 return expectedParent;
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));
2450 if (aAreaType == "toolbar" || aAreaType == "panel") {
2451 targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
2452 while (targetNode && targetNode.parentNode != expectedParent) {
2453 targetNode = targetNode.parentNode;
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);
2464 return targetNode || aEvent.target;
2467 _onMouseDown(aEvent) {
2468 lazy.log.debug("_onMouseDown");
2469 if (aEvent.button != 0) {
2472 let doc = aEvent.target.ownerDocument;
2473 doc.documentElement.setAttribute("customizing-movingItem", true);
2474 let item = this._getWrapper(aEvent.target);
2476 item.setAttribute("mousedown", "true");
2480 _onMouseUp(aEvent) {
2481 lazy.log.debug("_onMouseUp");
2482 if (aEvent.button != 0) {
2485 let doc = aEvent.target.ownerDocument;
2486 doc.documentElement.removeAttribute("customizing-movingItem");
2487 let item = this._getWrapper(aEvent.target);
2489 item.removeAttribute("mousedown");
2493 _getWrapper(aElement) {
2494 while (aElement && aElement.localName != "toolbarpaletteitem") {
2495 if (aElement.localName == "toolbar") {
2498 aElement = aElement.parentNode;
2503 _findVisiblePreviousSiblingNode(aReferenceNode) {
2506 aReferenceNode.localName == "toolbarpaletteitem" &&
2507 aReferenceNode.firstElementChild.hidden
2509 aReferenceNode = aReferenceNode.previousElementSibling;
2511 return aReferenceNode;
2514 onPaletteContextMenuShowing(event) {
2515 let isFlexibleSpace = event.target.triggerNode.id.includes(
2516 "wrapper-customizableui-special-spring"
2518 event.target.querySelector(".customize-context-addToPanel").disabled =
2522 onPanelContextMenuShowing(event) {
2523 let inPermanentArea = !!event.target.triggerNode.closest(
2524 "#widget-overflow-fixed-list"
2526 let doc = event.target.ownerDocument;
2527 doc.getElementById("customizationPanelItemContextMenuUnpin").hidden =
2529 doc.getElementById("customizationPanelItemContextMenuPin").hidden =
2532 doc.ownerGlobal.MozXULElement.insertFTLIfNeeded(
2533 "browser/toolbarContextMenu.ftl"
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");
2541 _checkForDownloadsClick(event) {
2543 event.target.closest("#wrapper-downloads-button") &&
2546 event.view.gCustomizeMode._showDownloadsAutoHidePanel();
2550 _setupDownloadAutoHideToggle() {
2551 this.window.addEventListener("click", this._checkForDownloadsClick, true);
2554 _teardownDownloadAutoHideToggle() {
2555 this.window.removeEventListener(
2557 this._checkForDownloadsClick,
2560 this.$(kDownloadAutohidePanelId).hidePopup();
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.
2568 !CustomizableUI.getPlacementOfWidget("downloads-button") &&
2569 this._moveDownloadsButtonToNavBar &&
2570 this.window.DownloadsButton.autoHideDownloadsButton
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:
2578 widget != "search-container" &&
2579 !(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring"))
2584 CustomizableUI.addWidgetToArea(
2589 lazy.BrowserUsageTelemetry.recordWidgetChange(
2597 async _showDownloadsAutoHidePanel() {
2598 let doc = this.document;
2599 let panel = doc.getElementById(kDownloadAutohidePanelId);
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")) {
2609 let panelOnTheLeft = false;
2610 let toolbarContainer = button.closest("toolbar");
2611 if (toolbarContainer && toolbarContainer.id == "nav-bar") {
2612 let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar");
2614 navbarWidgets.indexOf("urlbar-container") <=
2615 navbarWidgets.indexOf("downloads-button")
2617 panelOnTheLeft = true;
2620 await this.window.promiseDocumentFlushed(() => {});
2622 if (!this._customizing || !this._wantToBeInCustomizeMode) {
2625 let buttonBounds = this._getBoundsWithoutFlushing(button);
2626 let windowBounds = this._getBoundsWithoutFlushing(doc.documentElement);
2628 buttonBounds.left + buttonBounds.width / 2 > windowBounds.width / 2;
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) {
2639 position = "topright topleft";
2640 if (toolbarContainer) {
2645 let checkbox = doc.getElementById(kDownloadAutohideCheckboxId);
2646 if (this.window.DownloadsButton.autoHideDownloadsButton) {
2647 checkbox.setAttribute("checked", "true");
2649 checkbox.removeAttribute("checked");
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);
2657 onDownloadsAutoHideChange(event) {
2658 let checkbox = event.target.ownerDocument.getElementById(
2659 kDownloadAutohideCheckboxId
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;
2666 customizeTouchBar() {
2667 let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService(
2668 Ci.nsITouchBarUpdater
2670 updater.enterCustomizeMode();
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;
2681 this.visiblePalette.setAttribute("whimsypong", "true");
2682 this.pongArena.hidden = false;
2683 if (!this.uninitWhimsy) {
2684 this.uninitWhimsy = this.whimsypong();
2687 this.visiblePalette.removeAttribute("whimsypong");
2688 if (this.uninitWhimsy) {
2689 this.uninitWhimsy();
2690 this.uninitWhimsy = null;
2692 this.pongArena.hidden = true;
2702 function updateBall() {
2703 if (ball[1] <= 0 || ball[1] >= gameSide) {
2705 (ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) ||
2706 (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth))
2708 updateScore(ball[1] <= 0 ? 0 : 1);
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))
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();
2727 ball[1] = ball[1] <= 0 ? 0 : gameSide;
2731 Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0),
2732 Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0),
2734 if (ball[0] <= 0 || ball[0] >= gameSide) {
2739 function updatePlayers() {
2743 (keydown == 37 && !window.RTL_UI) ||
2744 (keydown == 39 && window.RTL_UI)
2748 p1 += p1Adj * 10 * keydownAdj;
2751 let sign = Math.sign(ballDxDy[0]);
2753 (sign > 0 && ball[0] > p2 + paddleWidth / 2) ||
2754 (sign < 0 && ball[0] < p2 + paddleWidth / 2)
2758 (sign > 0 && ball[0] > p2 + paddleWidth / 1.1) ||
2759 (sign < 0 && ball[0] < p2 + paddleWidth / 1.1)
2764 if (score >= winScore) {
2768 p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0);
2769 p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0);
2772 function updateScore(adj) {
2775 } else if (--lives == 0) {
2778 ball = ballDef.slice();
2779 ballDxDy = ballDxDyDef.slice();
2780 ballDxDy[1] *= score / winScore + 1;
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)";
2797 (window.RTL_UI ? gameSide : 0) + xAdj * ball[0] - 10
2798 }px ${ball[1] - 10}px`;
2799 let repeat = "no-repeat";
2801 if (arena.style.backgroundImage) {
2802 if (arena.style.backgroundImage.split(",").length >= 160) {
2806 image += ", " + arena.style.backgroundImage;
2807 position += ", " + arena.style.backgroundPosition;
2808 repeat += ", " + arena.style.backgroundRepeat;
2809 size += ", " + arena.style.backgroundSize;
2811 arena.style.backgroundImage = image;
2812 arena.style.backgroundPosition = position;
2813 arena.style.backgroundRepeat = repeat;
2814 arena.style.backgroundSize = size;
2818 function onkeydown(event) {
2819 keys.push(event.which);
2820 if (keys.length > 10) {
2822 let codeEntered = true;
2823 for (let i = 0; i < keys.length; i++) {
2824 if (keys[i] != keysCode[i]) {
2825 codeEntered = false;
2830 elements.arena.setAttribute("kcode", "true");
2831 let spacer = document.querySelector(
2832 "#customization-palette > toolbarpaletteitem"
2834 spacer.setAttribute("kcode", "true");
2837 if (event.which == 37 /* left */ || event.which == 39 /* right */) {
2838 keydown = event.which;
2843 function onkeyup(event) {
2844 if (event.which == 37 || event.which == 39) {
2851 document.removeEventListener("keydown", onkeydown);
2852 document.removeEventListener("keyup", onkeyup);
2854 window.cancelAnimationFrame(rAFHandle);
2856 let arena = elements.arena;
2857 while (arena.firstChild) {
2858 arena.firstChild.remove();
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"
2870 spacer.removeAttribute("kcode");
2876 if (this.uninitWhimsy) {
2877 return this.uninitWhimsy;
2880 let ballDef = [10, 10];
2881 let ball = [10, 10];
2882 let ballDxDyDef = [2, 2];
2883 let ballDxDy = [2, 2];
2888 let paddleEdge = 30;
2889 let paddleWidth = 84;
2893 let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
2897 let document = this.document;
2900 arena: document.getElementById("customization-pong-arena"),
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");
2909 elements[el.id] = elements.arena.appendChild(el);
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;
2918 let window = this.window;
2919 rAFHandle = window.requestAnimationFrame(function animate() {
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);
2929 rAFHandle = window.requestAnimationFrame(animate);
2937 function __dumpDragData(aEvent, caller) {
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"]) {
2954 aEvent[el].localName +
2960 for (let prop in aEvent.dataTransfer) {
2961 if (typeof aEvent.dataTransfer[prop] != "function") {
2963 " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
2967 lazy.log.debug(str);
2970 function dispatchFunction(aFunc) {
2971 Services.tm.dispatchToMainThread(aFunc);