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
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { SearchWidgetTracker } from "resource:///modules/SearchWidgetTracker.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
CustomizableWidgets: "resource:///modules/CustomizableWidgets.sys.mjs",
HomePage: "resource:///modules/HomePage.sys.mjs",
PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ShortcutUtils: "resource://gre/modules/ShortcutUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
const kUrl =
"chrome://browser/locale/customizableui/customizableWidgets.properties";
return Services.strings.createBundle(kUrl);
});
const kDefaultThemeID = "default-theme@mozilla.org";
const kSpecialWidgetPfx = "customizableui-special-";
const kPrefCustomizationState = "browser.uiCustomization.state";
const kPrefCustomizationHorizontalTabstrip =
"browser.uiCustomization.horizontalTabstrip";
const kPrefCustomizationHorizontalTabsBackup =
"browser.uiCustomization.horizontalTabsBackup";
const kPrefCustomizationNavBarWhenVerticalTabs =
"browser.uiCustomization.navBarWhenVerticalTabs";
const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPrefDrawInTitlebar = "browser.tabs.inTitlebar";
const kPrefUIDensity = "browser.uidensity";
const kPrefAutoTouchMode = "browser.touchmode.auto";
const kPrefAutoHideDownloadsButton = "browser.download.autohideButton";
const kPrefProtonToolbarVersion = "browser.proton.toolbar.version";
const kPrefHomeButtonUsed = "browser.engagement.home-button.has-used";
const kPrefLibraryButtonUsed = "browser.engagement.library-button.has-used";
const kPrefSidebarButtonUsed = "browser.engagement.sidebar-button.has-used";
const kPrefSidebarRevampEnabled = "sidebar.revamp";
const kPrefSidebarVerticalTabsEnabled = "sidebar.verticalTabs";
const kExpectedWindowURL = AppConstants.BROWSER_CHROME_URL;
var gDefaultTheme;
var gSelectedTheme;
/**
* The keys are the handlers that are fired when the event type (the value)
* is fired on the subview. A widget that provides a subview has the option
* of providing onViewShowing and onViewHiding event handlers.
*/
const kSubviewEvents = ["ViewShowing", "ViewHiding"];
/**
* The current version. We can use this to auto-add new default widgets as necessary.
* (would be const but isn't because of testing purposes)
*/
var kVersion = 21;
/**
* Buttons removed from built-ins by version they were removed. kVersion must be
* bumped any time a new id is added to this. Use the button id as key, and
* version the button is removed in as the value. e.g. "pocket-button": 5
*/
var ObsoleteBuiltinButtons = {
"feed-button": 15,
};
/**
* gPalette is a map of every widget that CustomizableUI.sys.mjs knows about, keyed
* on their IDs.
*/
var gPalette = new Map();
/**
* gAreas maps area IDs to Sets of properties about those areas. An area is a
* place where a widget can be put.
*/
var gAreas = new Map();
/**
* gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
* are placed within that area (either directly in the area node, or in the
* customizationTarget of the node).
*/
var gPlacements = new Map();
/**
* gFuturePlacements represent placements that will happen for areas that have
* not yet loaded (due to lazy-loading). This can occur when add-ons register
* widgets.
*/
var gFuturePlacements = new Map();
var gSupportedWidgetTypes = new Set([
// A button that does a command.
"button",
// A button that opens a view in a panel (or in a subview of the panel).
"view",
// A combination of the above, which looks different depending on whether it's
// located in the toolbar or in the panel: When located in the toolbar, shown
// as a combined item of a button and a dropmarker button. The button triggers
// the command and the dropmarker button opens the view. When located in the
// panel, shown as one item which opens the view, and the button command
// cannot be triggered separately.
"button-and-view",
// A custom widget that defines its own markup.
"custom",
]);
/**
* gPanelsForWindow is a list of known panels in a window which we may need to close
* should command events fire which target them.
*/
var gPanelsForWindow = new WeakMap();
/**
* gSeenWidgets remembers which widgets the user has seen for the first time
* before. This way, if a new widget is created, and the user has not seen it
* before, it can be put in its default location. Otherwise, it remains in the
* palette.
*/
var gSeenWidgets = new Set();
/**
* gDirtyAreaCache is a set of area IDs for areas where items have been added,
* moved or removed at least once. This set is persisted, and is used to
* optimize building of toolbars in the default case where no toolbars should
* be "dirty".
*/
var gDirtyAreaCache = new Set();
/**
* gPendingBuildAreas is a map from area IDs to map from build nodes to their
* existing children at the time of node registration, that are waiting
* for the area to be registered
*/
var gPendingBuildAreas = new Map();
var gSavedState = null;
var gRestoring = false;
var gDirty = false;
var gInBatchStack = 0;
var gResetting = false;
var gUndoResetting = false;
/**
* gBuildAreas maps area IDs to actual area nodes within browser windows.
*/
var gBuildAreas = new Map();
/**
* gBuildWindows is a map of windows that have registered build areas, mapped
* to a Set of known toolboxes in that window.
*/
var gBuildWindows = new Map();
var gNewElementCount = 0;
var gGroupWrapperCache = new Map();
var gSingleWrapperCache = new WeakMap();
var gListeners = new Set();
var gUIStateBeforeReset = {
uiCustomizationState: null,
drawInTitlebar: null,
currentTheme: null,
uiDensity: null,
autoTouchMode: null,
};
/*
* The current tab orientation: initially null until initialization,
* true for vertical, false for horizontal
*/
var gCurrentVerticalTabs = null;
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gDebuggingEnabled",
kPrefCustomizationDebug,
false,
(pref, oldVal, newVal) => {
if (typeof lazy.log != "undefined") {
lazy.log.maxLogLevel = newVal ? "all" : "log";
}
}
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"resetPBMToolbarButtonEnabled",
"browser.privatebrowsing.resetPBM.enabled",
false
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"sidebarRevampEnabled",
"sidebar.revamp",
false,
(pref, oldVal, newVal) => {
if (!newVal) {
return;
}
let navbarPlacements = CustomizableUI.getWidgetIdsInArea(
CustomizableUI.AREA_NAVBAR
);
if (!navbarPlacements.includes("sidebar-button")) {
CustomizableUI.addWidgetToArea(
"sidebar-button",
CustomizableUI.AREA_NAVBAR,
0
);
}
}
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"verticalTabsPref",
"sidebar.verticalTabs",
false,
(pref, oldVal, newVal) => {
lazy.log.debug(
`sidebar.verticalTabs change handler, calling updateTabStripOrientation with value: ${newVal}, gCurrentVerticalTabs: ${gCurrentVerticalTabs}`
);
CustomizableUIInternal.updateTabStripOrientation();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"horizontalPlacementsPref",
kPrefCustomizationHorizontalTabstrip,
""
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"verticalPlacementsPref",
kPrefCustomizationNavBarWhenVerticalTabs,
""
);
ChromeUtils.defineLazyGetter(lazy, "log", () => {
let { ConsoleAPI } = ChromeUtils.importESModule(
"resource://gre/modules/Console.sys.mjs"
);
let consoleOptions = {
maxLogLevel: lazy.gDebuggingEnabled ? "all" : "log",
prefix: "CustomizableUI",
};
return new ConsoleAPI(consoleOptions);
});
var CustomizableUIInternal = {
initialize() {
lazy.log.debug("Initializing");
lazy.AddonManagerPrivate.databaseReady.then(async () => {
lazy.AddonManager.addAddonListener(this);
let addons = await lazy.AddonManager.getAddonsByTypes(["theme"]);
gDefaultTheme = addons.find(addon => addon.id == kDefaultThemeID);
gSelectedTheme = addons.find(addon => addon.isActive) || gDefaultTheme;
});
this.addListener(this);
this._defineBuiltInWidgets();
this.loadSavedState();
this._updateForNewVersion();
this._updateForNewProtonVersion();
this._markObsoleteBuiltinButtonsSeen();
this.registerArea(
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
{
type: CustomizableUI.TYPE_PANEL,
defaultPlacements: [],
anchor: "nav-bar-overflow-button",
},
true
);
this.registerArea(
CustomizableUI.AREA_ADDONS,
{
type: CustomizableUI.TYPE_PANEL,
defaultPlacements: [],
anchor: "unified-extensions-button",
},
false
);
let navbarPlacements = [
lazy.sidebarRevampEnabled ? "sidebar-button" : null,
"back-button",
"forward-button",
"stop-reload-button",
Services.policies.isAllowed("removeHomeButtonByDefault")
? null
: "home-button",
"spring",
"vertical-spacer",
"urlbar-container",
"spring",
"save-to-pocket-button",
"downloads-button",
AppConstants.MOZ_DEV_EDITION ? "developer-button" : null,
"fxa-toolbar-menu-button",
lazy.resetPBMToolbarButtonEnabled ? "reset-pbm-toolbar-button" : null,
].filter(name => name);
this.registerArea(
CustomizableUI.AREA_NAVBAR,
{
type: CustomizableUI.TYPE_TOOLBAR,
overflowable: true,
defaultPlacements: navbarPlacements,
verticalTabsDefaultPlacements: [
"firefox-view-button",
"alltabs-button",
],
defaultCollapsed: false,
},
true
);
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
this.registerArea(
CustomizableUI.AREA_MENUBAR,
{
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: ["menubar-items"],
defaultCollapsed: true,
},
true
);
}
this.registerArea(
CustomizableUI.AREA_TABSTRIP,
{
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: [
"firefox-view-button",
"tabbrowser-tabs",
"new-tab-button",
"alltabs-button",
],
verticalTabsDefaultPlacements: [],
defaultCollapsed: null,
},
true
);
this.registerArea(
CustomizableUI.AREA_VERTICAL_TABSTRIP,
{
type: "toolbar",
defaultPlacements: [],
verticalTabsDefaultPlacements: ["tabbrowser-tabs"],
defaultCollapsed: null,
},
true
);
this.registerArea(
CustomizableUI.AREA_BOOKMARKS,
{
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: ["personal-bookmarks"],
defaultCollapsed: "newtab",
},
true
);
lazy.log.debug(`All the areas registered: ${[...gAreas.keys()]}`);
// At initialization, if we find vertical tabs enabled but not sidebar.revamp
// we'll enable revamp rather than disable vertical tabs.
this.reconcileSidebarPrefs(kPrefSidebarVerticalTabsEnabled);
this.initializeForTabsOrientation(CustomizableUI.verticalTabsEnabled);
SearchWidgetTracker.init();
Services.obs.addObserver(this, "browser-set-toolbar-visibility");
Services.prefs.addObserver(kPrefSidebarVerticalTabsEnabled, this);
Services.prefs.addObserver(kPrefSidebarRevampEnabled, this);
},
onEnabled(addon) {
if (addon.type == "theme") {
gSelectedTheme = addon;
}
},
get _builtinAreas() {
return new Set([
...this._builtinToolbars,
CustomizableUI.AREA_FIXED_OVERFLOW_PANEL,
CustomizableUI.AREA_ADDONS,
]);
},
get _builtinToolbars() {
let toolbars = new Set([
CustomizableUI.AREA_NAVBAR,
CustomizableUI.AREA_BOOKMARKS,
CustomizableUI.AREA_TABSTRIP,
]);
if (AppConstants.platform != "macosx") {
toolbars.add(CustomizableUI.AREA_MENUBAR);
}
return toolbars;
},
_defineBuiltInWidgets() {
for (let widgetDefinition of lazy.CustomizableWidgets) {
this.createBuiltinWidget(widgetDefinition);
}
},
// eslint-disable-next-line complexity
_updateForNewVersion() {
// We should still enter even if gSavedState.currentVersion >= kVersion
// because the per-widget pref facility is independent of versioning.
if (!gSavedState) {
// Flip all the prefs so we don't try to re-introduce later:
for (let [, widget] of gPalette) {
if (widget.defaultArea && widget._introducedInVersion === "pref") {
let prefId = "browser.toolbarbuttons.introduced." + widget.id;
Services.prefs.setBoolPref(prefId, true);
}
}
return;
}
let currentVersion = gSavedState.currentVersion;
for (let [id, widget] of gPalette) {
if (widget.defaultArea) {
let shouldAdd = false;
let shouldSetPref = false;
let prefId = "browser.toolbarbuttons.introduced." + widget.id;
if (widget._introducedInVersion === "pref") {
try {
shouldAdd = !Services.prefs.getBoolPref(prefId);
} catch (ex) {
// Pref doesn't exist:
shouldAdd = true;
}
shouldSetPref = shouldAdd;
} else if (widget._introducedInVersion > currentVersion) {
shouldAdd = true;
} else if (
widget._introducedByPref &&
Services.prefs.getBoolPref(widget._introducedByPref)
) {
shouldSetPref = shouldAdd = !Services.prefs.getBoolPref(
prefId,
false
);
}
if (shouldAdd) {
let futurePlacements = gFuturePlacements.get(widget.defaultArea);
if (futurePlacements) {
futurePlacements.add(id);
} else {
gFuturePlacements.set(widget.defaultArea, new Set([id]));
}
if (shouldSetPref) {
Services.prefs.setBoolPref(prefId, true);
}
}
}
}
// Nothing to migrate now if we don't have placements.
if (!gSavedState.placements) {
return;
}
if (
currentVersion < 7 &&
gSavedState.placements[CustomizableUI.AREA_NAVBAR]
) {
let placements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
let newPlacements = [
"back-button",
"forward-button",
"stop-reload-button",
"home-button",
];
for (let button of placements) {
if (!newPlacements.includes(button)) {
newPlacements.push(button);
}
}
if (!newPlacements.includes("sidebar-button")) {
newPlacements.unshift("sidebar-button");
}
gSavedState.placements[CustomizableUI.AREA_NAVBAR] = newPlacements;
}
if (currentVersion < 8 && gSavedState.placements["PanelUI-contents"]) {
let savedPanelPlacements = gSavedState.placements["PanelUI-contents"];
delete gSavedState.placements["PanelUI-contents"];
let defaultPlacements = [
"edit-controls",
"zoom-controls",
"new-window-button",
"privatebrowsing-button",
"save-page-button",
"print-button",
"history-panelmenu",
"fullscreen-button",
"find-button",
"preferences-button",
"add-ons-button",
"sync-button",
];
if (!AppConstants.MOZ_DEV_EDITION) {
defaultPlacements.splice(-1, 0, "developer-button");
}
let showCharacterEncoding = Services.prefs.getComplexValue(
"browser.menu.showCharacterEncoding",
Ci.nsIPrefLocalizedString
).data;
if (showCharacterEncoding == "true") {
defaultPlacements.push("characterencoding-button");
}
savedPanelPlacements = savedPanelPlacements.filter(
id => !defaultPlacements.includes(id)
);
if (savedPanelPlacements.length) {
gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] =
savedPanelPlacements;
}
}
if (currentVersion < 9 && gSavedState.placements["nav-bar"]) {
let placements = gSavedState.placements["nav-bar"];
if (placements.includes("urlbar-container")) {
let urlbarIndex = placements.indexOf("urlbar-container");
let secondSpringIndex = urlbarIndex + 1;
// Insert if there isn't already a spring before the urlbar
if (
urlbarIndex == 0 ||
!placements[urlbarIndex - 1].startsWith(kSpecialWidgetPfx + "spring")
) {
placements.splice(urlbarIndex, 0, "spring");
// The url bar is now 1 index later, so increment the insertion point for
// the second spring.
secondSpringIndex++;
}
// If the search container is present, insert after the search container
// instead of after the url bar
let searchContainerIndex = placements.indexOf("search-container");
if (searchContainerIndex != -1) {
secondSpringIndex = searchContainerIndex + 1;
}
if (
secondSpringIndex == placements.length ||
!placements[secondSpringIndex].startsWith(
kSpecialWidgetPfx + "spring"
)
) {
placements.splice(secondSpringIndex, 0, "spring");
}
}
// Finally, replace the bookmarks menu button with the library one if present
if (placements.includes("bookmarks-menu-button")) {
let bmbIndex = placements.indexOf("bookmarks-menu-button");
placements.splice(bmbIndex, 1);
let downloadButtonIndex = placements.indexOf("downloads-button");
let libraryIndex =
downloadButtonIndex == -1 ? bmbIndex : downloadButtonIndex + 1;
placements.splice(libraryIndex, 0, "library-button");
}
}
if (currentVersion < 10) {
for (let placements of Object.values(gSavedState.placements)) {
if (placements.includes("webcompat-reporter-button")) {
placements.splice(placements.indexOf("webcompat-reporter-button"), 1);
break;
}
}
}
// Move the downloads button to the default position in the navbar if it's
// not there already.
if (currentVersion < 11) {
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
// First remove from wherever it currently lives, if anywhere:
for (let placements of Object.values(gSavedState.placements)) {
let existingIndex = placements.indexOf("downloads-button");
if (existingIndex != -1) {
placements.splice(existingIndex, 1);
break; // It can only be in 1 place, so no point looking elsewhere.
}
}
// Now put the button in the navbar in the correct spot:
if (navbarPlacements) {
let insertionPoint = navbarPlacements.indexOf("urlbar-container");
// Deliberately iterate to 1 past the end of the array to insert at the
// end if need be.
while (++insertionPoint < navbarPlacements.length) {
let widget = navbarPlacements[insertionPoint];
// If we find a non-searchbar, non-spacer node, break out of the loop:
if (
widget != "search-container" &&
!this.matchingSpecials(widget, "spring")
) {
break;
}
}
// We either found the right spot, or reached the end of the
// placements, so insert here:
navbarPlacements.splice(insertionPoint, 0, "downloads-button");
}
}
if (currentVersion < 12) {
const removedButtons = [
"loop-call-button",
"loop-button-throttled",
"pocket-button",
];
for (let placements of Object.values(gSavedState.placements)) {
for (let button of removedButtons) {
let buttonIndex = placements.indexOf(button);
if (buttonIndex != -1) {
placements.splice(buttonIndex, 1);
}
}
}
}
// Remove the old placements from the now-gone Nightly-only
// "New non-e10s window" button.
if (currentVersion < 13) {
for (let placements of Object.values(gSavedState.placements)) {
let buttonIndex = placements.indexOf("e10s-button");
if (buttonIndex != -1) {
placements.splice(buttonIndex, 1);
}
}
}
// Remove unsupported custom toolbar saved placements
if (currentVersion < 14) {
for (let area in gSavedState.placements) {
if (!this._builtinAreas.has(area)) {
delete gSavedState.placements[area];
}
}
}
// Add the FxA toolbar menu as the right most button item
if (currentVersion < 16) {
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
// Place the menu item as the first item to the left of the hamburger menu
if (navbarPlacements) {
navbarPlacements.push("fxa-toolbar-menu-button");
}
}
// Add the save to Pocket button left of downloads button.
if (currentVersion < 17) {
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
let persistedPageActionsPref = Services.prefs.getCharPref(
"browser.pageActions.persistedActions",
""
);
let pocketPreviouslyInUrl = true;
try {
let persistedPageActionsData = JSON.parse(persistedPageActionsPref);
// If Pocket was previously not in the url bar, let's not put it in the toolbar.
// It'll still be an option to add from the customization page.
pocketPreviouslyInUrl =
persistedPageActionsData.idsInUrlbar.includes("pocket");
} catch (e) {}
if (navbarPlacements && pocketPreviouslyInUrl) {
// Pocket's new home is next to the downloads button, or the next best spot.
let newPosition =
navbarPlacements.indexOf("downloads-button") ??
navbarPlacements.indexOf("fxa-toolbar-menu-button") ??
navbarPlacements.length;
navbarPlacements.splice(newPosition, 0, "save-to-pocket-button");
}
}
// Add firefox-view if not present
if (currentVersion < 18) {
let tabstripPlacements =
gSavedState.placements[CustomizableUI.AREA_TABSTRIP];
if (
tabstripPlacements &&
!tabstripPlacements.includes("firefox-view-button")
) {
tabstripPlacements.unshift("firefox-view-button");
}
}
// Unified Extensions addon button migration, which puts any browser action
// buttons in the overflow menu into the addons panel instead.
if (currentVersion < 19) {
let overflowPlacements =
gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] || [];
// The most likely case is that there are no AREA_ADDONS placements, in which case the
// array won't exist.
let addonsPlacements =
gSavedState.placements[CustomizableUI.AREA_ADDONS] || [];
// Migration algorithm for transitioning to Unified Extensions:
//
// 1. Create two arrays, one for extension widgets, one for built-in widgets.
// 2. Iterate all items in the overflow panel, and push them into the
// appropriate array based on whether or not its an extension widget.
// 3. Overwrite the overflow panel placements with the built-in widgets array.
// 4. Prepend the extension widgets to the addonsPlacements array. Note that this
// does not overwrite this array as a precaution because it's possible
// (though pretty unlikely) that some widgets are already there.
//
// For extension widgets that were in the palette, they will be appended to the
// addons area when they're created within createWidget.
let extWidgets = [];
let builtInWidgets = [];
for (let widgetId of overflowPlacements) {
if (CustomizableUI.isWebExtensionWidget(widgetId)) {
extWidgets.push(widgetId);
} else {
builtInWidgets.push(widgetId);
}
}
gSavedState.placements[CustomizableUI.AREA_FIXED_OVERFLOW_PANEL] =
builtInWidgets;
gSavedState.placements[CustomizableUI.AREA_ADDONS] = [
...extWidgets,
...addonsPlacements,
];
}
// Add the PBM reset button as the right most button item
if (currentVersion < 20) {
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
// Place the button as the first item to the left of the hamburger menu
if (
navbarPlacements &&
!navbarPlacements.includes("reset-pbm-toolbar-button")
) {
navbarPlacements.push("reset-pbm-toolbar-button");
}
}
if (currentVersion < 21) {
// If the vertical-spacer has not yet been added, ensure its to the left of the urlbar initially
let navbarPlacements = gSavedState.placements[CustomizableUI.AREA_NAVBAR];
if (!navbarPlacements.includes("vertical-spacer")) {
let urlbarContainerPosition =
navbarPlacements.indexOf("urlbar-container");
gSavedState.placements[CustomizableUI.AREA_NAVBAR].splice(
urlbarContainerPosition - 1,
0,
"vertical-spacer"
);
}
}
},
_updateForNewProtonVersion() {
const VERSION = 3;
let currentVersion = Services.prefs.getIntPref(
kPrefProtonToolbarVersion,
0
);
if (currentVersion >= VERSION) {
return;
}
let placements = gSavedState?.placements?.[CustomizableUI.AREA_NAVBAR];
if (!placements) {
// The profile was created with this version, so no need to migrate.
Services.prefs.setIntPref(kPrefProtonToolbarVersion, VERSION);
return;
}
// Remove the home button if it hasn't been used and is set to about:home
if (currentVersion < 1) {
let homePage = lazy.HomePage.get();
if (
placements.includes("home-button") &&
!Services.prefs.getBoolPref(kPrefHomeButtonUsed) &&
(homePage == "about:home" || homePage == "about:blank") &&
Services.policies.isAllowed("removeHomeButtonByDefault")
) {
placements.splice(placements.indexOf("home-button"), 1);
}
}
// Remove the library button if it hasn't been used
if (currentVersion < 2) {
if (
placements.includes("library-button") &&
!Services.prefs.getBoolPref(kPrefLibraryButtonUsed)
) {
placements.splice(placements.indexOf("library-button"), 1);
}
}
// Remove the library button if it hasn't been used
if (currentVersion < 3) {
if (
placements.includes("sidebar-button") &&
!Services.prefs.getBoolPref(kPrefSidebarButtonUsed)
) {
placements.splice(placements.indexOf("sidebar-button"), 1);
}
}
Services.prefs.setIntPref(kPrefProtonToolbarVersion, VERSION);
},
/**
* _markObsoleteBuiltinButtonsSeen
* when upgrading, ensure obsoleted buttons are in seen state.
*/
_markObsoleteBuiltinButtonsSeen() {
if (!gSavedState) {
return;
}
let currentVersion = gSavedState.currentVersion;
if (currentVersion >= kVersion) {
return;
}
// we're upgrading, update state if necessary
for (let id in ObsoleteBuiltinButtons) {
let version = ObsoleteBuiltinButtons[id];
if (version == kVersion) {
gSeenWidgets.add(id);
gDirty = true;
}
}
},
_placeNewDefaultWidgetsInArea(aArea) {
let futurePlacedWidgets = gFuturePlacements.get(aArea);
let savedPlacements =
gSavedState && gSavedState.placements && gSavedState.placements[aArea];
let defaultPlacements;
if (
CustomizableUI.verticalTabsEnabled &&
gAreas.get(aArea).has("verticalTabsDefaultPlacements")
) {
defaultPlacements = gAreas
.get(aArea)
.get("verticalTabsDefaultPlacements");
} else {
defaultPlacements = gAreas.get(aArea).get("defaultPlacements");
}
if (
!savedPlacements ||
!savedPlacements.length ||
!futurePlacedWidgets ||
!defaultPlacements ||
!defaultPlacements.length
) {
return;
}
let defaultWidgetIndex = -1;
for (let widgetId of futurePlacedWidgets) {
let widget = gPalette.get(widgetId);
if (
!widget ||
widget.source !== CustomizableUI.SOURCE_BUILTIN ||
!widget.defaultArea ||
!(widget._introducedInVersion || widget._introducedByPref) ||
savedPlacements.includes(widget.id)
) {
continue;
}
defaultWidgetIndex = defaultPlacements.indexOf(widget.id);
if (defaultWidgetIndex === -1) {
continue;
}
// Now we know that this widget should be here by default, was newly introduced,
// and we have a saved state to insert into, and a default state to work off of.
// Try introducing after widgets that come before it in the default placements:
for (let i = defaultWidgetIndex; i >= 0; i--) {
// Special case: if the defaults list this widget as coming first, insert at the beginning:
if (i === 0 && i === defaultWidgetIndex) {
savedPlacements.splice(0, 0, widget.id);
// Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
// safe, and we won't skip any items.
futurePlacedWidgets.delete(widget.id);
gDirty = true;
break;
}
// Otherwise, if we're somewhere other than the beginning, check if the previous
// widget is in the saved placements.
if (i) {
let previousWidget = defaultPlacements[i - 1];
let previousWidgetIndex = savedPlacements.indexOf(previousWidget);
if (previousWidgetIndex != -1) {
savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
futurePlacedWidgets.delete(widget.id);
gDirty = true;
break;
}
}
}
// The loop above either inserts the item or doesn't - either way, we can get away
// with doing nothing else now; if the item remains in gFuturePlacements, we'll
// add it at the end in restoreStateForArea.
}
this.saveState();
},
getCustomizationTarget(aElement) {
if (!aElement) {
return null;
}
if (
!aElement._customizationTarget &&
aElement.hasAttribute("customizable")
) {
let id = aElement.getAttribute("customizationtarget");
if (id) {
aElement._customizationTarget =
aElement.ownerDocument.getElementById(id);
}
if (!aElement._customizationTarget) {
aElement._customizationTarget = aElement;
}
}
return aElement._customizationTarget;
},
wrapWidget(aWidgetId) {
if (gGroupWrapperCache.has(aWidgetId)) {
return gGroupWrapperCache.get(aWidgetId);
}
let provider = this.getWidgetProvider(aWidgetId);
if (!provider) {
return null;
}
if (provider == CustomizableUI.PROVIDER_API) {
let widget = gPalette.get(aWidgetId);
if (!widget.wrapper) {
widget.wrapper = new WidgetGroupWrapper(widget);
gGroupWrapperCache.set(aWidgetId, widget.wrapper);
}
return widget.wrapper;
}
// PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
// XXXgijs: this causes bugs in code that depends on widgetWrapper.provider
let wrapper = new XULWidgetGroupWrapper(aWidgetId);
gGroupWrapperCache.set(aWidgetId, wrapper);
return wrapper;
},
registerArea(aName, aProperties, aInternalCaller) {
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
throw new Error("Invalid area name");
}
let areaIsKnown = gAreas.has(aName);
let props = areaIsKnown ? gAreas.get(aName) : new Map();
const kImmutableProperties = new Set(["type", "overflowable"]);
for (let key in aProperties) {
if (
areaIsKnown &&
kImmutableProperties.has(key) &&
props.get(key) != aProperties[key]
) {
throw new Error("An area cannot change the property for '" + key + "'");
}
props.set(key, aProperties[key]);
}
// Default to a toolbar:
if (!props.has("type")) {
props.set("type", CustomizableUI.TYPE_TOOLBAR);
}
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
// Check aProperties instead of props because this check is only interested
// in the passed arguments, not the state of a potentially pre-existing area.
if (!aInternalCaller && aProperties.defaultCollapsed) {
throw new Error(
"defaultCollapsed is only allowed for default toolbars."
);
}
if (!props.has("defaultCollapsed")) {
props.set("defaultCollapsed", true);
}
} else if (props.has("defaultCollapsed")) {
throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
}
// Sanity check type:
let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_PANEL];
if (!allTypes.includes(props.get("type"))) {
throw new Error("Invalid area type " + props.get("type"));
}
// And to no placements:
if (!props.has("defaultPlacements")) {
props.set("defaultPlacements", []);
}
// Sanity check default placements array:
if (!Array.isArray(props.get("defaultPlacements"))) {
throw new Error("Should provide an array of default placements");
}
if (!areaIsKnown) {
gAreas.set(aName, props);
// Reconcile new default widgets. Have to do this before we start restoring things.
this._placeNewDefaultWidgetsInArea(aName);
if (
props.get("type") == CustomizableUI.TYPE_TOOLBAR &&
!gPlacements.has(aName)
) {
lazy.log.debug(
`registerArea ${aName}, no gPlacements yet, nothing to restore`
);
// Guarantee this area exists in gFuturePlacements, to avoid checking it in
// various places elsewhere.
if (!gFuturePlacements.has(aName)) {
gFuturePlacements.set(aName, new Set());
}
} else {
this.restoreStateForArea(aName);
}
// If we have pending build area nodes, register all of them
if (gPendingBuildAreas.has(aName)) {
let pendingNodes = gPendingBuildAreas.get(aName);
for (let pendingNode of pendingNodes) {
this.registerToolbarNode(pendingNode);
}
gPendingBuildAreas.delete(aName);
}
}
},
unregisterArea(aName, aDestroyPlacements) {
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
throw new Error("Invalid area name");
}
if (!gAreas.has(aName) && !gPlacements.has(aName)) {
throw new Error("Area not registered");
}
// Move all the widgets out
this.beginBatchUpdate();
try {
let placements = gPlacements.get(aName);
if (placements) {
// Need to clone this array so removeWidgetFromArea doesn't modify it
placements = [...placements];
placements.forEach(this.removeWidgetFromArea, this);
}
// Delete all remaining traces.
gAreas.delete(aName);
// Only destroy placements when necessary:
if (aDestroyPlacements) {
gPlacements.delete(aName);
} else {
// Otherwise we need to re-set them, as removeFromArea will have emptied
// them out:
gPlacements.set(aName, placements);
}
gFuturePlacements.delete(aName);
let existingAreaNodes = gBuildAreas.get(aName);
if (existingAreaNodes) {
for (let areaNode of existingAreaNodes) {
this.notifyListeners(
"onAreaNodeUnregistered",
aName,
this.getCustomizationTarget(areaNode),
CustomizableUI.REASON_AREA_UNREGISTERED
);
}
}
gBuildAreas.delete(aName);
} finally {
this.endBatchUpdate(true);
}
},
registerToolbarNode(aToolbar) {
let area = aToolbar.id;
if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
return;
}
let areaProperties = gAreas.get(area);
// If this area is not registered, try to do it automatically:
if (!areaProperties) {
if (!gPendingBuildAreas.has(area)) {
gPendingBuildAreas.set(area, []);
}
gPendingBuildAreas.get(area).push(aToolbar);
return;
}
this.beginBatchUpdate();
try {
let placements = gPlacements.get(area);
if (
!placements &&
areaProperties.get("type") == CustomizableUI.TYPE_TOOLBAR
) {
this.restoreStateForArea(area);
placements = gPlacements.get(area);
}
// For toolbars that need it, mark as dirty.
let defaultPlacements = areaProperties.get("defaultPlacements");
if (
!this._builtinToolbars.has(area) ||
placements.length != defaultPlacements.length ||
!placements.every((id, i) => id == defaultPlacements[i])
) {
gDirtyAreaCache.add(area);
}
if (areaProperties.get("overflowable")) {
aToolbar.overflowable = new OverflowableToolbar(aToolbar);
}
this.registerBuildArea(area, aToolbar);
// We only build the toolbar if it's been marked as "dirty". Dirty means
// one of the following things:
// 1) Items have been added, moved or removed from this toolbar before.
// 2) The number of children of the toolbar does not match the length of
// the placements array for that area.
//
// This notion of being "dirty" is stored in a cache which is persisted
// in the saved state.
//
// Secondly, if the list of placements contains an API-provided widget,
// we need to call `buildArea` or it won't be built and put in the toolbar.
if (
gDirtyAreaCache.has(area) ||
placements.some(id => gPalette.has(id))
) {
this.buildArea(area, placements, aToolbar);
} else {
// We must have a builtin toolbar that's in the default state. We need
// to only make sure that all the special nodes are correct.
let specials = placements.filter(p => this.isSpecialWidget(p));
if (specials.length) {
this.updateSpecialsForBuiltinToolbar(aToolbar, specials);
}
}
this.notifyListeners(
"onAreaNodeRegistered",
area,
this.getCustomizationTarget(aToolbar)
);
} finally {
lazy.log.debug(
`registerToolbarNode for ${area}, tabstripAreasReady? ${this.tabstripAreasReady}`
);
this.endBatchUpdate();
}
},
updateSpecialsForBuiltinToolbar(aToolbar, aSpecialIDs) {
// Nodes are going to be in the correct order, so we can do this straightforwardly:
let { children } = this.getCustomizationTarget(aToolbar);
for (let kid of children) {
if (
this.matchingSpecials(aSpecialIDs[0], kid) &&
kid.getAttribute("skipintoolbarset") != "true"
) {
kid.id = aSpecialIDs.shift();
}
if (!aSpecialIDs.length) {
return;
}
}
},
buildArea(aArea, aPlacements, aAreaNode) {
let document = aAreaNode.ownerDocument;
let window = document.defaultView;
let inPrivateWindow = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
let container = this.getCustomizationTarget(aAreaNode);
let areaIsPanel =
gAreas.get(aArea).get("type") == CustomizableUI.TYPE_PANEL;
if (!container) {
throw new Error(
"Expected area " + aArea + " to have a customizationTarget attribute."
);
}
// Restore nav-bar visibility since it may have been hidden
if (aArea == CustomizableUI.AREA_NAVBAR) {
aAreaNode.collapsed = false;
}
this.beginBatchUpdate();
try {
let currentNode = container.firstElementChild;
let placementsToRemove = new Set();
for (let id of aPlacements) {
while (
currentNode &&
currentNode.getAttribute("skipintoolbarset") == "true"
) {
currentNode = currentNode.nextElementSibling;
}
// Fix ids for specials and continue, for correctly placed specials.
if (
currentNode &&
(!currentNode.id || CustomizableUI.isSpecialWidget(currentNode)) &&
this.matchingSpecials(id, currentNode)
) {
currentNode.id = id;
}
if (currentNode && currentNode.id == id) {
currentNode = currentNode.nextElementSibling;
continue;
}
if (this.isSpecialWidget(id) && areaIsPanel) {
placementsToRemove.add(id);
continue;
}
let [provider, node] = this.getWidgetNode(id, window);
if (!node) {
lazy.log.debug("Unknown widget: " + id);
continue;
}
let widget = null;
// If the placements have items in them which are (now) no longer removable,
// we shouldn't be moving them:
if (provider == CustomizableUI.PROVIDER_API) {
widget = gPalette.get(id);
if (!widget.removable && aArea != widget.defaultArea) {
placementsToRemove.add(id);
continue;
}
} else if (
provider == CustomizableUI.PROVIDER_XUL &&
node.parentNode != container &&
!this.isWidgetRemovable(node)
) {
placementsToRemove.add(id);
continue;
} // Special widgets are always removable, so no need to check them
if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) {
continue;
}
if (!inPrivateWindow && widget?.hideInNonPrivateBrowsing) {
continue;
}
this.ensureButtonContextMenu(node, aAreaNode);
// This needs updating in case we're resetting / undoing a reset.
if (widget) {
widget.currentArea = aArea;
}
this.insertWidgetBefore(node, currentNode, container, aArea);
if (gResetting) {
this.notifyListeners("onWidgetReset", node, container);
} else if (gUndoResetting) {
this.notifyListeners("onWidgetUndoMove", node, container);
}
}
if (currentNode) {
let palette = window.gNavToolbox ? window.gNavToolbox.palette : null;
let limit = currentNode.previousElementSibling;
let node = container.lastElementChild;
while (node && node != limit) {
let previousSibling = node.previousElementSibling;
// Nodes opt-in to removability. If they're removable, and we haven't
// seen them in the placements array, then we toss them into the palette
// if one exists. If no palette exists, we just remove the node. If the
// node is not removable, we leave it where it is. However, we can only
// safely touch elements that have an ID - both because we depend on
// IDs (or are specials), and because such elements are not intended to
// be widgets (eg, titlebar-spacer elements).
if (
(node.id || this.isSpecialWidget(node)) &&
node.getAttribute("skipintoolbarset") != "true"
) {
if (this.isWidgetRemovable(node)) {
if (node.id && (gResetting || gUndoResetting)) {
let widget = gPalette.get(node.id);
if (widget) {
widget.currentArea = null;
}
}
this.notifyDOMChange(node, null, container, true, () => {
if (palette && !this.isSpecialWidget(node.id)) {
palette.appendChild(node);
this.removeLocationAttributes(node);
} else {
container.removeChild(node);
}
});
} else {
node.setAttribute("removable", false);
lazy.log.debug(
"Adding non-removable widget to placements of " +
aArea +
": " +
node.id
);
gPlacements.get(aArea).push(node.id);
gDirty = true;
}
}
node = previousSibling;
}
}
// If there are placements in here which aren't removable from their original area,
// we remove them from this area's placement array. They will (have) be(en) added
// to their original area's placements array in the block above this one.
if (placementsToRemove.size) {
let placementAry = gPlacements.get(aArea);
for (let id of placementsToRemove) {
let index = placementAry.indexOf(id);
placementAry.splice(index, 1);
}
}
if (gResetting) {
this.notifyListeners("onAreaReset", aArea, container);
}
} finally {
this.endBatchUpdate();
}
},
addPanelCloseListeners(aPanel) {
aPanel.addEventListener("click", this, { mozSystemGroup: true });
aPanel.addEventListener("keypress", this, { mozSystemGroup: true });
let win = aPanel.ownerGlobal;
if (!gPanelsForWindow.has(win)) {
gPanelsForWindow.set(win, new Set());
}
gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
},
removePanelCloseListeners(aPanel) {
aPanel.removeEventListener("click", this, { mozSystemGroup: true });
aPanel.removeEventListener("keypress", this, { mozSystemGroup: true });
let win = aPanel.ownerGlobal;
let panels = gPanelsForWindow.get(win);
if (panels) {
panels.delete(this._getPanelForNode(aPanel));
}
},
ensureButtonContextMenu(aNode, aAreaNode, forcePanel) {
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
let currentContextMenu =
aNode.getAttribute("context") || aNode.getAttribute("contextmenu");
let contextMenuForPlace;
if (
CustomizableUI.isWebExtensionWidget(aNode.id) &&
(aAreaNode?.id == CustomizableUI.AREA_ADDONS ||
aNode.getAttribute("overflowedItem") == "true")
) {
contextMenuForPlace = null;
} else {
contextMenuForPlace =
forcePanel || "panel" == CustomizableUI.getPlaceForItem(aAreaNode)
? kPanelItemContextMenu
: null;
}
if (contextMenuForPlace && !currentContextMenu) {
aNode.setAttribute("context", contextMenuForPlace);
} else if (
currentContextMenu == kPanelItemContextMenu &&
contextMenuForPlace != kPanelItemContextMenu
) {
aNode.removeAttribute("context");
aNode.removeAttribute("contextmenu");
}
},
getWidgetProvider(aWidgetId) {
if (this.isSpecialWidget(aWidgetId)) {
return CustomizableUI.PROVIDER_SPECIAL;
}
if (gPalette.has(aWidgetId)) {
return CustomizableUI.PROVIDER_API;
}
// If this was an API widget that was destroyed, return null:
if (gSeenWidgets.has(aWidgetId)) {
return null;
}
// We fall back to the XUL provider, but we don't know for sure (at this
// point) whether it exists there either. So the API is technically lying.
// Ideally, it would be able to return an error value (or throw an
// exception) if it really didn't exist. Our code calling this function
// handles that fine, but this is a public API.
return CustomizableUI.PROVIDER_XUL;
},
getWidgetNode(aWidgetId, aWindow) {
let document = aWindow.document;
if (this.isSpecialWidget(aWidgetId)) {
let widgetNode =
document.getElementById(aWidgetId) ||
this.createSpecialWidget(aWidgetId, document);
return [CustomizableUI.PROVIDER_SPECIAL, widgetNode];
}
let widget = gPalette.get(aWidgetId);
if (widget) {
// If we have an instance of this widget already, just use that.
if (widget.instances.has(document)) {
lazy.log.debug(
"An instance of widget " +
aWidgetId +
" already exists in this " +
"document. Reusing."
);
return [CustomizableUI.PROVIDER_API, widget.instances.get(document)];
}
return [CustomizableUI.PROVIDER_API, this.buildWidget(document, widget)];
}
lazy.log.debug("Searching for " + aWidgetId + " in toolbox.");
let node = this.findWidgetInWindow(aWidgetId, aWindow);
if (node) {
return [CustomizableUI.PROVIDER_XUL, node];
}
lazy.log.debug("No node for " + aWidgetId + " found.");
return [null, null];
},
registerPanelNode(aNode, aArea) {
if (gBuildAreas.has(aArea) && gBuildAreas.get(aArea).has(aNode)) {
return;
}
aNode._customizationTarget = aNode;
this.addPanelCloseListeners(this._getPanelForNode(aNode));
let placements = gPlacements.get(aArea);
this.buildArea(aArea, placements, aNode);
this.notifyListeners("onAreaNodeRegistered", aArea, aNode);
for (let child of aNode.children) {
if (child.localName != "toolbarbutton") {
if (child.localName == "toolbaritem") {
this.ensureButtonContextMenu(child, aNode, true);
}
continue;
}
this.ensureButtonContextMenu(child, aNode, true);
}
this.registerBuildArea(aArea, aNode);
},
onWidgetAdded(aWidgetId, aArea, aPosition) {
this.insertNode(aWidgetId, aArea, aPosition, true);
if (!gResetting) {
this._clearPreviousUIState();
}
},
onWidgetRemoved(aWidgetId, aArea) {
let areaNodes = gBuildAreas.get(aArea);
if (!areaNodes) {
return;
}
let area = gAreas.get(aArea);
let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
let isOverflowable = isToolbar && area.get("overflowable");
let showInPrivateBrowsing = gPalette.has(aWidgetId)
? gPalette.get(aWidgetId).showInPrivateBrowsing
: true;
let hideInNonPrivateBrowsing =
gPalette.get(aWidgetId)?.hideInNonPrivateBrowsing ?? false;
for (let areaNode of areaNodes) {
let window = areaNode.ownerGlobal;
if (
!showInPrivateBrowsing &&
lazy.PrivateBrowsingUtils.isWindowPrivate(window)
) {
continue;
}
if (
hideInNonPrivateBrowsing &&
!lazy.PrivateBrowsingUtils.isWindowPrivate(window)
) {
continue;
}
let container = this.getCustomizationTarget(areaNode);
let widgetNode = window.document.getElementById(aWidgetId);
if (widgetNode && isOverflowable) {
container = areaNode.overflowable.getContainerFor(widgetNode);
}
if (!widgetNode || !container.contains(widgetNode)) {
lazy.log.info(
"Widget " + aWidgetId + " not found, unable to remove from " + aArea
);
continue;
}
this.notifyDOMChange(widgetNode, null, container, true, () => {
// We remove location attributes here to make sure they're gone too when a
this.removeLocationAttributes(widgetNode);
// We also need to remove the panel context menu if it's there:
this.ensureButtonContextMenu(widgetNode);
if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
container.removeChild(widgetNode);
} else {
window.gNavToolbox.palette.appendChild(widgetNode);
}
});
let windowCache = gSingleWrapperCache.get(window);
if (windowCache) {
windowCache.delete(aWidgetId);
}
}
if (!gResetting) {
this._clearPreviousUIState();
}
},