From b9d18250f01a1ec8cc584f9bcf5df7ad301927c6 Mon Sep 17 00:00:00 2001 From: arookieofc Date: Mon, 9 Nov 2026 02:12:28 +0800 Subject: [PATCH] feat(settings): toggle main menu focus on a lone Alt press (#2174) A lone Alt press always opened or focused the main menu, with no way to turn it off. Add `appearance.mainMenuAlt` (default on, mirroring IDEA's ide.windows.main.menu.focus.on.alt) in Appearance > UI options; when off, Alt key combinations keep working but pressing Alt alone does nothing. Closes l0ng-ai/openidea#1155 --- crates/ide-core/src/settings.rs | 7 +++++ src/bindings.ts | 2 ++ src/features/settings/model/behaviorSchema.ts | 4 +++ src/features/settings/pages/behavior.tsx | 5 ++++ src/platform/locales/text/es.json | 2 ++ src/platform/locales/text/fr.json | 2 ++ src/platform/locales/text/ko.json | 2 ++ src/platform/locales/text/pl.json | 2 ++ src/platform/locales/text/pt.json | 2 ++ src/platform/locales/text/zh.json | 2 ++ src/workbench/MainMenu.interaction.test.tsx | 29 +++++++++++++++++++ src/workbench/MainMenu.tsx | 12 +++++--- 12 files changed, 67 insertions(+), 4 deletions(-) diff --git a/crates/ide-core/src/settings.rs b/crates/ide-core/src/settings.rs index 11548028d..0596b556e 100644 --- a/crates/ide-core/src/settings.rs +++ b/crates/ide-core/src/settings.rs @@ -386,6 +386,8 @@ pub struct AppearanceSettings { pub menu_icons: bool, /// `hamburger` | `merged`: main menu presentation in the frameless title bar. pub main_menu_style: String, + /// A lone Alt press focuses or opens the main menu (IDEA `ide.windows.main.menu.focus.on.alt`). + pub main_menu_alt: bool, /// `subpixel` | `greyscale` | `none` pub ide_antialiasing: String, pub editor_antialiasing: String, @@ -456,6 +458,7 @@ impl Default for AppearanceSettings { dnd_with_alt_only: false, menu_icons: true, main_menu_style: "hamburger".into(), + main_menu_alt: true, ide_antialiasing: "greyscale".into(), editor_antialiasing: "greyscale".into(), show_tool_window_bars: true, @@ -1248,9 +1251,13 @@ mod tests { fn main_menu_style_defaults_and_round_trips() { let old: Settings = serde_json::from_str(r#"{"appearance":{"compactMode":true}}"#).unwrap(); assert_eq!(old.appearance.main_menu_style, "hamburger"); + assert!(old.appearance.main_menu_alt); let merged: Settings = serde_json::from_str(r#"{"appearance":{"mainMenuStyle":"merged"}}"#).unwrap(); assert_eq!(merged.appearance.main_menu_style, "merged"); assert_eq!(serde_json::to_value(merged).unwrap()["appearance"]["mainMenuStyle"], "merged"); + let off: Settings = serde_json::from_str(r#"{"appearance":{"mainMenuAlt":false}}"#).unwrap(); + assert!(!off.appearance.main_menu_alt); + assert_eq!(serde_json::to_value(off).unwrap()["appearance"]["mainMenuAlt"], false); } #[test] diff --git a/src/bindings.ts b/src/bindings.ts index 446f3f58a..412365a3e 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1902,6 +1902,8 @@ export type AppearanceSettings = { menuIcons?: boolean, /** `hamburger` | `merged`: main menu presentation in the frameless title bar. */ mainMenuStyle?: string, + /** A lone Alt press focuses or opens the main menu (IDEA `ide.windows.main.menu.focus.on.alt`). */ + mainMenuAlt?: boolean, /** `subpixel` | `greyscale` | `none` */ ideAntialiasing?: string, editorAntialiasing?: string, diff --git a/src/features/settings/model/behaviorSchema.ts b/src/features/settings/model/behaviorSchema.ts index e72fa31c7..b2c08a0f9 100644 --- a/src/features/settings/model/behaviorSchema.ts +++ b/src/features/settings/model/behaviorSchema.ts @@ -37,6 +37,8 @@ export interface AppearanceExtras { dndWithAltOnly: boolean; menuIcons: boolean; mainMenuStyle: "hamburger" | "merged"; + /** A lone Alt press focuses or opens the main menu (IDEA `ide.windows.main.menu.focus.on.alt`). */ + mainMenuAlt: boolean; ideAntialiasing: Antialiasing; editorAntialiasing: Antialiasing; showToolWindowBars: boolean; @@ -96,6 +98,7 @@ export const DEFAULT_APPEARANCE_EXTRAS: AppearanceExtras = { dndWithAltOnly: false, menuIcons: true, mainMenuStyle: "hamburger", + mainMenuAlt: true, ideAntialiasing: "greyscale", editorAntialiasing: "greyscale", showToolWindowBars: true, @@ -153,6 +156,7 @@ export function validateAppearanceExtras(raw: unknown): AppearanceExtras { dndWithAltOnly: b("dndWithAltOnly"), menuIcons: b("menuIcons"), mainMenuStyle: oneOf(a.mainMenuStyle, ["hamburger", "merged"], d.mainMenuStyle), + mainMenuAlt: b("mainMenuAlt"), ideAntialiasing: oneOf(a.ideAntialiasing, AA, d.ideAntialiasing), editorAntialiasing: oneOf(a.editorAntialiasing, AA, d.editorAntialiasing), showToolWindowBars: b("showToolWindowBars"), diff --git a/src/features/settings/pages/behavior.tsx b/src/features/settings/pages/behavior.tsx index ca58b2d44..dfd8927f6 100644 --- a/src/features/settings/pages/behavior.tsx +++ b/src/features/settings/pages/behavior.tsx @@ -344,6 +344,11 @@ function UiOptionsGrid() { hint="Project, Structure, Commit and other tool window trees indent less per level" /> + } /> diff --git a/src/platform/locales/text/es.json b/src/platform/locales/text/es.json index 2c6d802dd..abbcfd6e6 100644 --- a/src/platform/locales/text/es.json +++ b/src/platform/locales/text/es.json @@ -1439,6 +1439,7 @@ "Open in New Window": "Abrir en una nueva ventana", "Open in Terminal": "Abrir en la terminal", "Open in editor": "Abrir en el editor", + "Open main menu with a lone Alt press": "Abrir el menú principal solo pulsando Alt", "Open moved files in editor": "Abrir archivos movidos en el editor", "Open new tabs at the end": "Abrir nuevas pestañas al final", "Open on GitHub": "Abrir en GitHub", @@ -4767,6 +4768,7 @@ "Only one of these runs at a time. Automatic uses the first one available: {order}.": "Solo se ejecuta uno de estos a la vez. Automático utiliza el primero disponible: {order}.", "Configure the settings related to version control used in your project": "Configure los ajustes relacionados con el control de versiones utilizado en su proyecto", "Use non-modal commit interface": "Usar interfaz de confirmación no modal", + "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "Si está desactivado, las combinaciones con Alt siguen funcionando, pero pulsar Alt solo no enfoca el menú principal", "When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "Cuando está desactivado, Cambios locales y Stash son pestañas de la ventana de herramientas de Git y Confirmar abre un diálogo.", "Clear initial commit message": "Limpiar mensaje inicial de confirmación", "Show options before Update": "Mostrar opciones antes de Actualizar", diff --git a/src/platform/locales/text/fr.json b/src/platform/locales/text/fr.json index 10c2b2414..fcb68c1de 100644 --- a/src/platform/locales/text/fr.json +++ b/src/platform/locales/text/fr.json @@ -1367,6 +1367,7 @@ "Open in New Window": "Ouvrir dans une nouvelle fenêtre", "Open in Terminal": "Ouvrir dans le terminal", "Open in editor": "Ouvrir dans l'éditeur", + "Open main menu with a lone Alt press": "Ouvrir le menu principal avec Alt seul", "Open moved files in editor": "Ouvrir les fichiers déplacés dans l'éditeur", "Open new tabs at the end": "Ouvrir les nouveaux onglets à la fin", "Open on GitHub": "Ouvrir sur GitHub", @@ -4615,6 +4616,7 @@ "Only one of these runs at a time. Automatic uses the first one available: {order}.": "Un seul de ces serveurs s'exécute à la fois. Automatique utilise le premier disponible : {order}.", "Configure the settings related to version control used in your project": "Configurez les paramètres du contrôle de version utilisé dans votre projet", "Use non-modal commit interface": "Utiliser l'interface de commit non modale", + "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "Si désactivé, les raccourcis avec Alt fonctionnent toujours, mais Alt seul ne donne pas le focus au menu principal", "When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "Si désactivé, Modifications locales et Remise sont des onglets de la fenêtre d'outil Git et Commit ouvre une boîte de dialogue.", "Clear initial commit message": "Effacer le message de commit initial", "Show options before Update": "Afficher les options avant la mise à jour", diff --git a/src/platform/locales/text/ko.json b/src/platform/locales/text/ko.json index 8b0fcf329..ae7f94cf6 100644 --- a/src/platform/locales/text/ko.json +++ b/src/platform/locales/text/ko.json @@ -1481,6 +1481,7 @@ "Open in New Window": "새 창에서 열기", "Open in Terminal": "터미널에서 열기", "Open in editor": "편집기에서 열기", + "Open main menu with a lone Alt press": "Alt 키만 눌러 메인 메뉴 열기", "Open moved files in editor": "이동한 파일을 편집기에서 열기", "Open new tabs at the end": "새 탭을 맨 끝에 열기", "Open on GitHub": "GitHub에서 열기", @@ -4840,6 +4841,7 @@ "Only one of these runs at a time. Automatic uses the first one available: {order}.": "한 번에 하나만 실행됩니다. 자동은 사용 가능한 첫 번째 항목을 사용합니다: {order}.", "Configure the settings related to version control used in your project": "프로젝트에서 사용하는 버전 관리 관련 설정을 구성합니다", "Use non-modal commit interface": "비모달 커밋 인터페이스 사용", + "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "끄면 Alt 조합키는 계속 동작하지만 Alt 단독 키는 메인 메뉴에 포커스를 주지 않습니다", "When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "끄면 로컬 변경 사항과 스태시가 Git 도구 창의 탭이 되고 커밋은 대화 상자로 열립니다.", "Clear initial commit message": "초기 커밋 메시지 삭제", "Show options before Update": "업데이트 전에 옵션 표시", diff --git a/src/platform/locales/text/pl.json b/src/platform/locales/text/pl.json index 6cf17dfe2..326d62f60 100644 --- a/src/platform/locales/text/pl.json +++ b/src/platform/locales/text/pl.json @@ -1429,6 +1429,7 @@ "Open in New Window": "Otwórz w nowym oknie", "Open in Terminal": "Otwórz w terminalu", "Open in editor": "Otwórz w edytorze", + "Open main menu with a lone Alt press": "Otwórz menu główne samym naciśnięciem Alt", "Open moved files in editor": "Otwieraj przeniesione pliki w edytorze", "Open new tabs at the end": "Otwieraj nowe karty na końcu", "Open on GitHub": "Otwórz w serwisie GitHub", @@ -4718,6 +4719,7 @@ "Only one of these runs at a time. Automatic uses the first one available: {order}.": "Tylko jeden z nich działa w danym momencie. Tryb automatyczny korzysta z pierwszego dostępnego: {order}.", "Configure the settings related to version control used in your project": "Konfiguruj ustawienia związane z systemem kontroli wersji używanym w projekcie", "Use non-modal commit interface": "Użyj niemodalnego interfejsu zatwierdzania (commit)", + "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "Gdy wyłączone, kombinacje z Alt nadal działają, ale samo naciśnięcie Alt nie przenosi fokusa na menu główne", "When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "Gdy wyłączone, Zmiany lokalne i Schowek (Stash) są kartami w oknie narzędziowym Git, a Zatwierdzenie (Commit) otwiera okno dialogowe.", "Clear initial commit message": "Wyczyść wstępną wiadomość zatwierdzenia", "Show options before Update": "Pokaż opcje przed aktualizacją", diff --git a/src/platform/locales/text/pt.json b/src/platform/locales/text/pt.json index e31a05e8f..8cabbd1bb 100644 --- a/src/platform/locales/text/pt.json +++ b/src/platform/locales/text/pt.json @@ -1429,6 +1429,7 @@ "Open in New Window": "Abrir em nova janela", "Open in Terminal": "Abrir no terminal", "Open in editor": "Abrir no editor", + "Open main menu with a lone Alt press": "Abrir o menu principal com um toque só em Alt", "Open moved files in editor": "Abrir arquivos movidos no editor", "Open new tabs at the end": "Abrir novas abas no final", "Open on GitHub": "Abrir no GitHub", @@ -4732,6 +4733,7 @@ "Only one of these runs at a time. Automatic uses the first one available: {order}.": "Apenas um deles é executado por vez. O modo Automático usa o primeiro disponível: {order}.", "Configure the settings related to version control used in your project": "Configure as opções relacionadas ao controle de versão usado em seu projeto", "Use non-modal commit interface": "Usar interface de commit não modal", + "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "Quando desativado, combinações com Alt continuam funcionando, mas pressionar Alt sozinho não focaliza o menu principal", "When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "Quando desativado, Alterações locais e Stash são abas da janela de ferramentas do Git e Commit abre uma caixa de diálogo.", "Clear initial commit message": "Limpar mensagem inicial de commit", "Show options before Update": "Mostrar opções antes de Atualizar", diff --git a/src/platform/locales/text/zh.json b/src/platform/locales/text/zh.json index 93cf42838..d3a893947 100644 --- a/src/platform/locales/text/zh.json +++ b/src/platform/locales/text/zh.json @@ -1579,6 +1579,7 @@ "Open in New Window": "在新窗口中打开", "Open in Terminal": "在终端中打开", "Open in editor": "在编辑器中打开", + "Open main menu with a lone Alt press": "单独按下 Alt 打开主菜单", "Open moved files in editor": "在编辑器中打开已移动的文件", "Open new tabs at the end": "在末尾打开新标签页", "Open on GitHub": "在 GitHub 上打开", @@ -5085,6 +5086,7 @@ "Only one of these runs at a time. Automatic uses the first one available: {order}.": "这些服务器同一时间只运行一个。自动模式使用第一个可用的:{order}。", "Configure the settings related to version control used in your project": "配置项目中使用的版本控制相关设置", "Use non-modal commit interface": "使用非模态提交界面", + "When off, Alt key combinations still work but pressing Alt alone does not focus the main menu": "关闭后,Alt 组合键仍然可用,但单独按下 Alt 不会聚焦主菜单", "When off, Local Changes and Stash are tabs of the Git tool window and Commit opens a dialog.": "关闭时,本地更改和贮藏作为 Git 工具窗口的标签页显示,提交时打开对话框。", "Clear initial commit message": "清除初始提交消息", "Show options before Update": "更新前显示选项", diff --git a/src/workbench/MainMenu.interaction.test.tsx b/src/workbench/MainMenu.interaction.test.tsx index 0d003e88b..823de7323 100644 --- a/src/workbench/MainMenu.interaction.test.tsx +++ b/src/workbench/MainMenu.interaction.test.tsx @@ -108,6 +108,35 @@ describe("main menu presentation", () => { expect(expanded).not.toHaveBeenCalledWith(true); }); + it.each(["hamburger", "merged"])("%s ignores a lone Alt press when the setting is off", async (style) => { + setStyle(style); + const setAltMenu = (on: boolean) => + act(() => + useProject.setState({ settings: { appearance: { mainMenuStyle: style, mainMenuAlt: on } } as Settings }), + ); + await setAltMenu(false); + render( + <> + + + , + ); + if (style === "merged") await screen.findByRole("menubar"); + const editor = screen.getByRole("button", { name: "Editor" }); + editor.focus(); + alt(); + await act(() => new Promise((resolve) => setTimeout(resolve))); + expect(screen.queryByRole("menu")).toBeNull(); + if (style === "hamburger") expect(screen.queryByRole("menubar")).toBeNull(); + else expect(editor).toHaveFocus(); + // Turning it back on restores the behavior without a remount. + await setAltMenu(true); + alt(); + await act(() => new Promise((resolve) => setTimeout(resolve))); + if (style === "hamburger") expect(await screen.findByRole("menu", { name: "File" })).toBeInTheDocument(); + else expect(titles().getByRole("menuitem", { name: "File" })).toHaveFocus(); + }); + it.each(["hamburger", "merged"])( "%s switches on title clicks without an outside dismissal, then closes for toolbar clicks", async (style) => { diff --git a/src/workbench/MainMenu.tsx b/src/workbench/MainMenu.tsx index 53dc658e0..d8e8e9c50 100644 --- a/src/workbench/MainMenu.tsx +++ b/src/workbench/MainMenu.tsx @@ -161,8 +161,12 @@ function MainMenuBar({ merged, onExpandedChange }: MainMenuProps & { merged: boo else close(); }); }; - useEffect(() => - onLoneAlt(window, () => { + // IDEA `ide.windows.main.menu.focus.on.alt`: when off, a lone Alt press does + // nothing instead of opening or focusing the menu. + const altMenu = useProject((s) => s.settings?.appearance?.mainMenuAlt !== false); + useEffect(() => { + if (!altMenu) return; + return onLoneAlt(window, () => { if (open !== null || expanded) { close(); if (persistent) focusTitle(focused); @@ -173,8 +177,8 @@ function MainMenuBar({ merged, onExpandedChange }: MainMenuProps & { merged: boo focusTitle(0); } } else void expand(); - }), - ); + }); + }); const onKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowLeft" || e.key === "ArrowRight") { if (!menus.length) return;