From a8ad73d2d95a29dfa38c7b3a10e4452c2233b3cf Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Wed, 27 May 2026 15:37:05 +0200 Subject: [PATCH] add shortcuts in tray for quit and settings item --- .../frontend/src/components/DropdownMenu.tsx | 2 +- client/ui/frontend/src/layouts/Header.tsx | 23 +++++++++++++++---- client/ui/tray.go | 22 ++++++++++++++++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/client/ui/frontend/src/components/DropdownMenu.tsx b/client/ui/frontend/src/components/DropdownMenu.tsx index eb31f2852..48c5835c7 100644 --- a/client/ui/frontend/src/components/DropdownMenu.tsx +++ b/client/ui/frontend/src/components/DropdownMenu.tsx @@ -208,7 +208,7 @@ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => ( ); diff --git a/client/ui/frontend/src/layouts/Header.tsx b/client/ui/frontend/src/layouts/Header.tsx index 6e151c0f2..be7d269c3 100644 --- a/client/ui/frontend/src/layouts/Header.tsx +++ b/client/ui/frontend/src/layouts/Header.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { ArrowUpCircleIcon, @@ -15,24 +15,34 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, + DropdownMenuShortcut, DropdownMenuTrigger, } from "@/components/DropdownMenu"; import { IconButton } from "@/components/IconButton"; import { ProfileDropdown } from "@/components/ProfileDropdown"; import { useClientVersion } from "@/modules/auto-update/ClientVersionContext"; import { cn } from "@/lib/cn"; +import { formatShortcut, useKeyboardShortcut } from "@/lib/useKeyboardShortcut"; import { useViewMode, type ViewMode } from "@/lib/viewMode"; +const SETTINGS_SHORTCUT = { key: ",", cmd: true } as const; + export const Header = () => { const { t } = useTranslation(); const [menuOpen, setMenuOpen] = useState(false); const { viewMode, setViewMode } = useViewMode(); const { updateAvailable } = useClientVersion(); - const openSettings = () => { + const openSettings = useCallback(() => { setMenuOpen(false); void WindowManager.OpenSettings("").catch(() => {}); - }; + }, []); + + // Mirror the tray's Settings accelerator so the keystroke works while + // the main window has focus too. The tray's SetAccelerator paints the + // glyph on macOS/Linux but only fires the menu item — it can't reach the + // webview's input loop, hence the parallel React-side listener. + useKeyboardShortcut(SETTINGS_SHORTCUT, openSettings); const openAbout = () => { setMenuOpen(false); @@ -79,9 +89,12 @@ export const Header = () => { )} -
+
- {t("header.menu.settings")} + {t("header.menu.settings")} + + {formatShortcut(SETTINGS_SHORTCUT)} +
diff --git a/client/ui/tray.go b/client/ui/tray.go index e99482eda..00223df1e 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -372,6 +372,20 @@ func (t *Tray) buildMenu() *application.Menu { // The tray icon's left-click handler is intentionally unbound (see // NewTray for the rationale), so expose the window through an explicit // menu entry on every platform. + // + // Accelerators are wired on the Settings and Quit entries below. + // Cross-platform behaviour in Wails v3 alpha.95: + // - macOS: SetAccelerator calls NSMenuItem.setKeyEquivalent — the + // glyph row paints to the right of the label and the combo fires + // when the menu is open OR while the app is the frontmost app. + // - Linux (GTK): SetAccelerator binds the GTK accel — the combo + // fires while the menu is open and the label paints the row. On + // XEmbed/AppIndicator hosts the visual hint may not render but + // activation through the keyboard still resolves. + // - Windows: SetAccelerator is a no-op in alpha.95 (the impl is + // commented out in menuitem_windows.go), so the row is plain + // text. We still call it for forward compatibility — a future + // Wails release picks the labels up without churn here. menu.Add(t.loc.T("tray.menu.open")).OnClick(func(*application.Context) { t.ShowWindow() }) menu.AddSeparator() @@ -396,7 +410,9 @@ func (t *Tray) buildMenu() *application.Menu { // surfaces the day-to-day actions. The trailing ellipsis on the label // (i18n string) follows the macOS HIG convention for menu items that // open a dialog/window rather than performing an inline action. - t.settingsItem = menu.Add(t.loc.T("tray.menu.settings")).OnClick(func(*application.Context) { t.svc.WindowManager.OpenSettings("") }) + t.settingsItem = menu.Add(t.loc.T("tray.menu.settings")). + SetAccelerator("CmdOrCtrl+,"). + OnClick(func(*application.Context) { t.svc.WindowManager.OpenSettings("") }) aboutLabel := t.loc.T("tray.menu.about") about := menu.AddSubmenu(aboutLabel) @@ -427,7 +443,9 @@ func (t *Tray) buildMenu() *application.Menu { t.updater.attach(updateItem) menu.AddSeparator() - menu.Add(t.loc.T("tray.menu.quit")).OnClick(func(*application.Context) { t.app.Quit() }) + menu.Add(t.loc.T("tray.menu.quit")). + SetAccelerator("CmdOrCtrl+Q"). + OnClick(func(*application.Context) { t.app.Quit() }) return menu }