Source code
Revision control
Copy as Markdown
Other Tools
/* 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
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",
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.BrowserCommands.openTab();
}
win.gBrowser.removeTab(gTab, { animate: true });
gTab = null;
}
var gTabsProgressListener = {
onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) {
// 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
);
}
this._attachEventListeners();
// 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_minimizeWindow",
"cmd_quitApplication",
"View:FullScreen",
"Browser:NextTab",
"Browser:PrevTab",
"Browser:NewUserContextTab",
"Tools:PrivateBrowsing",
"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");
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.toggleAttribute("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.toggleAttribute("customizing", true);
let customizableToolbars = document.querySelectorAll(
"toolbar[customizable=true]:not([autohide=true], [collapsed=true])"
);
for (let toolbar of customizableToolbars) {
toolbar.toggleAttribute("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) {
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);
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;
toolbar.toggleAttribute(
"customizing",
aEvent.detail.visible && toolbar.getAttribute("customizable") == "true"
);
this._onUIChange();
},
onWidgetMoved() {
this._onUIChange();
},
onWidgetAdded() {
this._onUIChange();
},
onWidgetRemoved() {
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() {
},
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;