diff options
Diffstat (limited to '')
-rw-r--r-- | browser/components/customizableui/CustomizeMode.sys.mjs | 2971 |
1 files changed, 2971 insertions, 0 deletions
diff --git a/browser/components/customizableui/CustomizeMode.sys.mjs b/browser/components/customizableui/CustomizeMode.sys.mjs new file mode 100644 index 0000000000..5f6d01d833 --- /dev/null +++ b/browser/components/customizableui/CustomizeMode.sys.mjs @@ -0,0 +1,2971 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const kPrefCustomizationDebug = "browser.uiCustomization.debug"; +const kPaletteId = "customization-palette"; +const kDragDataTypePrefix = "text/toolbarwrapper-id/"; +const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck"; +const kDrawInTitlebarPref = "browser.tabs.inTitlebar"; +const kCompactModeShowPref = "browser.compactmode.show"; +const kBookmarksToolbarPref = "browser.toolbars.bookmarks.visibility"; +const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing"; + +const kPanelItemContextMenu = "customizationPanelItemContextMenu"; +const kPaletteItemContextMenu = "customizationPaletteItemContextMenu"; + +const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox"; +const kDownloadAutohidePanelId = "downloads-button-autohide-panel"; +const kDownloadAutoHidePref = "browser.download.autohideButton"; + +import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs", + DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs", +}); +ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () { + const kUrl = + "chrome://browser/locale/customizableui/customizableWidgets.properties"; + return Services.strings.createBundle(kUrl); +}); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "gTouchBarUpdater", + "@mozilla.org/widget/touchbarupdater;1", + "nsITouchBarUpdater" +); + +let gDebug; +ChromeUtils.defineLazyGetter(lazy, "log", () => { + let { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" + ); + gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false); + let consoleOptions = { + maxLogLevel: gDebug ? "all" : "log", + prefix: "CustomizeMode", + }; + return new ConsoleAPI(consoleOptions); +}); + +var gDraggingInToolbars; + +var gTab; + +function closeGlobalTab() { + let win = gTab.ownerGlobal; + if (win.gBrowser.browsers.length == 1) { + win.BrowserOpenTab(); + } + win.gBrowser.removeTab(gTab, { animate: true }); + gTab = null; +} + +var gTabsProgressListener = { + onLocationChange(aBrowser, aWebProgress, aRequest, aLocation, aFlags) { + // Tear down customize mode when the customize mode tab loads some other page. + // Customize mode will be re-entered if "about:blank" is loaded again, so + // don't tear down in this case. + if ( + !gTab || + gTab.linkedBrowser != aBrowser || + aLocation.spec == "about:blank" + ) { + return; + } + + unregisterGlobalTab(); + }, +}; + +function unregisterGlobalTab() { + gTab.removeEventListener("TabClose", unregisterGlobalTab); + let win = gTab.ownerGlobal; + win.removeEventListener("unload", unregisterGlobalTab); + win.gBrowser.removeTabsProgressListener(gTabsProgressListener); + + gTab.removeAttribute("customizemode"); + + gTab = null; +} + +export function CustomizeMode(aWindow) { + this.window = aWindow; + this.document = aWindow.document; + this.browser = aWindow.gBrowser; + this.areas = new Set(); + + this._translationObserver = new aWindow.MutationObserver(mutations => + this._onTranslations(mutations) + ); + this._ensureCustomizationPanels(); + + let content = this.$("customization-content-container"); + if (!content) { + this.window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl"); + let container = this.$("customization-container"); + container.replaceChild( + this.window.MozXULElement.parseXULToFragment(container.firstChild.data), + container.lastChild + ); + } + // There are two palettes - there's the palette that can be overlayed with + // toolbar items in browser.xhtml. This is invisible, and never seen by the + // user. Then there's the visible palette, which gets populated and displayed + // to the user when in customizing mode. + this.visiblePalette = this.$(kPaletteId); + this.pongArena = this.$("customization-pong-arena"); + + if (this._canDrawInTitlebar()) { + this._updateTitlebarCheckbox(); + Services.prefs.addObserver(kDrawInTitlebarPref, this); + } else { + this.$("customization-titlebar-visibility-checkbox").hidden = true; + } + + // Observe pref changes to the bookmarks toolbar visibility, + // since we won't get a toolbarvisibilitychange event if the + // toolbar is changing from 'newtab' to 'always' in Customize mode + // since the toolbar is shown with the 'newtab' setting. + Services.prefs.addObserver(kBookmarksToolbarPref, this); + + this.window.addEventListener("unload", this); +} + +CustomizeMode.prototype = { + _changed: false, + _transitioning: false, + window: null, + document: null, + // areas is used to cache the customizable areas when in customization mode. + areas: null, + // When in customizing mode, we swap out the reference to the invisible + // palette in gNavToolbox.palette for our visiblePalette. This way, for the + // customizing browser window, when widgets are removed from customizable + // areas and added to the palette, they're added to the visible palette. + // _stowedPalette is a reference to the old invisible palette so we can + // restore gNavToolbox.palette to its original state after exiting + // customization mode. + _stowedPalette: null, + _dragOverItem: null, + _customizing: false, + _skipSourceNodeCheck: null, + _mainViewContext: null, + + // These are the commands we continue to leave enabled while in customize mode. + // All other commands are disabled, and we remove the disabled attribute when + // leaving customize mode. + _enabledCommands: new Set([ + "cmd_newNavigator", + "cmd_newNavigatorTab", + "cmd_newNavigatorTabNoEvent", + "cmd_close", + "cmd_closeWindow", + "cmd_quitApplication", + "View:FullScreen", + "Browser:NextTab", + "Browser:PrevTab", + "Browser:NewUserContextTab", + "Tools:PrivateBrowsing", + "minimizeWindow", + "zoomWindow", + ]), + + get _handler() { + return this.window.CustomizationHandler; + }, + + uninit() { + if (this._canDrawInTitlebar()) { + Services.prefs.removeObserver(kDrawInTitlebarPref, this); + } + Services.prefs.removeObserver(kBookmarksToolbarPref, this); + }, + + $(id) { + return this.document.getElementById(id); + }, + + toggle() { + if ( + this._handler.isEnteringCustomizeMode || + this._handler.isExitingCustomizeMode + ) { + this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode; + return; + } + if (this._customizing) { + this.exit(); + } else { + this.enter(); + } + }, + + setTab(aTab) { + if (gTab == aTab) { + return; + } + + if (gTab) { + closeGlobalTab(); + } + + gTab = aTab; + + gTab.setAttribute("customizemode", "true"); + lazy.SessionStore.persistTabAttribute("customizemode"); + + if (gTab.linkedPanel) { + gTab.linkedBrowser.stop(); + } + + let win = gTab.ownerGlobal; + + win.gBrowser.setTabTitle(gTab); + win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg"); + + gTab.addEventListener("TabClose", unregisterGlobalTab); + + win.gBrowser.addTabsProgressListener(gTabsProgressListener); + + win.addEventListener("unload", unregisterGlobalTab); + + if (gTab.selected) { + win.gCustomizeMode.enter(); + } + }, + + enter() { + if (!this.window.toolbar.visible) { + let w = lazy.URILoadingHelper.getTargetWindow(this.window, { + skipPopups: true, + }); + if (w) { + w.gCustomizeMode.enter(); + return; + } + let obs = () => { + Services.obs.removeObserver(obs, "browser-delayed-startup-finished"); + w = lazy.URILoadingHelper.getTargetWindow(this.window, { + skipPopups: true, + }); + w.gCustomizeMode.enter(); + }; + Services.obs.addObserver(obs, "browser-delayed-startup-finished"); + this.window.openTrustedLinkIn("about:newtab", "window"); + return; + } + this._wantToBeInCustomizeMode = true; + + if (this._customizing || this._handler.isEnteringCustomizeMode) { + return; + } + + // Exiting; want to re-enter once we've done that. + if (this._handler.isExitingCustomizeMode) { + lazy.log.debug( + "Attempted to enter while we're in the middle of exiting. " + + "We'll exit after we've entered" + ); + return; + } + + if (!gTab) { + this.setTab( + this.browser.addTab("about:blank", { + inBackground: false, + forceNotRemote: true, + skipAnimation: true, + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + }) + ); + return; + } + if (!gTab.selected) { + // This will force another .enter() to be called via the + // onlocationchange handler of the tabbrowser, so we return early. + gTab.ownerGlobal.gBrowser.selectedTab = gTab; + return; + } + gTab.ownerGlobal.focus(); + if (gTab.ownerDocument != this.document) { + return; + } + + let window = this.window; + let document = this.document; + + this._handler.isEnteringCustomizeMode = true; + + // Always disable the reset button at the start of customize mode, it'll be re-enabled + // if necessary when we finish entering: + let resetButton = this.$("customization-reset-button"); + resetButton.setAttribute("disabled", "true"); + + (async () => { + // We shouldn't start customize mode until after browser-delayed-startup has finished: + if (!this.window.gBrowserInit.delayedStartupFinished) { + await new Promise(resolve => { + let delayedStartupObserver = aSubject => { + if (aSubject == this.window) { + Services.obs.removeObserver( + delayedStartupObserver, + "browser-delayed-startup-finished" + ); + resolve(); + } + }; + + Services.obs.addObserver( + delayedStartupObserver, + "browser-delayed-startup-finished" + ); + }); + } + + CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window); + CustomizableUI.notifyStartCustomizing(this.window); + + // Add a keypress listener to the document so that we can quickly exit + // customization mode when pressing ESC. + document.addEventListener("keypress", this); + + // Same goes for the menu button - if we're customizing, a click on the + // menu button means a quick exit from customization mode. + window.PanelUI.hide(); + + let panelHolder = document.getElementById("customization-panelHolder"); + let panelContextMenu = document.getElementById(kPanelItemContextMenu); + this._previousPanelContextMenuParent = panelContextMenu.parentNode; + document.getElementById("mainPopupSet").appendChild(panelContextMenu); + panelHolder.appendChild(window.PanelUI.overflowFixedList); + + window.PanelUI.overflowFixedList.setAttribute("customizing", true); + window.PanelUI.menuButton.disabled = true; + document.getElementById("nav-bar-overflow-button").disabled = true; + + this._transitioning = true; + + let customizer = document.getElementById("customization-container"); + let browser = document.getElementById("browser"); + browser.hidden = true; + customizer.hidden = false; + + this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP); + + this.document.documentElement.setAttribute("customizing", true); + + let customizableToolbars = document.querySelectorAll( + "toolbar[customizable=true]:not([autohide=true], [collapsed=true])" + ); + for (let toolbar of customizableToolbars) { + toolbar.setAttribute("customizing", true); + } + + this._updateOverflowPanelArrowOffset(); + + // Let everybody in this window know that we're about to customize. + CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window); + + await this._wrapToolbarItems(); + this.populatePalette(); + + this._setupPaletteDragging(); + + window.gNavToolbox.addEventListener("toolbarvisibilitychange", this); + + this._updateResetButton(); + this._updateUndoResetButton(); + this._updateTouchBarButton(); + this._updateDensityMenu(); + + this._skipSourceNodeCheck = + Services.prefs.getPrefType(kSkipSourceNodePref) == + Ci.nsIPrefBranch.PREF_BOOL && + Services.prefs.getBoolPref(kSkipSourceNodePref); + + CustomizableUI.addListener(this); + this._customizing = true; + this._transitioning = false; + + // Show the palette now that the transition has finished. + this.visiblePalette.hidden = false; + window.setTimeout(() => { + // Force layout reflow to ensure the animation runs, + // and make it async so it doesn't affect the timing. + this.visiblePalette.clientTop; + this.visiblePalette.setAttribute("showing", "true"); + }, 0); + this._updateEmptyPaletteNotice(); + + lazy.AddonManager.addAddonListener(this); + + this._setupDownloadAutoHideToggle(); + + this._handler.isEnteringCustomizeMode = false; + + CustomizableUI.dispatchToolboxEvent("customizationready", {}, window); + + if (!this._wantToBeInCustomizeMode) { + this.exit(); + } + })().catch(e => { + lazy.log.error("Error entering customize mode", e); + this._handler.isEnteringCustomizeMode = false; + // Exit customize mode to ensure proper clean-up when entering failed. + this.exit(); + }); + }, + + exit() { + this._wantToBeInCustomizeMode = false; + + if (!this._customizing || this._handler.isExitingCustomizeMode) { + return; + } + + // Entering; want to exit once we've done that. + if (this._handler.isEnteringCustomizeMode) { + lazy.log.debug( + "Attempted to exit while we're in the middle of entering. " + + "We'll exit after we've entered" + ); + return; + } + + if (this.resetting) { + lazy.log.debug( + "Attempted to exit while we're resetting. " + + "We'll exit after resetting has finished." + ); + return; + } + + this._handler.isExitingCustomizeMode = true; + + this._translationObserver.disconnect(); + + this._teardownDownloadAutoHideToggle(); + + lazy.AddonManager.removeAddonListener(this); + CustomizableUI.removeListener(this); + + let window = this.window; + let document = this.document; + + document.removeEventListener("keypress", this); + + this.togglePong(false); + + // Disable the reset and undo reset buttons while transitioning: + let resetButton = this.$("customization-reset-button"); + let undoResetButton = this.$("customization-undo-reset-button"); + undoResetButton.hidden = resetButton.disabled = true; + + this._transitioning = true; + + this._depopulatePalette(); + + // We need to set this._customizing to false and remove the `customizing` + // attribute before removing the tab or else + // XULBrowserWindow.onLocationChange might think that we're still in + // customization mode and need to exit it for a second time. + this._customizing = false; + document.documentElement.removeAttribute("customizing"); + + if (this.browser.selectedTab == gTab) { + closeGlobalTab(); + } + + let customizer = document.getElementById("customization-container"); + let browser = document.getElementById("browser"); + customizer.hidden = true; + browser.hidden = false; + + window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this); + + this._teardownPaletteDragging(); + + (async () => { + await this._unwrapToolbarItems(); + + // And drop all area references. + this.areas.clear(); + + // Let everybody in this window know that we're starting to + // exit customization mode. + CustomizableUI.dispatchToolboxEvent("customizationending", {}, window); + + window.PanelUI.menuButton.disabled = false; + let overflowContainer = document.getElementById( + "widget-overflow-mainView" + ).firstElementChild; + overflowContainer.appendChild(window.PanelUI.overflowFixedList); + document.getElementById("nav-bar-overflow-button").disabled = false; + let panelContextMenu = document.getElementById(kPanelItemContextMenu); + this._previousPanelContextMenuParent.appendChild(panelContextMenu); + + let customizableToolbars = document.querySelectorAll( + "toolbar[customizable=true]:not([autohide=true])" + ); + for (let toolbar of customizableToolbars) { + toolbar.removeAttribute("customizing"); + } + + this._maybeMoveDownloadsButtonToNavBar(); + + delete this._lastLightweightTheme; + this._changed = false; + this._transitioning = false; + this._handler.isExitingCustomizeMode = false; + CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); + CustomizableUI.notifyEndCustomizing(window); + + if (this._wantToBeInCustomizeMode) { + this.enter(); + } + })().catch(e => { + lazy.log.error("Error exiting customize mode", e); + this._handler.isExitingCustomizeMode = false; + }); + }, + + /** + * The overflow panel in customize mode should have its arrow pointing + * at the overflow button. In order to do this correctly, we pass the + * distance between the inside of window and the middle of the button + * to the customize mode markup in which the arrow and panel are placed. + */ + async _updateOverflowPanelArrowOffset() { + let currentDensity = + this.document.documentElement.getAttribute("uidensity"); + let offset = await this.window.promiseDocumentFlushed(() => { + let overflowButton = this.$("nav-bar-overflow-button"); + let buttonRect = overflowButton.getBoundingClientRect(); + let endDistance; + if (this.window.RTL_UI) { + endDistance = buttonRect.left; + } else { + endDistance = this.window.innerWidth - buttonRect.right; + } + return endDistance + buttonRect.width / 2; + }); + if ( + !this.document || + currentDensity != this.document.documentElement.getAttribute("uidensity") + ) { + return; + } + this.$("customization-panelWrapper").style.setProperty( + "--panel-arrow-offset", + offset + "px" + ); + }, + + _getCustomizableChildForNode(aNode) { + // NB: adjusted from _getCustomizableParent to keep that method fast + // (it's used during drags), and avoid multiple DOM loops + let areas = CustomizableUI.areas; + // Caching this length is important because otherwise we'll also iterate + // over items we add to the end from within the loop. + let numberOfAreas = areas.length; + for (let i = 0; i < numberOfAreas; i++) { + let area = areas[i]; + let areaNode = aNode.ownerDocument.getElementById(area); + let customizationTarget = CustomizableUI.getCustomizationTarget(areaNode); + if (customizationTarget && customizationTarget != areaNode) { + areas.push(customizationTarget.id); + } + let overflowTarget = + areaNode && areaNode.getAttribute("default-overflowtarget"); + if (overflowTarget) { + areas.push(overflowTarget); + } + } + areas.push(kPaletteId); + + while (aNode && aNode.parentNode) { + let parent = aNode.parentNode; + if (areas.includes(parent.id)) { + return aNode; + } + aNode = parent; + } + return null; + }, + + _promiseWidgetAnimationOut(aNode) { + if ( + this.window.gReduceMotion || + aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" || + (aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") || + (aNode.id == "downloads-button" && aNode.hidden) + ) { + return null; + } + + let animationNode; + if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) { + animationNode = aNode.parentNode; + } else { + animationNode = aNode; + } + return new Promise(resolve => { + function cleanupCustomizationExit() { + resolveAnimationPromise(); + } + + function cleanupWidgetAnimationEnd(e) { + if ( + e.animationName == "widget-animate-out" && + e.target.id == animationNode.id + ) { + resolveAnimationPromise(); + } + } + + function resolveAnimationPromise() { + animationNode.removeEventListener( + "animationend", + cleanupWidgetAnimationEnd + ); + animationNode.removeEventListener( + "customizationending", + cleanupCustomizationExit + ); + resolve(animationNode); + } + + // Wait until the next frame before setting the class to ensure + // we do start the animation. + this.window.requestAnimationFrame(() => { + this.window.requestAnimationFrame(() => { + animationNode.classList.add("animate-out"); + animationNode.ownerGlobal.gNavToolbox.addEventListener( + "customizationending", + cleanupCustomizationExit + ); + animationNode.addEventListener( + "animationend", + cleanupWidgetAnimationEnd + ); + }); + }); + }); + }, + + async addToToolbar(aNode, aReason) { + aNode = this._getCustomizableChildForNode(aNode); + if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { + aNode = aNode.firstElementChild; + } + let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode); + let animationNode; + if (widgetAnimationPromise) { + animationNode = await widgetAnimationPromise; + } + + let widgetToAdd = aNode.id; + if ( + CustomizableUI.isSpecialWidget(widgetToAdd) && + aNode.closest("#customization-palette") + ) { + widgetToAdd = widgetToAdd.match( + /^customizableui-special-(spring|spacer|separator)/ + )[1]; + } + + CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR); + lazy.BrowserUsageTelemetry.recordWidgetChange( + widgetToAdd, + CustomizableUI.AREA_NAVBAR + ); + if (!this._customizing) { + CustomizableUI.dispatchToolboxEvent("customizationchange"); + } + + // If the user explicitly moves this item, turn off autohide. + if (aNode.id == "downloads-button") { + Services.prefs.setBoolPref(kDownloadAutoHidePref, false); + if (this._customizing) { + this._showDownloadsAutoHidePanel(); + } + } + + if (animationNode) { + animationNode.classList.remove("animate-out"); + } + }, + + async addToPanel(aNode, aReason) { + aNode = this._getCustomizableChildForNode(aNode); + if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { + aNode = aNode.firstElementChild; + } + let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode); + let animationNode; + if (widgetAnimationPromise) { + animationNode = await widgetAnimationPromise; + } + + let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL; + CustomizableUI.addWidgetToArea(aNode.id, panel); + lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, panel, aReason); + if (!this._customizing) { + CustomizableUI.dispatchToolboxEvent("customizationchange"); + } + + // If the user explicitly moves this item, turn off autohide. + if (aNode.id == "downloads-button") { + Services.prefs.setBoolPref(kDownloadAutoHidePref, false); + if (this._customizing) { + this._showDownloadsAutoHidePanel(); + } + } + + if (animationNode) { + animationNode.classList.remove("animate-out"); + } + if (!this.window.gReduceMotion) { + let overflowButton = this.$("nav-bar-overflow-button"); + overflowButton.setAttribute("animate", "true"); + overflowButton.addEventListener( + "animationend", + function onAnimationEnd(event) { + if (event.animationName.startsWith("overflow-animation")) { + this.removeEventListener("animationend", onAnimationEnd); + this.removeAttribute("animate"); + } + } + ); + } + }, + + async removeFromArea(aNode, aReason) { + aNode = this._getCustomizableChildForNode(aNode); + if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) { + aNode = aNode.firstElementChild; + } + let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode); + let animationNode; + if (widgetAnimationPromise) { + animationNode = await widgetAnimationPromise; + } + + CustomizableUI.removeWidgetFromArea(aNode.id); + lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason); + if (!this._customizing) { + CustomizableUI.dispatchToolboxEvent("customizationchange"); + } + + // If the user explicitly removes this item, turn off autohide. + if (aNode.id == "downloads-button") { + Services.prefs.setBoolPref(kDownloadAutoHidePref, false); + if (this._customizing) { + this._showDownloadsAutoHidePanel(); + } + } + if (animationNode) { + animationNode.classList.remove("animate-out"); + } + }, + + populatePalette() { + let fragment = this.document.createDocumentFragment(); + let toolboxPalette = this.window.gNavToolbox.palette; + + try { + let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette); + for (let widget of unusedWidgets) { + let paletteItem = this.makePaletteItem(widget, "palette"); + if (!paletteItem) { + continue; + } + fragment.appendChild(paletteItem); + } + + let flexSpace = CustomizableUI.createSpecialWidget( + "spring", + this.document + ); + fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette")); + + this.visiblePalette.appendChild(fragment); + this._stowedPalette = this.window.gNavToolbox.palette; + this.window.gNavToolbox.palette = this.visiblePalette; + + // Now that the palette items are all here, disable all commands. + // We do this here rather than directly in `enter` because we + // need to do/undo this when we're called from reset(), too. + this._updateCommandsDisabledState(true); + } catch (ex) { + lazy.log.error(ex); + } + }, + + // XXXunf Maybe this should use -moz-element instead of wrapping the node? + // Would ensure no weird interactions/event handling from original node, + // and makes it possible to put this in a lazy-loaded iframe/real tab + // while still getting rid of the need for overlays. + makePaletteItem(aWidget, aPlace) { + let widgetNode = aWidget.forWindow(this.window).node; + if (!widgetNode) { + lazy.log.error( + "Widget with id " + aWidget.id + " does not return a valid node" + ); + return null; + } + // Do not build a palette item for hidden widgets; there's not much to show. + if (widgetNode.hidden) { + return null; + } + + let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace); + wrapper.appendChild(widgetNode); + return wrapper; + }, + + _depopulatePalette() { + // Quick, undo the command disabling before we depopulate completely: + this._updateCommandsDisabledState(false); + + this.visiblePalette.hidden = true; + let paletteChild = this.visiblePalette.firstElementChild; + let nextChild; + while (paletteChild) { + nextChild = paletteChild.nextElementSibling; + let itemId = paletteChild.firstElementChild.id; + if (CustomizableUI.isSpecialWidget(itemId)) { + this.visiblePalette.removeChild(paletteChild); + } else { + // XXXunf Currently this doesn't destroy the (now unused) node in the + // API provider case. It would be good to do so, but we need to + // keep strong refs to it in CustomizableUI (can't iterate of + // WeakMaps), and there's the question of what behavior + // wrappers should have if consumers keep hold of them. + let unwrappedPaletteItem = this.unwrapToolbarItem(paletteChild); + this._stowedPalette.appendChild(unwrappedPaletteItem); + } + + paletteChild = nextChild; + } + this.visiblePalette.hidden = false; + this.window.gNavToolbox.palette = this._stowedPalette; + }, + + _updateCommandsDisabledState(shouldBeDisabled) { + for (let command of this.document.querySelectorAll("command")) { + if (!command.id || !this._enabledCommands.has(command.id)) { + if (shouldBeDisabled) { + if (command.getAttribute("disabled") != "true") { + command.setAttribute("disabled", true); + } else { + command.setAttribute("wasdisabled", true); + } + } else if (command.getAttribute("wasdisabled") != "true") { + command.removeAttribute("disabled"); + } else { + command.removeAttribute("wasdisabled"); + } + } + } + }, + + isCustomizableItem(aNode) { + return ( + aNode.localName == "toolbarbutton" || + aNode.localName == "toolbaritem" || + aNode.localName == "toolbarseparator" || + aNode.localName == "toolbarspring" || + aNode.localName == "toolbarspacer" + ); + }, + + isWrappedToolbarItem(aNode) { + return aNode.localName == "toolbarpaletteitem"; + }, + + deferredWrapToolbarItem(aNode, aPlace) { + return new Promise(resolve => { + dispatchFunction(() => { + let wrapper = this.wrapToolbarItem(aNode, aPlace); + resolve(wrapper); + }); + }); + }, + + wrapToolbarItem(aNode, aPlace) { + if (!this.isCustomizableItem(aNode)) { + return aNode; + } + let wrapper = this.createOrUpdateWrapper(aNode, aPlace); + + // It's possible that this toolbar node is "mid-flight" and doesn't have + // a parent, in which case we skip replacing it. This can happen if a + // toolbar item has been dragged into the palette. In that case, we tell + // CustomizableUI to remove the widget from its area before putting the + // widget in the palette - so the node will have no parent. + if (aNode.parentNode) { + aNode = aNode.parentNode.replaceChild(wrapper, aNode); + } + wrapper.appendChild(aNode); + return wrapper; + }, + + /** + * Helper to set the label, either directly or to set up the translation + * observer so we can set the label once it's available. + */ + _updateWrapperLabel(aNode, aIsUpdate, aWrapper = aNode.parentElement) { + if (aNode.hasAttribute("label")) { + aWrapper.setAttribute("title", aNode.getAttribute("label")); + aWrapper.setAttribute("tooltiptext", aNode.getAttribute("label")); + } else if (aNode.hasAttribute("title")) { + aWrapper.setAttribute("title", aNode.getAttribute("title")); + aWrapper.setAttribute("tooltiptext", aNode.getAttribute("title")); + } else if (aNode.hasAttribute("data-l10n-id") && !aIsUpdate) { + this._translationObserver.observe(aNode, { + attributes: true, + attributeFilter: ["label", "title"], + }); + } + }, + + /** + * Called when a node without a label or title is updated. + */ + _onTranslations(aMutations) { + for (let mut of aMutations) { + let { target } = mut; + if ( + target.parentElement?.localName == "toolbarpaletteitem" && + (target.hasAttribute("label") || mut.target.hasAttribute("title")) + ) { + this._updateWrapperLabel(target, true); + } + } + }, + + createOrUpdateWrapper(aNode, aPlace, aIsUpdate) { + let wrapper; + if ( + aIsUpdate && + aNode.parentNode && + aNode.parentNode.localName == "toolbarpaletteitem" + ) { + wrapper = aNode.parentNode; + aPlace = wrapper.getAttribute("place"); + } else { + wrapper = this.document.createXULElement("toolbarpaletteitem"); + // "place" is used to show the label when it's sitting in the palette. + wrapper.setAttribute("place", aPlace); + } + + // Ensure the wrapped item doesn't look like it's in any special state, and + // can't be interactved with when in the customization palette. + // Note that some buttons opt out of this with the + // keepbroadcastattributeswhencustomizing attribute. + if ( + aNode.hasAttribute("command") && + aNode.getAttribute(kKeepBroadcastAttributes) != "true" + ) { + wrapper.setAttribute("itemcommand", aNode.getAttribute("command")); + aNode.removeAttribute("command"); + } + + if ( + aNode.hasAttribute("observes") && + aNode.getAttribute(kKeepBroadcastAttributes) != "true" + ) { + wrapper.setAttribute("itemobserves", aNode.getAttribute("observes")); + aNode.removeAttribute("observes"); + } + + if (aNode.getAttribute("checked") == "true") { + wrapper.setAttribute("itemchecked", "true"); + aNode.removeAttribute("checked"); + } + + if (aNode.hasAttribute("id")) { + wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id")); + } + + this._updateWrapperLabel(aNode, aIsUpdate, wrapper); + + if (aNode.hasAttribute("flex")) { + wrapper.setAttribute("flex", aNode.getAttribute("flex")); + } + + let removable = + aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode); + wrapper.setAttribute("removable", removable); + + // Allow touch events to initiate dragging in customize mode. + // This is only supported on Windows for now. + wrapper.setAttribute("touchdownstartsdrag", "true"); + + let contextMenuAttrName = ""; + if (aNode.getAttribute("context")) { + contextMenuAttrName = "context"; + } else if (aNode.getAttribute("contextmenu")) { + contextMenuAttrName = "contextmenu"; + } + let currentContextMenu = aNode.getAttribute(contextMenuAttrName); + let contextMenuForPlace = + aPlace == "panel" ? kPanelItemContextMenu : kPaletteItemContextMenu; + if (aPlace != "toolbar") { + wrapper.setAttribute("context", contextMenuForPlace); + } + // Only keep track of the menu if it is non-default. + if (currentContextMenu && currentContextMenu != contextMenuForPlace) { + aNode.setAttribute("wrapped-context", currentContextMenu); + aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName); + aNode.removeAttribute(contextMenuAttrName); + } else if (currentContextMenu == contextMenuForPlace) { + aNode.removeAttribute(contextMenuAttrName); + } + + // Only add listeners for newly created wrappers: + if (!aIsUpdate) { + wrapper.addEventListener("mousedown", this); + wrapper.addEventListener("mouseup", this); + } + + if (CustomizableUI.isSpecialWidget(aNode.id)) { + wrapper.setAttribute( + "title", + lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label") + ); + } + + return wrapper; + }, + + deferredUnwrapToolbarItem(aWrapper) { + return new Promise(resolve => { + dispatchFunction(() => { + let item = null; + try { + item = this.unwrapToolbarItem(aWrapper); + } catch (ex) { + console.error(ex); + } + resolve(item); + }); + }); + }, + + unwrapToolbarItem(aWrapper) { + if (aWrapper.nodeName != "toolbarpaletteitem") { + return aWrapper; + } + aWrapper.removeEventListener("mousedown", this); + aWrapper.removeEventListener("mouseup", this); + + let place = aWrapper.getAttribute("place"); + + let toolbarItem = aWrapper.firstElementChild; + if (!toolbarItem) { + lazy.log.error( + "no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id + ); + aWrapper.remove(); + return null; + } + + if (aWrapper.hasAttribute("itemobserves")) { + toolbarItem.setAttribute( + "observes", + aWrapper.getAttribute("itemobserves") + ); + } + + if (aWrapper.hasAttribute("itemchecked")) { + toolbarItem.checked = true; + } + + if (aWrapper.hasAttribute("itemcommand")) { + let commandID = aWrapper.getAttribute("itemcommand"); + toolbarItem.setAttribute("command", commandID); + + // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing + let command = this.$(commandID); + if (command && command.hasAttribute("disabled")) { + toolbarItem.setAttribute("disabled", command.getAttribute("disabled")); + } + } + + let wrappedContext = toolbarItem.getAttribute("wrapped-context"); + if (wrappedContext) { + let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName"); + toolbarItem.setAttribute(contextAttrName, wrappedContext); + toolbarItem.removeAttribute("wrapped-contextAttrName"); + toolbarItem.removeAttribute("wrapped-context"); + } else if (place == "panel") { + toolbarItem.setAttribute("context", kPanelItemContextMenu); + } + + if (aWrapper.parentNode) { + aWrapper.parentNode.replaceChild(toolbarItem, aWrapper); + } + return toolbarItem; + }, + + async _wrapToolbarItem(aArea) { + let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window); + if (!target || this.areas.has(target)) { + return null; + } + + this._addDragHandlers(target); + for (let child of target.children) { + if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) { + await this.deferredWrapToolbarItem( + child, + CustomizableUI.getPlaceForItem(child) + ).catch(lazy.log.error); + } + } + this.areas.add(target); + return target; + }, + + _wrapToolbarItemSync(aArea) { + let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window); + if (!target || this.areas.has(target)) { + return null; + } + + this._addDragHandlers(target); + try { + for (let child of target.children) { + if ( + this.isCustomizableItem(child) && + !this.isWrappedToolbarItem(child) + ) { + this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); + } + } + } catch (ex) { + lazy.log.error(ex, ex.stack); + } + + this.areas.add(target); + return target; + }, + + async _wrapToolbarItems() { + for (let area of CustomizableUI.areas) { + await this._wrapToolbarItem(area); + } + }, + + _addDragHandlers(aTarget) { + // Allow dropping on the padding of the arrow panel. + if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { + aTarget = this.$("customization-panelHolder"); + } + aTarget.addEventListener("dragstart", this, true); + aTarget.addEventListener("dragover", this, true); + aTarget.addEventListener("dragleave", this, true); + aTarget.addEventListener("drop", this, true); + aTarget.addEventListener("dragend", this, true); + }, + + _wrapItemsInArea(target) { + for (let child of target.children) { + if (this.isCustomizableItem(child)) { + this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); + } + } + }, + + _removeDragHandlers(aTarget) { + // Remove handler from different target if it was added to + // allow dropping on the padding of the arrow panel. + if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { + aTarget = this.$("customization-panelHolder"); + } + aTarget.removeEventListener("dragstart", this, true); + aTarget.removeEventListener("dragover", this, true); + aTarget.removeEventListener("dragleave", this, true); + aTarget.removeEventListener("drop", this, true); + aTarget.removeEventListener("dragend", this, true); + }, + + _unwrapItemsInArea(target) { + for (let toolbarItem of target.children) { + if (this.isWrappedToolbarItem(toolbarItem)) { + this.unwrapToolbarItem(toolbarItem); + } + } + }, + + _unwrapToolbarItems() { + return (async () => { + for (let target of this.areas) { + for (let toolbarItem of target.children) { + if (this.isWrappedToolbarItem(toolbarItem)) { + await this.deferredUnwrapToolbarItem(toolbarItem); + } + } + this._removeDragHandlers(target); + } + this.areas.clear(); + })().catch(lazy.log.error); + }, + + reset() { + this.resetting = true; + // Disable the reset button temporarily while resetting: + let btn = this.$("customization-reset-button"); + btn.disabled = true; + return (async () => { + this._depopulatePalette(); + await this._unwrapToolbarItems(); + + CustomizableUI.reset(); + + await this._wrapToolbarItems(); + this.populatePalette(); + + this._updateResetButton(); + this._updateUndoResetButton(); + this._updateEmptyPaletteNotice(); + this._moveDownloadsButtonToNavBar = false; + this.resetting = false; + if (!this._wantToBeInCustomizeMode) { + this.exit(); + } + })().catch(lazy.log.error); + }, + + undoReset() { + this.resetting = true; + + return (async () => { + this._depopulatePalette(); + await this._unwrapToolbarItems(); + + CustomizableUI.undoReset(); + + await this._wrapToolbarItems(); + this.populatePalette(); + + this._updateResetButton(); + this._updateUndoResetButton(); + this._updateEmptyPaletteNotice(); + this._moveDownloadsButtonToNavBar = false; + this.resetting = false; + })().catch(lazy.log.error); + }, + + _onToolbarVisibilityChange(aEvent) { + let toolbar = aEvent.target; + if ( + aEvent.detail.visible && + toolbar.getAttribute("customizable") == "true" + ) { + toolbar.setAttribute("customizing", "true"); + } else { + toolbar.removeAttribute("customizing"); + } + this._onUIChange(); + }, + + onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) { + this._onUIChange(); + }, + + onWidgetAdded(aWidgetId, aArea, aPosition) { + this._onUIChange(); + }, + + onWidgetRemoved(aWidgetId, aArea) { + this._onUIChange(); + }, + + onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) { + if (aContainer.ownerGlobal != this.window || this.resetting) { + return; + } + // If we get called for widgets that aren't in the window yet, they might not have + // a parentNode at all. + if (aNodeToChange.parentNode) { + this.unwrapToolbarItem(aNodeToChange.parentNode); + } + if (aSecondaryNode) { + this.unwrapToolbarItem(aSecondaryNode.parentNode); + } + }, + + onWidgetAfterDOMChange(aNodeToChange, aSecondaryNode, aContainer) { + if (aContainer.ownerGlobal != this.window || this.resetting) { + return; + } + // If the node is still attached to the container, wrap it again: + if (aNodeToChange.parentNode) { + let place = CustomizableUI.getPlaceForItem(aNodeToChange); + this.wrapToolbarItem(aNodeToChange, place); + if (aSecondaryNode) { + this.wrapToolbarItem(aSecondaryNode, place); + } + } else { + // If not, it got removed. + + // If an API-based widget is removed while customizing, append it to the palette. + // The _applyDrop code itself will take care of positioning it correctly, if + // applicable. We need the code to be here so removing widgets using CustomizableUI's + // API also does the right thing (and adds it to the palette) + let widgetId = aNodeToChange.id; + let widget = CustomizableUI.getWidget(widgetId); + if (widget.provider == CustomizableUI.PROVIDER_API) { + let paletteItem = this.makePaletteItem(widget, "palette"); + this.visiblePalette.appendChild(paletteItem); + } + } + }, + + onWidgetDestroyed(aWidgetId) { + let wrapper = this.$("wrapper-" + aWidgetId); + if (wrapper) { + wrapper.remove(); + } + }, + + onWidgetAfterCreation(aWidgetId, aArea) { + // If the node was added to an area, we would have gotten an onWidgetAdded notification, + // plus associated DOM change notifications, so only do stuff for the palette: + if (!aArea) { + let widgetNode = this.$(aWidgetId); + if (widgetNode) { + this.wrapToolbarItem(widgetNode, "palette"); + } else { + let widget = CustomizableUI.getWidget(aWidgetId); + this.visiblePalette.appendChild( + this.makePaletteItem(widget, "palette") + ); + } + } + }, + + onAreaNodeRegistered(aArea, aContainer) { + if (aContainer.ownerDocument == this.document) { + this._wrapItemsInArea(aContainer); + this._addDragHandlers(aContainer); + this.areas.add(aContainer); + } + }, + + onAreaNodeUnregistered(aArea, aContainer, aReason) { + if ( + aContainer.ownerDocument == this.document && + aReason == CustomizableUI.REASON_AREA_UNREGISTERED + ) { + this._unwrapItemsInArea(aContainer); + this._removeDragHandlers(aContainer); + this.areas.delete(aContainer); + } + }, + + openAddonsManagerThemes() { + this.window.BrowserOpenAddonsMgr("addons://list/theme"); + }, + + getMoreThemes(aEvent) { + aEvent.target.parentNode.parentNode.hidePopup(); + let getMoreURL = Services.urlFormatter.formatURLPref( + "lightweightThemes.getMoreURL" + ); + this.window.openTrustedLinkIn(getMoreURL, "tab"); + }, + + updateUIDensity(mode) { + this.window.gUIDensity.update(mode); + this._updateOverflowPanelArrowOffset(); + }, + + setUIDensity(mode) { + let win = this.window; + let gUIDensity = win.gUIDensity; + let currentDensity = gUIDensity.getCurrentDensity(); + let panel = win.document.getElementById("customization-uidensity-menu"); + + Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode); + + // If the user is choosing a different UI density mode while + // the mode is overriden to Touch, remove the override. + if (currentDensity.overridden) { + Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false); + } + + this._onUIChange(); + panel.hidePopup(); + this._updateOverflowPanelArrowOffset(); + }, + + resetUIDensity() { + this.window.gUIDensity.update(); + this._updateOverflowPanelArrowOffset(); + }, + + onUIDensityMenuShowing() { + let win = this.window; + let doc = win.document; + let gUIDensity = win.gUIDensity; + let currentDensity = gUIDensity.getCurrentDensity(); + + let normalItem = doc.getElementById( + "customization-uidensity-menuitem-normal" + ); + normalItem.mode = gUIDensity.MODE_NORMAL; + + let items = [normalItem]; + + let compactItem = doc.getElementById( + "customization-uidensity-menuitem-compact" + ); + compactItem.mode = gUIDensity.MODE_COMPACT; + + if (Services.prefs.getBoolPref(kCompactModeShowPref)) { + compactItem.hidden = false; + items.push(compactItem); + } else { + compactItem.hidden = true; + } + + let touchItem = doc.getElementById( + "customization-uidensity-menuitem-touch" + ); + // Touch mode can not be enabled in OSX right now. + if (touchItem) { + touchItem.mode = gUIDensity.MODE_TOUCH; + items.push(touchItem); + } + + // Mark the active mode menuitem. + for (let item of items) { + if (item.mode == currentDensity.mode) { + item.setAttribute("aria-checked", "true"); + item.setAttribute("active", "true"); + } else { + item.removeAttribute("aria-checked"); + item.removeAttribute("active"); + } + } + + // Add menu items for automatically switching to Touch mode in Windows Tablet Mode. + if (AppConstants.platform == "win") { + let spacer = doc.getElementById("customization-uidensity-touch-spacer"); + let checkbox = doc.getElementById( + "customization-uidensity-autotouchmode-checkbox" + ); + spacer.removeAttribute("hidden"); + checkbox.removeAttribute("hidden"); + + // Show a hint that the UI density was overridden automatically. + if (currentDensity.overridden) { + let sb = Services.strings.createBundle( + "chrome://browser/locale/uiDensity.properties" + ); + touchItem.setAttribute( + "acceltext", + sb.GetStringFromName("uiDensity.menuitem-touch.acceltext") + ); + } else { + touchItem.removeAttribute("acceltext"); + } + + let autoTouchMode = Services.prefs.getBoolPref( + win.gUIDensity.autoTouchModePref + ); + if (autoTouchMode) { + checkbox.setAttribute("checked", "true"); + } else { + checkbox.removeAttribute("checked"); + } + } + }, + + updateAutoTouchMode(checked) { + Services.prefs.setBoolPref("browser.touchmode.auto", checked); + // Re-render the menu items since the active mode might have + // change because of this. + this.onUIDensityMenuShowing(); + this._onUIChange(); + }, + + _onUIChange() { + this._changed = true; + if (!this.resetting) { + this._updateResetButton(); + this._updateUndoResetButton(); + this._updateEmptyPaletteNotice(); + } + CustomizableUI.dispatchToolboxEvent("customizationchange"); + }, + + _updateEmptyPaletteNotice() { + let paletteItems = + this.visiblePalette.getElementsByTagName("toolbarpaletteitem"); + let whimsyButton = this.$("whimsy-button"); + + if ( + paletteItems.length == 1 && + paletteItems[0].id.includes("wrapper-customizableui-special-spring") + ) { + whimsyButton.hidden = false; + } else { + this.togglePong(false); + whimsyButton.hidden = true; + } + }, + + _updateResetButton() { + let btn = this.$("customization-reset-button"); + btn.disabled = CustomizableUI.inDefaultState; + }, + + _updateUndoResetButton() { + let undoResetButton = this.$("customization-undo-reset-button"); + undoResetButton.hidden = !CustomizableUI.canUndoReset; + }, + + _updateTouchBarButton() { + if (AppConstants.platform != "macosx") { + return; + } + let touchBarButton = this.$("customization-touchbar-button"); + let touchBarSpacer = this.$("customization-touchbar-spacer"); + + let isTouchBarInitialized = lazy.gTouchBarUpdater.isTouchBarInitialized(); + touchBarButton.hidden = !isTouchBarInitialized; + touchBarSpacer.hidden = !isTouchBarInitialized; + }, + + _updateDensityMenu() { + // If we're entering Customize Mode, and we're using compact mode, + // then show the button after that. + let gUIDensity = this.window.gUIDensity; + if (gUIDensity.getCurrentDensity().mode == gUIDensity.MODE_COMPACT) { + Services.prefs.setBoolPref(kCompactModeShowPref, true); + } + + let button = this.document.getElementById("customization-uidensity-button"); + button.hidden = + !Services.prefs.getBoolPref(kCompactModeShowPref) && + !button.querySelector("#customization-uidensity-menuitem-touch"); + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "toolbarvisibilitychange": + this._onToolbarVisibilityChange(aEvent); + break; + case "dragstart": + this._onDragStart(aEvent); + break; + case "dragover": + this._onDragOver(aEvent); + break; + case "drop": + this._onDragDrop(aEvent); + break; + case "dragleave": + this._onDragLeave(aEvent); + break; + case "dragend": + this._onDragEnd(aEvent); + break; + case "mousedown": + this._onMouseDown(aEvent); + break; + case "mouseup": + this._onMouseUp(aEvent); + break; + case "keypress": + if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { + this.exit(); + } + break; + case "unload": + this.uninit(); + break; + } + }, + + /** + * We handle dragover/drop on the outer palette separately + * to avoid overlap with other drag/drop handlers. + */ + _setupPaletteDragging() { + this._addDragHandlers(this.visiblePalette); + + this.paletteDragHandler = aEvent => { + let originalTarget = aEvent.originalTarget; + if ( + this._isUnwantedDragDrop(aEvent) || + this.visiblePalette.contains(originalTarget) || + this.$("customization-panelHolder").contains(originalTarget) + ) { + return; + } + // We have a dragover/drop on the palette. + if (aEvent.type == "dragover") { + this._onDragOver(aEvent, this.visiblePalette); + } else { + this._onDragDrop(aEvent, this.visiblePalette); + } + }; + let contentContainer = this.$("customization-content-container"); + contentContainer.addEventListener( + "dragover", + this.paletteDragHandler, + true + ); + contentContainer.addEventListener("drop", this.paletteDragHandler, true); + }, + + _teardownPaletteDragging() { + lazy.DragPositionManager.stop(); + this._removeDragHandlers(this.visiblePalette); + + let contentContainer = this.$("customization-content-container"); + contentContainer.removeEventListener( + "dragover", + this.paletteDragHandler, + true + ); + contentContainer.removeEventListener("drop", this.paletteDragHandler, true); + delete this.paletteDragHandler; + }, + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "nsPref:changed": + this._updateResetButton(); + this._updateUndoResetButton(); + if (this._canDrawInTitlebar()) { + this._updateTitlebarCheckbox(); + } + break; + } + }, + + async onInstalled(addon) { + await this.onEnabled(addon); + }, + + async onEnabled(addon) { + if (addon.type != "theme") { + return; + } + + if (this._nextThemeChangeUserTriggered) { + this._onUIChange(); + } + this._nextThemeChangeUserTriggered = false; + }, + + _canDrawInTitlebar() { + return this.window.TabsInTitlebar.systemSupported; + }, + + _ensureCustomizationPanels() { + let template = this.$("customizationPanel"); + template.replaceWith(template.content); + + let wrapper = this.$("customModeWrapper"); + wrapper.replaceWith(wrapper.content); + }, + + _updateTitlebarCheckbox() { + let drawInTitlebar = Services.appinfo.drawInTitlebar; + let checkbox = this.$("customization-titlebar-visibility-checkbox"); + // Drawing in the titlebar means 'hiding' the titlebar. + // We use the attribute rather than a property because if we're not in + // customize mode the button is hidden and properties don't work. + if (drawInTitlebar) { + checkbox.removeAttribute("checked"); + } else { + checkbox.setAttribute("checked", "true"); + } + }, + + toggleTitlebar(aShouldShowTitlebar) { + // Drawing in the titlebar means not showing the titlebar, hence the negation: + Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar); + }, + + _getBoundsWithoutFlushing(element) { + return this.window.windowUtils.getBoundsWithoutFlushing(element); + }, + + _onDragStart(aEvent) { + __dumpDragData(aEvent); + let item = aEvent.target; + while (item && item.localName != "toolbarpaletteitem") { + if ( + item.localName == "toolbar" || + item.id == kPaletteId || + item.id == "customization-panelHolder" + ) { + return; + } + item = item.parentNode; + } + + let draggedItem = item.firstElementChild; + let placeForItem = CustomizableUI.getPlaceForItem(item); + + let dt = aEvent.dataTransfer; + let documentId = aEvent.target.ownerDocument.documentElement.id; + + dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0); + dt.effectAllowed = "move"; + + let itemRect = this._getBoundsWithoutFlushing(draggedItem); + let itemCenter = { + x: itemRect.left + itemRect.width / 2, + y: itemRect.top + itemRect.height / 2, + }; + this._dragOffset = { + x: aEvent.clientX - itemCenter.x, + y: aEvent.clientY - itemCenter.y, + }; + + let toolbarParent = draggedItem.closest("toolbar"); + if (toolbarParent) { + let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent); + toolbarParent.style.minHeight = toolbarRect.height + "px"; + } + + gDraggingInToolbars = new Set(); + + // Hack needed so that the dragimage will still show the + // item as it appeared before it was hidden. + this._initializeDragAfterMove = () => { + // For automated tests, we sometimes start exiting customization mode + // before this fires, which leaves us with placeholders inserted after + // we've exited. So we need to check that we are indeed customizing. + if (this._customizing && !this._transitioning) { + item.hidden = true; + lazy.DragPositionManager.start(this.window); + let canUsePrevSibling = + placeForItem == "toolbar" || placeForItem == "panel"; + if (item.nextElementSibling) { + this._setDragActive( + item.nextElementSibling, + "before", + draggedItem.id, + placeForItem + ); + this._dragOverItem = item.nextElementSibling; + } else if (canUsePrevSibling && item.previousElementSibling) { + this._setDragActive( + item.previousElementSibling, + "after", + draggedItem.id, + placeForItem + ); + this._dragOverItem = item.previousElementSibling; + } + let currentArea = this._getCustomizableParent(item); + currentArea.setAttribute("draggingover", "true"); + } + this._initializeDragAfterMove = null; + this.window.clearTimeout(this._dragInitializeTimeout); + }; + this._dragInitializeTimeout = this.window.setTimeout( + this._initializeDragAfterMove, + 0 + ); + }, + + _onDragOver(aEvent, aOverrideTarget) { + if (this._isUnwantedDragDrop(aEvent)) { + return; + } + if (this._initializeDragAfterMove) { + this._initializeDragAfterMove(); + } + + __dumpDragData(aEvent); + + let document = aEvent.target.ownerDocument; + let documentId = document.documentElement.id; + if (!aEvent.dataTransfer.mozTypesAt(0).length) { + return; + } + + let draggedItemId = aEvent.dataTransfer.mozGetDataAt( + kDragDataTypePrefix + documentId, + 0 + ); + let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); + let targetArea = this._getCustomizableParent( + aOverrideTarget || aEvent.currentTarget + ); + let originArea = this._getCustomizableParent(draggedWrapper); + + // Do nothing if the target or origin are not customizable. + if (!targetArea || !originArea) { + return; + } + + // Do nothing if the widget is not allowed to be removed. + if ( + targetArea.id == kPaletteId && + !CustomizableUI.isWidgetRemovable(draggedItemId) + ) { + return; + } + + // Do nothing if the widget is not allowed to move to the target area. + if (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) { + return; + } + + let targetAreaType = CustomizableUI.getPlaceForItem(targetArea); + let targetNode = this._getDragOverNode( + aEvent, + targetArea, + targetAreaType, + draggedItemId + ); + + // We need to determine the place that the widget is being dropped in + // the target. + let dragOverItem, dragValue; + if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) { + // We'll assume if the user is dragging directly over the target, that + // they're attempting to append a child to that target. + dragOverItem = + (targetAreaType == "toolbar" + ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild) + : targetNode.lastElementChild) || targetNode; + dragValue = "after"; + } else { + let targetParent = targetNode.parentNode; + let position = Array.prototype.indexOf.call( + targetParent.children, + targetNode + ); + if (position == -1) { + dragOverItem = + targetAreaType == "toolbar" + ? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild) + : targetNode.lastElementChild; + dragValue = "after"; + } else { + dragOverItem = targetParent.children[position]; + if (targetAreaType == "toolbar") { + // Check if the aDraggedItem is hovered past the first half of dragOverItem + let itemRect = this._getBoundsWithoutFlushing(dragOverItem); + let dropTargetCenter = itemRect.left + itemRect.width / 2; + let existingDir = dragOverItem.getAttribute("dragover"); + let dirFactor = this.window.RTL_UI ? -1 : 1; + if (existingDir == "before") { + dropTargetCenter += + ((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) * + dirFactor; + } else { + dropTargetCenter -= + ((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) * + dirFactor; + } + let before = this.window.RTL_UI + ? aEvent.clientX > dropTargetCenter + : aEvent.clientX < dropTargetCenter; + dragValue = before ? "before" : "after"; + } else if (targetAreaType == "panel") { + let itemRect = this._getBoundsWithoutFlushing(dragOverItem); + let dropTargetCenter = itemRect.top + itemRect.height / 2; + let existingDir = dragOverItem.getAttribute("dragover"); + if (existingDir == "before") { + dropTargetCenter += + (parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2; + } else { + dropTargetCenter -= + (parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2; + } + dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after"; + } else { + dragValue = "before"; + } + } + } + + if (this._dragOverItem && dragOverItem != this._dragOverItem) { + this._cancelDragActive(this._dragOverItem, dragOverItem); + } + + if ( + dragOverItem != this._dragOverItem || + dragValue != dragOverItem.getAttribute("dragover") + ) { + if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) { + this._setDragActive( + dragOverItem, + dragValue, + draggedItemId, + targetAreaType + ); + } + this._dragOverItem = dragOverItem; + targetArea.setAttribute("draggingover", "true"); + } + + aEvent.preventDefault(); + aEvent.stopPropagation(); + }, + + _onDragDrop(aEvent, aOverrideTarget) { + if (this._isUnwantedDragDrop(aEvent)) { + return; + } + + __dumpDragData(aEvent); + this._initializeDragAfterMove = null; + this.window.clearTimeout(this._dragInitializeTimeout); + + let targetArea = this._getCustomizableParent( + aOverrideTarget || aEvent.currentTarget + ); + let document = aEvent.target.ownerDocument; + let documentId = document.documentElement.id; + let draggedItemId = aEvent.dataTransfer.mozGetDataAt( + kDragDataTypePrefix + documentId, + 0 + ); + let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); + let originArea = this._getCustomizableParent(draggedWrapper); + if (this._dragSizeMap) { + this._dragSizeMap = new WeakMap(); + } + // Do nothing if the target area or origin area are not customizable. + if (!targetArea || !originArea) { + return; + } + let targetNode = this._dragOverItem; + let dropDir = targetNode.getAttribute("dragover"); + // Need to insert *after* this node if we promised the user that: + if (targetNode != targetArea && dropDir == "after") { + if (targetNode.nextElementSibling) { + targetNode = targetNode.nextElementSibling; + } else { + targetNode = targetArea; + } + } + if (targetNode.tagName == "toolbarpaletteitem") { + targetNode = targetNode.firstElementChild; + } + + this._cancelDragActive(this._dragOverItem, null, true); + + try { + this._applyDrop( + aEvent, + targetArea, + originArea, + draggedItemId, + targetNode + ); + } catch (ex) { + lazy.log.error(ex, ex.stack); + } + + // If the user explicitly moves this item, turn off autohide. + if (draggedItemId == "downloads-button") { + Services.prefs.setBoolPref(kDownloadAutoHidePref, false); + this._showDownloadsAutoHidePanel(); + } + }, + + _applyDrop(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) { + let document = aEvent.target.ownerDocument; + let draggedItem = document.getElementById(aDraggedItemId); + draggedItem.hidden = false; + draggedItem.removeAttribute("mousedown"); + + let toolbarParent = draggedItem.closest("toolbar"); + if (toolbarParent) { + toolbarParent.style.removeProperty("min-height"); + } + + // Do nothing if the target was dropped onto itself (ie, no change in area + // or position). + if (draggedItem == aTargetNode) { + return; + } + + if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) { + return; + } + + // Is the target area the customization palette? + if (aTargetArea.id == kPaletteId) { + // Did we drag from outside the palette? + if (aOriginArea.id !== kPaletteId) { + if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) { + return; + } + + CustomizableUI.removeWidgetFromArea(aDraggedItemId, "drag"); + lazy.BrowserUsageTelemetry.recordWidgetChange( + aDraggedItemId, + null, + "drag" + ); + // Special widgets are removed outright, we can return here: + if (CustomizableUI.isSpecialWidget(aDraggedItemId)) { + return; + } + } + draggedItem = draggedItem.parentNode; + + // If the target node is the palette itself, just append + if (aTargetNode == this.visiblePalette) { + this.visiblePalette.appendChild(draggedItem); + } else { + // The items in the palette are wrapped, so we need the target node's parent here: + this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode); + } + this._onDragEnd(aEvent); + return; + } + + // Skipintoolbarset items won't really be moved: + let areaCustomizationTarget = + CustomizableUI.getCustomizationTarget(aTargetArea); + if (draggedItem.getAttribute("skipintoolbarset") == "true") { + // These items should never leave their area: + if (aTargetArea != aOriginArea) { + return; + } + let place = draggedItem.parentNode.getAttribute("place"); + this.unwrapToolbarItem(draggedItem.parentNode); + if (aTargetNode == areaCustomizationTarget) { + areaCustomizationTarget.appendChild(draggedItem); + } else { + this.unwrapToolbarItem(aTargetNode.parentNode); + areaCustomizationTarget.insertBefore(draggedItem, aTargetNode); + this.wrapToolbarItem(aTargetNode, place); + } + this.wrapToolbarItem(draggedItem, place); + return; + } + + // Force creating a new spacer/spring/separator if dragging from the palette + if ( + CustomizableUI.isSpecialWidget(aDraggedItemId) && + aOriginArea.id == kPaletteId + ) { + aDraggedItemId = aDraggedItemId.match( + /^customizableui-special-(spring|spacer|separator)/ + )[1]; + } + + // Is the target the customization area itself? If so, we just add the + // widget to the end of the area. + if (aTargetNode == areaCustomizationTarget) { + CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id); + lazy.BrowserUsageTelemetry.recordWidgetChange( + aDraggedItemId, + aTargetArea.id, + "drag" + ); + this._onDragEnd(aEvent); + return; + } + + // We need to determine the place that the widget is being dropped in + // the target. + let placement; + let itemForPlacement = aTargetNode; + // Skip the skipintoolbarset items when determining the place of the item: + while ( + itemForPlacement && + itemForPlacement.getAttribute("skipintoolbarset") == "true" && + itemForPlacement.parentNode && + itemForPlacement.parentNode.nodeName == "toolbarpaletteitem" + ) { + itemForPlacement = itemForPlacement.parentNode.nextElementSibling; + if ( + itemForPlacement && + itemForPlacement.nodeName == "toolbarpaletteitem" + ) { + itemForPlacement = itemForPlacement.firstElementChild; + } + } + if (itemForPlacement) { + let targetNodeId = + itemForPlacement.nodeName == "toolbarpaletteitem" + ? itemForPlacement.firstElementChild && + itemForPlacement.firstElementChild.id + : itemForPlacement.id; + placement = CustomizableUI.getPlacementOfWidget(targetNodeId); + } + if (!placement) { + lazy.log.debug( + "Could not get a position for " + + aTargetNode.nodeName + + "#" + + aTargetNode.id + + "." + + aTargetNode.className + ); + } + let position = placement ? placement.position : null; + + // Is the target area the same as the origin? Since we've already handled + // the possibility that the target is the customization palette, we know + // that the widget is moving within a customizable area. + if (aTargetArea == aOriginArea) { + CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position); + lazy.BrowserUsageTelemetry.recordWidgetChange( + aDraggedItemId, + aTargetArea.id, + "drag" + ); + } else { + CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position); + lazy.BrowserUsageTelemetry.recordWidgetChange( + aDraggedItemId, + aTargetArea.id, + "drag" + ); + } + + this._onDragEnd(aEvent); + + // If we dropped onto a skipintoolbarset item, manually correct the drop location: + if (aTargetNode != itemForPlacement) { + let draggedWrapper = draggedItem.parentNode; + let container = draggedWrapper.parentNode; + container.insertBefore(draggedWrapper, aTargetNode.parentNode); + } + }, + + _onDragLeave(aEvent) { + if (this._isUnwantedDragDrop(aEvent)) { + return; + } + + __dumpDragData(aEvent); + + // When leaving customization areas, cancel the drag on the last dragover item + // We've attached the listener to areas, so aEvent.currentTarget will be the area. + // We don't care about dragleave events fired on descendants of the area, + // so we check that the event's target is the same as the area to which the listener + // was attached. + if (this._dragOverItem && aEvent.target == aEvent.currentTarget) { + this._cancelDragActive(this._dragOverItem); + this._dragOverItem = null; + } + }, + + /** + * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired. + * + * Note that that means that this function may be called multiple times by a single drag operation. + */ + _onDragEnd(aEvent) { + if (this._isUnwantedDragDrop(aEvent)) { + return; + } + this._initializeDragAfterMove = null; + this.window.clearTimeout(this._dragInitializeTimeout); + __dumpDragData(aEvent, "_onDragEnd"); + + let document = aEvent.target.ownerDocument; + document.documentElement.removeAttribute("customizing-movingItem"); + + let documentId = document.documentElement.id; + if (!aEvent.dataTransfer.mozTypesAt(0)) { + return; + } + + let draggedItemId = aEvent.dataTransfer.mozGetDataAt( + kDragDataTypePrefix + documentId, + 0 + ); + + let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); + + // DraggedWrapper might no longer available if a widget node is + // destroyed after starting (but before stopping) a drag. + if (draggedWrapper) { + draggedWrapper.hidden = false; + draggedWrapper.removeAttribute("mousedown"); + + let toolbarParent = draggedWrapper.closest("toolbar"); + if (toolbarParent) { + toolbarParent.style.removeProperty("min-height"); + } + } + + if (this._dragOverItem) { + this._cancelDragActive(this._dragOverItem); + this._dragOverItem = null; + } + lazy.DragPositionManager.stop(); + }, + + _isUnwantedDragDrop(aEvent) { + // The synthesized events for tests generated by synthesizePlainDragAndDrop + // and synthesizeDrop in mochitests are used only for testing whether the + // right data is being put into the dataTransfer. Neither cause a real drop + // to occur, so they don't set the source node. There isn't a means of + // testing real drag and drops, so this pref skips the check but it should + // only be set by test code. + if (this._skipSourceNodeCheck) { + return false; + } + + /* Discard drag events that originated from a separate window to + prevent content->chrome privilege escalations. */ + let mozSourceNode = aEvent.dataTransfer.mozSourceNode; + // mozSourceNode is null in the dragStart event handler or if + // the drag event originated in an external application. + return !mozSourceNode || mozSourceNode.ownerGlobal != this.window; + }, + + _setDragActive(aItem, aValue, aDraggedItemId, aAreaType) { + if (!aItem) { + return; + } + + if (aItem.getAttribute("dragover") != aValue) { + aItem.setAttribute("dragover", aValue); + + let window = aItem.ownerGlobal; + let draggedItem = window.document.getElementById(aDraggedItemId); + if (aAreaType == "palette") { + this._setGridDragActive(aItem, draggedItem, aValue); + } else { + let targetArea = this._getCustomizableParent(aItem); + let makeSpaceImmediately = false; + if (!gDraggingInToolbars.has(targetArea.id)) { + gDraggingInToolbars.add(targetArea.id); + let draggedWrapper = this.$("wrapper-" + aDraggedItemId); + let originArea = this._getCustomizableParent(draggedWrapper); + makeSpaceImmediately = originArea == targetArea; + } + let propertyToMeasure = aAreaType == "toolbar" ? "width" : "height"; + // Calculate width/height of the item when it'd be dropped in this position. + let borderWidth = this._getDragItemSize(aItem, draggedItem)[ + propertyToMeasure + ]; + let layoutSide = aAreaType == "toolbar" ? "Inline" : "Block"; + let prop, otherProp; + if (aValue == "before") { + prop = "border" + layoutSide + "StartWidth"; + otherProp = "border-" + layoutSide.toLowerCase() + "-end-width"; + } else { + prop = "border" + layoutSide + "EndWidth"; + otherProp = "border-" + layoutSide.toLowerCase() + "-start-width"; + } + if (makeSpaceImmediately) { + aItem.setAttribute("notransition", "true"); + } + aItem.style[prop] = borderWidth + "px"; + aItem.style.removeProperty(otherProp); + if (makeSpaceImmediately) { + // Force a layout flush: + aItem.getBoundingClientRect(); + aItem.removeAttribute("notransition"); + } + } + } + }, + _cancelDragActive(aItem, aNextItem, aNoTransition) { + let currentArea = this._getCustomizableParent(aItem); + if (!currentArea) { + return; + } + let nextArea = aNextItem ? this._getCustomizableParent(aNextItem) : null; + if (currentArea != nextArea) { + currentArea.removeAttribute("draggingover"); + } + let areaType = CustomizableUI.getAreaType(currentArea.id); + if (areaType) { + if (aNoTransition) { + aItem.setAttribute("notransition", "true"); + } + aItem.removeAttribute("dragover"); + // Remove all property values in the case that the end padding + // had been set. + aItem.style.removeProperty("border-inline-start-width"); + aItem.style.removeProperty("border-inline-end-width"); + aItem.style.removeProperty("border-block-start-width"); + aItem.style.removeProperty("border-block-end-width"); + if (aNoTransition) { + // Force a layout flush: + aItem.getBoundingClientRect(); + aItem.removeAttribute("notransition"); + } + } else { + aItem.removeAttribute("dragover"); + if (aNextItem) { + if (nextArea == currentArea) { + // No need to do anything if we're still dragging in this area: + return; + } + } + // Otherwise, clear everything out: + let positionManager = + lazy.DragPositionManager.getManagerForArea(currentArea); + positionManager.clearPlaceholders(currentArea, aNoTransition); + } + }, + + _setGridDragActive(aDragOverNode, aDraggedItem, aValue) { + let targetArea = this._getCustomizableParent(aDragOverNode); + let draggedWrapper = this.$("wrapper-" + aDraggedItem.id); + let originArea = this._getCustomizableParent(draggedWrapper); + let positionManager = + lazy.DragPositionManager.getManagerForArea(targetArea); + let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem); + positionManager.insertPlaceholder( + targetArea, + aDragOverNode, + draggedSize, + originArea == targetArea + ); + }, + + _getDragItemSize(aDragOverNode, aDraggedItem) { + // Cache it good, cache it real good. + if (!this._dragSizeMap) { + this._dragSizeMap = new WeakMap(); + } + if (!this._dragSizeMap.has(aDraggedItem)) { + this._dragSizeMap.set(aDraggedItem, new WeakMap()); + } + let itemMap = this._dragSizeMap.get(aDraggedItem); + let targetArea = this._getCustomizableParent(aDragOverNode); + let currentArea = this._getCustomizableParent(aDraggedItem); + // Return the size for this target from cache, if it exists. + let size = itemMap.get(targetArea); + if (size) { + return size; + } + + // Calculate size of the item when it'd be dropped in this position. + let currentParent = aDraggedItem.parentNode; + let currentSibling = aDraggedItem.nextElementSibling; + const kAreaType = "cui-areatype"; + let areaType, currentType; + + if (targetArea != currentArea) { + // Move the widget temporarily next to the placeholder. + aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode); + // Update the node's areaType. + areaType = CustomizableUI.getAreaType(targetArea.id); + currentType = + aDraggedItem.hasAttribute(kAreaType) && + aDraggedItem.getAttribute(kAreaType); + if (areaType) { + aDraggedItem.setAttribute(kAreaType, areaType); + } + this.wrapToolbarItem(aDraggedItem, areaType || "palette"); + CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id); + } else { + aDraggedItem.parentNode.hidden = false; + } + + // Fetch the new size. + let rect = aDraggedItem.parentNode.getBoundingClientRect(); + size = { width: rect.width, height: rect.height }; + // Cache the found value of size for this target. + itemMap.set(targetArea, size); + + if (targetArea != currentArea) { + this.unwrapToolbarItem(aDraggedItem.parentNode); + // Put the item back into its previous position. + currentParent.insertBefore(aDraggedItem, currentSibling); + // restore the areaType + if (areaType) { + if (currentType === false) { + aDraggedItem.removeAttribute(kAreaType); + } else { + aDraggedItem.setAttribute(kAreaType, currentType); + } + } + this.createOrUpdateWrapper(aDraggedItem, null, true); + CustomizableUI.onWidgetDrag(aDraggedItem.id); + } else { + aDraggedItem.parentNode.hidden = true; + } + return size; + }, + + _getCustomizableParent(aElement) { + if (aElement) { + // Deal with drag/drop on the padding of the panel. + let containingPanelHolder = aElement.closest( + "#customization-panelHolder" + ); + if (containingPanelHolder) { + return containingPanelHolder.querySelector( + "#widget-overflow-fixed-list" + ); + } + } + + let areas = CustomizableUI.areas; + areas.push(kPaletteId); + return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(",")); + }, + + _getDragOverNode(aEvent, aAreaElement, aAreaType, aDraggedItemId) { + let expectedParent = + CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement; + if (!expectedParent.contains(aEvent.target)) { + return expectedParent; + } + // Offset the drag event's position with the offset to the center of + // the thing we're dragging + let dragX = aEvent.clientX - this._dragOffset.x; + let dragY = aEvent.clientY - this._dragOffset.y; + + // Ensure this is within the container + let boundsContainer = expectedParent; + let bounds = this._getBoundsWithoutFlushing(boundsContainer); + dragX = Math.min(bounds.right, Math.max(dragX, bounds.left)); + dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top)); + + let targetNode; + if (aAreaType == "toolbar" || aAreaType == "panel") { + targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY); + while (targetNode && targetNode.parentNode != expectedParent) { + targetNode = targetNode.parentNode; + } + } else { + let positionManager = + lazy.DragPositionManager.getManagerForArea(aAreaElement); + // Make it relative to the container: + dragX -= bounds.left; + dragY -= bounds.top; + // Find the closest node: + targetNode = positionManager.find(aAreaElement, dragX, dragY); + } + return targetNode || aEvent.target; + }, + + _onMouseDown(aEvent) { + lazy.log.debug("_onMouseDown"); + if (aEvent.button != 0) { + return; + } + let doc = aEvent.target.ownerDocument; + doc.documentElement.setAttribute("customizing-movingItem", true); + let item = this._getWrapper(aEvent.target); + if (item) { + item.setAttribute("mousedown", "true"); + } + }, + + _onMouseUp(aEvent) { + lazy.log.debug("_onMouseUp"); + if (aEvent.button != 0) { + return; + } + let doc = aEvent.target.ownerDocument; + doc.documentElement.removeAttribute("customizing-movingItem"); + let item = this._getWrapper(aEvent.target); + if (item) { + item.removeAttribute("mousedown"); + } + }, + + _getWrapper(aElement) { + while (aElement && aElement.localName != "toolbarpaletteitem") { + if (aElement.localName == "toolbar") { + return null; + } + aElement = aElement.parentNode; + } + return aElement; + }, + + _findVisiblePreviousSiblingNode(aReferenceNode) { + while ( + aReferenceNode && + aReferenceNode.localName == "toolbarpaletteitem" && + aReferenceNode.firstElementChild.hidden + ) { + aReferenceNode = aReferenceNode.previousElementSibling; + } + return aReferenceNode; + }, + + onPaletteContextMenuShowing(event) { + let isFlexibleSpace = event.target.triggerNode.id.includes( + "wrapper-customizableui-special-spring" + ); + event.target.querySelector(".customize-context-addToPanel").disabled = + isFlexibleSpace; + }, + + onPanelContextMenuShowing(event) { + let inPermanentArea = !!event.target.triggerNode.closest( + "#widget-overflow-fixed-list" + ); + let doc = event.target.ownerDocument; + doc.getElementById("customizationPanelItemContextMenuUnpin").hidden = + !inPermanentArea; + doc.getElementById("customizationPanelItemContextMenuPin").hidden = + inPermanentArea; + + doc.ownerGlobal.MozXULElement.insertFTLIfNeeded( + "browser/toolbarContextMenu.ftl" + ); + event.target.querySelectorAll("[data-lazy-l10n-id]").forEach(el => { + el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id")); + el.removeAttribute("data-lazy-l10n-id"); + }); + }, + + _checkForDownloadsClick(event) { + if ( + event.target.closest("#wrapper-downloads-button") && + event.button == 0 + ) { + event.view.gCustomizeMode._showDownloadsAutoHidePanel(); + } + }, + + _setupDownloadAutoHideToggle() { + this.window.addEventListener("click", this._checkForDownloadsClick, true); + }, + + _teardownDownloadAutoHideToggle() { + this.window.removeEventListener( + "click", + this._checkForDownloadsClick, + true + ); + this.$(kDownloadAutohidePanelId).hidePopup(); + }, + + _maybeMoveDownloadsButtonToNavBar() { + // If the user toggled the autohide checkbox while the item was in the + // palette, and hasn't moved it since, move the item to the default + // location in the navbar for them. + if ( + !CustomizableUI.getPlacementOfWidget("downloads-button") && + this._moveDownloadsButtonToNavBar && + this.window.DownloadsButton.autoHideDownloadsButton + ) { + let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar"); + let insertionPoint = navbarPlacements.indexOf("urlbar-container"); + while (++insertionPoint < navbarPlacements.length) { + let widget = navbarPlacements[insertionPoint]; + // If we find a non-searchbar, non-spacer node, break out of the loop: + if ( + widget != "search-container" && + !(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring")) + ) { + break; + } + } + CustomizableUI.addWidgetToArea( + "downloads-button", + "nav-bar", + insertionPoint + ); + lazy.BrowserUsageTelemetry.recordWidgetChange( + "downloads-button", + "nav-bar", + "move-downloads" + ); + } + }, + + async _showDownloadsAutoHidePanel() { + let doc = this.document; + let panel = doc.getElementById(kDownloadAutohidePanelId); + panel.hidePopup(); + let button = doc.getElementById("downloads-button"); + // We don't show the tooltip if the button is in the panel. + if (button.closest("#widget-overflow-fixed-list")) { + return; + } + + let offsetX = 0, + offsetY = 0; + let panelOnTheLeft = false; + let toolbarContainer = button.closest("toolbar"); + if (toolbarContainer && toolbarContainer.id == "nav-bar") { + let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar"); + if ( + navbarWidgets.indexOf("urlbar-container") <= + navbarWidgets.indexOf("downloads-button") + ) { + panelOnTheLeft = true; + } + } else { + await this.window.promiseDocumentFlushed(() => {}); + + if (!this._customizing || !this._wantToBeInCustomizeMode) { + return; + } + let buttonBounds = this._getBoundsWithoutFlushing(button); + let windowBounds = this._getBoundsWithoutFlushing(doc.documentElement); + panelOnTheLeft = + buttonBounds.left + buttonBounds.width / 2 > windowBounds.width / 2; + } + let position; + if (panelOnTheLeft) { + // Tested in RTL, these get inverted automatically, so this does the + // right thing without taking RTL into account explicitly. + position = "topleft topright"; + if (toolbarContainer) { + offsetX = 8; + } + } else { + position = "topright topleft"; + if (toolbarContainer) { + offsetX = -8; + } + } + + let checkbox = doc.getElementById(kDownloadAutohideCheckboxId); + if (this.window.DownloadsButton.autoHideDownloadsButton) { + checkbox.setAttribute("checked", "true"); + } else { + checkbox.removeAttribute("checked"); + } + + // We don't use the icon to anchor because it might be resizing because of + // the animations for drag/drop. Hence the use of offsets. + panel.openPopup(button, position, offsetX, offsetY); + }, + + onDownloadsAutoHideChange(event) { + let checkbox = event.target.ownerDocument.getElementById( + kDownloadAutohideCheckboxId + ); + Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked); + // Ensure we move the button (back) after the user leaves customize mode. + event.view.gCustomizeMode._moveDownloadsButtonToNavBar = checkbox.checked; + }, + + customizeTouchBar() { + let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService( + Ci.nsITouchBarUpdater + ); + updater.enterCustomizeMode(); + }, + + togglePong(enabled) { + // It's possible we're toggling for a reason other than hitting + // the button (we might be exiting, for example), so make sure that + // the state and checkbox are in sync. + let whimsyButton = this.$("whimsy-button"); + whimsyButton.checked = enabled; + + if (enabled) { + this.visiblePalette.setAttribute("whimsypong", "true"); + this.pongArena.hidden = false; + if (!this.uninitWhimsy) { + this.uninitWhimsy = this.whimsypong(); + } + } else { + this.visiblePalette.removeAttribute("whimsypong"); + if (this.uninitWhimsy) { + this.uninitWhimsy(); + this.uninitWhimsy = null; + } + this.pongArena.hidden = true; + } + }, + + whimsypong() { + function update() { + updateBall(); + updatePlayers(); + } + + function updateBall() { + if (ball[1] <= 0 || ball[1] >= gameSide) { + if ( + (ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) || + (ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth)) + ) { + updateScore(ball[1] <= 0 ? 0 : 1); + } else { + if ( + (ball[1] <= 0 && + (ball[0] - p1 < paddleEdge || + p1 + paddleWidth - ball[0] < paddleEdge)) || + (ball[1] >= gameSide && + (ball[0] - p2 < paddleEdge || + p2 + paddleWidth - ball[0] < paddleEdge)) + ) { + ballDxDy[0] *= Math.random() + 1.3; + ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6); + if (Math.abs(ballDxDy[0]) == 6) { + ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random(); + } + } else { + ballDxDy[0] /= 1.1; + } + ballDxDy[1] *= -1; + ball[1] = ball[1] <= 0 ? 0 : gameSide; + } + } + ball = [ + Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0), + Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0), + ]; + if (ball[0] <= 0 || ball[0] >= gameSide) { + ballDxDy[0] *= -1; + } + } + + function updatePlayers() { + if (keydown) { + let p1Adj = 1; + if ( + (keydown == 37 && !window.RTL_UI) || + (keydown == 39 && window.RTL_UI) + ) { + p1Adj = -1; + } + p1 += p1Adj * 10 * keydownAdj; + } + + let sign = Math.sign(ballDxDy[0]); + if ( + (sign > 0 && ball[0] > p2 + paddleWidth / 2) || + (sign < 0 && ball[0] < p2 + paddleWidth / 2) + ) { + p2 += sign * 3; + } else if ( + (sign > 0 && ball[0] > p2 + paddleWidth / 1.1) || + (sign < 0 && ball[0] < p2 + paddleWidth / 1.1) + ) { + p2 += sign * 9; + } + + if (score >= winScore) { + p1 = ball[0]; + p2 = ball[0]; + } + p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0); + p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0); + } + + function updateScore(adj) { + if (adj) { + score += adj; + } else if (--lives == 0) { + quit = true; + } + ball = ballDef.slice(); + ballDxDy = ballDxDyDef.slice(); + ballDxDy[1] *= score / winScore + 1; + } + + function draw() { + let xAdj = window.RTL_UI ? -1 : 1; + elements["wp-player1"].style.transform = + "translate(" + xAdj * p1 + "px, -37px)"; + elements["wp-player2"].style.transform = + "translate(" + xAdj * p2 + "px, " + gameSide + "px)"; + elements["wp-ball"].style.transform = + "translate(" + xAdj * ball[0] + "px, " + ball[1] + "px)"; + elements["wp-score"].textContent = score; + elements["wp-lives"].setAttribute("lives", lives); + if (score >= winScore) { + let arena = elements.arena; + let image = "url(chrome://browser/skin/customizableui/whimsy.png)"; + let position = `${ + (window.RTL_UI ? gameSide : 0) + xAdj * ball[0] - 10 + }px ${ball[1] - 10}px`; + let repeat = "no-repeat"; + let size = "20px"; + if (arena.style.backgroundImage) { + if (arena.style.backgroundImage.split(",").length >= 160) { + quit = true; + } + + image += ", " + arena.style.backgroundImage; + position += ", " + arena.style.backgroundPosition; + repeat += ", " + arena.style.backgroundRepeat; + size += ", " + arena.style.backgroundSize; + } + arena.style.backgroundImage = image; + arena.style.backgroundPosition = position; + arena.style.backgroundRepeat = repeat; + arena.style.backgroundSize = size; + } + } + + function onkeydown(event) { + keys.push(event.which); + if (keys.length > 10) { + keys.shift(); + let codeEntered = true; + for (let i = 0; i < keys.length; i++) { + if (keys[i] != keysCode[i]) { + codeEntered = false; + break; + } + } + if (codeEntered) { + elements.arena.setAttribute("kcode", "true"); + let spacer = document.querySelector( + "#customization-palette > toolbarpaletteitem" + ); + spacer.setAttribute("kcode", "true"); + } + } + if (event.which == 37 /* left */ || event.which == 39 /* right */) { + keydown = event.which; + keydownAdj *= 1.05; + } + } + + function onkeyup(event) { + if (event.which == 37 || event.which == 39) { + keydownAdj = 1; + keydown = 0; + } + } + + function uninit() { + document.removeEventListener("keydown", onkeydown); + document.removeEventListener("keyup", onkeyup); + if (rAFHandle) { + window.cancelAnimationFrame(rAFHandle); + } + let arena = elements.arena; + while (arena.firstChild) { + arena.firstChild.remove(); + } + arena.removeAttribute("score"); + arena.removeAttribute("lives"); + arena.removeAttribute("kcode"); + arena.style.removeProperty("background-image"); + arena.style.removeProperty("background-position"); + arena.style.removeProperty("background-repeat"); + arena.style.removeProperty("background-size"); + let spacer = document.querySelector( + "#customization-palette > toolbarpaletteitem" + ); + spacer.removeAttribute("kcode"); + elements = null; + document = null; + quit = true; + } + + if (this.uninitWhimsy) { + return this.uninitWhimsy; + } + + let ballDef = [10, 10]; + let ball = [10, 10]; + let ballDxDyDef = [2, 2]; + let ballDxDy = [2, 2]; + let score = 0; + let p1 = 0; + let p2 = 10; + let gameSide = 300; + let paddleEdge = 30; + let paddleWidth = 84; + let keydownAdj = 1; + let keydown = 0; + let keys = []; + let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65]; + let lives = 5; + let winScore = 11; + let quit = false; + let document = this.document; + let rAFHandle = 0; + let elements = { + arena: document.getElementById("customization-pong-arena"), + }; + + document.addEventListener("keydown", onkeydown); + document.addEventListener("keyup", onkeyup); + + for (let id of ["player1", "player2", "ball", "score", "lives"]) { + let el = document.createXULElement("box"); + el.id = "wp-" + id; + elements[el.id] = elements.arena.appendChild(el); + } + + let spacer = this.visiblePalette.querySelector("toolbarpaletteitem"); + for (let player of ["#wp-player1", "#wp-player2"]) { + let val = "-moz-element(#" + spacer.id + ") no-repeat"; + elements.arena.querySelector(player).style.background = val; + } + + let window = this.window; + rAFHandle = window.requestAnimationFrame(function animate() { + update(); + draw(); + if (quit) { + elements["wp-score"].textContent = score; + elements["wp-lives"] && + elements["wp-lives"].setAttribute("lives", lives); + elements.arena.setAttribute("score", score); + elements.arena.setAttribute("lives", lives); + } else { + rAFHandle = window.requestAnimationFrame(animate); + } + }); + + return uninit; + }, +}; + +function __dumpDragData(aEvent, caller) { + if (!gDebug) { + return; + } + let str = + "Dumping drag data (" + + (caller ? caller + " in " : "") + + "CustomizeMode.sys.mjs) {\n"; + str += " type: " + aEvent.type + "\n"; + for (let el of ["target", "currentTarget", "relatedTarget"]) { + if (aEvent[el]) { + str += + " " + + el + + ": " + + aEvent[el] + + "(localName=" + + aEvent[el].localName + + "; id=" + + aEvent[el].id + + ")\n"; + } + } + for (let prop in aEvent.dataTransfer) { + if (typeof aEvent.dataTransfer[prop] != "function") { + str += + " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n"; + } + } + str += "}"; + lazy.log.debug(str); +} + +function dispatchFunction(aFunc) { + Services.tm.dispatchToMainThread(aFunc); +} |