Source code

Revision control

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
* 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.drawInTitlebar";
const kExtraDragSpacePref = "browser.tabs.extraDragSpace";
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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { CustomizableUI } = ChromeUtils.import(
);
const { XPCOMUtils } = ChromeUtils.import(
);
const { AppConstants } = ChromeUtils.import(
);
XPCOMUtils.defineLazyGlobalGetters(this, ["CSS"]);
ChromeUtils.defineModuleGetter(
this,
"AddonManager",
);
ChromeUtils.defineModuleGetter(
this,
"AMTelemetry",
);
ChromeUtils.defineModuleGetter(
this,
"DragPositionManager",
);
ChromeUtils.defineModuleGetter(
this,
"BrowserUtils",
);
ChromeUtils.defineModuleGetter(
this,
"BrowserUsageTelemetry",
);
ChromeUtils.defineModuleGetter(
this,
"SessionStore",
);
XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
const kUrl =
return Services.strings.createBundle(kUrl);
});
XPCOMUtils.defineLazyServiceGetter(
this,
"gTouchBarUpdater",
"@mozilla.org/widget/touchbarupdater;1",
"nsITouchBarUpdater"
);
let gDebug;
XPCOMUtils.defineLazyGetter(this, "log", () => {
let scope = {};
ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
let consoleOptions = {
maxLogLevel: gDebug ? "all" : "log",
prefix: "CustomizeMode",
};
return new scope.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._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();
this._updateDragSpaceCheckbox();
Services.prefs.addObserver(kDrawInTitlebarPref, this);
Services.prefs.addObserver(kExtraDragSpacePref, this);
} else {
this.$("customization-titlebar-visibility-checkbox").hidden = true;
this.$("customization-extra-drag-space-checkbox").hidden = true;
}
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,
get _handler() {
return this.window.CustomizationHandler;
},
uninit() {
if (this._canDrawInTitlebar()) {
Services.prefs.removeObserver(kDrawInTitlebarPref, this);
Services.prefs.removeObserver(kExtraDragSpacePref, 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();
}
},
async _updateThemeButtonIcon() {
let lwthemeButton = this.$("customization-lwtheme-button");
let lwthemeIcon = lwthemeButton.icon;
let theme = (await AddonManager.getAddonsByTypes(["theme"])).find(
addon => addon.isActive
);
lwthemeIcon.style.backgroundImage =
theme && theme.iconURL ? "url(" + theme.iconURL + ")" : "";
},
setTab(aTab) {
if (gTab == aTab) {
return;
}
if (gTab) {
closeGlobalTab();
}
gTab = aTab;
gTab.setAttribute("customizemode", "true");
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 = this.window.getTopWin(true);
if (w) {
w.gCustomizeMode.enter();
return;
}
let obs = () => {
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
w = this.window.getTopWin(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) {
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.loadOneTab("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]):not([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._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();
this._updateThemeButtonIcon();
AddonManager.addAddonListener(this);
this._setupDownloadAutoHideToggle();
this._handler.isEnteringCustomizeMode = false;
CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
if (!this._wantToBeInCustomizeMode) {
this.exit();
}
})().catch(e => {
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) {
log.debug(
"Attempted to exit while we're in the middle of entering. " +
"We'll exit after we've entered"
);
return;
}
if (this.resetting) {
log.debug(
"Attempted to exit while we're resetting. " +
"We'll exit after resetting has finished."
);
return;
}
this._handler.isExitingCustomizeMode = true;
this._teardownDownloadAutoHideToggle();
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 => {
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("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);
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);
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");
BrowserUtils.setToolbarButtonHeightProperty(overflowButton).then(() => {
overflowButton.setAttribute("animate", "true");
overflowButton.addEventListener("animationend", function onAnimationEnd(
event
) {
if (event.animationName.startsWith("overflow-animation")) {
this.setAttribute("fade", "true");
} else if (event.animationName == "overflow-fade") {
this.removeEventListener("animationend", onAnimationEnd);
this.removeAttribute("animate");
this.removeAttribute("fade");
}
});
});
}
},
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);
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;
} catch (ex) {
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) {
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() {
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;
},
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;
},
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"));
}
if (aNode.hasAttribute("label")) {
wrapper.setAttribute("title", aNode.getAttribute("label"));
wrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
} else if (aNode.hasAttribute("title")) {
wrapper.setAttribute("title", aNode.getAttribute("title"));
wrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
}
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 == "menu-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",
gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
);
}
return wrapper;
},
deferredUnwrapToolbarItem(aWrapper) {
return new Promise(resolve => {
dispatchFunction(() => {
let item = null;
try {
item = this.unwrapToolbarItem(aWrapper);
} catch (ex) {
Cu.reportError(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) {
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 == "menu-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(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) {
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("dragexit", 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("dragexit", 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(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(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(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(aEvent) {
aEvent.target.parentNode.parentNode.hidePopup();
AMTelemetry.recordLinkEvent({ object: "customize", value: "manageThemes" });
this.window.BrowserOpenAddonsMgr("addons://list/theme");
},
getMoreThemes(aEvent) {
aEvent.target.parentNode.parentNode.hidePopup();
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 compactItem = doc.getElementById(
"customization-uidensity-menuitem-compact"
);
compactItem.mode = gUIDensity.MODE_COMPACT;
let items = [normalItem, compactItem];
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(
);
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();
},
async onThemesMenuShowing(aEvent) {
const DEFAULT_THEME_ID = "default-theme@mozilla.org";
const LIGHT_THEME_ID = "firefox-compact-light@mozilla.org";
const DARK_THEME_ID = "firefox-compact-dark@mozilla.org";
const MAX_THEME_COUNT = 6;
this._clearThemesMenu(aEvent.target);
let onThemeSelected = panel => {
// This causes us to call _onUIChange when the LWT actually changes,
// so the restore defaults / undo reset button is updated correctly.
this._nextThemeChangeUserTriggered = true;
panel.hidePopup();
};
let doc = this.window.document;
function buildToolbarButton(aTheme) {
let tbb = doc.createXULElement("toolbarbutton");
tbb.theme = aTheme;
tbb.setAttribute("label", aTheme.name);
tbb.setAttribute(
"image",
);
if (aTheme.description) {
tbb.setAttribute("tooltiptext", aTheme.description);
}
tbb.setAttribute("tabindex", "0");
tbb.classList.add("customization-lwtheme-menu-theme");
let isActive = aTheme.isActive;
tbb.setAttribute("aria-checked", isActive);
tbb.setAttribute("role", "menuitemradio");
if (isActive) {
tbb.setAttribute("active", "true");
}
return tbb;
}
let themes = await AddonManager.getAddonsByTypes(["theme"]);
let currentTheme = themes.find(theme => theme.isActive);
// Move the current theme (if any) and the light/dark themes to the start:
let importantThemes = new Set([
DEFAULT_THEME_ID,
LIGHT_THEME_ID,
DARK_THEME_ID,
]);
if (currentTheme) {
importantThemes.add(currentTheme.id);
}
let importantList = [];
for (let importantTheme of importantThemes) {
importantList.push(
...themes.splice(
themes.findIndex(theme => theme.id == importantTheme),
1
)
);
}
// Sort the remainder alphabetically:
themes.sort((a, b) => a.name.localeCompare(b.name));
themes = importantList.concat(themes);
if (themes.length > MAX_THEME_COUNT) {
themes.length = MAX_THEME_COUNT;
}
let footer = doc.getElementById("customization-lwtheme-menu-footer");
let panel = footer.parentNode;
for (let theme of themes) {
let button = buildToolbarButton(theme);
button.addEventListener("command", async () => {
await button.theme.enable();
onThemeSelected(panel);
AMTelemetry.recordActionEvent({
object: "customize",
action: "enable",
extra: { type: "theme", addonId: theme.id },
});
});
panel.insertBefore(button, footer);
}
},
_clearThemesMenu(panel) {
let footer = this.$("customization-lwtheme-menu-footer");
let element = footer;
while (
element.previousElementSibling &&
element.previousElementSibling.localName == "toolbarbutton"
) {
element.previousElementSibling.remove();
}
// Workaround for bug 1059934
panel.removeAttribute("height");
},
_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 = gTouchBarUpdater.isTouchBarInitialized();
touchBarButton.hidden = !isTouchBarInitialized;
touchBarSpacer.hidden = !isTouchBarInitialized;
},
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 "dragexit":
this._onDragExit(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() {
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();
this._updateDragSpaceCheckbox();
}
break;
}
},
async onInstalled(addon) {
await this.onEnabled(addon);
},
async onEnabled(addon) {
if (addon.type != "theme") {
return;
}
await this._updateThemeButtonIcon();
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.prefs.getBoolPref(
kDrawInTitlebarPref,
this.window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches
);
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");
}
},
_updateDragSpaceCheckbox() {
let extraDragSpace = Services.prefs.getBoolPref(kExtraDragSpacePref);
let drawInTitlebar = Services.prefs.getBoolPref(
kDrawInTitlebarPref,
this.window.matchMedia("(-moz-gtk-csd-hide-titlebar-by-default)").matches
);
let menuBar = this.$("toolbar-menubar");
let menuBarEnabled =
menuBar &&
AppConstants.platform != "macosx" &&
menuBar.getAttribute("autohide") != "true";
let checkbox = this.$("customization-extra-drag-space-checkbox");
if (extraDragSpace) {
checkbox.setAttribute("checked", "true");
} else {
checkbox.removeAttribute("checked");
}
if (!drawInTitlebar || menuBarEnabled) {
checkbox.setAttribute("disabled", "true");
} else {
checkbox.removeAttribute("disabled");
}
},
toggleTitlebar(aShouldShowTitlebar) {
// Drawing in the titlebar means not showing the titlebar, hence the negation:
Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
this._updateDragSpaceCheckbox();
},
toggleDragSpace(aShouldShowDragSpace) {
Services.prefs.setBoolPref(kExtraDragSpacePref, aShouldShowDragSpace);
},
_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;
DragPositionManager.start(this.window);
let canUsePrevSibling =
placeForItem == "toolbar" || placeForItem == "menu-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 (
targetArea.id != kPaletteId &&
!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 == "menu-panel") {
let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
let dropTargetCenter = itemRect.top + itemRect.height / 2;
let existingDir = dragOverItem.getAttribute("dragover");