refactor(app): reuse menu shortcut metadata
This commit is contained in:
parent
6c8fd8fc59
commit
1a03c0e8e8
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<AppMenuEntry, { type
|
|||
return entry.type === 'separator'
|
||||
}
|
||||
|
||||
export function useAppMenu(mod: string) {
|
||||
export function useAppMenu() {
|
||||
const store = useEditorStore()
|
||||
const { menuItem: commandMenuItem } = useEditorCommands()
|
||||
const { locale, availableLocales, localeLabels, setLocale } = useI18n()
|
||||
|
|
@ -55,6 +51,7 @@ export function useAppMenu(mod: string) {
|
|||
save: () => 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),
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const COMMAND_MENU_IDS = new Set<string>([
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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 }}</span
|
||||
>
|
||||
<Tip :label="`${t.toggleUI} (${mod}\\)`">
|
||||
<Tip :label="`${t.toggleUI} (${formatShortcut(appMenuShortcut('toggle-ui'))})`">
|
||||
<button
|
||||
data-test-id="app-toggle-ui"
|
||||
class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import {
|
|||
import type { EditorCommandId } from '@open-pencil/vue'
|
||||
|
||||
import { useEditorStore } from '@/app/editor/active-store'
|
||||
import { createCanvasMenuActions } from '@/app/editor/canvas/menu-actions'
|
||||
import { appMenuShortcut } from '@/app/shell/menu/shortcut'
|
||||
import { COPY_AS_PNG_SHORTCUT, createCanvasMenuActions } from '@/app/editor/canvas/menu-actions'
|
||||
import { canvasMenuItemClass, canvasMenuShortcutClass } from '@/app/editor/canvas/menu-model'
|
||||
import { menu, useMenuUI } from '@/components/ui/menu'
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ function contextCommandTestId(id: EditorCommandId | undefined): string | undefin
|
|||
@select="execCommand('copy')"
|
||||
>
|
||||
<span>{{ t.copy }}</span
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut('MOD+C') }}</span>
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut(appMenuShortcut('copy')) }}</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-cut"
|
||||
|
|
@ -72,11 +73,11 @@ function contextCommandTestId(id: EditorCommandId | undefined): string | undefin
|
|||
@select="execCommand('cut')"
|
||||
>
|
||||
<span>{{ t.cut }}</span
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut('MOD+X') }}</span>
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut(appMenuShortcut('cut')) }}</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem data-test-id="context-paste" :class="cls.item" @select="execCommand('paste')">
|
||||
<span>{{ t.pasteHere }}</span
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut('MOD+V') }}</span>
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut(appMenuShortcut('paste')) }}</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-duplicate"
|
||||
|
|
@ -85,7 +86,7 @@ function contextCommandTestId(id: EditorCommandId | undefined): string | undefin
|
|||
@select="getCommand('selection.duplicate').run()"
|
||||
>
|
||||
<span>{{ getCommand('selection.duplicate').label }}</span
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut('MOD+D') }}</span>
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut(editorCommandMetadata('selection.duplicate').shortcut) }}</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-delete"
|
||||
|
|
@ -162,7 +163,7 @@ function contextCommandTestId(id: EditorCommandId | undefined): string | undefin
|
|||
>
|
||||
<ContextMenuItem :class="cls.item" @select="copyAsPNG">
|
||||
<span>{{ t.copyAsPNG }}</span
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut('MOD+SHIFT+C') }}</span>
|
||||
><span class="text-[11px] text-muted">{{ formatShortcut(COPY_AS_PNG_SHORTCUT) }}</span>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
data-test-id="context-copy-as-jsx"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { nextTick, ref, watch } from 'vue'
|
|||
import { useEditorCommands, useI18n, formatShortcut } from '@open-pencil/vue'
|
||||
import { menuItem, useMenuUI } from '@/components/ui/menu'
|
||||
import { useEditorStore } from '@/app/editor/active-store'
|
||||
import { appMenuShortcut } from '@/app/shell/menu/shortcut'
|
||||
|
||||
const store = useEditorStore()
|
||||
const { getCommand } = useEditorCommands()
|
||||
|
|
@ -69,7 +70,7 @@ function zoomOut() {
|
|||
|
||||
const ZOOM_PRESETS: ReadonlyArray<{ label: string; level: number; shortcut?: string }> = [
|
||||
{ label: '50%', level: 0.5 },
|
||||
{ label: '100%', level: 1, shortcut: 'MOD+0' },
|
||||
{ label: '100%', level: 1, shortcut: appMenuShortcut('view.zoom100') },
|
||||
{ label: '200%', level: 2 }
|
||||
]
|
||||
|
||||
|
|
@ -126,15 +127,15 @@ watch(open, (v) => {
|
|||
|
||||
<DropdownMenuItem :class="itemCls" @select="zoomIn">
|
||||
<span class="flex-1">{{ menuText.zoomIn }}</span>
|
||||
<span class="text-[11px] text-muted">{{ formatShortcut('MOD++') }}</span>
|
||||
<span class="text-[11px] text-muted">{{ formatShortcut(appMenuShortcut('zoom-in')) }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem :class="itemCls" @select="zoomOut">
|
||||
<span class="flex-1">{{ menuText.zoomOut }}</span>
|
||||
<span class="text-[11px] text-muted">{{ formatShortcut('MOD+−') }}</span>
|
||||
<span class="text-[11px] text-muted">{{ formatShortcut(appMenuShortcut('zoom-out')) }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem :class="itemCls" @select="getCommand('view.zoomFit').run()">
|
||||
<span class="flex-1">{{ commands.zoomToFit }}</span>
|
||||
<span class="text-[11px] text-muted">{{ formatShortcut('SHIFT+1') }}</span>
|
||||
<span class="text-[11px] text-muted">{{ formatShortcut(appMenuShortcut('view.zoomFit')) }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-for="preset in ZOOM_PRESETS"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import AppSelect from '@/components/ui/AppSelect.vue'
|
|||
import Tip from '@/components/ui/Tip.vue'
|
||||
import { useSectionUI } from '@/components/ui/section'
|
||||
import { loadFont } from '@/app/editor/fonts'
|
||||
import { appMenuShortcut } from '@/app/shell/menu/shortcut'
|
||||
|
||||
const { panels, menu } = useI18n()
|
||||
const sectionCls = useSectionUI()
|
||||
|
|
@ -125,7 +126,7 @@ const fontLoader = { load: loadFont }
|
|||
</ToggleGroupItem>
|
||||
</ToggleGroupRoot>
|
||||
<div class="flex gap-0.5">
|
||||
<Tip :label="`${menu.bold} (${formatShortcut('MOD+B')})`">
|
||||
<Tip :label="`${menu.bold} (${formatShortcut(appMenuShortcut('text.bold'))})`">
|
||||
<button
|
||||
data-test-id="typography-bold-button"
|
||||
class="flex cursor-pointer items-center justify-center rounded border border-border bg-input px-2 py-1 font-bold text-muted hover:bg-hover hover:text-surface data-[state=on]:border-accent data-[state=on]:bg-accent data-[state=on]:text-white"
|
||||
|
|
@ -135,7 +136,7 @@ const fontLoader = { load: loadFont }
|
|||
<icon-lucide-bold class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip :label="`${menu.italic} (${formatShortcut('MOD+I')})`">
|
||||
<Tip :label="`${menu.italic} (${formatShortcut(appMenuShortcut('text.italic'))})`">
|
||||
<button
|
||||
class="flex cursor-pointer items-center justify-center rounded border border-border bg-input px-2 py-1 text-muted hover:bg-hover hover:text-surface data-[state=on]:border-accent data-[state=on]:bg-accent data-[state=on]:text-white"
|
||||
:data-state="ctx.activeFormatting.value.includes('italic') ? 'on' : 'off'"
|
||||
|
|
@ -144,7 +145,7 @@ const fontLoader = { load: loadFont }
|
|||
<icon-lucide-italic class="size-3.5" />
|
||||
</button>
|
||||
</Tip>
|
||||
<Tip :label="`${menu.underline} (${formatShortcut('MOD+U')})`">
|
||||
<Tip :label="`${menu.underline} (${formatShortcut(appMenuShortcut('text.underline'))})`">
|
||||
<button
|
||||
class="flex cursor-pointer items-center justify-center rounded border border-border bg-input px-2 py-1 text-muted hover:bg-hover hover:text-surface data-[state=on]:border-accent data-[state=on]:bg-accent data-[state=on]:text-white"
|
||||
:data-state="ctx.activeFormatting.value.includes('underline') ? 'on' : 'off'"
|
||||
|
|
|
|||
Loading…
Reference in a new issue