1
0
Fork 0
firefox/browser/components/customizableui/ToolbarContextMenu.sys.mjs
Daniel Baumann 5e9a113729
Adding upstream version 140.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-25 09:37:52 +02:00

570 lines
20 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
ExtensionsUI: "resource:///modules/ExtensionsUI.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gAlwaysOpenPanel",
"browser.download.alwaysOpenPanel",
true
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gAddonAbuseReportEnabled",
"extensions.abuseReport.enabled",
false
);
// Whether the Extensions button can be hidden via UI. The button can be hidden
// even with this pref set to false. TODO bug 1967773: Remove this pref.
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"gEnableCustomizableExtensionsButton",
"extensions.unifiedExtensions.button.customizable",
true
);
/**
* Various events handlers to set the state of the toolbar-context-menu popup,
* as well as to handle some commands from that popup.
*/
export var ToolbarContextMenu = {
/**
* Makes visible the "autohide the downloads button" checkbox in the popup
* in the event that the downloads button was context clicked. Otherwise,
* hides that checkbox.
*
* This method also sets the checkbox state depending on the current user
* configuration for hiding the downloads button.
*
* @param {Element} popup
* The toolbar-context-menu element for a window.
*/
updateDownloadsAutoHide(popup) {
let { document, DownloadsButton } = popup.ownerGlobal;
let checkbox = document.getElementById(
"toolbar-context-autohide-downloads-button"
);
let isDownloads =
popup.triggerNode &&
["downloads-button", "wrapper-downloads-button"].includes(
popup.triggerNode.id
);
checkbox.hidden = !isDownloads;
if (DownloadsButton.autoHideDownloadsButton) {
checkbox.setAttribute("checked", "true");
} else {
checkbox.removeAttribute("checked");
}
},
/**
* Handler for the toolbar-context-autohide-downloads-button command event
* that is fired when the checkbox for autohiding the downloads button is
* changed. This method does the work of updating the internal preference
* state for auto-hiding the downloads button.
*
* @param {CommandEvent} event
*/
onDownloadsAutoHideChange(event) {
let autoHide = event.target.getAttribute("checked") == "true";
Services.prefs.setBoolPref("browser.download.autohideButton", autoHide);
},
/**
* Makes visible the "always open downloads panel" checkbox in the popup
* in the event that the downloads button was context clicked. Otherwise,
* hides that checkbox.
*
* This method also sets the checkbox state depending on the current user
* configuration for always showing the panel.
*
* @param {Element} popup
* The toolbar-context-menu element for a window.
*/
updateDownloadsAlwaysOpenPanel(popup) {
let { document } = popup.ownerGlobal;
let separator = document.getElementById(
"toolbarDownloadsAnchorMenuSeparator"
);
let checkbox = document.getElementById(
"toolbar-context-always-open-downloads-panel"
);
let isDownloads =
popup.triggerNode &&
["downloads-button", "wrapper-downloads-button"].includes(
popup.triggerNode.id
);
separator.hidden = checkbox.hidden = !isDownloads;
lazy.gAlwaysOpenPanel
? checkbox.setAttribute("checked", "true")
: checkbox.removeAttribute("checked");
},
/**
* Handler for the toolbar-context-always-open-downloads-panel command event
* that is fired when the checkbox for always showing the downloads panel is
* changed. This method does the work of updating the internal preference
* state for always showing the downloads panel.
*
* @param {CommandEvent} event
*/
onDownloadsAlwaysOpenPanelChange(event) {
let alwaysOpen = event.target.getAttribute("checked") == "true";
Services.prefs.setBoolPref("browser.download.alwaysOpenPanel", alwaysOpen);
},
/**
* This is called when a menupopup for configuring toolbars fires its
* popupshowing event. There are multiple such menupopups, and this logic
* tries to work for all of them. This method will insert menuitems into
* the popup to allow for controlling the toolbars within the browser
* toolbox.
*
* @param {Event} aEvent
* The popupshowing event for the menupopup.
* @param {DOMNode} aInsertPoint
* The point within the menupopup to insert the controls for each toolbar.
*/
onViewToolbarsPopupShowing(aEvent, aInsertPoint) {
var popup = aEvent.target;
let window = popup.ownerGlobal;
let {
document,
BookmarkingUI,
MozXULElement,
onViewToolbarCommand,
showFullScreenViewContextMenuItems,
gBrowser,
CustomizationHandler,
gNavToolbox,
} = window;
// triggerNode can be a nested child element of a toolbaritem.
let toolbarItem = popup.triggerNode;
while (toolbarItem) {
let localName = toolbarItem.localName;
if (localName == "toolbar") {
toolbarItem = null;
break;
}
if (localName == "toolbarpaletteitem") {
toolbarItem = toolbarItem.firstElementChild;
break;
}
if (localName == "menupopup") {
aEvent.preventDefault();
aEvent.stopPropagation();
return;
}
let parent = toolbarItem.parentElement;
if (parent) {
if (
parent.classList.contains("customization-target") ||
parent.getAttribute("overflowfortoolbar") || // Needs to work in the overflow list as well.
parent.localName == "toolbarpaletteitem" ||
parent.localName == "toolbar" ||
parent.id == "vertical-tabs"
) {
break;
}
}
toolbarItem = parent;
}
// Empty the menu
for (var i = popup.children.length - 1; i >= 0; --i) {
var deadItem = popup.children[i];
if (deadItem.hasAttribute("toolbarId")) {
popup.removeChild(deadItem);
}
}
let showTabStripItems = toolbarItem?.id == "tabbrowser-tabs";
let isVerticalTabStripMenu =
showTabStripItems && toolbarItem.parentElement.id == "vertical-tabs";
if (aInsertPoint) {
aInsertPoint.hidden = isVerticalTabStripMenu;
}
document.getElementById("toolbar-context-customize").hidden =
isVerticalTabStripMenu;
if (!isVerticalTabStripMenu) {
MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
let firstMenuItem = aInsertPoint || popup.firstElementChild;
let toolbarNodes = gNavToolbox.querySelectorAll("toolbar");
for (let toolbar of toolbarNodes) {
if (!toolbar.hasAttribute("toolbarname")) {
continue;
}
if (toolbar.id == "PersonalToolbar") {
let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(toolbar);
popup.insertBefore(menu, firstMenuItem);
} else {
let menuItem = document.createXULElement("menuitem");
menuItem.setAttribute("id", "toggle_" + toolbar.id);
menuItem.setAttribute("toolbarId", toolbar.id);
menuItem.setAttribute("type", "checkbox");
menuItem.setAttribute("label", toolbar.getAttribute("toolbarname"));
let hidingAttribute =
toolbar.getAttribute("type") == "menubar"
? "autohide"
: "collapsed";
menuItem.setAttribute(
"checked",
toolbar.getAttribute(hidingAttribute) != "true"
);
menuItem.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
if (popup.id != "toolbar-context-menu") {
menuItem.setAttribute("key", toolbar.getAttribute("key"));
}
popup.insertBefore(menuItem, firstMenuItem);
menuItem.addEventListener("command", onViewToolbarCommand);
}
}
}
let moveToPanel = popup.querySelector(".customize-context-moveToPanel");
let removeFromToolbar = popup.querySelector(
".customize-context-removeFromToolbar"
);
// Show/hide fullscreen context menu items and set the
// autohide item's checked state to mirror the autohide pref.
showFullScreenViewContextMenuItems(popup);
// Show/hide sidebar and vertical tabs menu items
let sidebarRevampEnabled = Services.prefs.getBoolPref("sidebar.revamp");
let showSidebarActions =
["tabbrowser-tabs", "sidebar-button"].includes(toolbarItem?.id) ||
toolbarItem?.localName == "toolbarspring";
let toggleVerticalTabsItem = document.getElementById(
"toolbar-context-toggle-vertical-tabs"
);
toggleVerticalTabsItem.hidden = !showSidebarActions;
document.l10n.setAttributes(
toggleVerticalTabsItem,
gBrowser.tabContainer?.verticalMode
? "toolbar-context-turn-off-vertical-tabs"
: "toolbar-context-turn-on-vertical-tabs"
);
document.getElementById("toolbar-context-customize-sidebar").hidden =
!sidebarRevampEnabled ||
(toolbarItem?.id != "sidebar-button" &&
!gBrowser.tabContainer?.verticalMode) ||
(!["tabbrowser-tabs", "sidebar-button"].includes(toolbarItem?.id) &&
gBrowser.tabContainer?.verticalMode);
document.getElementById("sidebarRevampSeparator").hidden =
!showSidebarActions || isVerticalTabStripMenu;
document.getElementById("customizationMenuSeparator").hidden =
toolbarItem?.id == "tabbrowser-tabs" ||
toolbarItem?.localName == "toolbarspring";
// View -> Toolbars menu doesn't have the moveToPanel or removeFromToolbar items.
if (!moveToPanel || !removeFromToolbar) {
return;
}
for (let node of popup.querySelectorAll(
'menuitem[contexttype="toolbaritem"]'
)) {
node.hidden = showTabStripItems;
}
for (let node of popup.querySelectorAll('menuitem[contexttype="tabbar"]')) {
node.hidden = !showTabStripItems;
}
document
.getElementById("toolbar-context-menu")
.querySelectorAll("[data-lazy-l10n-id]")
.forEach(el => {
el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
el.removeAttribute("data-lazy-l10n-id");
});
// The "normal" toolbar items menu separator is hidden because it's unused
// when hiding the "moveToPanel" and "removeFromToolbar" items on flexible
// space items. But we need to ensure its hidden state is reset in the case
// the context menu is subsequently opened on a non-flexible space item.
let menuSeparator = document.getElementById("tabbarItemsMenuSeparator");
menuSeparator.hidden = false;
document.getElementById("toolbarNavigatorItemsMenuSeparator").hidden =
!showTabStripItems;
if (
!CustomizationHandler.isCustomizing() &&
(toolbarItem?.localName.includes("separator") ||
toolbarItem?.localName.includes("spring") ||
toolbarItem?.localName.includes("spacer") ||
toolbarItem?.id.startsWith("customizableui-special"))
) {
moveToPanel.hidden = true;
removeFromToolbar.hidden = true;
menuSeparator.hidden = !showTabStripItems;
}
if (toolbarItem?.id != "tabbrowser-tabs") {
menuSeparator.hidden = true;
}
if (showTabStripItems) {
let multipleTabsSelected = !!gBrowser.multiSelectedTabsCount;
document.getElementById("toolbar-context-bookmarkSelectedTabs").hidden =
!multipleTabsSelected;
document.getElementById("toolbar-context-bookmarkSelectedTab").hidden =
multipleTabsSelected;
document.getElementById("toolbar-context-reloadSelectedTabs").hidden =
!multipleTabsSelected;
document.getElementById("toolbar-context-reloadSelectedTab").hidden =
multipleTabsSelected;
document.getElementById("toolbar-context-selectAllTabs").disabled =
gBrowser.allTabsSelected();
let closedCount = lazy.SessionStore.getLastClosedTabCount(window);
document
.getElementById("History:UndoCloseTab")
.setAttribute("disabled", closedCount == 0);
document.l10n.setArgs(
document.getElementById("toolbar-context-undoCloseTab"),
{ tabCount: closedCount }
);
return;
}
let movable =
toolbarItem?.id && lazy.CustomizableUI.isWidgetRemovable(toolbarItem);
if (movable) {
if (lazy.CustomizableUI.isSpecialWidget(toolbarItem.id)) {
moveToPanel.setAttribute("disabled", true);
} else {
moveToPanel.removeAttribute("disabled");
}
removeFromToolbar.removeAttribute("disabled");
} else {
removeFromToolbar.setAttribute("disabled", true);
moveToPanel.setAttribute("disabled", true);
}
},
/**
* Given an opened menupopup, returns the triggerNode that opened that
* menupopup. If customize mode is enabled, this will return the unwrapped
* underlying triggerNode, rather than the customize mode wrapper around it.
*
* @param {DOMNode} popup
* The menupopup to get the unwrapped trigger node for.
* @returns {DOMNode}
* The underlying trigger node that opened the menupopup.
*/
_getUnwrappedTriggerNode(popup) {
// Toolbar buttons are wrapped in customize mode. Unwrap if necessary.
let { triggerNode } = popup;
let { gCustomizeMode } = popup.ownerGlobal;
if (triggerNode && gCustomizeMode.isWrappedToolbarItem(triggerNode)) {
return triggerNode.firstElementChild;
}
return triggerNode;
},
/**
* For an opened menupopup, if the triggerNode was provided by an extension,
* returns the extension ID. Otherwise, return the empty string.
*
* @param {DOMNode} popup
* The menupopup that was opened.
* @returns {string}
* The ID of the extension that provided the triggerNode, or the empty
* string if the triggerNode was not provided by an extension.
*/
_getExtensionId(popup) {
let node = this._getUnwrappedTriggerNode(popup);
return node && node.getAttribute("data-extensionid");
},
/**
* For an opened menupopup, if the triggerNode was provided by an extension,
* returns the widget ID of the triggerNode. Otherwise, return the empty
* string.
*
* @param {DOMNode} popup
* The menupopup that was opened.
* @returns {string}
* The ID of the extension-provided widget that was the triggerNode, or the
* empty string if the trigger node was not provided by an extension
* widget.
*/
_getWidgetId(popup) {
let node = this._getUnwrappedTriggerNode(popup);
return node?.closest(".unified-extensions-item")?.id;
},
/**
* Updates the toolbar context menu items unique to gUnifiedExtensions.button.
*
* @param {Element} popup
* The toolbar-context-menu element for a window.
*/
updateExtensionsButtonContextMenu(popup) {
const isExtsButton = popup.triggerNode?.id === "unified-extensions-button";
const isCustomizingExtsButton =
popup.triggerNode?.id === "wrapper-unified-extensions-button";
const { gUnifiedExtensions } = popup.ownerGlobal;
const checkbox = popup.querySelector(
"#toolbar-context-always-show-extensions-button"
);
if (isCustomizingExtsButton && lazy.gEnableCustomizableExtensionsButton) {
checkbox.hidden = false;
if (gUnifiedExtensions.buttonAlwaysVisible) {
checkbox.setAttribute("checked", "true");
} else {
checkbox.removeAttribute("checked");
}
} else if (
isExtsButton &&
!gUnifiedExtensions.buttonAlwaysVisible &&
lazy.gEnableCustomizableExtensionsButton
) {
// The button may be visible despite the user's preference, which could
// remind the user of the button's existence. Offer an option to unhide
// the button, in case the user is looking for a way to do so.
checkbox.hidden = false;
checkbox.removeAttribute("checked");
} else {
checkbox.hidden = true;
}
// removeFromToolbar is shown but disabled by default, via an earlier call
// to ToolbarContextMenu.onViewToolbarsPopupShowing. Enable/hide if needed.
if (isExtsButton && lazy.gEnableCustomizableExtensionsButton) {
const removeFromToolbar = popup.querySelector(
".customize-context-removeFromToolbar"
);
if (gUnifiedExtensions.buttonAlwaysVisible) {
removeFromToolbar.removeAttribute("disabled");
} else {
// No need to show "Remove from Toolbar" even if disabled, because the
// "Always Show in Toolbar" checkbox is already shown above.
removeFromToolbar.hidden = true;
}
}
},
/**
* Updates the toolbar context menu to show the right state if an
* extension-provided widget acted as the triggerNode. This will, for example,
* show or hide items for managing the underlying addon.
*
* @param {DOMNode} popup
* The menupopup for the toolbar context menu.
* @returns {Promise<undefined>}
* Resolves once the menupopup state has been set.
*/
async updateExtension(popup) {
let removeExtension = popup.querySelector(
".customize-context-removeExtension"
);
let manageExtension = popup.querySelector(
".customize-context-manageExtension"
);
let reportExtension = popup.querySelector(
".customize-context-reportExtension"
);
let pinToToolbar = popup.querySelector(".customize-context-pinToToolbar");
let separator = reportExtension.nextElementSibling;
let id = this._getExtensionId(popup);
let addon = id && (await lazy.AddonManager.getAddonByID(id));
for (let element of [removeExtension, manageExtension, separator]) {
element.hidden = !addon;
}
if (pinToToolbar) {
pinToToolbar.hidden = !addon;
}
reportExtension.hidden = !addon || !lazy.gAddonAbuseReportEnabled;
if (addon) {
popup.querySelector(".customize-context-moveToPanel").hidden = true;
popup.querySelector(".customize-context-removeFromToolbar").hidden = true;
if (pinToToolbar) {
let widgetId = this._getWidgetId(popup);
if (widgetId) {
let area = lazy.CustomizableUI.getPlacementOfWidget(widgetId).area;
let inToolbar = area != lazy.CustomizableUI.AREA_ADDONS;
pinToToolbar.setAttribute("checked", inToolbar);
}
}
removeExtension.disabled = !(
addon.permissions & lazy.AddonManager.PERM_CAN_UNINSTALL
);
if (popup.id === "toolbar-context-menu") {
lazy.ExtensionsUI.originControlsMenu(popup, id);
}
}
},
/**
* Handler for the context menu item for removing an extension.
*
* @param {DOMNode} popup
* The menupopup that triggered the extension removal.
* @returns {Promise<undefined>}
* Resolves when the extension has been removed.
*/
async removeExtensionForContextAction(popup) {
let { BrowserAddonUI } = popup.ownerGlobal;
let id = this._getExtensionId(popup);
await BrowserAddonUI.removeAddon(id, "browserAction");
},
/**
* Handler for the context menu item for issuing a report on an extension.
*
* @param {DOMNode} popup
* The menupopup that triggered the extension report.
* @param {string} reportEntryPoint
* A string describing the UI entrypoint for the report.
* @returns {Promise<undefined>}
* Resolves when the extension has been removed.
*/
async reportExtensionForContextAction(popup, reportEntryPoint) {
let { BrowserAddonUI } = popup.ownerGlobal;
let id = this._getExtensionId(popup);
await BrowserAddonUI.reportAddon(id, reportEntryPoint);
},
/**
* Handler for the context menu item for managing an extension.
*
* @param {DOMNode} popup
* The menupopup that triggered extension management.
* @returns {Promise<undefined>}
* Resolves when the extension's about:addons management page has been
* opened.
*/
async openAboutAddonsForContextAction(popup) {
let { BrowserAddonUI } = popup.ownerGlobal;
let id = this._getExtensionId(popup);
await BrowserAddonUI.manageAddon(id, "browserAction");
},
};