From 6d493c9c730449d9b048bfa934e9678420387247 Mon Sep 17 00:00:00 2001 From: FredApps Date: Wed, 14 Oct 2026 22:23:01 +0200 Subject: [PATCH] Open the active grid photo with Enter --- crates/ui-egui/src/shortcuts.rs | 179 ++++++++++++++++++++++++++++++++ docs/library-shortcuts.md | 3 + 2 files changed, 182 insertions(+) diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index b10387c76..dcc6c9427 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -357,6 +357,16 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) { // Esc that just took a text field's focus away (it gives back the field's text) does nothing else let popup_open = popup_open || app.text_focus; ctx.input(|i| { + // Enter opens the active grid photo; widgets, popups and custom bindings keep the key. + if grid + && app.session.active().is_some() + && !popup_open + && !native("Enter") + && !taken.contains(&(Modifiers::NONE, Key::Enter)) + && matches(i, Modifiers::NONE, Key::Enter) + { + fire.push("view.detail".to_string()); + } for b in bindable() { if let Some(sc) = binding(keymap, b.id, b.default) && let Some((m, k)) = parse(sc) @@ -1009,6 +1019,175 @@ mod tests { } } } + mod issue1771 { + use super::*; + use crate::state::{Dialog, ViewMode}; + use lightcraft_catalog::PhotoId; + + fn grid(view: ViewMode) -> crate::headless::Headless { + let mut h = library(); + h.app.ui.view = view; + h.app.session.execute("library.select", &json!({"ids": [1, 2, 3], "active": 2})).unwrap(); + for _ in 0..3 { + h.step(); + } + h + } + + #[test] + fn issue1771_photo_grid_enter_opens_active_preserving_multiselection() { + opens(ViewMode::PhotoGrid); + } + + #[test] + fn issue1771_square_grid_enter_opens_active_preserving_multiselection() { + opens(ViewMode::SquareGrid); + } + + fn opens(view: ViewMode) { + let mut h = grid(view); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, ViewMode::Detail, "Enter from {view:?}"); + assert_eq!(h.app.session.active(), Some(PhotoId(2))); + assert_eq!(h.app.session.selection.ids, vec![PhotoId(1), PhotoId(2), PhotoId(3)]); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, ViewMode::Detail, "second Enter remains in Detail"); + assert_eq!(h.app.session.active(), Some(PhotoId(2))); + } + + #[test] + fn issue1771_no_selection_and_modifier_chords_do_not_open() { + for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid] { + let mut h = grid(view); + for modifiers in [json!({"shift":true}), json!({"cmd":true}), json!({"alt":true}), json!({"ctrl":true})] { + let mut args = modifiers; + args["key"] = json!("Enter"); + assert_eq!(h.request("ui.key", args, std::time::Duration::from_secs(20))["ok"], true); + assert_eq!(h.app.ui.view, view); + } + h.app.session.selection = Default::default(); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, view); + assert!(h.app.session.selection.ids.is_empty()); + assert_eq!(h.app.session.active(), None); + } + } + + #[test] + fn issue1771_search_dialog_and_shortcut_recording_keep_enter() { + for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid] { + let mut h = grid(view); + assert_eq!(h.request("ui.clickWidget", json!({"id":"field:search"}), std::time::Duration::from_secs(20))["ok"], true); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, view, "focused search"); + h.app.run("app.shortcuts", json!({})).unwrap(); + h.step(); + h.step(); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, view, "shortcut dialog"); + assert!(matches!(h.app.ui.dialog, Some(Dialog::Shortcuts))); + h.app.recording_shortcut = Some("view.survey".into()); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, view, "recording owns Enter"); + assert_eq!(super::super::shortcut_of(&h.app.ui.settings.keymap, "view.survey"), Some("Enter")); + } + } + + #[test] + fn issue1771_custom_binding_and_native_menu_keep_enter() { + for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid] { + let mut h = grid(view); + h.app.run("app.setShortcut", json!({"id":"view.survey", "shortcut":"Enter"})).unwrap(); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, ViewMode::Survey, "custom binding wins"); + assert_eq!(h.app.session.active(), Some(PhotoId(2))); + assert_eq!(h.app.session.selection.ids.len(), 3); + let mut h = grid(view); + h.app.native_shortcuts.insert("Enter".into()); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, view, "native menu owns Enter"); + } + } + + #[test] + fn issue1771_popup_keeps_enter() { + for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid] { + let mut h = grid(view); + let ctx = egui::Context::default(); + let mut output = ctx.run_ui( + egui::RawInput { + events: vec![egui::Event::Key { + key: egui::Key::Enter, + physical_key: None, + pressed: true, + repeat: false, + modifiers: egui::Modifiers::NONE, + }], + ..Default::default() + }, + |ui| { + let ctx = ui.ctx(); + egui::Popup::open_id(ctx, egui::Id::new("issue1771-menu")); + assert!(egui::Popup::is_any_open(ctx)); + super::super::handle(&mut h.app, ctx); + }, + ); + output.textures_delta.clear(); + assert_eq!(h.app.ui.view, view); + } + } + + #[test] + fn issue1771_detail_enter_keeps_active_and_multiselection() { + let mut h = grid(ViewMode::Detail); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, ViewMode::Detail); + assert_eq!(h.app.session.active(), Some(PhotoId(2))); + assert_eq!(h.app.session.selection.ids, vec![PhotoId(1), PhotoId(2), PhotoId(3)]); + } + + #[test] + fn issue1771_open_file_menu_keeps_enter() { + for view in [ViewMode::PhotoGrid, ViewMode::SquareGrid] { + let mut h = grid(view); + assert_eq!(h.request("ui.clickWidget", json!({"id":"menu:File"}), std::time::Duration::from_secs(20))["ok"], true); + h.step(); + assert!(egui::Popup::is_any_open(&h.view.ctx), "File menu open before Enter"); + key(&mut h, "Enter", false); + assert_eq!(h.app.ui.view, view); + assert_eq!(h.app.session.active(), Some(PhotoId(2))); + assert_eq!(h.app.session.selection.ids.len(), 3); + } + } + + #[test] + fn issue1771_repeat_event_opens_once_and_detail_keeps_selection() { + let mut h = grid(ViewMode::PhotoGrid); + h.events.push(egui::Event::Key { + key: egui::Key::Enter, + physical_key: None, + pressed: true, + repeat: true, + modifiers: egui::Modifiers::NONE, + }); + h.step(); + h.step(); + assert_eq!(h.app.ui.view, ViewMode::Detail); + assert_eq!(h.app.session.active(), Some(PhotoId(2))); + assert_eq!(h.app.session.selection.ids.len(), 3); + h.events.push(egui::Event::Key { + key: egui::Key::Enter, + physical_key: None, + pressed: true, + repeat: true, + modifiers: egui::Modifiers::NONE, + }); + h.step(); + h.step(); + assert_eq!(h.app.ui.view, ViewMode::Detail); + assert_eq!(h.app.session.selection.ids.len(), 3); + } + } } #[cfg(test)] diff --git a/docs/library-shortcuts.md b/docs/library-shortcuts.md index ca3100fc8..e6b9af6ba 100644 --- a/docs/library-shortcuts.md +++ b/docs/library-shortcuts.md @@ -4,6 +4,7 @@ Select photos in Photo Grid or Square Grid, then use: | Key | Action | |---|---| +| `Enter` | Open the active photo in Detail from Photo Grid or Square Grid | | `Cmd+Shift+D` (macOS) / `Ctrl+Shift+D` (Windows/Linux) | Select only the active photo | | `0` | Clear the star rating | | `1`–`5` | Set the star rating | @@ -14,6 +15,8 @@ Select photos in Photo Grid or Square Grid, then use: | `U` | Clear the flag | | `Shift` + `0`–`9`, `P`, `X` or `U` | Apply the action and select the next photo | +Enter opens the active photo without changing the selection. Text fields, dialogs and open popups keep the key; a custom Enter binding takes precedence. Enter has no additional action in Detail. + Select Only Active Photo (Edit menu) narrows the selection to the active photo. It does not change the photo, its edits or the last edit Undo step, and it works in any view that has an active photo. It can be reassigned or disabled in Help → Keyboard Shortcuts; text fields and dialogs keep the key.