From c76f4b7d39a620c7710c2046bb13b140a4793881 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 16 Aug 2021 17:25:45 -0300 Subject: [PATCH] feat(core): set parent window on ask and message dialog APIs (#2454) --- .changes/dialog-ask-message-parent.md | 5 ++ core/tauri/Cargo.toml | 2 +- core/tauri/src/api/dialog.rs | 109 ++++++++++++++++++----- core/tauri/src/endpoints.rs | 2 +- core/tauri/src/endpoints/dialog.rs | 46 ++++------ core/tauri/src/endpoints/notification.rs | 14 ++- core/tauri/src/updater/mod.rs | 16 +++- examples/api/src-tauri/src/main.rs | 24 +++-- 8 files changed, 149 insertions(+), 69 deletions(-) create mode 100644 .changes/dialog-ask-message-parent.md diff --git a/.changes/dialog-ask-message-parent.md b/.changes/dialog-ask-message-parent.md new file mode 100644 index 000000000..2e02eb4f1 --- /dev/null +++ b/.changes/dialog-ask-message-parent.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +**Breaking change:** Added `window_parent: Option<&Window>` as first argument to the `ask` and `message` APIs on the `tauri::api::dialog` module. diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 2ee4573d9..6d5f6a12b 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -66,7 +66,7 @@ attohttpc = { version = "0.17", features = [ "json", "form" ] } open = { version = "2.0", optional = true } shared_child = { version = "0.3", optional = true } os_pipe = { version = "0.9", optional = true } -rfd = "0.4.2" +rfd = { version = "0.4.3", features = ["parent"] } raw-window-handle = { version = "0.3.3", optional = true } minisign-verify = { version = "0.1", optional = true } os_info = { version = "3.0.6", optional = true } diff --git a/core/tauri/src/api/dialog.rs b/core/tauri/src/api/dialog.rs index 859e76e6a..a95a723aa 100644 --- a/core/tauri/src/api/dialog.rs +++ b/core/tauri/src/api/dialog.rs @@ -7,6 +7,8 @@ #[cfg(any(dialog_open, dialog_save))] use std::path::{Path, PathBuf}; +use crate::{Runtime, Window}; + #[cfg(not(target_os = "linux"))] macro_rules! run_dialog { ($e:expr, $h: ident) => {{ @@ -30,6 +32,48 @@ macro_rules! run_dialog { }}; } +/// Window parent definition. +#[cfg(any(windows, target_os = "macos"))] +#[cfg_attr(doc_cfg, doc(cfg(any(windows, target_os = "macos"))))] +pub struct WindowParent { + #[cfg(windows)] + hwnd: *mut std::ffi::c_void, + #[cfg(target_os = "macos")] + ns_window: *mut std::ffi::c_void, +} + +#[cfg(any(windows, target_os = "macos"))] +unsafe impl raw_window_handle::HasRawWindowHandle for WindowParent { + #[cfg(windows)] + fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + let mut handle = raw_window_handle::windows::WindowsHandle::empty(); + handle.hwnd = self.hwnd; + raw_window_handle::RawWindowHandle::Windows(handle) + } + + #[cfg(target_os = "macos")] + fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + let mut handle = raw_window_handle::macos::MacOSHandle::empty(); + handle.ns_window = self.ns_window; + raw_window_handle::RawWindowHandle::MacOS(handle) + } +} + +#[cfg(any(windows, target_os = "macos"))] +#[cfg_attr(doc_cfg, doc(cfg(any(windows, target_os = "macos"))))] +#[doc(hidden)] +pub fn window_parent(window: &Window) -> crate::Result { + #[cfg(windows)] + let w = WindowParent { + hwnd: window.hwnd()?, + }; + #[cfg(target_os = "macos")] + let w = WindowParent { + ns_window: window.ns_window()?, + }; + Ok(w) +} + /// The file dialog builder. /// /// Constructs file picker dialogs that can select single/multiple files or directories. @@ -62,7 +106,6 @@ impl FileDialogBuilder { self } - #[cfg(windows)] /// Sets the parent window of the dialog. pub fn set_parent(mut self, parent: &W) -> Self { self.0 = self.0.set_parent(parent); @@ -91,36 +134,60 @@ impl FileDialogBuilder { } /// Displays a dialog with a message and an optional title with a "yes" and a "no" button. -pub fn ask( +#[allow(unused_variables)] +pub fn ask( + parent_window: Option<&Window>, title: impl AsRef, message: impl AsRef, f: F, ) { let title = title.as_ref().to_string(); let message = message.as_ref().to_string(); - run_dialog!( - rfd::MessageDialog::new() - .set_title(&title) - .set_description(&message) - .set_buttons(rfd::MessageButtons::YesNo) - .set_level(rfd::MessageLevel::Info) - .show(), - f - ) + #[allow(unused_mut)] + let mut builder = rfd::MessageDialog::new() + .set_title(&title) + .set_description(&message) + .set_buttons(rfd::MessageButtons::YesNo) + .set_level(rfd::MessageLevel::Info); + + #[cfg(any(windows, target_os = "macos"))] + { + if let Some(window) = parent_window { + if let Ok(parent) = window_parent(window) { + builder = builder.set_parent(&parent); + } + } + } + + run_dialog!(builder.show(), f) } /// Displays a message dialog. -pub fn message(title: impl AsRef, message: impl AsRef) { +#[allow(unused_variables)] +pub fn message( + parent_window: Option<&Window>, + title: impl AsRef, + message: impl AsRef, +) { let title = title.as_ref().to_string(); let message = message.as_ref().to_string(); let cb = |_| {}; - run_dialog!( - rfd::MessageDialog::new() - .set_title(&title) - .set_description(&message) - .set_buttons(rfd::MessageButtons::Ok) - .set_level(rfd::MessageLevel::Info) - .show(), - cb - ) + + #[allow(unused_mut)] + let mut builder = rfd::MessageDialog::new() + .set_title(&title) + .set_description(&message) + .set_buttons(rfd::MessageButtons::Ok) + .set_level(rfd::MessageLevel::Info); + + #[cfg(any(windows, target_os = "macos"))] + { + if let Some(window) = parent_window { + if let Ok(parent) = window_parent(window) { + builder = builder.set_parent(&parent); + } + } + } + + run_dialog!(builder.show(), cb) } diff --git a/core/tauri/src/endpoints.rs b/core/tauri/src/endpoints.rs index 8844e4828..3d7b4195c 100644 --- a/core/tauri/src/endpoints.rs +++ b/core/tauri/src/endpoints.rs @@ -128,7 +128,7 @@ impl Module { } Self::Notification(cmd) => resolver.respond_closure(move || { cmd - .run(config, &package_info) + .run(window, config, &package_info) .and_then(|r| r.json) .map_err(InvokeError::from) }), diff --git a/core/tauri/src/endpoints/dialog.rs b/core/tauri/src/endpoints/dialog.rs index dd3311f5f..7d3097a41 100644 --- a/core/tauri/src/endpoints/dialog.rs +++ b/core/tauri/src/endpoints/dialog.rs @@ -3,6 +3,8 @@ // SPDX-License-Identifier: MIT use super::InvokeResponse; +#[cfg(any(windows, target_os = "macos"))] +use crate::api::dialog::window_parent; #[cfg(any(dialog_open, dialog_save))] use crate::api::dialog::FileDialogBuilder; use crate::{ @@ -77,7 +79,7 @@ impl Cmd { pub fn run(self, window: Window) -> crate::Result { match self { #[cfg(dialog_open)] - Self::OpenDialog { options } => open(window, options), + Self::OpenDialog { options } => open(&window, options), #[cfg(not(dialog_open))] Self::OpenDialog { .. } => Err(crate::Error::ApiNotAllowlisted("dialog > open".to_string())), @@ -93,12 +95,13 @@ impl Cmd { .expect("failed to get binary filename") .to_string_lossy() .to_string(); - message_dialog(app_name, message); + message_dialog(Some(&window), app_name, message); Ok(().into()) } Self::AskDialog { title, message } => { let exe = std::env::current_exe()?; let answer = ask( + &window, title.unwrap_or_else(|| { exe .file_stem() @@ -132,38 +135,17 @@ fn set_default_path( } } -#[cfg(all(windows, any(dialog_open, dialog_save)))] -struct WindowParent { - hwnd: *mut std::ffi::c_void, -} - -#[cfg(all(windows, any(dialog_open, dialog_save)))] -unsafe impl raw_window_handle::HasRawWindowHandle for WindowParent { - fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { - let mut handle = raw_window_handle::windows::WindowsHandle::empty(); - handle.hwnd = self.hwnd; - raw_window_handle::RawWindowHandle::Windows(handle) - } -} - -#[cfg(all(windows, any(dialog_open, dialog_save)))] -fn parent(window: Window) -> crate::Result { - Ok(WindowParent { - hwnd: window.hwnd()?, - }) -} - /// Shows an open dialog. #[cfg(dialog_open)] #[allow(unused_variables)] pub fn open( - window: Window, + window: &Window, options: OpenDialogOptions, ) -> crate::Result { let mut dialog_builder = FileDialogBuilder::new(); - #[cfg(windows)] + #[cfg(any(windows, target_os = "macos"))] { - dialog_builder = dialog_builder.set_parent(&parent(window)?); + dialog_builder = dialog_builder.set_parent(&window_parent(window)?); } if let Some(default_path) = options.default_path { if !default_path.exists() { @@ -197,9 +179,9 @@ pub fn save( options: SaveDialogOptions, ) -> crate::Result { let mut dialog_builder = FileDialogBuilder::new(); - #[cfg(windows)] + #[cfg(any(windows, target_os = "macos"))] { - dialog_builder = dialog_builder.set_parent(&parent(window)?); + dialog_builder = dialog_builder.set_parent(&window_parent(&window)?); } if let Some(default_path) = options.default_path { dialog_builder = set_default_path(dialog_builder, default_path); @@ -214,8 +196,12 @@ pub fn save( } /// Shows a dialog with a yes/no question. -pub fn ask(title: String, message: String) -> crate::Result { +pub fn ask( + window: &Window, + title: String, + message: String, +) -> crate::Result { let (tx, rx) = channel(); - ask_dialog(title, message, move |m| tx.send(m).unwrap()); + ask_dialog(Some(window), title, message, move |m| tx.send(m).unwrap()); Ok(rx.recv().unwrap().into()) } diff --git a/core/tauri/src/endpoints/notification.rs b/core/tauri/src/endpoints/notification.rs index 8f84b3995..8a3fa6baf 100644 --- a/core/tauri/src/endpoints/notification.rs +++ b/core/tauri/src/endpoints/notification.rs @@ -7,7 +7,7 @@ use serde::Deserialize; #[cfg(notification_all)] use crate::api::notification::Notification; -use crate::{Config, PackageInfo}; +use crate::{Config, PackageInfo, Runtime, Window}; use std::sync::Arc; @@ -42,8 +42,9 @@ pub enum Cmd { impl Cmd { #[allow(unused_variables)] - pub fn run( + pub fn run( self, + window: Window, config: Arc, package_info: &PackageInfo, ) -> crate::Result { @@ -60,7 +61,7 @@ impl Cmd { } Self::RequestNotificationPermission => { #[cfg(notification_all)] - return request_permission(&config, package_info).map(Into::into); + return request_permission(&window, &config, package_info).map(Into::into); #[cfg(not(notification_all))] Ok(PERMISSION_DENIED.into()) } @@ -96,7 +97,11 @@ pub fn is_permission_granted( } #[cfg(notification_all)] -pub fn request_permission(config: &Config, package_info: &PackageInfo) -> crate::Result { +pub fn request_permission( + window: &Window, + config: &Config, + package_info: &PackageInfo, +) -> crate::Result { let mut settings = crate::settings::read_settings(config, package_info); if let Some(allow_notification) = settings.allow_notification { return Ok(if allow_notification { @@ -107,6 +112,7 @@ pub fn request_permission(config: &Config, package_info: &PackageInfo) -> crate: } let (tx, rx) = std::sync::mpsc::channel(); crate::api::dialog::ask( + Some(window), "Permissions", "This app wants to show notifications. Do you allow?", move |answer| { diff --git a/core/tauri/src/updater/mod.rs b/core/tauri/src/updater/mod.rs index 31f48bc12..932b907bd 100644 --- a/core/tauri/src/updater/mod.rs +++ b/core/tauri/src/updater/mod.rs @@ -394,8 +394,15 @@ pub(crate) async fn check_update_with_dialog( // if dialog enabled only if updater.should_update && updater_config.dialog { let body = updater.body.clone().unwrap_or_else(|| String::from("")); - let dialog = - prompt_for_install(&updater.clone(), &package_info.name, &body.clone(), pubkey).await; + let window_ = window.clone(); + let dialog = prompt_for_install( + window_, + &updater.clone(), + &package_info.name, + &body.clone(), + pubkey, + ) + .await; if dialog.is_err() { send_status_update( @@ -516,7 +523,8 @@ fn send_status_update(window: Window, status: &str, error: Option // Prompt a dialog asking if the user want to install the new version // Maybe we should add an option to customize it in future versions. -async fn prompt_for_install( +async fn prompt_for_install( + window: Window, updater: &self::core::Update, app_name: &str, body: &str, @@ -530,6 +538,7 @@ async fn prompt_for_install( // todo(lemarier): We should review this and make sure we have // something more conventional. ask( + Some(&window), format!(r#"A new version of {} is available! "#, app_name), format!( r#"{} {} is now available -- you have {}. @@ -552,6 +561,7 @@ Release Notes: // Ask user if we need to restart the application ask( + Some(&window), "Ready to Restart", "The installation was successful, do you want to restart the application now?", |should_exit| { diff --git a/examples/api/src-tauri/src/main.rs b/examples/api/src-tauri/src/main.rs index d4c581773..aba673c85 100644 --- a/examples/api/src-tauri/src/main.rs +++ b/examples/api/src-tauri/src/main.rs @@ -183,18 +183,24 @@ fn main() { // Triggered when a window is trying to close Event::CloseRequested { label, api, .. } => { let app_handle = app_handle.clone(); + let window = app_handle.get_window(&label).unwrap(); // use the exposed close api, and prevent the event loop to close api.prevent_close(); // ask the user if he wants to quit - ask( - "Tauri API", - "Are you sure that you want to close this window?", - move |answer| { - if answer { - app_handle.get_window(&label).unwrap().close().unwrap(); - } - }, - ); + // we need to run this on another thread because this is the event loop callback handler + // and the dialog API needs to communicate with the event loop. + std::thread::spawn(move || { + ask( + Some(&window), + "Tauri API", + "Are you sure that you want to close this window?", + move |answer| { + if answer { + app_handle.get_window(&label).unwrap().close().unwrap(); + } + }, + ); + }); } // Keep the event loop running even if all windows are closed