fix(menu): make View → Zoom In accept the unshifted Ctrl+= key (#55)

The app relied on Electron's default application menu, whose View → Zoom In
binds only `CommandOrControl+Plus`. On US / most keyboard layouts "+" is the
shifted form of `=`, so pressing Ctrl with the unshifted +/= key sends Ctrl+=
and nothing happened — while Zoom Out (`Ctrl+-`, no Shift) worked, making zoom
feel half-broken.

Install an explicit application menu that mirrors Electron's default via
role-based submenus and hand-builds only View, where Zoom In also accepts
`Ctrl+=` and numpad `+` (hidden sibling items keep `Ctrl+Shift+=` and numpad
working). Strictly additive — no other menu behaviour changes.


Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF

Co-authored-by: ChrisBeWithYou <chris@rifflarr.local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChrisBeWithYou
2026-07-02 20:58:10 +02:00
committed by GitHub
co-authored by ChrisBeWithYou Claude Opus 4.8
parent c8415113c8
commit dcfa6be19d
2 changed files with 56 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
import { Menu, MenuItemConstructorOptions } from 'electron';
// Application menu.
//
// We previously relied on Electron's DEFAULT application menu. Its View
// submenu binds Zoom In to `CommandOrControl+Plus` only. On US / most
// keyboard layouts "+" is the SHIFTED form of the `=` key, so pressing
// Ctrl with the (unshifted) +/= key sends `Ctrl+=`, which the default
// accelerator does NOT match — Zoom In appears broken while Zoom Out
// (`Ctrl+-`, no Shift needed) works. That asymmetry is the reported bug.
//
// This template reproduces Electron's default menu via role-based submenus
// (so File/Edit/Window behaviour and their accelerators are unchanged) and
// hand-builds only the View submenu, where Zoom In additionally accepts the
// unshifted `=` key and the numpad `+`. Result: every way a user presses
// "Ctrl and plus" — `Ctrl+=`, `Ctrl+Shift+=` (the literal `+`), and numpad
// `Ctrl++` — now zooms in, restoring parity with Zoom Out.
export function installAppMenu(): void {
const isMac = process.platform === 'darwin';
const viewSubmenu: MenuItemConstructorOptions[] = [
{ role: 'reload' },
{ role: 'forceReload' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' },
// Primary, shown in the menu: the unshifted +/= key. This is the
// keystroke the bug report says did nothing.
{ role: 'zoomIn', accelerator: 'CommandOrControl+=' },
// Hidden siblings keep the other "plus" keystrokes working so this is
// strictly additive (no regression for users who pressed Shift, and
// numpad support for good measure). A MenuItem carries one accelerator,
// so extra bindings are extra (invisible) items.
{ role: 'zoomIn', accelerator: 'CommandOrControl+Plus', visible: false },
{ role: 'zoomIn', accelerator: 'CommandOrControl+numadd', visible: false },
{ role: 'zoomOut' },
{ role: 'zoomOut', accelerator: 'CommandOrControl+numsub', visible: false },
{ type: 'separator' },
{ role: 'togglefullscreen' },
];
const template: MenuItemConstructorOptions[] = [
...(isMac ? [{ role: 'appMenu' } as MenuItemConstructorOptions] : []),
{ role: 'fileMenu' },
{ role: 'editMenu' },
{ label: 'View', submenu: viewSubmenu },
{ role: 'windowMenu' },
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
+5
View File
@@ -124,6 +124,7 @@ import { initPluginManager } from './plugin-manager';
import { initSoundfontManager, getDesktopConfig, setDesktopConfig } from './soundfont-manager';
import * as updateManager from './update-manager';
import type { UpdateChannel } from './update-manager';
import { installAppMenu } from './app-menu';
// Linux: enable Chromium's PipeWire capturer feature so getUserMedia can see
// audio devices on PipeWire-only distros (Fedora 36+, recent Ubuntu, Arch).
@@ -1026,6 +1027,10 @@ async function startup(): Promise<void> {
// Create the main window
createWindow(port);
// Install our application menu (replaces Electron's default so View →
// Zoom In also accepts the unshifted Ctrl+= key — see app-menu.ts).
installAppMenu();
// Register file picker IPC
ipcMain.handle('dialog:pickFile', async (_event, filters?: { name: string; extensions: string[] }[]) => {
if (!mainWindow) return null;