From 14a0ee323e8e596f45d4a57d2d86abcf0a848bc8 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Fri, 30 Dec 2022 20:47:18 +0200 Subject: [PATCH] feat: add document title changed handler, closes #804 (#825) * feat: add document title changed handler, closes #804 * linux * android * macOS * fix selector and change read * use `of_object` instead * fix macOS Co-authored-by: Lucas Nogueira --- .changes/document-title-changed-handler.md | 5 ++ src/webview/android/binding.rs | 15 +++- .../android/kotlin/RustWebChromeClient.kt | 9 +++ src/webview/android/mod.rs | 22 ++++++ src/webview/mod.rs | 13 ++++ src/webview/webkitgtk/mod.rs | 11 +++ src/webview/webview2/mod.rs | 21 ++++++ src/webview/wkwebview/mod.rs | 70 +++++++++++++++++-- 8 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 .changes/document-title-changed-handler.md diff --git a/.changes/document-title-changed-handler.md b/.changes/document-title-changed-handler.md new file mode 100644 index 000000000..24ab9f183 --- /dev/null +++ b/.changes/document-title-changed-handler.md @@ -0,0 +1,5 @@ +--- +"wry": "minor" +--- + +Add APIs to process webview document title change. \ No newline at end of file diff --git a/src/webview/android/binding.rs b/src/webview/android/binding.rs index 0d0900cf9..ca1fe133d 100644 --- a/src/webview/android/binding.rs +++ b/src/webview/android/binding.rs @@ -13,7 +13,7 @@ use tao::platform::android::ndk_glue::jni::{ JNIEnv, }; -use super::{IPC, REQUEST_HANDLER}; +use super::{IPC, REQUEST_HANDLER, TITLE_CHANGE_HANDLER}; fn handle_request(env: JNIEnv, request: JObject) -> Result { let mut request_builder = Request::builder(); @@ -167,3 +167,16 @@ pub unsafe fn ipc(env: JNIEnv, _: JClass, arg: JString) { Err(e) => log::warn!("Failed to parse JString: {}", e), } } + +#[allow(non_snake_case)] +pub unsafe fn handleReceivedTitle(env: JNIEnv, _: JClass, _webview: JObject, title: JString) { + match env.get_string(title) { + Ok(title) => { + let title = title.to_string_lossy().to_string(); + if let Some(w) = TITLE_CHANGE_HANDLER.get() { + (w.0)(&w.1, title) + } + } + Err(e) => log::warn!("Failed to parse JString: {}", e), + } +} diff --git a/src/webview/android/kotlin/RustWebChromeClient.kt b/src/webview/android/kotlin/RustWebChromeClient.kt index 4e4eb7e3e..86814135b 100644 --- a/src/webview/android/kotlin/RustWebChromeClient.kt +++ b/src/webview/android/kotlin/RustWebChromeClient.kt @@ -492,4 +492,13 @@ class RustWebChromeClient(appActivity: AppCompatActivity) : WebChromeClient() { val storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES) return File.createTempFile(imageFileName, ".jpg", storageDir) } + + override fun onReceivedTitle( + view: WebView, + title: String + ) { + handleReceivedTitle(view, title) + } + + private external fun handleReceivedTitle(webview: WebView, title: String); } diff --git a/src/webview/android/mod.rs b/src/webview/android/mod.rs index 4b1de989f..cec06ba88 100644 --- a/src/webview/android/mod.rs +++ b/src/webview/android/mod.rs @@ -52,11 +52,19 @@ macro_rules! android_binding { jobject ); android_fn!($domain, $package, Ipc, ipc, [JString]); + android_fn!( + $domain, + $package, + RustWebChromeClient, + handleReceivedTitle, + [JObject, JString], + ); }; } pub static IPC: OnceCell = OnceCell::new(); pub static REQUEST_HANDLER: OnceCell = OnceCell::new(); +pub static TITLE_CHANGE_HANDLER: OnceCell = OnceCell::new(); pub struct UnsafeIpc(Box, Rc); impl UnsafeIpc { @@ -78,6 +86,15 @@ impl UnsafeRequestHandler { unsafe impl Send for UnsafeRequestHandler {} unsafe impl Sync for UnsafeRequestHandler {} +pub struct UnsafeTitleHandler(Box, Rc); +impl UnsafeTitleHandler { + pub fn new(f: Box, w: Rc) -> Self { + Self(f, w) + } +} +unsafe impl Send for UnsafeTitleHandler {} +unsafe impl Sync for UnsafeTitleHandler {} + pub unsafe fn setup(env: JNIEnv, looper: &ForeignLooper, activity: GlobalRef) { // we must create the WebChromeClient here because it calls `registerForActivityResult`, // which gives an `LifecycleOwners must call register before they are STARTED.` error when called outside the onCreate hook @@ -224,6 +241,11 @@ impl InnerWebView { IPC.get_or_init(move || UnsafeIpc::new(Box::new(i), w)); } + let w = window.clone(); + if let Some(i) = attributes.document_title_changed_handler { + TITLE_CHANGE_HANDLER.get_or_init(move || UnsafeTitleHandler::new(i, w)); + } + Ok(Self { window }) } diff --git a/src/webview/mod.rs b/src/webview/mod.rs index 7e7d0264d..c9b2f923d 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -224,6 +224,9 @@ pub struct WebViewAttributes { /// This configuration only impacts macOS. /// [Documentation](https://developer.apple.com/documentation/webkit/wkwebview/1414995-allowsbackforwardnavigationgestu). pub back_forward_navigation_gestures: bool, + + /// Set a handler closure to process the change of the webview's document title. + pub document_title_changed_handler: Option>, } impl Default for WebViewAttributes { @@ -251,6 +254,7 @@ impl Default for WebViewAttributes { zoom_hotkeys_enabled: false, accept_first_mouse: false, back_forward_navigation_gestures: false, + document_title_changed_handler: None, } } } @@ -577,6 +581,15 @@ impl<'a> WebViewBuilder<'a> { self } + /// Set a handler closure to process the change of the webview's document title. + pub fn with_document_title_changed_handler( + mut self, + callback: impl Fn(&Window, String) + 'static, + ) -> Self { + self.webview.document_title_changed_handler = Some(Box::new(callback)); + self + } + /// Consume the builder and create the [`WebView`]. /// /// Platform-specific behavior: diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index 48364e08a..5f6b8af3a 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -106,6 +106,17 @@ impl InnerWebView { close_window.gtk_window().close(); }); + // document title changed handler + if let Some(document_title_changed_handler) = attributes.document_title_changed_handler { + let w = window_rc.clone(); + webview.connect_title_notify(move |webview| { + document_title_changed_handler( + &w, + webview.title().map(|t| t.to_string()).unwrap_or_default(), + ) + }); + } + webview.add_events( EventMask::POINTER_MOTION_MASK | EventMask::BUTTON1_MOTION_MASK diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index d2ab5043f..49ba9e935 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -272,6 +272,27 @@ impl InnerWebView { .map_err(webview2_com::Error::WindowsError)?; } + // document title changed handler + if let Some(document_title_changed_handler) = attributes.document_title_changed_handler { + let window_c = window.clone(); + unsafe { + webview + .add_DocumentTitleChanged( + &DocumentTitleChangedEventHandler::create(Box::new(move |webview, _| { + let mut title = PWSTR::null(); + if let Some(webview) = webview { + webview.DocumentTitle(&mut title)?; + let title = take_pwstr(title); + document_title_changed_handler(&window_c, title); + } + Ok(()) + })), + &mut token, + ) + .map_err(webview2_com::Error::WindowsError)?; + } + } + // Initialize scripts Self::add_script_to_execute_on_document_created( &webview, diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index c4859b763..cb70603ef 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -76,6 +76,7 @@ pub(crate) struct InnerWebView { // Note that if following functions signatures are changed in the future, // all functions pointer declarations in objc callbacks below all need to get updated. ipc_handler_ptr: *mut (Box, Rc), + document_title_changed_handler: *mut (Box, Rc), navigation_decide_policy_ptr: *mut Box bool>, #[cfg(target_os = "macos")] file_drop_ptr: *mut (Box bool>, Rc), @@ -378,6 +379,60 @@ impl InnerWebView { null_mut() }; + // Document title changed handler + let document_title_changed_handler = if let Some(document_title_changed_handler) = + attributes.document_title_changed_handler + { + let cls = ClassDecl::new("DocumentTitleChangedDelegate", class!(NSObject)); + let cls = match cls { + Some(mut cls) => { + cls.add_ivar::<*mut c_void>("function"); + cls.add_method( + sel!(observeValueForKeyPath:ofObject:change:context:), + observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, id), + ); + extern "C" fn observe_value_for_key_path( + this: &Object, + _sel: Sel, + key_path: id, + of_object: id, + _change: id, + _context: id, + ) { + let key = NSString(key_path); + if key.to_str() == "title" { + unsafe { + let function = this.get_ivar::<*mut c_void>("function"); + if !function.is_null() { + let function = &mut *(*function + as *mut (Box Fn(&'r Window, String)>, Rc)); + let title: id = msg_send![of_object, title]; + (function.0)(&function.1, NSString(title).to_str().to_string()); + } + } + } + } + cls.register() + } + None => class!(DocumentTitleChangedDelegate), + }; + + let handler: id = msg_send![cls, new]; + let document_title_changed_handler = + Box::into_raw(Box::new((document_title_changed_handler, window.clone()))); + + (*handler).set_ivar( + "function", + document_title_changed_handler as *mut _ as *mut c_void, + ); + + let _: () = msg_send![webview, addObserver:handler forKeyPath:NSString::new("title") options:0x01 context:nil ]; + + document_title_changed_handler + } else { + null_mut() + }; + // Navigation handler extern "C" fn navigation_policy(this: &Object, _: Sel, _: id, action: id, handler: id) { unsafe { @@ -684,6 +739,7 @@ impl InnerWebView { manager, pending_scripts, ipc_handler_ptr, + document_title_changed_handler, navigation_decide_policy_ptr, #[cfg(target_os = "macos")] file_drop_ptr, @@ -935,28 +991,32 @@ impl Drop for InnerWebView { // We need to drop handler closures here unsafe { if !self.ipc_handler_ptr.is_null() { - let _ = Box::from_raw(self.ipc_handler_ptr); + drop(Box::from_raw(self.ipc_handler_ptr)); let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME); let _: () = msg_send![self.manager, removeScriptMessageHandlerForName: ipc]; } + if !self.document_title_changed_handler.is_null() { + drop(Box::from_raw(self.document_title_changed_handler)); + } + if !self.navigation_decide_policy_ptr.is_null() { - let _ = Box::from_raw(self.navigation_decide_policy_ptr); + drop(Box::from_raw(self.navigation_decide_policy_ptr)); } #[cfg(target_os = "macos")] if !self.file_drop_ptr.is_null() { - let _ = Box::from_raw(self.file_drop_ptr); + drop(Box::from_raw(self.file_drop_ptr)); } if !self.download_delegate.is_null() { - let _ = self.download_delegate.drop_in_place(); + drop(self.download_delegate.drop_in_place()); } for ptr in self.protocol_ptrs.iter() { if !ptr.is_null() { - let _ = Box::from_raw(*ptr); + drop(Box::from_raw(*ptr)); } }