fune/browser/components/customizableui/CustomizeMode.jsm
Gijs Kruitbosch 3e5d710873 Bug 1817443 - move url loading method implementations from utilityOverlay.js into their own helper module, r=mossop
openLinkIn would really benefit from being split up a bit, and adding more
globals to the browser window is icky. Also, the story for opening new tabs if
you're not inside a window is a nightmare right now. Moving this code
to a module is a first step to making that story nicer.

I kept wrappers for all the functions I'm moving, and added the `window` as the
first argument. In the future we can update these functions to support being
called without a `window` ref. The one exception is getTopWin, where I updated
the callers in this patch.

I had to tweak the parameter detection of the different arguments supported by
openUILinkIn because forwarding calls means arguments.length is always larger
than 3... but then also removed it in the next commit.

Differential Revision: https://phabricator.services.mozilla.com/D170210
2023-02-23 17:02:43 +00:00

3014 lines
94 KiB
JavaScript

/* 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/. */
"use strict";
var EXPORTED_SYMBOLS = ["CustomizeMode"];
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";
const { CustomizableUI } = ChromeUtils.import(
"resource:///modules/CustomizableUI.jsm"
);
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
);
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const lazy = {};
ChromeUtils.defineModuleGetter(
lazy,
"AddonManager",
"resource://gre/modules/AddonManager.jsm"
);
ChromeUtils.defineModuleGetter(
lazy,
"AMTelemetry",
"resource://gre/modules/AddonManager.jsm"
);
ChromeUtils.defineModuleGetter(
lazy,
"DragPositionManager",
"resource:///modules/DragPositionManager.jsm"
);
ChromeUtils.defineModuleGetter(
lazy,
"BrowserUsageTelemetry",
"resource:///modules/BrowserUsageTelemetry.jsm"
);
ChromeUtils.defineESModuleGetters(lazy, {
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
});
XPCOMUtils.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;
XPCOMUtils.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;
}
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.collapsed = 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.collapsed = 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() {
lazy.AMTelemetry.recordLinkEvent({
object: "customize",
value: "manageThemes",
});
this.window.BrowserOpenAddonsMgr("addons://list/theme");
},
getMoreThemes(aEvent) {
aEvent.target.parentNode.parentNode.hidePopup();
lazy.AMTelemetry.recordLinkEvent({
object: "customize",
value: "getThemes",
});
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,
// which is only available in Windows 10.
if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
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.jsm) {\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);
}