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

This commit is contained in:
Lucas Fernandes Nogueira 2021-08-16 17:25:45 -03:00 committed by GitHub
parent b0a8c38a73
commit c76f4b7d39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 149 additions and 69 deletions

View File

@ -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.

View File

@ -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 }

View File

@ -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)
}

View File

@ -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)
}),

View File

@ -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())
}

View File

@ -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| {

View File

@ -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| {

View File

@ -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