mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-01 22:56:54 -06:00
Merge PR #3083
This commit is contained in:
@@ -52,6 +52,10 @@ muda = { version = "0.21", default-features = false }
|
||||
# by PdfCraft and PhotoCraft) registers the handler (Objective-C interop needs unsafe, which our
|
||||
# workspace forbids) behind a safe API. Pinned exactly: review the diff before bumping.
|
||||
fmv-macos-events = "=0.1.0"
|
||||
# apple_events.rs brings the app forward for a Dock quit's question. Already in Cargo.lock (muda,
|
||||
# winit); pinned exactly to the same versions.
|
||||
objc2 = "=0.6.5"
|
||||
objc2-app-kit = { version = "=0.3.2", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
|
||||
|
||||
# tests/mac_menu_appkit.rs: reads AppKit's menu bar back. Already in Cargo.lock (muda, winit);
|
||||
# pinned exactly to the same versions.
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
|
||||
use fmv_macos_events::{Event, Inbox, Registration};
|
||||
use gridcraft_ui_egui::SheetApp;
|
||||
use objc2::MainThreadMarker;
|
||||
use objc2_app_kit::{NSApplication, NSRequestUserAttentionType};
|
||||
|
||||
/// Keeps the Apple-event handlers registered; hold it until the event loop returns.
|
||||
pub struct AppleEvents {
|
||||
@@ -44,7 +46,32 @@ pub fn poll(inbox: &Inbox, app: &mut SheetApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
// Quit from the Dock or the system: each unsaved workbook is asked about in its window.
|
||||
Event::Quit => app.request_quit(ctx),
|
||||
Event::Quit => {
|
||||
app.request_quit(ctx);
|
||||
if app.dialog.is_some() {
|
||||
bring_app_forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Dock quit reaches GridCraft behind other apps, hidden or minimized, and its question has to
|
||||
/// be seen. egui's Focus goes through winit, which skips a minimized window or a hidden app, and
|
||||
/// macOS may refuse an activation it didn't expect, so ask AppKit directly and bounce the Dock
|
||||
/// icon until the user switches over.
|
||||
fn bring_app_forward() {
|
||||
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||
let ns_app = NSApplication::sharedApplication(mtm);
|
||||
ns_app.unhide(None);
|
||||
for window in ns_app.windows().to_vec() {
|
||||
if window.isMiniaturized() {
|
||||
window.deminiaturize(None);
|
||||
}
|
||||
}
|
||||
#[allow(deprecated)] // `activate` needs macOS 14; this works on every supported version.
|
||||
ns_app.activateIgnoringOtherApps(true);
|
||||
if !ns_app.isActive() {
|
||||
ns_app.requestUserAttention(NSRequestUserAttentionType::CriticalRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ impl SheetApp {
|
||||
macros: macro_ui::MacroUi::default(),
|
||||
started: now_ms(),
|
||||
allow_close: false,
|
||||
bring_forward: false,
|
||||
open_switch_windows: false,
|
||||
ui_window: None,
|
||||
parked: HashMap::new(),
|
||||
|
||||
@@ -42,6 +42,7 @@ impl SheetApp {
|
||||
self.enter_window(window);
|
||||
let title = self.session.active().map(|d| d.display_title()).unwrap_or_default();
|
||||
self.open_dialog("saveChanges", json!({"title": title, "quit": true}));
|
||||
self.bring_forward = true;
|
||||
}
|
||||
None => {
|
||||
self.allow_close = true;
|
||||
|
||||
@@ -160,6 +160,15 @@ impl DocWindows {
|
||||
self.list.focus(id);
|
||||
self.viewports.focus(ctx, id);
|
||||
}
|
||||
// macOS ignores Focus for a minimized window or a hidden app, so restore and show first.
|
||||
if std::mem::take(&mut app.bring_forward)
|
||||
&& let Some(id) = self.list.active()
|
||||
{
|
||||
let vp = self.viewports.viewport_id(id);
|
||||
ctx.send_viewport_cmd_to(vp, egui::ViewportCommand::Visible(true));
|
||||
ctx.send_viewport_cmd_to(vp, egui::ViewportCommand::Minimized(false));
|
||||
ctx.send_viewport_cmd_to(vp, egui::ViewportCommand::Focus);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws every window. Call from the root viewport's UI each frame, in place of
|
||||
|
||||
@@ -284,6 +284,9 @@ pub struct SheetApp {
|
||||
/// Every unsaved workbook has been answered for quitting (or `app.quit` asked): the next
|
||||
/// window close goes through.
|
||||
pub allow_close: bool,
|
||||
/// A quit question is waiting in the active window: [`doc_windows`] brings its native window
|
||||
/// to the front, also when the app is hidden or the window minimized (a Dock quit).
|
||||
pub bring_forward: bool,
|
||||
/// View › Switch Windows opens its list on the next frame the View tab draws (asked for by a
|
||||
/// key tip, the command search or a menu, possibly while another tab was showing).
|
||||
pub(crate) open_switch_windows: bool,
|
||||
|
||||
@@ -232,6 +232,23 @@ fn quit_asks_about_each_unsaved_workbook_in_its_window_and_cancel_stops() {
|
||||
assert!(sent_to(&sent, ViewportId::ROOT, &ViewportCommand::Close), "both answered: the app quits: {sent:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_quit_question_brings_its_window_to_the_front_from_the_background() {
|
||||
// A Dock quit reaches the app while it is behind other apps, hidden or minimized.
|
||||
let mut desk = Desk::new(true);
|
||||
desk.app.run("cell.set", json!({"cell": "A1", "input": "1"})).unwrap();
|
||||
desk.frames(3);
|
||||
let w = desk.window_of(0);
|
||||
|
||||
let sent = desk.frame_with(|app, ctx| app.request_quit(ctx));
|
||||
assert_eq!(desk.app.dialog.as_ref().map(|d| d.name()), Some("saveChanges"));
|
||||
for cmd in [ViewportCommand::Visible(true), ViewportCommand::Minimized(false), ViewportCommand::Focus] {
|
||||
assert!(sent_to(&sent, desk.viewport(w), &cmd), "{cmd:?} missing: {sent:?}");
|
||||
}
|
||||
let sent = desk.frames(2);
|
||||
assert!(!sent_to(&sent, desk.viewport(w), &ViewportCommand::Focus), "only once: {sent:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_root_adopts_the_remaining_window_with_its_workbook_and_state() {
|
||||
let mut desk = Desk::new(true);
|
||||
|
||||
Reference in New Issue
Block a user