Files
paseo/packages/desktop/src/features/menu.ts
T
c0b801b805 Fix shortcut capture ignoring - = ; ' keys (#2047)
* Fix shortcut capture ignoring - = ; ' keys

KEY_MAP was missing Minus/Equal/Semicolon/Quote, so
keyboardEventToComboString returned null for those physical keys and
the capture UI silently dropped them. Add the four entries with their
shifted variants.

Also suppress the Electron View > Zoom accelerators (Cmd+-/=/0) while
capturing, so those combos reach the renderer instead of zooming the
window. Driven by the existing capturingShortcut flag via a new
paseo:menu:set-capturing-shortcut IPC.

* fix: address review feedback from PR #2047

Trim the capturing useEffect so it only fires when capture is active
(removes the redundant on-mount false and the double-fire on true->false),
and reset the main-process capturingShortcut flag on renderer reload via
browser-window-created + did-finish-load so a Cmd+R mid-capture can't
leave the zoom accelerators disabled.

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-14 13:30:32 +02:00

224 lines
6.0 KiB
TypeScript

import { app, Menu, BrowserWindow, ipcMain } from "electron";
import { getMostRecentWorkspaceActivePaseoBrowserWebContents } from "./browser-webviews/index.js";
interface ShowContextMenuInput {
kind?: "terminal";
hasSelection?: boolean;
}
interface ApplicationMenuOptions {
onNewWindow: () => void;
}
function withBrowserWindow(
callback: (win: BrowserWindow) => void,
): (_item: Electron.MenuItem, baseWin: Electron.BaseWindow | undefined) => void {
return (_item, baseWin) => {
const win = baseWin instanceof BrowserWindow ? baseWin : BrowserWindow.getFocusedWindow();
if (win) callback(win);
};
}
function getReloadTargetBrowserWebContents(): Electron.WebContents | null {
return getMostRecentWorkspaceActivePaseoBrowserWebContents();
}
function reloadFocusedContentsOrWindow(win: BrowserWindow, options?: { ignoreCache?: boolean }) {
const browserContents = getReloadTargetBrowserWebContents();
if (browserContents) {
if (options?.ignoreCache) {
browserContents.reloadIgnoringCache();
return;
}
if (browserContents.isLoadingMainFrame()) {
browserContents.stop();
return;
}
browserContents.reload();
return;
}
if (options?.ignoreCache) {
win.webContents.reloadIgnoringCache();
return;
}
win.webContents.reload();
}
function buildApplicationMenuTemplate(
options: ApplicationMenuOptions,
capturing: boolean,
): Electron.MenuItemConstructorOptions[] {
const isMac = process.platform === "darwin";
const zoomEnabled = !capturing;
return [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" as const },
{ type: "separator" as const },
{ role: "services" as const },
{ type: "separator" as const },
{ role: "hide" as const },
{ role: "hideOthers" as const },
{ role: "unhide" as const },
{ type: "separator" as const },
{ role: "quit" as const },
],
},
]
: []),
{
label: "File",
submenu: [
{
label: "New Window",
accelerator: "CmdOrCtrl+Shift+N",
click: () => {
options.onNewWindow();
},
},
],
},
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "selectAll" },
],
},
{
label: "View",
submenu: [
{
label: "Zoom In",
accelerator: "CmdOrCtrl+=",
enabled: zoomEnabled,
click: withBrowserWindow((win) => {
win.webContents.setZoomLevel(win.webContents.getZoomLevel() + 0.5);
}),
},
{
label: "Zoom Out",
accelerator: "CmdOrCtrl+-",
enabled: zoomEnabled,
click: withBrowserWindow((win) => {
win.webContents.setZoomLevel(win.webContents.getZoomLevel() - 0.5);
}),
},
{
label: "Actual Size",
accelerator: "CmdOrCtrl+0",
enabled: zoomEnabled,
click: withBrowserWindow((win) => {
win.webContents.setZoomLevel(0);
}),
},
{ type: "separator" },
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
click: withBrowserWindow((win) => {
reloadFocusedContentsOrWindow(win);
}),
},
{
label: "Force Reload",
accelerator: "CmdOrCtrl+Shift+R",
click: withBrowserWindow((win) => {
reloadFocusedContentsOrWindow(win, { ignoreCache: true });
}),
},
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Window",
submenu: [
{ role: "minimize" },
{ role: "zoom" },
...(isMac
? [{ type: "separator" as const }, { role: "front" as const }]
: [{ role: "close" as const }]),
],
},
];
}
let applicationMenuOptions: ApplicationMenuOptions | null = null;
let capturingShortcut = false;
function rebuildApplicationMenu(): void {
if (!applicationMenuOptions) return;
const menu = Menu.buildFromTemplate(
buildApplicationMenuTemplate(applicationMenuOptions, capturingShortcut),
);
Menu.setApplicationMenu(menu);
}
export function setupApplicationMenu(options: ApplicationMenuOptions): void {
applicationMenuOptions = options;
rebuildApplicationMenu();
ipcMain.handle("paseo:menu:showContextMenu", (event, input?: ShowContextMenuInput) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) {
return;
}
if (input?.kind !== "terminal") {
return;
}
const contextMenu = Menu.buildFromTemplate([
{
label: "Copy",
role: "copy",
enabled: input.hasSelection === true,
},
{
label: "Paste",
role: "paste",
},
{
type: "separator",
},
{
label: "Select All",
role: "selectAll",
},
]);
contextMenu.popup({ window: win });
});
// Disable the zoom accelerators while capturing a shortcut so combos like
// Cmd+- / Cmd+= reach the renderer instead of zooming the window.
ipcMain.handle("paseo:menu:set-capturing-shortcut", (_event, capturing?: boolean) => {
capturingShortcut = capturing === true;
rebuildApplicationMenu();
});
// If the renderer reloads mid-capture (e.g. Cmd+R) the renderer-side effect
// never gets to send `false`, so reset the flag from the main process when a
// main window finishes loading. Workspace browser webviews are not
// BrowserWindows, so they don't trigger this.
app.on("browser-window-created", (_event, win) => {
win.webContents.on("did-finish-load", () => {
if (!capturingShortcut) return;
capturingShortcut = false;
rebuildApplicationMenu();
});
});
}