浏览代码

feat(core): set parent window on ask and message dialog APIs (#2454)

Lucas Fernandes Nogueira 4 年之前
父节点
当前提交
c76f4b7d39

+ 5 - 0
.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.

+ 1 - 1
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 }

+ 88 - 21
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<R: Runtime>(window: &Window<R>) -> crate::Result<WindowParent> {
+  #[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<W: raw_window_handle::HasRawWindowHandle>(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<F: FnOnce(bool) + Send + 'static>(
+#[allow(unused_variables)]
+pub fn ask<R: Runtime, F: FnOnce(bool) + Send + 'static>(
+  parent_window: Option<&Window<R>>,
   title: impl AsRef<str>,
   message: impl AsRef<str>,
   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<str>, message: impl AsRef<str>) {
+#[allow(unused_variables)]
+pub fn message<R: Runtime>(
+  parent_window: Option<&Window<R>>,
+  title: impl AsRef<str>,
+  message: impl AsRef<str>,
+) {
   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)
 }

+ 1 - 1
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)
       }),

+ 16 - 30
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<R: Runtime>(self, window: Window<R>) -> crate::Result<InvokeResponse> {
     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<R: Runtime>(window: Window<R>) -> crate::Result<WindowParent> {
-  Ok(WindowParent {
-    hwnd: window.hwnd()?,
-  })
-}
-
 /// Shows an open dialog.
 #[cfg(dialog_open)]
 #[allow(unused_variables)]
 pub fn open<R: Runtime>(
-  window: Window<R>,
+  window: &Window<R>,
   options: OpenDialogOptions,
 ) -> crate::Result<InvokeResponse> {
   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<R: Runtime>(
   options: SaveDialogOptions,
 ) -> crate::Result<InvokeResponse> {
   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<R: Runtime>(
 }
 
 /// Shows a dialog with a yes/no question.
-pub fn ask(title: String, message: String) -> crate::Result<InvokeResponse> {
+pub fn ask<R: Runtime>(
+  window: &Window<R>,
+  title: String,
+  message: String,
+) -> crate::Result<InvokeResponse> {
   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())
 }

+ 10 - 4
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<R: Runtime>(
     self,
+    window: Window<R>,
     config: Arc<Config>,
     package_info: &PackageInfo,
   ) -> crate::Result<InvokeResponse> {
@@ -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<String> {
+pub fn request_permission<R: Runtime>(
+  window: &Window<R>,
+  config: &Config,
+  package_info: &PackageInfo,
+) -> crate::Result<String> {
   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| {

+ 13 - 3
core/tauri/src/updater/mod.rs

@@ -394,8 +394,15 @@ pub(crate) async fn check_update_with_dialog<R: Runtime>(
         // 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<R: Runtime>(window: Window<R>, 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<R: Runtime>(
+  window: Window<R>,
   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| {

+ 15 - 9
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