Skip to content

Commit d99fafc

Browse files
committed
Bugfix: ⌘A selects text in the Settings and viewer windows (was dead outside the main window)
⌘A did nothing in non-main windows (for example the shortcut search field in Settings > Keyboard shortcuts). "Select all" is a custom MenuItem with a ⌘A accelerator, so macOS intercepts the keystroke before the webview ever sees it — and `handle_menu_event`'s `FileScoped` focus guard then silently dropped the event because the main window wasn't focused. No path led to the text field. - `Select all` now rides the same focus-routed exception as Cut/Copy/Paste: main window focused → `execute-command selection.selectAll` (unchanged behavior); any other window → the native `selectAll:` selector through the responder chain via `NSApplication.sendAction:to:from:`, so WKWebView does text select-all natively, exactly like a PredefinedMenuItem would. - `send_native_clipboard_action` renamed to `send_native_edit_action` (it's no longer clipboard-only). - `Deselect all` (⌘⇧A) intentionally keeps the plain FileScoped path: AppKit has no standard "deselect all" responder action for text fields, so there's nothing native to forward to. - Docs: updated the `menu/CLAUDE.md` gotchas (⌘A dual routing, custom Edit MenuItems) and the `mod.rs` mapping comments.
1 parent 762b395 commit d99fafc

3 files changed

Lines changed: 44 additions & 33 deletions

File tree

apps/desktop/src-tauri/src/menu/CLAUDE.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,21 +224,26 @@ distinction is the load-bearing reason.
224224
defaults that added unwanted items.
225225
- **Tab as accelerator**: Switch pane uses Tab, which could conflict with menu bar accessibility
226226
navigation. If issues arise, omit the accelerator and rely on JS dispatch.
227-
- **Custom MenuItems for Cut/Copy/Paste**: The Edit menu uses custom MenuItems (not
228-
PredefinedMenuItems) for Cut, Copy, Paste, and Move here. In `handle_menu_event`, these are handled
229-
specially: if the main window is focused, they route through `execute-command` so the frontend can
230-
decide between file clipboard and text clipboard (via `document.activeElement` check). If a
231-
non-main window is focused (viewer, settings), `send_native_clipboard_action()` in `menu_handlers.rs` sends
232-
the native `copy:`/`cut:`/`paste:` selector through the responder chain via
227+
- **Custom MenuItems for Cut/Copy/Paste/Select all**: The Edit menu uses custom MenuItems (not
228+
PredefinedMenuItems) for Cut, Copy, Paste, and Move here; the Select menu does the same for
229+
Select all. In `handle_menu_event`, these are handled specially: if the main window is focused,
230+
they route through `execute-command` so the frontend can decide between file and text semantics
231+
(via `document.activeElement` check). If a non-main window is focused (viewer, settings),
232+
`send_native_edit_action()` in `menu_handlers.rs` sends the native
233+
`copy:`/`cut:`/`paste:`/`selectAll:` selector through the responder chain via
233234
`NSApplication.sendAction:to:from:`, replicating what PredefinedMenuItems do internally. This
234-
ensures text clipboard works natively in all windows. Undo and Redo remain PredefinedMenuItems
235-
since they only apply to text fields.
235+
ensures text clipboard and text select-all work natively in all windows. Undo and Redo remain
236+
PredefinedMenuItems since they only apply to text fields.
236237
- **⌘A dual routing**: "Select all" uses ⌘A as a native menu accelerator (so it's visible in the
237238
Select menu — see § "Decision: Select all and Deselect all live in the new Select top-level menu"
238-
above). Since macOS intercepts it before the webview,
239-
the frontend's `handleCommandExecute` checks `document.activeElement`: if it's an input/textarea, it calls `.select()`
240-
for text selection; otherwise it selects files. This avoids PredefinedMenuItem::select_all which would conflict with
241-
the custom MenuItem.
239+
above). Since macOS intercepts it before the webview, the keystroke must be re-routed per focus:
240+
main window → `execute-command`, where the frontend's `handleCommandExecute` checks
241+
`document.activeElement` (input/textarea → `.select()` for text, otherwise select files);
242+
non-main window → native `selectAll:` via `send_native_edit_action()` (without this branch ⌘A is
243+
dead in settings text fields — the `FileScoped` focus guard would silently drop it). This avoids
244+
PredefinedMenuItem::select_all which would conflict with the custom MenuItem. Deselect all (⌘⇧A)
245+
stays on the plain `FileScoped` path: AppKit has no standard "deselect all" responder action for
246+
text fields, so there's nothing native to forward to.
242247
- **Pin tab label**: `pin_tab` in MenuState is updated dynamically by the frontend to show
243248
"Pin tab" or "Unpin tab" based on the active tab's state.
244249
- **Reopen closed tab item**: The Tab submenu includes "Reopen closed tab" (⌘⇧T on macOS) between

apps/desktop/src-tauri/src/menu/menu_handlers.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ use super::menu_items::{brief_view_label, full_view_label};
1919
use super::{
2020
CLOSE_TAB_ID, CommandScope, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_PASTE_ID, EJECT_VOLUME_ID, MenuItemEntry, MenuSort,
2121
MenuState, NETWORK_HOST_DISCONNECT_ID, NETWORK_HOST_FORGET_PASSWORD_ID, NETWORK_HOST_FORGET_SERVER_ID,
22-
SHOW_HIDDEN_FILES_ID, SORT_ASCENDING_ID, SORT_BY_CREATED_ID, SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID,
23-
SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SORT_DESCENDING_ID, SettingsChanged, TAB_CLOSE_ID, TAB_CLOSE_OTHERS_ID,
24-
TAB_PIN_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID,
25-
VIEWER_WORD_WRAP_ID, ViewMode, ViewModeChanged, menu_id_to_command,
22+
SELECT_ALL_ID, SHOW_HIDDEN_FILES_ID, SORT_ASCENDING_ID, SORT_BY_CREATED_ID, SORT_BY_EXTENSION_ID,
23+
SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SORT_DESCENDING_ID, SettingsChanged, TAB_CLOSE_ID,
24+
TAB_CLOSE_OTHERS_ID, TAB_PIN_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID,
25+
VIEW_MODE_FULL_RIGHT_ID, VIEWER_WORD_WRAP_ID, ViewMode, ViewModeChanged, menu_id_to_command,
2626
};
2727

2828
/// Removes macOS system-injected items from the Edit menu and registers the Help menu.
@@ -297,24 +297,25 @@ pub fn update_menu_item_accelerator<R: Runtime>(
297297
Ok(())
298298
}
299299

300-
/// Sends a native clipboard action (copy:/cut:/paste:) through the responder chain.
300+
/// Sends a native edit action (copy:/cut:/paste:/selectAll:) through the responder chain.
301301
///
302-
/// Used when a non-main window is focused: the custom Edit menu items can't use the native
303-
/// responder chain like PredefinedMenuItems do, so we replicate it manually via
302+
/// Used when a non-main window is focused: the custom Edit/Select menu items can't use the
303+
/// native responder chain like PredefinedMenuItems do, so we replicate it manually via
304304
/// `NSApplication.sendAction:to:from:` with nil target (routes to the first responder).
305305
#[cfg(target_os = "macos")]
306-
fn send_native_clipboard_action(menu_id: &str) {
306+
fn send_native_edit_action(menu_id: &str) {
307307
use objc2::sel;
308308
use objc2_app_kit::NSApplication;
309309

310310
let selector = match menu_id {
311311
EDIT_CUT_ID => sel!(cut:),
312312
EDIT_COPY_ID => sel!(copy:),
313313
EDIT_PASTE_ID => sel!(paste:),
314+
SELECT_ALL_ID => sel!(selectAll:),
314315
_ => return,
315316
};
316317

317-
let mtm = objc2::MainThreadMarker::new().expect("send_native_clipboard_action must be called from the main thread");
318+
let mtm = objc2::MainThreadMarker::new().expect("send_native_edit_action must be called from the main thread");
318319
let ns_app = NSApplication::sharedApplication(mtm);
319320

320321
// sendAction:to:from: with nil `to` sends to the first responder, exactly like
@@ -503,30 +504,33 @@ pub fn handle_menu_event(app: &AppHandle<tauri::Wry>, event: tauri::menu::MenuEv
503504
return;
504505
}
505506

506-
// === Clipboard exception: file clipboard in main window, native text clipboard elsewhere ===
507-
// Custom MenuItems for Cut/Copy/Paste route through execute-command in the main window
508-
// so the frontend can decide between file and text clipboard. In non-main windows
509-
// (viewer, settings), we send the native action through the responder chain so
510-
// WKWebView handles text clipboard natively, just like PredefinedMenuItems would.
511-
if id == EDIT_CUT_ID || id == EDIT_COPY_ID || id == EDIT_PASTE_ID {
507+
// === Edit-action exception: file semantics in main window, native text semantics elsewhere ===
508+
// Custom MenuItems for Cut/Copy/Paste/Select all route through execute-command in the main
509+
// window so the frontend can decide between file and text semantics. In non-main windows
510+
// (viewer, settings), we send the native action through the responder chain so WKWebView
511+
// handles text clipboard / text select-all natively, just like PredefinedMenuItems would.
512+
// Without the Select-all branch, ⌘A is dead in settings text fields: the accelerator fires
513+
// before the webview ever sees the key, and the FileScoped focus guard would drop it.
514+
if id == EDIT_CUT_ID || id == EDIT_COPY_ID || id == EDIT_PASTE_ID || id == SELECT_ALL_ID {
512515
let main_focused = app
513516
.get_webview_window("main")
514517
.is_some_and(|w| w.is_focused().unwrap_or(false));
515518
if main_focused {
516519
let command_id = match id {
517520
EDIT_CUT_ID => "edit.cut",
518521
EDIT_COPY_ID => "edit.copy",
519-
_ => "edit.paste",
522+
EDIT_PASTE_ID => "edit.paste",
523+
_ => "selection.selectAll",
520524
};
521525
use tauri_specta::Event as _;
522526
let _ = crate::window_events::ExecuteCommand {
523527
command_id: command_id.to_string(),
524528
}
525529
.emit_to(app, "main");
526530
} else {
527-
// Send native clipboard action to the first responder chain
531+
// Send the native action to the first responder chain
528532
#[cfg(target_os = "macos")]
529-
send_native_clipboard_action(id);
533+
send_native_edit_action(id);
530534
}
531535
return;
532536
}

apps/desktop/src-tauri/src/menu/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,9 @@ pub fn menu_id_to_command(menu_id: &str) -> Option<(&'static str, CommandScope)>
264264
PIN_TAB_MENU_ID => Some(("tab.togglePin", CommandScope::FileScoped)),
265265
CLOSE_OTHER_TABS_ID => Some(("tab.closeOthers", CommandScope::FileScoped)),
266266

267-
// Clipboard operations: cut/copy/paste are handled specially in on_menu_event
268-
// (native responder chain for non-main windows, execute-command for main window).
269-
// They're still listed here for command_id_to_menu_id reverse lookups.
267+
// Edit actions: cut/copy/paste (and select_all_files below) are handled specially in
268+
// on_menu_event (native responder chain for non-main windows, execute-command for the
269+
// main window). They're still listed here for command_id_to_menu_id reverse lookups.
270270
EDIT_CUT_ID => Some(("edit.cut", CommandScope::App)),
271271
EDIT_COPY_ID => Some(("edit.copy", CommandScope::App)),
272272
EDIT_PASTE_ID => Some(("edit.paste", CommandScope::App)),
@@ -288,6 +288,8 @@ pub fn menu_id_to_command(menu_id: &str) -> Option<(&'static str, CommandScope)>
288288
COPY_FILENAME_ID => Some(("file.copyFilename", CommandScope::FileScoped)),
289289
GET_INFO_ID => Some(("file.getInfo", CommandScope::FileScoped)),
290290
QUICK_LOOK_ID => Some(("file.quickLook", CommandScope::FileScoped)),
291+
// Intercepted by on_menu_event before this lookup (like cut/copy/paste): main window →
292+
// execute-command, non-main → native selectAll: so ⌘A still works in text fields there.
291293
SELECT_ALL_ID => Some(("selection.selectAll", CommandScope::FileScoped)),
292294
DESELECT_ALL_ID => Some(("selection.deselectAll", CommandScope::FileScoped)),
293295
SELECT_FILES_ID => Some(("selection.selectFiles", CommandScope::FileScoped)),

0 commit comments

Comments
 (0)