import { computed } from 'vue' import { useRouter } from 'vue-router' import type { MenuEntry } from '@open-pencil/vue' import { useEditorCommands, useI18n } from '@open-pencil/vue' import { useEditorStore } from '@/app/editor/active-store' import { openSettingsDialog } from '@/app/settings/dialog' import { createSharedEditorMenuActions } from '@/app/shell/menu/editor-actions' import { openStorageWorkspace } from '@/app/shell/menu/navigation' import type { AppMenuActionItem, AppMenuEntry, AppMenuGroupSchema } from '@/app/shell/menu/schema' import { APP_MENU_SCHEMA } from '@/app/shell/menu/schema' import { createSelectionMenuActions } from '@/app/shell/menu/selection-actions' import { appMenuShortcutLabel } from '@/app/shell/menu/shortcut' import { openFileDialog } from '@/app/shell/menu/use' import { useAppTheme } from '@/app/shell/theme' import { closeTab, activeTab } from '@/app/tabs' export interface AppMenuGroup { label: string items: MenuEntry[] } function isVisible(entry: { target?: string }): boolean { return entry.target !== 'native' } function isSeparator(entry: AppMenuEntry): entry is Extract { return entry.type === 'separator' } export function useAppMenu() { const store = useEditorStore() const router = useRouter() const { commands, menuItem: commandMenuItem, otherPages, moveSelectionToPage } = useEditorCommands() const { menu, locale, availableLocales, localeLabels, setLocale } = useI18n() const { theme, setTheme } = useAppTheme() const translatedMenuItemLabels: Partial> = { new: 'new', open: 'open', 'open-storage-workspace': 'openStorageWorkspace', save: 'save', 'save-as': 'saveAs', 'export-selection': 'exportSelection', autosave: 'autosave', close: 'closeTab', copy: 'copy', cut: 'cut', paste: 'paste', 'paste-to-replace': 'pasteToReplace', 'selection.rename': 'renameSelection', 'selection.moveToPage': 'moveToPage', language: 'language', settings: 'settings', 'view-rulers': 'rulers', 'view-multiplayer-cursors': 'multiplayerCursors', profiler: 'profiler', 'toggle-ui': 'toggleUI', theme: 'theme', 'theme-light': 'themeLight', 'theme-dark': 'themeDark', 'theme-auto': 'themeAuto', 'zoom-in': 'zoomIn', 'zoom-out': 'zoomOut', 'text.bold': 'bold', 'text.italic': 'italic', 'text.underline': 'underline', 'arrange.align-left': 'arrangeAlignLeft', 'arrange.align-center': 'arrangeAlignCenter', 'arrange.align-right': 'arrangeAlignRight', 'arrange.align-top': 'arrangeAlignTop', 'arrange.align-middle': 'arrangeAlignMiddle', 'arrange.align-bottom': 'arrangeAlignBottom' } const languageMenu = computed(() => availableLocales.map((code) => ({ label: localeLabels[code], checked: locale.value === code, onCheckedChange: (checked: boolean) => { if (checked) setLocale(code) } })) ) function exportSelection(format: 'png' | 'svg' | 'pptx' | 'fig') { if (store.state.selectedIds.size > 0) void store.exportSelection(1, format) } const actions: Partial void>> = { new: () => { void import('@/app/tabs').then((m) => m.createTab()) }, open: () => void openFileDialog(), 'open-storage-workspace': () => openStorageWorkspace(router), save: () => void store.saveFigFile(), 'save-as': () => void store.saveFigFileAs(), 'export-selection': () => exportSelection('png'), ...createSelectionMenuActions(store), close: () => { if (activeTab.value) closeTab(activeTab.value.id) }, settings: openSettingsDialog, 'export-png': () => exportSelection('png'), 'export-svg': () => exportSelection('svg'), 'export-pptx': () => exportSelection('pptx'), 'export-fig': () => exportSelection('fig'), ...createSharedEditorMenuActions(setTheme) } function itemAction(item: AppMenuActionItem): (() => void) | undefined { return actions[item.id] } function checked(item: AppMenuActionItem): boolean | undefined { switch (item.id) { case 'autosave': return store.state.autosaveEnabled case 'profiler': return store.renderer?.profiler.hudVisible ?? false case 'view-rulers': return store.state.showRulers case 'view-multiplayer-cursors': return store.state.showRemoteCursors case 'theme-light': return theme.value === 'light' case 'theme-dark': return theme.value === 'dark' case 'theme-auto': return theme.value === 'auto' default: return undefined } } function onCheckedChange(item: AppMenuActionItem): ((checked: boolean) => void) | undefined { switch (item.id) { case 'autosave': return (value: boolean) => { store.state.autosaveEnabled = value } case 'profiler': return () => store.toggleProfiler() case 'view-rulers': return (value: boolean) => { if (store.state.showRulers !== value) itemAction(item)?.() } case 'view-multiplayer-cursors': return (value: boolean) => { if (store.state.showRemoteCursors !== value) itemAction(item)?.() } case 'theme-light': case 'theme-dark': case 'theme-auto': return (value: boolean) => { if (value) itemAction(item)?.() } default: return undefined } } function menuLabel(entry: AppMenuActionItem): string { const key = translatedMenuItemLabels[entry.id] return key ? menu.value[key] : entry.label } function buildEntry(entry: AppMenuEntry): MenuEntry | null { if (!isVisible(entry)) return null if (isSeparator(entry)) return { separator: true } if (entry.id === 'language') { return { label: menuLabel(entry), sub: languageMenu.value } } if (entry.id === 'selection.moveToPage') { if (otherPages.value.length === 0) return null const disabled = !commands['selection.moveToPage'].enabled.value return { label: menuLabel(entry), disabled, sub: otherPages.value.map((page) => ({ label: page.name, disabled, action: () => moveSelectionToPage(page.id) })) } } if (entry.command) { return commandMenuItem(entry.command, appMenuShortcutLabel(entry.id)) } return { label: menuLabel(entry), shortcut: appMenuShortcutLabel(entry.id), action: itemAction(entry), checked: checked(entry), onCheckedChange: onCheckedChange(entry), sub: entry.sub?.map(buildEntry).filter((item): item is MenuEntry => item !== null) } } function groupLabel(group: AppMenuGroupSchema): string { const key = group.label.toLowerCase() as keyof typeof menu.value return menu.value[key] ?? group.label } function buildGroup(group: AppMenuGroupSchema): AppMenuGroup | null { if (!isVisible(group)) return null return { label: groupLabel(group), items: group.items.map(buildEntry).filter((item): item is MenuEntry => item !== null) } } const topMenus = computed(() => APP_MENU_SCHEMA.map(buildGroup).filter((group): group is AppMenuGroup => group !== null) ) return { topMenus } }