From 4dfd78a8a86582b612f55ab825d63e13db669fc1 Mon Sep 17 00:00:00 2001 From: eliets2 <287273517+eliets2@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:16:46 +0300 Subject: [PATCH] Library grids: Home/End and Shift+Left/Right selection keys; Cmd+O and Shift+H In Photo Grid and Square Grid, Home and End make the first or last photo of the view the active, selected one, and Shift+Left / Shift+Right add the previous or next photo of the view to the selection. These keys wait while the full-screen preview covers the grid, in every other view, while a text field or a dialog has the keyboard, and when the user gave the key to a command. They are listed under Fixed keys in the keyboard shortcuts dialog. Open Library now has Cmd+O and Visualize HDR has Shift+H (Cmd+Shift+H stays the Histogram). --- crates/ui-egui/src/menus.rs | 4 +-- crates/ui-egui/src/panels/keymap.rs | 9 +++++++ crates/ui-egui/src/shortcuts.rs | 40 +++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 141d6d782..8bf3f0a8f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -87,7 +87,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("view.zoomIn", "Zoom In", Some("Cmd+="), "View"), ("view.zoomOut", "Zoom Out", Some("Cmd+-"), "View"), ("view.clipping", "Show Clipping", Some("J"), "View"), - ("view.visualizeHdr", "Visualize HDR", None, "View"), + ("view.visualizeHdr", "Visualize HDR", Some("Shift+H"), "View"), // in grids S expands/collapses stacks (the engine command it shadows) ("view.softProof", "Soft Proofing", Some("S"), "View"), ("view.histogram", "Histogram", Some("Cmd+Shift+H"), "View"), @@ -196,7 +196,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ ("file.exportCurvePresets", "Export Point Curve Presets…", None, ""), ("app.settings", "Settings…", Some("Cmd+,"), "Edit"), ("app.newLibrary", "New Library…", None, "File"), - ("app.openLibrary", "Open Library…", None, "File"), + ("app.openLibrary", "Open Library…", Some("Cmd+O"), "File"), ("app.recentLibraries", "Recent Libraries", None, ""), ("app.clearRecentLibraries", "Clear Recent Libraries", None, ""), // Settings ▸ Display diff --git a/crates/ui-egui/src/panels/keymap.rs b/crates/ui-egui/src/panels/keymap.rs index a844b7b24..1bda17758 100644 --- a/crates/ui-egui/src/panels/keymap.rs +++ b/crates/ui-egui/src/panels/keymap.rs @@ -151,6 +151,15 @@ fn fixed_keys(app: &LightcraftApp, ui: &mut egui::Ui, t: &Tokens, mac: bool, nee for (sc, label) in [("6", "Red label"), ("7", "Yellow label"), ("8", "Green label"), ("9", "Blue label")] { fixed.push((sc.to_string(), label)); } + // grid navigation (Photo Grid and Square Grid): fixed keys that belong to no single command + for (sc, label) in [ + ("Home", "Grid: first photo of the view"), + ("End", "Grid: last photo of the view"), + ("Shift+Left", "Grid: add the previous photo to the selection"), + ("Shift+Right", "Grid: add the next photo to the selection"), + ] { + fixed.push((sc.to_string(), label)); + } let fixed: Vec<_> = fixed .into_iter() .filter(|(sc, label)| { diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 1a21ab2fc..aa21af98e 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -349,6 +349,9 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) { let mut aliased: Vec<(&str, serde_json::Value)> = Vec::new(); let keymap = &app.ui.settings.keymap; let grid = library_grid(app); + // The grid navigation keys act on the photos in view, so they wait while the full-screen preview + // covers the grid: a key must not move the selection onto photos nobody can see. + let grid_navigation = grid && !app.ui.fullscreen; // keys the user gave to a command: the fixed bindings below (aliases, ratings) yield to them let taken: Vec<(Modifiers, Key)> = keymap.values().filter_map(|s| parse(s)).collect(); // an open popup (a menu, a date picker's calendar) closes on Esc itself: Esc's command (Back, @@ -391,6 +394,20 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) { fire.push(format!("label:{label}")); } } + if grid_navigation { + // Home / End: the first / last photo of the view + for (key, id) in [(Key::Home, "grid:first"), (Key::End, "grid:last")] { + if matches(i, Modifiers::NONE, key) && !taken.contains(&(Modifiers::NONE, key)) { + fire.push(id.to_string()); + } + } + // Shift+← / Shift+→: the previous / next photo of the view joins the selection + for (key, step) in [(Key::ArrowLeft, -1i64), (Key::ArrowRight, 1)] { + if matches(i, Modifiers::SHIFT, key) && !taken.contains(&(Modifiers::SHIFT, key)) { + fire.push(format!("grid:extend:{step}")); + } + } + } }); use crate::panels::compare; // rating/flag/label keys: in Compare/Survey they act on the active photo only; Shift+key or @@ -448,6 +465,29 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) { } else if f == "view.softProof" && matches!(app.ui.view, crate::state::ViewMode::PhotoGrid | crate::state::ViewMode::SquareGrid) { // S in a grid: expand / collapse the stack (Lightroom's Library binding) let _ = app.run("stack.toggle", json!({})); + } else if f == "grid:first" || f == "grid:last" { + // Home / End in a grid: make the first / last photo of the view the active, selected one + let vis = app.session.visible_cloned(); + let edge = if f == "grid:first" { vis.first() } else { vis.last() }; + if let Some(id) = edge { + let _ = app.run("library.select", json!({"ids": [id.0], "active": id.0})); + } + } else if let Some(step) = f.strip_prefix("grid:extend:") { + // Shift+← / Shift+→ in a grid: the neighbour of the active photo joins the selection and + // becomes the active one. At either end of the view the key does nothing. + let step = step.parse::().unwrap_or(0); + let vis = app.session.visible_cloned(); + let neighbour = app + .session + .active() + .and_then(|active| vis.iter().position(|p| *p == active)) + .and_then(|pos| i64::try_from(pos).ok()) + .and_then(|pos| pos.checked_add(step)) + .and_then(|pos| usize::try_from(pos).ok()) + .and_then(|pos| vis.get(pos)); + if let Some(id) = neighbour { + let _ = app.run("library.select", json!({"ids": [id.0], "mode": "add"})); + } } else if compare::culling(app) && (f == "library.next" || f == "library.previous") { let d = if f == "library.next" { 1 } else { -1 }; let _ = if app.ui.view == crate::state::ViewMode::Compare { compare::compare_step(app, d) } else { compare::survey_step(app, d) };