fix(desktop): 显示完整菜单栏并启用快捷键
This commit is contained in:
@@ -21,7 +21,9 @@
|
||||
|
||||
桌面客户端顶部菜单栏的 **段落 → 导入为笔记属性…** 预留元数据格式导入功能,与标题、正文、列表等段落操作归组。它处理笔记内容中的元数据,不是主题包安装入口。
|
||||
|
||||
稳定的前端命令标识为 `editor.import-note-properties`。属性转换处理器、Tauri 原生菜单事件和无边框窗口内的可见“段落”菜单均分发同一命令,没有维护第二套转换逻辑。快捷键待第三阶段统一分配,不抢占现有编辑快捷键。
|
||||
稳定的前端命令标识为 `editor.import-note-properties`。属性转换处理器、Tauri 原生菜单事件和无边框窗口内的可见“段落”菜单均分发同一命令,没有维护第二套转换逻辑。默认快捷键为 Windows/Linux 的 `Ctrl+Alt+P`、macOS 的 `⌘⌥P`;Rust Host 使用 `CmdOrCtrl+Alt+P` 注册 accelerator,仍由活动编辑器能力检查决定是否可用。
|
||||
|
||||
无边框桌面窗口在标题栏下方固定显示“文件 / 编辑 / 段落 / 视图”菜单行。保存使用 `Ctrl+S`;撤销和重做沿用编辑器已有的 `Ctrl+Z` 与 `Ctrl+Shift+Z`,菜单只调用相同编辑器命令,不额外注册按键监听,以免一次按键执行两次。
|
||||
|
||||
Markdown 格式(含警告框)与元数据共用 `editorCommandService` 的能力查询和命令分发接口,详见 [警告框与桌面编辑命令开发说明](../development/警告框与桌面编辑命令开发说明.md)。Host 根据 supported / enabled 显隐或禁用菜单,不能把已预留的命令 ID 当作已可执行能力;原生快捷键不绕过活动文档、只读和冲突检查。
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
目录还预留删除线、任务列表、引用、Mermaid、链接、图片、表格、分隔线、硬换行、引用式链接、HTML、撤销/重做,以及 `editor.import-note-properties`、`editor.metadata.edit`、`editor.metadata.title`、`editor.metadata.tags`。这些单独的处理器尚未接入,返回 unsupported;现阶段复杂格式可通过 insert-markdown 插入。元数据不得通过普通正文插入接口冒充属性导入。
|
||||
|
||||
第三阶段由 Host 将原生菜单/快捷键映射到上述 ID。快捷键表需支持平台差异与用户改键,过滤表单、输入法组合与弹窗焦点;不要重复绑定浏览器和 Milkdown 已有按键。宿主只能调用允许的命令,不执行任意脚本。元数据处理器须按已有桌面需求完成无损 YAML 合并、版本检查、冲突提示及属性/正文一并撤销后才可标记 supported。
|
||||
第三阶段由 Host 将原生菜单/快捷键映射到上述 ID。`editor.import-note-properties` 默认使用 `CmdOrCtrl+Alt+P`,窗口保存使用 `Ctrl/Cmd+S`;撤销、重做等 Milkdown/CodeMirror 已有按键只在菜单中标示,不重复绑定。快捷键表需支持平台差异与用户改键,过滤输入法组合和不可用状态。宿主只能调用允许的命令,不执行任意脚本。元数据处理器须按已有桌面需求完成无损 YAML 合并、版本检查、冲突提示及属性/正文一并撤销后才可标记 supported。
|
||||
|
||||
## 3. 验证方法
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ fn main() {
|
||||
"editor.import-note-properties",
|
||||
"导入为笔记属性…",
|
||||
false,
|
||||
None::<&str>,
|
||||
Some("CmdOrCtrl+Alt+P"),
|
||||
)?;
|
||||
let paragraph = Submenu::with_items(app, "段落", true, &[&import])?;
|
||||
app.manage(import);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AppShell from '@/components/common/AppShell.vue'
|
||||
import TitleBar from '@/components/common/TitleBar.vue'
|
||||
import TitleBarMenu from '@/components/common/TitleBarMenu.vue'
|
||||
import { isDesktop } from '@/services/platform/desktop'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -13,6 +14,7 @@ const desktop = isDesktop()
|
||||
<template>
|
||||
<div v-if="isVaultEntry" class="entry-shell">
|
||||
<TitleBar v-if="desktop" />
|
||||
<TitleBarMenu v-if="desktop" />
|
||||
<router-view />
|
||||
</div>
|
||||
<AppShell v-else>
|
||||
|
||||
@@ -9,8 +9,10 @@ import PrimarySidebar from './PrimarySidebar.vue'
|
||||
import SecondarySidebar from './SecondarySidebar.vue'
|
||||
import StatusBar from './StatusBar.vue'
|
||||
import TitleBar from './TitleBar.vue'
|
||||
import TitleBarMenu from './TitleBarMenu.vue'
|
||||
import CommandPalette from './CommandPalette.vue'
|
||||
import { getIndexStatus } from '@/services/indexService'
|
||||
import { isDesktop } from '@/services/platform/desktop'
|
||||
import { navigateToCitation } from '@/composables/useCitationNavigation'
|
||||
import { useWorkspaceRefresh } from '@/composables/useWorkspaceRefresh'
|
||||
useWorkspaceRefresh()
|
||||
@@ -25,6 +27,7 @@ const editorStore = useEditorStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const desktop = isDesktop()
|
||||
|
||||
let statusTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let disposed = false
|
||||
@@ -75,6 +78,7 @@ defineExpose({ openCitation })
|
||||
<template>
|
||||
<div class="app-shell" :class="{ 'theme-dark': themeStore.isDark }">
|
||||
<TitleBar />
|
||||
<TitleBarMenu v-if="desktop" />
|
||||
<div class="app-body">
|
||||
<PrimarySidebar />
|
||||
<SecondarySidebar v-if="secondaryComponent" :component="secondaryComponent" />
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useEditorStore } from '@/stores/editor'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
||||
import AppIcon from './AppIcon.vue'
|
||||
import TitleBarMenu from './TitleBarMenu.vue'
|
||||
import { t } from '@/i18n'
|
||||
import { isDesktop } from '@/services/platform/desktop'
|
||||
import { minimizeWindow, requestWindowClose, toggleMaximizeWindow } from '@/services/platform/windowControls'
|
||||
@@ -62,7 +61,6 @@ function toggleFromTitlebar(event: MouseEvent) {
|
||||
{{ currentFileName }}
|
||||
<span v-if="isDirty" class="dirty-dot" />
|
||||
</span>
|
||||
<TitleBarMenu />
|
||||
</div>
|
||||
<div class="titlebar-center" data-tauri-drag-region>
|
||||
<span class="app-name" data-tauri-drag-region>NotesAgent</span>
|
||||
|
||||
@@ -6,11 +6,17 @@ import { useEditorStore } from '@/stores/editor'
|
||||
import TitleBarMenu from './TitleBarMenu.vue'
|
||||
|
||||
const execute = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/services/editorCommandService', () => ({ executeEditorCommand: execute }))
|
||||
const capabilities = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/services/editorCommandService', () => ({ executeEditorCommand: execute, getEditorCommandCapabilities: capabilities }))
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
execute.mockReset().mockResolvedValue({ ok: true })
|
||||
capabilities.mockReturnValue([
|
||||
{ id: 'editor.paragraph', supported: true, enabled: true },
|
||||
{ id: 'editor.heading', supported: true, enabled: true },
|
||||
{ id: 'editor.import-note-properties', supported: true, enabled: true },
|
||||
])
|
||||
})
|
||||
|
||||
describe('桌面顶部段落菜单', () => {
|
||||
@@ -20,8 +26,9 @@ describe('桌面顶部段落菜单', () => {
|
||||
editor.currentFilePath = '/示例.md'
|
||||
editor.saveStatus = 'saved'
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('.menu-trigger').trigger('click')
|
||||
const item = wrapper.get('[role="menuitem"]')
|
||||
expect(wrapper.text()).toContain('文件编辑段落视图')
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
const item = wrapper.get('.import-properties')
|
||||
expect((item.element as HTMLButtonElement).disabled).toBe(false)
|
||||
await item.trigger('click')
|
||||
expect(execute).toHaveBeenCalledWith('editor.import-note-properties')
|
||||
@@ -34,9 +41,21 @@ describe('桌面顶部段落菜单', () => {
|
||||
editor.mode = 'wysiwyg'
|
||||
editor.currentFilePath = '/示例.md'
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('.menu-trigger').trigger('click')
|
||||
expect((wrapper.get('[role="menuitem"]').element as HTMLButtonElement).disabled).toBe(true)
|
||||
capabilities.mockReturnValue([])
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
expect((wrapper.get('.import-properties').element as HTMLButtonElement).disabled).toBe(true)
|
||||
expect(wrapper.text()).toContain('请在无冲突的 Markdown 源码笔记中使用')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('标题命令传入对应级别并显示快捷键', async () => {
|
||||
const wrapper = mount(TitleBarMenu)
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
const headings = wrapper.findAll('[data-menu="paragraph"] [role="menuitem"]').filter(item => item.text().startsWith('标题'))
|
||||
await headings[1].trigger('click')
|
||||
expect(execute).toHaveBeenCalledWith('editor.heading', 2)
|
||||
await wrapper.get('[data-menu="paragraph"] .menu-trigger').trigger('click')
|
||||
expect(wrapper.get('.import-properties').text()).toContain('Ctrl+Alt+P')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,67 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useEditorStore } from '@/stores/editor'
|
||||
import { executeEditorCommand } from '@/services/editorCommandService'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
import { executeEditorCommand, getEditorCommandCapabilities, type EditorCommandId } from '@/services/editorCommandService'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
type MenuName = 'file' | 'edit' | 'paragraph' | 'view'
|
||||
const editor = useEditorStore()
|
||||
const open = ref(false)
|
||||
const menu = ref<HTMLElement | null>(null)
|
||||
const trigger = ref<HTMLButtonElement | null>(null)
|
||||
const importItem = ref<HTMLButtonElement | null>(null)
|
||||
const theme = useThemeStore()
|
||||
const open = ref<MenuName | null>(null)
|
||||
const bar = ref<HTMLElement | null>(null)
|
||||
const error = ref('')
|
||||
const importEnabled = computed(() => editor.mode === 'source' && !!editor.currentFilePath
|
||||
&& !['conflict', 'external_changed'].includes(editor.saveStatus))
|
||||
const shortcut = computed(() => /Mac|iPhone|iPad/.test(navigator.platform) ? '⌘⌥P' : 'Ctrl+Alt+P')
|
||||
|
||||
function dismiss(event: PointerEvent) {
|
||||
if (!menu.value?.contains(event.target as Node)) open.value = false
|
||||
function enabled(id: EditorCommandId) {
|
||||
return getEditorCommandCapabilities().find(item => item.id === id)?.enabled ?? false
|
||||
}
|
||||
|
||||
async function showAndFocus() {
|
||||
open.value = true
|
||||
function toggle(name: MenuName) { open.value = open.value === name ? null : name; error.value = '' }
|
||||
function dismiss(event: PointerEvent) { if (!bar.value?.contains(event.target as Node)) open.value = null }
|
||||
async function focusFirst(name: MenuName) {
|
||||
open.value = name
|
||||
await nextTick()
|
||||
importItem.value?.focus()
|
||||
bar.value?.querySelector<HTMLElement>(`[data-menu="${name}"] [role="menuitem"]:not(:disabled)`)?.focus()
|
||||
}
|
||||
|
||||
function closeAndFocus() {
|
||||
open.value = false
|
||||
trigger.value?.focus()
|
||||
}
|
||||
|
||||
async function importProperties() {
|
||||
if (!importEnabled.value) return
|
||||
const result = await executeEditorCommand('editor.import-note-properties')
|
||||
if (!result.ok) error.value = t('当前编辑器无法执行属性导入。', 'Property import is unavailable in the current editor.')
|
||||
else { error.value = ''; open.value = false }
|
||||
function close() { open.value = null }
|
||||
async function command(id: EditorCommandId, params?: unknown) {
|
||||
const result = params === undefined ? await executeEditorCommand(id) : await executeEditorCommand(id, params)
|
||||
if (result.ok) close()
|
||||
else error.value = t('当前编辑器无法执行此命令。', 'The active editor cannot run this command.')
|
||||
}
|
||||
async function save() { await editor.save(); close() }
|
||||
function setMode(mode: 'source' | 'wysiwyg') { editor.setMode(mode); close() }
|
||||
function toggleTheme() { theme.toggleTheme(); close() }
|
||||
|
||||
onMounted(() => window.addEventListener('pointerdown', dismiss))
|
||||
onBeforeUnmount(() => window.removeEventListener('pointerdown', dismiss))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="menu" class="titlebar-menu">
|
||||
<button ref="trigger" type="button" class="menu-trigger" aria-haspopup="menu" :aria-expanded="open" @click="open ? closeAndFocus() : showAndFocus()" @keydown.down.prevent="showAndFocus">
|
||||
{{ t('段落', 'Paragraph') }}
|
||||
</button>
|
||||
<div v-if="open" class="menu-popover" role="menu" @keydown.esc.stop.prevent="closeAndFocus">
|
||||
<button ref="importItem" type="button" role="menuitem" :disabled="!importEnabled" @click="importProperties">
|
||||
{{ t('导入为笔记属性…', 'Import as note properties…') }}
|
||||
</button>
|
||||
<small v-if="!importEnabled">{{ t('请在无冲突的 Markdown 源码笔记中使用', 'Available in a conflict-free Markdown source note') }}</small>
|
||||
<small v-if="error" role="alert">{{ error }}</small>
|
||||
<nav ref="bar" class="desktop-menu-bar" :aria-label="t('应用菜单', 'Application menu')" @keydown.esc.stop.prevent="close">
|
||||
<div class="menu-group" data-menu="file">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'file'" aria-haspopup="menu" @click="toggle('file')" @keydown.down.prevent="focusFirst('file')">{{ t('文件', 'File') }}</button>
|
||||
<div v-if="open === 'file'" class="menu-popover" role="menu">
|
||||
<button role="menuitem" :disabled="!editor.currentFilePath || ['saving','conflict','external_changed'].includes(editor.saveStatus)" @click="save">
|
||||
<span>{{ t('保存', 'Save') }}</span><kbd>Ctrl+S</kbd>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="edit">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'edit'" aria-haspopup="menu" @click="toggle('edit')" @keydown.down.prevent="focusFirst('edit')">{{ t('编辑', 'Edit') }}</button>
|
||||
<div v-if="open === 'edit'" class="menu-popover" role="menu">
|
||||
<button role="menuitem" :disabled="!enabled('editor.undo')" @click="command('editor.undo')"><span>{{ t('撤销', 'Undo') }}</span><kbd>Ctrl+Z</kbd></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.redo')" @click="command('editor.redo')"><span>{{ t('重做', 'Redo') }}</span><kbd>Ctrl+Shift+Z</kbd></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="paragraph">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'paragraph'" aria-haspopup="menu" @click="toggle('paragraph')" @keydown.down.prevent="focusFirst('paragraph')">{{ t('段落', 'Paragraph') }}</button>
|
||||
<div v-if="open === 'paragraph'" class="menu-popover paragraph-menu" role="menu">
|
||||
<button role="menuitem" :disabled="!enabled('editor.paragraph')" @click="command('editor.paragraph')"><span>{{ t('正文', 'Body text') }}</span></button>
|
||||
<button v-for="level in 3" :key="level" role="menuitem" :disabled="!enabled('editor.heading')" @click="command('editor.heading', level)"><span>{{ t(`标题 ${level}`, `Heading ${level}`) }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" :disabled="!enabled('editor.bullet-list')" @click="command('editor.bullet-list')"><span>{{ t('无序列表', 'Bullet list') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.ordered-list')" @click="command('editor.ordered-list')"><span>{{ t('有序列表', 'Ordered list') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.task-list')" @click="command('editor.task-list')"><span>{{ t('任务列表', 'Task list') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.blockquote')" @click="command('editor.blockquote')"><span>{{ t('引用', 'Blockquote') }}</span></button>
|
||||
<button role="menuitem" :disabled="!enabled('editor.code-block')" @click="command('editor.code-block')"><span>{{ t('代码块', 'Code block') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button class="import-properties" role="menuitem" :disabled="!enabled('editor.import-note-properties')" @click="command('editor.import-note-properties')">
|
||||
<span>{{ t('导入为笔记属性…', 'Import as note properties…') }}</span><kbd>{{ shortcut }}</kbd>
|
||||
</button>
|
||||
<small v-if="!enabled('editor.import-note-properties')">{{ t('请在无冲突的 Markdown 源码笔记中使用', 'Available in a conflict-free Markdown source note') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-group" data-menu="view">
|
||||
<button class="menu-trigger" :aria-expanded="open === 'view'" aria-haspopup="menu" @click="toggle('view')" @keydown.down.prevent="focusFirst('view')">{{ t('视图', 'View') }}</button>
|
||||
<div v-if="open === 'view'" class="menu-popover" role="menu">
|
||||
<button role="menuitemradio" :aria-checked="editor.mode === 'wysiwyg'" @click="setMode('wysiwyg')"><span>✓ {{ t('写作模式', 'Writing mode') }}</span></button>
|
||||
<button role="menuitemradio" :aria-checked="editor.mode === 'source'" @click="setMode('source')"><span>✓ {{ t('源码模式', 'Source mode') }}</span></button>
|
||||
<span class="menu-separator" role="separator" />
|
||||
<button role="menuitem" @click="toggleTheme"><span>{{ theme.isDark ? t('浅色主题', 'Light theme') : t('深色主题', 'Dark theme') }}</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="error" class="menu-error" role="alert">{{ error }}</span>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titlebar-menu { position: relative; flex: 0 0 auto; -webkit-app-region: no-drag; }
|
||||
.menu-trigger { min-height: 28px; padding: 3px 10px; border: 1px solid transparent; border-radius: var(--radius-sm); color: var(--color-text-secondary); }
|
||||
.menu-trigger:hover, .menu-trigger[aria-expanded='true'] { border-color: var(--color-border-default); background: var(--color-background-hover); color: var(--color-text-primary); }
|
||||
.menu-popover { position: absolute; top: calc(100% + 5px); left: 0; z-index: calc(var(--z-titlebar) + 2); display: grid; min-width: 220px; padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-elevated); box-shadow: var(--shadow-lg); }
|
||||
.menu-popover button { width: 100%; padding: var(--space-sm) var(--space-md); border-radius: var(--radius-sm); text-align: left; white-space: nowrap; }
|
||||
.desktop-menu-bar { position: relative; z-index: calc(var(--z-titlebar) - 1); display: flex; align-items: center; min-height: 32px; padding: 0 var(--space-md); border-bottom: 1px solid var(--color-border-default); background: var(--color-surface-primary); flex-shrink: 0; user-select: none; -webkit-app-region: no-drag; }
|
||||
.menu-group { position: relative; height: 100%; display: flex; align-items: center; }
|
||||
.menu-trigger { min-width: 52px; height: 28px; padding: 2px 12px; border-radius: var(--radius-sm); color: var(--color-text-secondary); }
|
||||
.menu-trigger:hover, .menu-trigger[aria-expanded='true'] { background: var(--color-background-hover); color: var(--color-text-primary); }
|
||||
.menu-popover { position: absolute; top: calc(100% + 2px); left: 0; z-index: calc(var(--z-titlebar) + 2); display: grid; min-width: 230px; padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-md); background: var(--color-surface-elevated); box-shadow: var(--shadow-lg); }
|
||||
.paragraph-menu { min-width: 270px; }
|
||||
.menu-popover button { display: flex; align-items: center; justify-content: space-between; gap: var(--space-xl); width: 100%; padding: 7px var(--space-md); border-radius: var(--radius-sm); text-align: left; white-space: nowrap; }
|
||||
.menu-popover button:hover:not(:disabled), .menu-popover button:focus-visible { background: var(--color-accent-soft); color: var(--color-accent-primary); }
|
||||
.menu-popover button:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.menu-popover button:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.menu-popover kbd { color: var(--color-text-tertiary); font: inherit; font-size: var(--font-size-xs); }
|
||||
.menu-separator { height: 1px; margin: var(--space-xs); background: var(--color-border-subtle); }
|
||||
.menu-popover small { padding: var(--space-xs) var(--space-md); color: var(--color-text-tertiary); white-space: normal; }
|
||||
.menu-popover small[role='alert'] { color: var(--color-error); }
|
||||
.menu-error { margin-left: var(--space-md); color: var(--color-error); font-size: var(--font-size-xs); }
|
||||
</style>
|
||||
|
||||
@@ -9,8 +9,22 @@ import { isDesktop } from './desktop'
|
||||
export async function installDesktopLifecycle() {
|
||||
if (!isDesktop()) return
|
||||
const activeEditor = useEditorStore()
|
||||
// 保存属于窗口级命令;编辑区原生处理撤销/重做,避免重复派发。
|
||||
window.addEventListener('keydown', event => {
|
||||
if (event.defaultPrevented || event.isComposing || event.repeat || event.altKey || event.shiftKey) return
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') return
|
||||
const target = event.target as HTMLElement | null
|
||||
if (target?.closest('input, textarea, select, dialog[open], [role="dialog"]')) return
|
||||
if (!activeEditor.currentFilePath || ['saving', 'conflict', 'external_changed'].includes(activeEditor.saveStatus)) return
|
||||
event.preventDefault()
|
||||
void activeEditor.save()
|
||||
})
|
||||
watch(() => [activeEditor.currentFilePath, activeEditor.saveStatus, activeEditor.mode], updateNativeEditorMenu, { flush: 'post' })
|
||||
await listen<string>('editor-command', event => { void executeEditorCommand(event.payload) })
|
||||
await listen<string>('editor-command', event => {
|
||||
const focused = document.activeElement as HTMLElement | null
|
||||
if (focused?.closest('input, textarea, select, dialog[open], [role="dialog"]')) return
|
||||
void executeEditorCommand(event.payload)
|
||||
})
|
||||
let closing = false
|
||||
await listen('host-close-requested', async () => {
|
||||
if (closing) return
|
||||
|
||||
Reference in New Issue
Block a user