mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
Fix #1354: the macOS menu bar is modal under dialogs, like shortcuts
Native menu clicks went straight to menus::invoke, so a menu command ran beneath an open dialog: rotating the canvas under Image Size left the dialog's stale size to be applied on OK. While a dialog, the unsaved changes prompt or Camera Raw is open, the native rows are now disabled and a click that raced the update is dropped; Zoom In/Out, Fit on Screen and 100% stay live, as their shortcuts do. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
This commit is contained in:
1 file changed
+53
-1
@@ -647,6 +647,17 @@ impl NativeMenu {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialogs, the unsaved-changes prompt and Camera Raw are modal, as they are for shortcuts
|
||||
/// ([`crate::shortcuts::handle`]): while one is open, only the view navigation Photoshop keeps
|
||||
/// live under a dialog may run from the menu bar. A rotation under Image Size used to leave the
|
||||
/// dialog's stale size to be applied on OK (#1354).
|
||||
fn modal_allows(app: &PhotocraftApp, id: &str) -> bool {
|
||||
if app.camera_raw.is_some() {
|
||||
return false;
|
||||
}
|
||||
(app.ui.dialogs.is_empty() && app.discard.is_none()) || crate::shortcuts::NAV_COMMANDS.contains(&id)
|
||||
}
|
||||
|
||||
/// Run the native menu clicks that arrived since the last frame, like in-window menu clicks.
|
||||
pub fn run(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let Some(menu) = app.services.native_menu.as_mut() else { return };
|
||||
@@ -656,6 +667,10 @@ pub fn run(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true));
|
||||
continue;
|
||||
}
|
||||
// The rows are disabled while a dialog is open; a click that raced the update is dropped.
|
||||
if !modal_allows(app, &id) {
|
||||
continue;
|
||||
}
|
||||
if let Err(e) = crate::menus::invoke(app, ctx, &id, serde_json::json!({})) {
|
||||
app.ui.status = e;
|
||||
app.ui.status_error = true;
|
||||
@@ -674,7 +689,11 @@ pub fn sync(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
return;
|
||||
}
|
||||
let lang = crate::i18n::current();
|
||||
let layout = photocraft_layout(&crate::menus::menu_items(app), lang, &app.session.prefs().interface.language);
|
||||
let mut items = crate::menus::menu_items(app);
|
||||
for it in &mut items {
|
||||
it.enabled &= modal_allows(app, &it.id);
|
||||
}
|
||||
let layout = photocraft_layout(&items, lang, &app.session.prefs().interface.language);
|
||||
let Some(menu) = app.services.native_menu.as_mut() else { return };
|
||||
if !menu.logged {
|
||||
for c in &layout.clashes {
|
||||
@@ -702,6 +721,7 @@ fn state_hash(app: &PhotocraftApp, input: Option<u64>) -> u64 {
|
||||
}
|
||||
let ui = &app.ui;
|
||||
(&ui.recent_files, &ui.workspace, ui.palette_open, ui.transform.is_some(), ui.text_edit.is_some(), ui.dialogs.len()).hash(&mut h);
|
||||
(app.discard.is_some(), app.camera_raw.is_some()).hash(&mut h);
|
||||
serde_json::to_string(&(&ui.panels, &ui.extras, &ui.view, ui.theme, ui.tool)).unwrap_or_default().hash(&mut h);
|
||||
s.prefs().interface.language.hash(&mut h);
|
||||
h.finish()
|
||||
@@ -958,6 +978,38 @@ mod tests {
|
||||
assert_eq!(bar.find("window.panel.layers").unwrap().checked, Some(!before));
|
||||
}
|
||||
|
||||
/// #1354: a dialog is modal for the menu bar too. Its rows are disabled (view navigation
|
||||
/// aside), and a click that arrives anyway doesn't run under the dialog.
|
||||
#[test]
|
||||
fn an_open_dialog_blocks_menu_clicks_except_view_navigation() {
|
||||
let synced = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
|
||||
let mut app = app(true);
|
||||
let ctx = egui::Context::default();
|
||||
crate::menus::invoke(&mut app, &ctx, "image.imageSize", json!({})).unwrap();
|
||||
assert!(!app.ui.dialogs.is_empty(), "Image Size opened its dialog");
|
||||
let size = |app: &PhotocraftApp| app.session.active().map(|d| (d.doc.size.width, d.doc.size.height));
|
||||
let zoom = app.ui.views[0].zoom;
|
||||
let events = vec![Event::Click("image.imageRotation.90cw".into()), Event::Click("view.zoomIn".into())];
|
||||
app.services.native_menu = Some(NativeMenu::new(Box::new(Fake { synced: synced.clone(), events })));
|
||||
let mut raw = egui::RawInput::default();
|
||||
if let Some(m) = app.services.native_menu.as_mut() {
|
||||
m.raw_input(&mut raw);
|
||||
}
|
||||
frame(&ctx, raw, |ui| {
|
||||
run(&mut app, ui.ctx());
|
||||
sync(&mut app, ui.ctx());
|
||||
});
|
||||
assert_eq!(size(&app), Some((64, 48)), "the rotation didn't run under the dialog");
|
||||
assert!(app.ui.views[0].zoom > zoom, "Zoom In still works under a dialog");
|
||||
let bar = synced.borrow().last().cloned().unwrap();
|
||||
assert!(!bar.find("image.imageRotation.90cw").unwrap().enabled);
|
||||
assert!(bar.find("view.zoomIn").unwrap().enabled);
|
||||
// With the dialog closed the rows come back and a click runs.
|
||||
app.ui.dialogs.clear();
|
||||
frame(&ctx, egui::RawInput::default(), |ui| sync(&mut app, ui.ctx()));
|
||||
assert!(synced.borrow().last().unwrap().find("image.imageRotation.90cw").unwrap().enabled);
|
||||
}
|
||||
|
||||
/// With the Mac menu bar the title bar draws no menu titles: each title shows once fewer.
|
||||
#[test]
|
||||
fn the_title_bar_hides_its_menus_with_the_mac_menu_bar() {
|
||||
|
||||
Reference in new issue
Block a user