Skip to content

Commit

Permalink
Bugfix: Fail to dismiss the Message Dialog merely in MacOS (#223)
Browse files Browse the repository at this point in the history
* 1. Bug phenomenon: Only in MacOS, the Message Dialog originating from the command-line context fails to dismissing itself posterior to click the button "OK".
2. Solution: If the parent-window handle is absent during the initialization of the MessageDialog instance, the ABI `CFUserNotificationDisplayAlert` will be invoked to pop up a message dialog, instead of instantiate a NSAlert instance.
3. Ramification: Only message dialog for MacOS
4. Unit Test: cover both sync and async message dialogs and run the unit test by `cargo run --example msg`

* 删除无用的 pub 可见性标记

* @PolyMeilex is right. Accordingly, I made the following enhancements:
1. Remove the conditional dependency on the `futures` crate.
2. Relinquish the heavy thread-pool instrument.
3. Spawn a background thread by means of the std lib.

* delete the temporary variable `win` by moving `self.parent.as_ref()` into `ModalFuture::new(..)`

* Add a new conditional branch for awaiting timeout.
  • Loading branch information
stuartZhang authored Nov 23, 2024
1 parent 5cb7002 commit c7c9537
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 17 deletions.
30 changes: 29 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ objc2-app-kit = { version = "0.2.0", features = [
"NSView",
"NSWindow",
] }
deferred-future = "0.1.5"
core-foundation = "0.10.0"
core-foundation-sys = "0.8.7"

[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.48", features = [
Expand Down
39 changes: 35 additions & 4 deletions examples/msg.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use ::std::io::{ Read, self };
use ::futures::executor::block_on;
fn main() {
#[cfg(not(feature = "gtk3"))]
let res = "";

#[cfg(any(
target_os = "windows",
target_os = "macos",
Expand All @@ -20,7 +21,37 @@ fn main() {
.set_title("Msg!")
.set_description("Description!")
.set_buttons(rfd::MessageButtons::OkCancel)
.set_level(rfd::MessageLevel::Error)
.show();

println!("{}", res);
}
println!("被点击按钮是 {}。敲击 Ctrl+D 继续。", res);
let mut stdin = io::stdin();
let mut buffer: Vec<u8> = vec![];
stdin.read_to_end(&mut buffer).unwrap();
#[cfg(any(
target_os = "windows",
target_os = "macos",
all(
any(
target_os = "linux",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "netbsd",
target_os = "openbsd"
),
feature = "gtk3"
)
))]
block_on(async move {
let res = rfd::AsyncMessageDialog::new()
.set_title("Msg!")
.set_description("Description!")
.set_buttons(rfd::MessageButtons::OkCancel)
.show().await;
println!("被点击按钮是 {}", res);
});
println!("敲击 Ctrl+D 继续。");
let mut stdin = io::stdin();
let mut buffer: Vec<u8> = vec![];
stdin.read_to_end(&mut buffer).unwrap();
println!("结束");
}
27 changes: 17 additions & 10 deletions src/backend/macos/message_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::message_dialog::{MessageButtons, MessageDialog, MessageDialogResult,
use super::modal_future::AsModal;
use super::{
modal_future::{InnerModal, ModalFuture},
utils::{run_on_main, FocusManager, PolicyManager},
utils::{run_on_main, FocusManager, PolicyManager, self},
};

use super::utils::window_from_raw_window_handle;
Expand Down Expand Up @@ -155,21 +155,28 @@ impl InnerModal for NSAlert {
use crate::backend::MessageDialogImpl;
impl MessageDialogImpl for MessageDialog {
fn show(self) -> MessageDialogResult {
autoreleasepool(move |_| run_on_main(move |mtm| Alert::new(self, mtm).run()))
autoreleasepool(move |_| run_on_main(move |mtm| {
if self.parent.is_none() {
utils::sync_pop_dialog(self, mtm)
} else {
Alert::new(self, mtm).run()
}
}))
}
}

use crate::backend::AsyncMessageDialogImpl;

impl AsyncMessageDialogImpl for MessageDialog {
fn show_async(self) -> DialogFutureType<MessageDialogResult> {
let win = self.parent.as_ref().map(window_from_raw_window_handle);

let future = ModalFuture::new(
win,
move |mtm| Alert::new(self, mtm),
|dialog, ret| dialog_result(&dialog.buttons, ret),
);
Box::pin(future)
if self.parent.is_none() {
utils::async_pop_dialog(self)
} else {
Box::pin(ModalFuture::new(
self.parent.as_ref().map(window_from_raw_window_handle),
move |mtm| Alert::new(self, mtm),
|dialog, ret| dialog_result(&dialog.buttons, ret),
))
}
}
}
6 changes: 4 additions & 2 deletions src/backend/macos/utils.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod focus_manager;
mod policy_manager;
mod user_alert;

pub use self::focus_manager::FocusManager;
pub use self::policy_manager::PolicyManager;
pub use focus_manager::FocusManager;
pub use policy_manager::PolicyManager;
pub use user_alert::{ async_pop_dialog, sync_pop_dialog };

use objc2::rc::Id;
use objc2_app_kit::{NSApplication, NSView, NSWindow};
Expand Down
114 changes: 114 additions & 0 deletions src/backend/macos/utils/user_alert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use ::objc2_foundation::MainThreadMarker;
use ::deferred_future::ThreadDeferredFuture;
use ::std::{ mem::MaybeUninit, ptr, sync::PoisonError, thread };
use ::core_foundation::{ base::TCFType, string::CFString };
use ::core_foundation_sys::{ base::CFOptionFlags, date::CFTimeInterval, url::CFURLRef, user_notification::{ CFUserNotificationDisplayAlert, kCFUserNotificationStopAlertLevel, kCFUserNotificationCautionAlertLevel, kCFUserNotificationNoteAlertLevel, kCFUserNotificationDefaultResponse, kCFUserNotificationAlternateResponse, kCFUserNotificationOtherResponse, kCFUserNotificationCancelResponse } };
use crate::{
message_dialog::{ MessageButtons, MessageDialog, MessageDialogResult, MessageLevel },
backend::{ DialogFutureType, macos::utils::{ FocusManager, PolicyManager } }
};
struct UserAlert {
timeout: CFTimeInterval,
flags: CFOptionFlags,
icon_url: CFURLRef,
sound_url: CFURLRef,
localization_url: CFURLRef,
alert_header: String,
alert_message: String,
default_button_title: Option<String>,
alternate_button_title: Option<String>,
other_button_title: Option<String>,
buttons: MessageButtons,
_focus_manager: Option<FocusManager>,
_policy_manager: Option<PolicyManager>,
}
impl UserAlert {
fn new(opt: MessageDialog, mtm: Option<MainThreadMarker>) -> Self {
let mut buttons: [Option<String>; 3] = match &opt.buttons {
MessageButtons::Ok => [None, None, None],
MessageButtons::OkCancel => [None, Some("Cancel".to_string()), None],
MessageButtons::YesNo => [Some("Yes".to_string()), Some("No".to_string()), None],
MessageButtons::YesNoCancel => [Some("Yes".to_string()), Some("No".to_string()), Some("Cancel".to_string())],
MessageButtons::OkCustom(ok_text) => [Some(ok_text.to_string()), None, None],
MessageButtons::OkCancelCustom(ok_text, cancel_text) => [Some(ok_text.to_string()), Some(cancel_text.to_string()), None],
MessageButtons::YesNoCancelCustom(yes_text, no_text, cancel_text) => [Some(yes_text.to_string()), Some(no_text.to_string()), Some(cancel_text.to_string())]
};
UserAlert {
timeout: 0_f64,
icon_url: ptr::null(),
sound_url: ptr::null(),
localization_url: ptr::null(),
flags: match opt.level {
MessageLevel::Info => kCFUserNotificationNoteAlertLevel,
MessageLevel::Warning => kCFUserNotificationCautionAlertLevel,
MessageLevel::Error => kCFUserNotificationStopAlertLevel
},
alert_header: opt.title,
alert_message: opt.description,
default_button_title: buttons[0].take(),
alternate_button_title: buttons[1].take(),
other_button_title: buttons[2].take(),
buttons: opt.buttons,
_policy_manager: mtm.map(|mtm| PolicyManager::new(mtm)),
_focus_manager: mtm.map(|mtm| FocusManager::new(mtm))
}
}
fn run(self) -> MessageDialogResult {
let alert_header = CFString::new(&self.alert_header[..]);
let alert_message = CFString::new(&self.alert_message[..]);
let default_button_title = self.default_button_title.map(|string| CFString::new(&string[..]));
let alternate_button_title = self.alternate_button_title.map(|value| CFString::new(&value[..]));
let other_button_title = self.other_button_title.map(|value| CFString::new(&value[..]));
let mut response_flags = MaybeUninit::<CFOptionFlags>::uninit();
let is_cancel = unsafe { CFUserNotificationDisplayAlert(
self.timeout,
self.flags,
self.icon_url,
self.sound_url,
self.localization_url,
alert_header.as_concrete_TypeRef(),
alert_message.as_concrete_TypeRef(),
default_button_title.map_or(ptr::null(), |value| value.as_concrete_TypeRef()),
alternate_button_title.map_or(ptr::null(), |value| value.as_concrete_TypeRef()),
other_button_title.map_or(ptr::null(), |value| value.as_concrete_TypeRef()),
response_flags.as_mut_ptr()
) };
if is_cancel != 0 {
return MessageDialogResult::Cancel;
}
let response = unsafe { response_flags.assume_init() };
if response == kCFUserNotificationCancelResponse {
return MessageDialogResult::Cancel;
}
match self.buttons {
MessageButtons::Ok if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Ok,
MessageButtons::OkCancel if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Ok,
MessageButtons::OkCancel if response == kCFUserNotificationAlternateResponse => MessageDialogResult::Cancel,
MessageButtons::YesNo if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Yes,
MessageButtons::YesNo if response == kCFUserNotificationAlternateResponse => MessageDialogResult::No,
MessageButtons::YesNoCancel if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Yes,
MessageButtons::YesNoCancel if response == kCFUserNotificationAlternateResponse => MessageDialogResult::No,
MessageButtons::YesNoCancel if response == kCFUserNotificationOtherResponse => MessageDialogResult::Cancel,
MessageButtons::OkCustom(custom) if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Custom(custom.to_owned()),
MessageButtons::OkCancelCustom(custom, _) if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Custom(custom.to_owned()),
MessageButtons::OkCancelCustom(_, custom) if response == kCFUserNotificationAlternateResponse => MessageDialogResult::Custom(custom.to_owned()),
MessageButtons::YesNoCancelCustom(custom, _, _) if response == kCFUserNotificationDefaultResponse => MessageDialogResult::Custom(custom.to_owned()),
MessageButtons::YesNoCancelCustom(_, custom, _) if response == kCFUserNotificationAlternateResponse => MessageDialogResult::Custom(custom.to_owned()),
MessageButtons::YesNoCancelCustom(_, _, custom) if response == kCFUserNotificationOtherResponse => MessageDialogResult::Custom(custom.to_owned()),
_ => MessageDialogResult::Cancel,
}
}
}
pub fn sync_pop_dialog(opt: MessageDialog, mtm: MainThreadMarker) -> MessageDialogResult {
UserAlert::new(opt, Some(mtm)).run()
}
pub fn async_pop_dialog(opt: MessageDialog) -> DialogFutureType<MessageDialogResult> {
let deferred_future = ThreadDeferredFuture::default();
let defer = deferred_future.defer();
thread::spawn(move || {
let mut defer = defer.lock().unwrap_or_else(PoisonError::into_inner);
let message_dialog_result = UserAlert::new(opt.clone(), None).run();
defer.complete(message_dialog_result);
});
Box::pin(deferred_future)
}

0 comments on commit c7c9537

Please sign in to comment.