diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5423c23..9b9376c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "Markpad" -version = "2.6.6" +version = "2.6.8" dependencies = [ "arboard", "base64 0.22.1", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e14385f..846f0e5 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -972,7 +972,52 @@ pub fn run() { .item(&PredefinedMenuItem::hide_others(app, None)?) .item(&PredefinedMenuItem::show_all(app, None)?) .separator() - .item(&PredefinedMenuItem::quit(app, None)?) + .item( + &MenuItemBuilder::with_id( + "menu-app-quit", + format!("Quit {}", app_name), + ) + .accelerator("CmdOrCtrl+Q") + .build(app)?, + ) + .build()?; + + let file_submenu = SubmenuBuilder::new(app, "File") + .item( + &MenuItemBuilder::with_id("menu-file-new", "New File") + .accelerator("CmdOrCtrl+T") + .build(app)?, + ) + .item( + &MenuItemBuilder::with_id("menu-file-open", "Open File…") + .accelerator("CmdOrCtrl+O") + .build(app)?, + ) + .item( + &MenuItemBuilder::with_id("menu-file-close", "Close") + .accelerator("CmdOrCtrl+W") + .build(app)?, + ) + .separator() + .item( + &MenuItemBuilder::with_id("menu-file-save", "Save") + .accelerator("CmdOrCtrl+S") + .build(app)?, + ) + .item( + &MenuItemBuilder::with_id("menu-file-save-as", "Save As…") + .accelerator("CmdOrCtrl+Shift+S") + .build(app)?, + ) + .separator() + .item( + &MenuItemBuilder::with_id("menu-file-export-html", "Export as HTML") + .build(app)?, + ) + .item( + &MenuItemBuilder::with_id("menu-file-export-pdf", "Export as PDF") + .build(app)?, + ) .build()?; let edit_submenu = SubmenuBuilder::new(app, "Edit") @@ -991,7 +1036,7 @@ pub fn run() { .build()?; let menu = MenuBuilder::new(app) - .items(&[&app_submenu, &edit_submenu, &window_submenu]) + .items(&[&app_submenu, &file_submenu, &edit_submenu, &window_submenu]) .build()?; app.set_menu(menu)?; @@ -1078,8 +1123,24 @@ pub fn run() { list_directory_contents ]) .on_menu_event(|app, event| { - if event.id().as_ref() == "check-updates" { - let _ = app.emit("menu-check-updates", ()); + let id = event.id().as_ref(); + // Emit to the focused webview window rather than `app.emit(...)`, + // which would broadcast to every webview. Markpad is currently + // single-window, but additional webviews (e.g. detached tabs) + // would otherwise receive duplicate New/Close/Save invocations. + // Falls back to "main" if no window is focused (e.g. menu fired + // while the app is in the background). + let target = app + .webview_windows() + .into_values() + .find(|w| w.is_focused().unwrap_or(false)) + .or_else(|| app.get_webview_window("main")); + let Some(window) = target else { return }; + + if id == "check-updates" { + let _ = window.emit("menu-check-updates", ()); + } else if id == "menu-app-quit" || id.starts_with("menu-file-") { + let _ = window.emit(id, ()); } }) .build(tauri::generate_context!()) diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index 2f1c84b..69d3fd1 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -2013,6 +2013,18 @@ import { t } from './utils/i18n.js'; const key = e.key.toLowerCase(); const code = e.code; + // On macOS the native menu accelerators (⌘T, ⌘W, ⌘S, ⌘Q) take priority + // via NSMenu; the JS keydown handler should not also fire for them, or + // we'd double-handle (e.g. open two new tabs on ⌘T). The !e.shiftKey + // guards keep ⌘⇧T (undo close tab) routed through this handler as + // before — only the bare combos are claimed by the menu. + if (settings.osType === 'macos' && cmdOrCtrl && !e.shiftKey) { + if (key === 'q') return; // → menu-app-quit + if (key === 'w') return; // → menu-file-close + if (key === 's') return; // → menu-file-save + if (key === 't') return; // → menu-file-new + } + const isSplit = tabManager.activeTab?.isSplit; if (cmdOrCtrl && key === 'w') { @@ -2360,6 +2372,21 @@ import { t } from './utils/i18n.js'; updateStore.openDialog(); }), ); + // Native macOS menubar — Markpad ▸ Quit and File ▸ * — bridged + // to the same handlers the in-window burger button uses, so the + // menu and the burger stay behaviourally identical. Save mirrors + // the keydown guard (`isEditing || isSplit`) so menu ⌘S in pure + // view mode is a no-op, matching the keyboard shortcut. + unlisteners.push(await listen('menu-app-quit', () => appExit())); + unlisteners.push(await listen('menu-file-new', () => handleNewFile())); + unlisteners.push(await listen('menu-file-open', () => selectFile())); + unlisteners.push(await listen('menu-file-close', () => closeFile())); + unlisteners.push(await listen('menu-file-save', () => { + if (isEditing || tabManager.activeTab?.isSplit) saveContent(); + })); + unlisteners.push(await listen('menu-file-save-as', () => saveContentAs())); + unlisteners.push(await listen('menu-file-export-html', () => exportAsHtml())); + unlisteners.push(await listen('menu-file-export-pdf', () => exportAsPdf())); unlisteners.push( await appWindow.onCloseRequested(async (event) => { console.log('onCloseRequested triggered');