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).
This commit is contained in:
eliets2
2026-10-13 15:16:46 +03:00
parent b3d392b499
commit 4dfd78a8a8
3 changed files with 51 additions and 2 deletions
+2 -2
View File
@@ -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
+9
View File
@@ -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)| {
+40
View File
@@ -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::<i64>().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) };