diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index ed905df0..16ffcaf5 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2302,6 +2302,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.1", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -2744,6 +2745,7 @@ dependencies = [ "libc", "objc2", "objc2-app-kit", + "objc2-foundation", "objc2-web-kit", "serde_json", "tauri", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index dd64b1e0..287a279c 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -24,8 +24,9 @@ libc = "0.2" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6" -objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSEvent"] } +objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSEvent", "NSWindow", "NSView"] } objc2-web-kit = { version = "0.3", features = ["WKWebView"] } +objc2-foundation = { version = "0.3", features = ["NSString", "NSArray"] } block2 = "0.6" [features] diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index c7f854f0..163fcefa 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -135,6 +135,44 @@ fn layout(app: &tauri::AppHandle) { } } +/// Evaluates `js` on every WKWebView in the key window's view hierarchy. This +/// reaches the Web Inspector (devtools) webview too — which is not a +/// Tauri-managed webview — so select-all/undo/redo work in the console. +#[cfg(target_os = "macos")] +fn eval_js_on_focused_webviews(js: &str) { + use objc2::MainThreadMarker; + use objc2_app_kit::NSApplication; + use objc2_foundation::NSString; + + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + let app = NSApplication::sharedApplication(mtm); + let Some(window) = app.keyWindow() else { + return; + }; + let Some(content) = window.contentView() else { + return; + }; + + let js = NSString::from_str(js); + walk_and_eval(&content, &js); +} + +#[cfg(target_os = "macos")] +fn walk_and_eval(view: &objc2_app_kit::NSView, js: &objc2_foundation::NSString) { + use objc2_web_kit::WKWebView; + + if let Some(webview) = view.downcast_ref::() { + unsafe { + webview.evaluateJavaScript_completionHandler(js, None); + } + } + for subview in view.subviews().iter() { + walk_and_eval(&subview, js); + } +} + /// Installs a local NSEvent monitor that intercepts Cmd+X/C/V/A/Z and /// dispatches the matching native editing selector to the first responder. /// @@ -143,7 +181,7 @@ fn layout(app: &tauri::AppHandle) { /// this is the only reliable way to make cut/copy/paste work in the devtools /// console. #[cfg(target_os = "macos")] -fn install_edit_shortcut_monitor(handle: &tauri::AppHandle) { +fn install_edit_shortcut_monitor() { use objc2::runtime::{AnyObject, Sel}; use objc2::MainThreadMarker; use objc2_app_kit::{NSApplication, NSEvent, NSEventMask, NSEventModifierFlags}; @@ -153,7 +191,6 @@ fn install_edit_shortcut_monitor(handle: &tauri::AppHandle) { return; }; let app = NSApplication::sharedApplication(mtm); - let handle = handle.clone(); let block = block2::RcBlock::new(move |event: NonNull| -> *mut NSEvent { let event = unsafe { event.as_ref() }; @@ -194,18 +231,22 @@ fn install_edit_shortcut_monitor(handle: &tauri::AppHandle) { ); } - // selectAll:/undo:/redo: don't always dispatch through the responder - // chain in WKWebView, so drive the content webview via execCommand too. + // selectAll:/undo:/redo: don't dispatch through the responder chain in + // WKWebView, so drive the focused webview(s) via execCommand too — but + // only when an editable element is actually focused, otherwise we'd + // select the whole document. let js = match key.as_str() { - "a" => Some("document.execCommand('selectAll')"), - "z" if flags.contains(NSEventModifierFlags::Shift) => Some("document.execCommand('redo')"), + "a" => Some( + "(function(){var e=document.activeElement;if(e&&(e.tagName==='INPUT'||e.tagName==='TEXTAREA'||e.isContentEditable))document.execCommand('selectAll')})()", + ), + "z" if flags.contains(NSEventModifierFlags::Shift) => { + Some("document.execCommand('redo')") + } "z" => Some("document.execCommand('undo')"), _ => None, }; if let Some(js) = js { - if let Some(content) = handle.get_webview("content") { - let _ = content.eval(js); - } + eval_js_on_focused_webviews(js); } std::ptr::null_mut() @@ -280,7 +321,7 @@ pub fn run() { enable_swipe_navigation(app.handle()); #[cfg(target_os = "macos")] - install_edit_shortcut_monitor(app.handle()); + install_edit_shortcut_monitor(); if let Some(toolbar) = app.get_webview("main") { let _ = toolbar.emit("url-changed", mask.mask(&navidrome_url));