mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #1555
# Conflicts: # docs/gaps.md # docs/library-shortcuts.md # docs/parity-checklist.md # docs/ui-parity.md
This commit is contained in:
@@ -95,7 +95,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
("view.thumbSizeUp", "Increase Grid Thumbnail Size", Some("="), "View"),
|
||||
("view.thumbSizeDown", "Decrease Grid Thumbnail Size", Some("-"), "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"),
|
||||
@@ -228,7 +228,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
|
||||
|
||||
@@ -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)| {
|
||||
|
||||
@@ -362,6 +362,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,
|
||||
@@ -414,6 +417,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
|
||||
@@ -474,6 +491,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) };
|
||||
|
||||
@@ -237,3 +237,236 @@ fn opening_numeric_release_keeps_keys_in_the_editor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Press `key` with the named modifiers held (`cmd`, `shift`, `alt`, `ctrl`).
|
||||
fn press(h: &mut Headless, key: &str, mods: &[&str]) {
|
||||
let mut p = json!({ "key": key });
|
||||
for m in mods {
|
||||
p[*m] = json!(true);
|
||||
}
|
||||
let r = h.request("ui.key", p, T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
}
|
||||
|
||||
fn view(h: &mut Headless, p: serde_json::Value) {
|
||||
let r = h.request("ui.set", p, T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
|
||||
/// Make `id` the only selected, active photo.
|
||||
fn select(h: &mut Headless, id: lightcraft_catalog::PhotoId) {
|
||||
let r = h.request("engine.execute", json!({"command": "library.select", "params": {"ids": [id.0], "active": id.0}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
|
||||
/// The demo library in Photo Grid with its middle photo selected: the photos in view, and that one.
|
||||
fn grid_demo() -> (Headless, Vec<lightcraft_catalog::PhotoId>, lightcraft_catalog::PhotoId) {
|
||||
let mut h = demo();
|
||||
view(&mut h, json!({ "view": "photoGrid" }));
|
||||
let vis = h.app.session.visible_cloned();
|
||||
assert!(vis.len() > 2, "{vis:?}");
|
||||
let middle = vis[vis.len() / 2];
|
||||
select(&mut h, middle);
|
||||
(h, vis, middle)
|
||||
}
|
||||
|
||||
/// Count how often the folder picker is asked for; every ask is cancelled.
|
||||
fn count_folder_picks(h: &mut Headless) -> std::sync::Arc<std::sync::atomic::AtomicUsize> {
|
||||
let calls = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let observed = calls.clone();
|
||||
h.app.services.pick_folder = Some(Box::new(move || {
|
||||
observed.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
None
|
||||
}));
|
||||
calls
|
||||
}
|
||||
|
||||
/// The declared keys of the two commands these tests bind.
|
||||
#[test]
|
||||
fn open_library_and_visualize_hdr_keys_are_declared() {
|
||||
use crate::shortcuts::{Keymap, parse, shortcut_of};
|
||||
|
||||
let keymap = Keymap::new();
|
||||
for (id, want) in [("view.visualizeHdr", "Shift+H"), ("app.openLibrary", "Cmd+O")] {
|
||||
assert_eq!(shortcut_of(&keymap, id), Some(want), "{id}");
|
||||
assert!(parse(want).is_some(), "{want} does not parse");
|
||||
}
|
||||
}
|
||||
|
||||
/// Opening a library uses the same folder picker as File > Open Library. Cancelling it
|
||||
/// leaves the current photo and library intact.
|
||||
#[test]
|
||||
fn cmd_o_opens_the_library_picker_once() {
|
||||
let mut h = demo();
|
||||
let calls = count_folder_picks(&mut h);
|
||||
let active = h.app.session.active();
|
||||
let photos = h.app.session.catalog.len();
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1, "one picker for Cmd+O");
|
||||
assert_eq!(h.app.session.active(), active);
|
||||
assert_eq!(h.app.session.catalog.len(), photos);
|
||||
}
|
||||
|
||||
/// Shift+H toggles View > Visualize HDR; Cmd+Shift+H (Histogram) is untouched.
|
||||
#[test]
|
||||
fn shift_h_toggles_the_hdr_view() {
|
||||
let mut h = demo();
|
||||
assert!(!h.app.ui.hdr_visualize);
|
||||
let histogram = h.app.ui.histogram;
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(h.app.ui.hdr_visualize, "Shift+H turns Visualize HDR on");
|
||||
assert_eq!(h.app.ui.histogram, histogram, "Shift+H leaves the Histogram unchanged");
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(!h.app.ui.hdr_visualize, "Shift+H turns it back off");
|
||||
}
|
||||
|
||||
/// Home / End make the first / last photo of the view the active one, only in the library grids,
|
||||
/// and yield to a key the user gave to another command.
|
||||
#[test]
|
||||
fn home_and_end_select_the_ends_of_the_grid() {
|
||||
let (mut h, vis, _) = grid_demo();
|
||||
let (first, last) = (vis[0], *vis.last().unwrap());
|
||||
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(first), "Home goes to the first photo");
|
||||
assert_eq!(h.app.session.selection.ids, vec![first], "and selects only it");
|
||||
press(&mut h, "End", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(last), "End goes to the last photo");
|
||||
assert_eq!(h.app.session.selection.ids, vec![last]);
|
||||
|
||||
view(&mut h, json!({ "view": "squareGrid" }));
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(first), "the Square Grid has the keys too");
|
||||
|
||||
select(&mut h, last);
|
||||
view(&mut h, json!({ "view": "detail" }));
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(last), "Home is a grid key");
|
||||
|
||||
view(&mut h, json!({ "view": "photoGrid" }));
|
||||
let r = h.app.run("app.setShortcut", json!({"id": "view.survey", "shortcut": "Home"})).unwrap();
|
||||
assert_eq!(r["shortcut"], "Home", "{r}");
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.ui.view, ViewMode::Survey, "the user's Home binding fires");
|
||||
assert_eq!(h.app.session.active(), Some(last), "the grid key stayed out of the way");
|
||||
}
|
||||
|
||||
/// Shift+Left / Shift+Right in a grid add the previous / next photo of the view to the selection and
|
||||
/// make it active, stop at either end, stay out of the loupe and yield to the user's keymap.
|
||||
#[test]
|
||||
fn shift_arrows_extend_the_grid_selection() {
|
||||
let (mut h, vis, middle) = grid_demo();
|
||||
let at = vis.iter().position(|p| *p == middle).unwrap();
|
||||
let (previous, next) = (vis[at - 1], vis[at + 1]);
|
||||
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle, next], "Shift+Right adds the next photo");
|
||||
assert_eq!(h.app.session.active(), Some(next), "the added photo becomes active");
|
||||
// Shift+Left steps back onto a photo that is already selected: the selection stays, the active moves
|
||||
press(&mut h, "Left", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle, next]);
|
||||
assert_eq!(h.app.session.active(), Some(middle));
|
||||
press(&mut h, "Left", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle, next, previous], "Shift+Left adds the previous photo");
|
||||
assert_eq!(h.app.session.active(), Some(previous));
|
||||
|
||||
// the ends of the view do nothing
|
||||
select(&mut h, vis[0]);
|
||||
press(&mut h, "Left", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![vis[0]], "Shift+Left on the first photo is a no-op");
|
||||
let last = *vis.last().unwrap();
|
||||
select(&mut h, last);
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![last], "Shift+Right on the last photo is a no-op");
|
||||
|
||||
// outside a grid it is not the extension key
|
||||
select(&mut h, middle);
|
||||
view(&mut h, json!({ "view": "detail" }));
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle], "the loupe has no grid extension key");
|
||||
|
||||
// a key the user gave to a command wins
|
||||
view(&mut h, json!({ "view": "photoGrid" }));
|
||||
h.app.run("app.setShortcut", json!({"id": "view.survey", "shortcut": "Shift+Right"})).unwrap();
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.ui.view, ViewMode::Survey, "the user's Shift+Right binding fires");
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle], "the grid key stayed out of the way");
|
||||
}
|
||||
|
||||
/// A text field with the keyboard focus keeps every one of these keys.
|
||||
#[test]
|
||||
fn navigation_open_library_and_hdr_keys_wait_for_a_text_field() {
|
||||
let (mut h, vis, middle) = grid_demo();
|
||||
let calls = count_folder_picks(&mut h);
|
||||
let picks = || calls.load(std::sync::atomic::Ordering::SeqCst);
|
||||
let keys: [(&str, &[&str]); 6] = [("Home", &[]), ("End", &[]), ("Right", &["shift"]), ("Left", &["shift"]), ("O", &["cmd"]), ("H", &["shift"])];
|
||||
|
||||
// with no text field focused, each of them acts
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(vis[0]));
|
||||
press(&mut h, "End", &[]);
|
||||
assert_eq!(h.app.session.active(), vis.last().copied());
|
||||
select(&mut h, middle);
|
||||
press(&mut h, "Right", &["shift"]);
|
||||
assert_eq!(h.app.session.selection.ids.len(), 2, "Shift+Right extends");
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(picks(), 1, "Cmd+O asks for a folder");
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(h.app.ui.hdr_visualize, "Shift+H turns Visualize HDR on");
|
||||
press(&mut h, "H", &["shift"]);
|
||||
assert!(!h.app.ui.hdr_visualize);
|
||||
select(&mut h, middle);
|
||||
|
||||
// with the search box focused, none does
|
||||
let r = h.request("ui.clickWidget", json!({"id": "field:search"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(h.view.ctx.egui_wants_keyboard_input(), "the search box has the keyboard");
|
||||
let before = (h.app.session.active(), h.app.session.selection.ids.clone(), h.app.ui.hdr_visualize, h.app.ui.view, picks());
|
||||
for (key, mods) in keys {
|
||||
press(&mut h, key, mods);
|
||||
let after = (h.app.session.active(), h.app.session.selection.ids.clone(), h.app.ui.hdr_visualize, h.app.ui.view, picks());
|
||||
assert_eq!(after, before, "{mods:?}+{key} reached the shortcuts while the search box had the focus");
|
||||
}
|
||||
assert!(h.app.ui.dialog.is_none());
|
||||
}
|
||||
|
||||
/// Home / End and Shift+Left / Shift+Right belong to the grid: while the full-screen preview covers it
|
||||
/// they leave the selection alone.
|
||||
#[test]
|
||||
fn grid_navigation_keys_do_nothing_in_the_full_screen_preview() {
|
||||
let (mut h, vis, middle) = grid_demo();
|
||||
h.app.run("view.fullScreenPreview", json!({})).unwrap();
|
||||
assert!(h.app.ui.fullscreen);
|
||||
assert_eq!(h.app.ui.view, ViewMode::PhotoGrid, "the grid is the view behind the preview");
|
||||
for (key, mods) in [("Home", &[][..]), ("End", &[][..]), ("Right", &["shift"][..]), ("Left", &["shift"][..])] {
|
||||
press(&mut h, key, mods);
|
||||
assert_eq!(h.app.session.active(), Some(middle), "{mods:?}+{key} moved the active photo in the full-screen preview");
|
||||
assert_eq!(h.app.session.selection.ids, vec![middle], "{mods:?}+{key} changed the selection in the full-screen preview");
|
||||
}
|
||||
// back in the grid the keys work again
|
||||
h.app.run("view.fullScreenPreview", json!({})).unwrap();
|
||||
assert!(!h.app.ui.fullscreen);
|
||||
press(&mut h, "Home", &[]);
|
||||
assert_eq!(h.app.session.active(), Some(vis[0]));
|
||||
}
|
||||
|
||||
/// A dialog owns the keyboard: Cmd+O does not ask for a folder behind it.
|
||||
#[test]
|
||||
fn cmd_o_does_not_fire_while_a_dialog_is_open() {
|
||||
let mut h = demo();
|
||||
let calls = count_folder_picks(&mut h);
|
||||
h.app.run("app.shortcuts", json!({})).unwrap();
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::Shortcuts)));
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 0, "Cmd+O reached the shortcuts behind a dialog");
|
||||
assert!(matches!(h.app.ui.dialog, Some(Dialog::Shortcuts)));
|
||||
// with the dialog closed the same key asks for a folder
|
||||
h.app.ui.dialog = None;
|
||||
press(&mut h, "O", &["cmd"]);
|
||||
assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
|
||||
| 2026-10-14 | minor | Gap 10: 21 Sony and Leica models verified on one CC0 file each (258 models) |
|
||||
| 2026-10-12 | minor | Gap 20 shrinks: Mamiya MEF decodes (`vendor/mef.rs`) and is imported; gap 10 count 240 → 241 |
|
||||
| 2026-10-12 | minor | Gap 20 shrinks: Epson ERF decodes (`vendor/erf.rs`), 3 bodies verified; gap 10 count 237 → 240 |
|
||||
| 2026-10-13 | minor | Gap 28: F6 (filmstrip) and F7 (left panel) are bound, so the entry no longer lists them as missing; hours unchanged |
|
||||
| 2026-10-11 | minor | Gap 10 shrinks: Leica Q3 verified by a recorded check of 38 private DNGs (236 models verified; Leica 14); gap not closed |
|
||||
| 2026-10-12 | minor | Camera-look deep shadows follow the JPEG toe (#1307); camera calibration remains partial |
|
||||
| 2026-10-12 | minor | Gap 23 (accessibility): crop handles, spots, gradient handles and mask pins by keyboard; 4–8 h → 2–4 h; gap not closed |
|
||||
|
||||
@@ -41,6 +41,14 @@ The separate Decrease Rating / Increase Rating commands can also be assigned in
|
||||
|
||||
Flag cycling and raise/lower use each photo's own flag, so a mixed selection steps together without being made uniform. Adobe's shortcut list names the backtick "Cycle flag settings" but does not state the order; unflagged → pick → reject is LightCraft's choice. The key is the logical grave key, so it follows the keyboard layout (on layouts where it is a dead accent key, assign another key in Help → Keyboard Shortcuts to `photo.cycleFlag`).
|
||||
|
||||
`Home` and `End` make the first or last photo of the view the active, selected one. `Shift` + `←` /
|
||||
`→` add the previous or next photo of the view to the selection and make the added photo active; the
|
||||
key never removes a photo from the selection, and at either end of the view it does nothing. These
|
||||
four keys work in Photo Grid and Square Grid only. They wait in Detail, Compare and Survey, in the
|
||||
full-screen preview (which covers the grid), while a text field or a dialog has the keyboard, and
|
||||
when you gave the same key to a command in Help → Keyboard Shortcuts, where they are listed under
|
||||
Fixed keys.
|
||||
|
||||
Grid actions apply to all selected photos and support Undo / Redo. In Compare and Survey, culling
|
||||
actions apply to the active photo. Auto Advance moves forward after an action; holding Shift with
|
||||
Auto Advance on still advances only once. The last photo stays selected. Text fields keep their
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -70,6 +70,8 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
|
||||
**Grid thumbnail size (KEYC-COMPARE):** Classic's `=` and `-` increase and decrease the Grid thumbnail size (same keys on macOS and Windows; `+` also works for the larger size here, as Shift+`=` or the keypad). They step the bottom-bar slider by 30 pt inside its 90–480 pt range, in Photo Grid, Square Grid and People only; Detail, Compare, Survey and Reference ignore them, and ⌘= / ⌘− stay Zoom In / Out. See [library shortcuts](library-shortcuts.md).
|
||||
|
||||
**Library navigation keys (KEYC-COMPARE):** in Photo Grid and Square Grid, `Home` / `End` make the first / last photo of the view the active, selected one, and `Shift+Left` / `Shift+Right` add the previous / next photo of the view to the selection (the added photo becomes active; the keys never remove a photo from the selection, and do nothing at either end of the view). They wait in the other views, in the full-screen preview, while a text field or a dialog has the keyboard, and when you gave the key to a command; they are listed under Fixed keys in Help → Keyboard Shortcuts. `Cmd+O` (`Ctrl+O` on Windows and Linux) runs File → Open Library… and `Shift+H` toggles View → Visualize HDR (`Cmd+Shift+H` stays the Histogram). Covered by `crates/ui-egui/src/tests_keymap.rs`; the macOS native menu path for `Cmd+O` is not exercised by the headless tests.
|
||||
|
||||
**Still missing / broken:**
|
||||
- No command yet: F1 help (verify the rest of the old list: full screen, settings, stacks,
|
||||
visualize spots and merges have commands now).
|
||||
@@ -132,6 +134,7 @@ a real screen reader on any platform yet.
|
||||
| 2026-10-13 | minor | Shortcuts: Tool Overlay modes for spot and mask pins on ⇧H (H stays Remove; issue #224), listed with the deliberate differences; dropped from the missing list |
|
||||
| 2026-10-13 | minor | Library culling: backtick cycles the flag; ⌘↑ / ⌘↓ raise / lower flag status |
|
||||
| 2026-10-13 | minor | KEYC-COMPARE: `=` / `-` grid thumbnail size keys noted in the shortcuts section |
|
||||
| 2026-10-13 | minor | Shortcuts: Home / End and Shift+Left / Right in the two library grids, Cmd+O (Open Library…) and Shift+H (Visualize HDR) get bindings and keyboard tests; percentages and the dated counts above are unchanged |
|
||||
| 2026-10-12 | minor | Accessibility: an AccessKit tree audit test over every view, Develop tool and 13 dialogs; unnamed fields, slider ranges and the Auto Stack slider fixed; 30% → 55%; UI remaining 40–70 h → 34–58 h |
|
||||
| 2026-10-11 | minor | Zoom row: Compare and Reference get zoom windows; what remains of LR-VIEW-ZOOM is overlays and soft proofing |
|
||||
| 2026-10-10 | major | Created: measured command and shortcut counts, assessment by area, estimates; moved the shortcut conflicts section here from the checklist |
|
||||
|
||||
Reference in New Issue
Block a user