From 1a03c0e8e8de5d1fe9010241395f4a11d098c5d4 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 17 May 2026 14:44:37 +0300 Subject: [PATCH] refactor(app): reuse menu shortcut metadata --- src/app/editor/canvas/menu-actions.ts | 2 ++ src/app/shell/menu/app-menu.ts | 13 ++++------ src/app/shell/menu/schema.ts | 25 ++++++++++--------- src/app/shell/menu/shortcut.ts | 4 +-- src/app/shell/menu/use.ts | 3 ++- src/components/AppMenu.vue | 9 +++---- src/components/CanvasMenu.vue | 13 +++++----- src/components/ZoomDropdown.vue | 9 ++++--- .../properties/TypographySection.vue | 7 +++--- 9 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/app/editor/canvas/menu-actions.ts b/src/app/editor/canvas/menu-actions.ts index e65789801..344fccdda 100644 --- a/src/app/editor/canvas/menu-actions.ts +++ b/src/app/editor/canvas/menu-actions.ts @@ -6,6 +6,8 @@ import { nodeToXPath } from '@open-pencil/core/xpath' import type { EditorStore } from '@/app/editor/active-store' import { toast } from '@/app/shell/ui' +export const COPY_AS_PNG_SHORTCUT = 'MOD+SHIFT+C' + function toArrayBuffer(data: Uint8Array): ArrayBuffer { const bytes = new Uint8Array(data.length) bytes.set(data) diff --git a/src/app/shell/menu/app-menu.ts b/src/app/shell/menu/app-menu.ts index 615c7e51e..8007793c1 100644 --- a/src/app/shell/menu/app-menu.ts +++ b/src/app/shell/menu/app-menu.ts @@ -1,6 +1,6 @@ import { computed } from 'vue' -import { useEditorCommands, useI18n } from '@open-pencil/vue' +import { useEditorCommands, useI18n, formatShortcut } from '@open-pencil/vue' import type { MenuEntry } from '@open-pencil/vue' import { useEditorStore } from '@/app/editor/active-store' @@ -15,10 +15,6 @@ export interface AppMenuGroup { items: MenuEntry[] } -function shortcutLabel(shortcut: string | undefined, mod: string): string | undefined { - return shortcut?.replaceAll('MOD', mod) -} - function isVisible(entry: { target?: string }): boolean { return entry.target !== 'native' } @@ -27,7 +23,7 @@ function isSeparator(entry: AppMenuEntry): entry is Extract void store.saveFigFile(), 'save-as': () => void store.saveFigFileAs(), 'export-selection': () => exportSelection('png'), + cut: () => document.execCommand('cut'), 'export-png': () => exportSelection('png'), 'export-svg': () => exportSelection('svg'), 'export-fig': () => exportSelection('fig'), @@ -110,12 +107,12 @@ export function useAppMenu(mod: string) { } if (entry.command) { - return commandMenuItem(entry.command, shortcutLabel(entry.shortcut, mod)) + return commandMenuItem(entry.command, formatShortcut(entry.shortcut)) } return { label: entry.label, - shortcut: shortcutLabel(entry.shortcut, mod), + shortcut: formatShortcut(entry.shortcut), action: itemAction(entry), checked: checked(entry), onCheckedChange: onCheckedChange(entry), diff --git a/src/app/shell/menu/schema.ts b/src/app/shell/menu/schema.ts index 4dfccc090..a514e0efc 100644 --- a/src/app/shell/menu/schema.ts +++ b/src/app/shell/menu/schema.ts @@ -35,12 +35,12 @@ export const APP_MENU_SCHEMA = [ { id: 'open', label: 'Open…', shortcut: 'MOD+O', accelerator: 'CmdOrCtrl+O' }, { type: 'separator' }, { id: 'save', label: 'Save', shortcut: 'MOD+S', accelerator: 'CmdOrCtrl+S' }, - { id: 'save-as', label: 'Save As…', shortcut: 'MOD+⇧S', accelerator: 'CmdOrCtrl+Shift+S' }, + { id: 'save-as', label: 'Save As…', shortcut: 'MOD+SHIFT+S', accelerator: 'CmdOrCtrl+Shift+S' }, { type: 'separator' }, { id: 'export-selection', label: 'Export Selection', - shortcut: 'MOD+⇧E', + shortcut: 'MOD+SHIFT+E', accelerator: 'CmdOrCtrl+Shift+E', sub: [ { id: 'export-png', label: 'PNG' }, @@ -66,12 +66,13 @@ export const APP_MENU_SCHEMA = [ { id: 'edit.redo', label: 'Redo', - shortcut: 'MOD+⇧Z', + shortcut: 'MOD+SHIFT+Z', accelerator: 'CmdOrCtrl+Shift+Z', command: 'edit.redo' }, { type: 'separator' }, { id: 'copy', label: 'Copy', shortcut: 'MOD+C', accelerator: 'CmdOrCtrl+C' }, + { id: 'cut', label: 'Cut', shortcut: 'MOD+X', accelerator: 'CmdOrCtrl+X' }, { id: 'paste', label: 'Paste', shortcut: 'MOD+V', accelerator: 'CmdOrCtrl+V' }, { id: 'selection.duplicate', @@ -158,7 +159,7 @@ export const APP_MENU_SCHEMA = [ { id: 'selection.ungroup', label: 'Ungroup Selection', - shortcut: 'MOD+⇧G', + shortcut: 'MOD+SHIFT+G', accelerator: 'CmdOrCtrl+Shift+G', command: 'selection.ungroup' }, @@ -166,7 +167,7 @@ export const APP_MENU_SCHEMA = [ { id: 'selection.createComponent', label: 'Create Component', - shortcut: 'MOD+⌥K', + shortcut: 'MOD+ALT+K', accelerator: 'CmdOrCtrl+Alt+K', command: 'selection.createComponent' }, @@ -211,18 +212,18 @@ export const APP_MENU_SCHEMA = [ { id: 'selection.wrapInAutoLayout', label: 'Wrap in Auto Layout', - shortcut: '⇧A', + shortcut: 'SHIFT+A', accelerator: 'Shift+A', command: 'selection.wrapInAutoLayout' }, { type: 'separator' }, - { id: 'align-left', label: 'Align Left', shortcut: '⌥A', accelerator: 'Alt+A' }, - { id: 'align-center', label: 'Align Center', shortcut: '⌥H', accelerator: 'Alt+H' }, - { id: 'align-right', label: 'Align Right', shortcut: '⌥D', accelerator: 'Alt+D' }, + { id: 'align-left', label: 'Align Left', shortcut: 'ALT+A', accelerator: 'Alt+A' }, + { id: 'align-center', label: 'Align Center', shortcut: 'ALT+H', accelerator: 'Alt+H' }, + { id: 'align-right', label: 'Align Right', shortcut: 'ALT+D', accelerator: 'Alt+D' }, { type: 'separator' }, - { id: 'align-top', label: 'Align Top', shortcut: '⌥W', accelerator: 'Alt+W' }, - { id: 'align-middle', label: 'Align Middle', shortcut: '⌥V', accelerator: 'Alt+V' }, - { id: 'align-bottom', label: 'Align Bottom', shortcut: '⌥S', accelerator: 'Alt+S' } + { id: 'align-top', label: 'Align Top', shortcut: 'ALT+W', accelerator: 'Alt+W' }, + { id: 'align-middle', label: 'Align Middle', shortcut: 'ALT+V', accelerator: 'Alt+V' }, + { id: 'align-bottom', label: 'Align Bottom', shortcut: 'ALT+S', accelerator: 'Alt+S' } ] } ] satisfies AppMenuGroupSchema[] diff --git a/src/app/shell/menu/shortcut.ts b/src/app/shell/menu/shortcut.ts index 185872b06..8fc4c5a7d 100644 --- a/src/app/shell/menu/shortcut.ts +++ b/src/app/shell/menu/shortcut.ts @@ -27,6 +27,6 @@ export function appMenuTinykeysShortcut(id: string): string | string[] | undefin const shortcut = appMenuShortcut(id) return shortcut ?.replaceAll('MOD', '$mod') - .replaceAll('⇧', 'Shift') - .replaceAll('⌥', 'Alt') + .replaceAll('SHIFT', 'Shift') + .replaceAll('ALT', 'Alt') } diff --git a/src/app/shell/menu/use.ts b/src/app/shell/menu/use.ts index 69e81f62d..e94f31188 100644 --- a/src/app/shell/menu/use.ts +++ b/src/app/shell/menu/use.ts @@ -34,7 +34,7 @@ const COMMAND_MENU_IDS = new Set([ export { importFileDialog, openFileDialog } export { openFileFromPath } from '@/app/shell/menu/files' -function execBrowserCommand(command: 'copy' | 'paste'): void { +function execBrowserCommand(command: 'copy' | 'cut' | 'paste'): void { document.execCommand(command) } @@ -70,6 +70,7 @@ export function useMenu() { store.state.autosaveEnabled = !store.state.autosaveEnabled }, copy: () => execBrowserCommand('copy'), + cut: () => execBrowserCommand('cut'), paste: () => execBrowserCommand('paste'), 'check-updates': () => void checkForAppUpdate({ messages: dialogs }), ...createSharedEditorMenuActions(setTheme) diff --git a/src/components/AppMenu.vue b/src/components/AppMenu.vue index 5b582373c..2690eb915 100644 --- a/src/components/AppMenu.vue +++ b/src/components/AppMenu.vue @@ -19,11 +19,12 @@ import { import IconChevronRight from '~icons/lucide/chevron-right' -import { vTestId, useI18n } from '@open-pencil/vue' +import { vTestId, useI18n, formatShortcut } from '@open-pencil/vue' import { useMenuUI } from '@/components/ui/menu' import { IS_TAURI } from '@/constants' import { useAppMenu } from '@/app/shell/menu/app-menu' import { useDocumentNameRename } from '@/app/shell/menu/document-name' +import { appMenuShortcut } from '@/app/shell/menu/shortcut' import { hasMenuSubItems, isMenuCheckbox, @@ -47,11 +48,9 @@ watch(nameInput, (input) => { if (input) void rename.focusInput(input) }) -const isMac = navigator.platform.includes('Mac') -const mod = isMac ? '⌘' : 'Ctrl+' const { menu: t } = useI18n() -const { topMenus } = useAppMenu(mod) +const { topMenus } = useAppMenu() const menuCls = useMenuUI() const mainMenuCls = useMenuUI({ content: 'min-w-52' }) const subMenuCls = useMenuUI({ content: 'min-w-44' }) @@ -77,7 +76,7 @@ const subMenuCls = useMenuUI({ content: 'min-w-44' }) @dblclick="startRename" >{{ store.state.documentName }} - + - + - +