LR-META-INFO: the Info panel shows Selected Photos by default; gaps.md lists its change notes once

This commit is contained in:
aiongg
2026-10-25 03:36:14 +00:00
parent e339afc263
commit c3feda15c0
5 changed files with 48 additions and 22 deletions
+41 -15
View File
@@ -2955,17 +2955,11 @@ mod tests {
h.settle(SETTLE);
let buttons = |h: &mut Headless| h.request("ui.widgets", json!({"filter": "infoFor"}), t)["result"].to_string();
assert!(buttons(&mut h).contains("button:infoFor-selected"), "the choice shows with two photos selected");
assert!(!h.app.ui.settings.info_selected_photos, "Target Photo by default");
// Target Photo: the title field holds the active photo's title, and what is typed after it
// goes to both photos
ask(&mut h, "ui.clickWidget", json!({"id": "field:title"}));
ask(&mut h, "ui.key", json!({"key": "End"}));
ask(&mut h, "ui.text", json!({"text": "!"}));
ask(&mut h, "ui.key", json!({"key": "Tab"}));
h.settle(SETTLE);
assert_eq!((meta(&h, a).title, meta(&h, b).title), ("Dawn!".into(), "Dawn!".into()));
// Selected Photos, from its button
exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "title": "Dusk"}));
assert!(h.app.ui.settings.info_selected_photos, "Selected Photos by default");
exec(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "title": "Dawn!"}));
// Target Photo and back to Selected Photos, from their buttons
ask(&mut h, "ui.clickWidget", json!({"id": "button:infoFor-target"}));
assert!(!h.app.ui.settings.info_selected_photos);
ask(&mut h, "ui.clickWidget", json!({"id": "button:infoFor-selected"}));
assert!(h.app.ui.settings.info_selected_photos);
// a mixed field left untouched saves nothing
@@ -3018,19 +3012,51 @@ mod tests {
assert_eq!(exec(&mut h, "view.infoSelectedPhotos", json!({"on": false}))["on"], false);
assert!(!h.app.ui.settings.info_selected_photos);
assert_eq!(exec(&mut h, "view.infoSelectedPhotos", json!({}))["on"], true, "no params toggles");
// the choice is saved with the app settings; settings saved before it read as Target Photo
// the choice is saved with the app settings; settings saved before it read as Selected Photos
exec(&mut h, "view.infoSelectedPhotos", json!({"on": false}));
let saved = serde_json::to_value(&h.app.ui.settings).unwrap();
assert_eq!(saved["infoSelectedPhotos"], true);
assert_eq!(saved["infoSelectedPhotos"], false);
let read: crate::state::AppSettings = serde_json::from_value(saved).unwrap();
assert!(read.info_selected_photos);
assert!(!read.info_selected_photos);
let old: crate::state::AppSettings = serde_json::from_value(json!({"filmNames": false})).unwrap();
assert!(!old.info_selected_photos);
assert!(old.info_selected_photos);
// one photo selected: no choice to make
exec(&mut h, "library.select", json!({"ids": [ids[0]]}));
h.settle(SETTLE);
assert!(!buttons(&mut h).contains("infoFor-"), "{}", buttons(&mut h));
}
/// With several photos selected and the settings as they come, an edit to a field where the
/// photos differ starts from an empty field: the photos get what was typed, not the active
/// photo's value with the typing added.
#[test]
fn info_edits_of_a_multi_selection_start_from_mixed_fields_by_default() {
let mut h = demo([1300.0, 1000.0]);
let t = Duration::from_secs(10);
let ask = |h: &mut Headless, method: &str, params: serde_json::Value| {
let r = h.request(method, params.clone(), t);
assert_eq!(r["ok"], true, "{method} {params}: {r}");
h.step();
h.step();
};
let exec = |h: &mut Headless, command: &str, params: serde_json::Value| {
ask(h, "engine.execute", json!({"command": command, "params": params}));
};
let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(2).map(|p| p.0).collect();
let caption = |h: &Headless, i: usize| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(ids[i])).unwrap().meta.caption.clone();
exec(&mut h, "photo.setMeta", json!({"ids": [ids[0]], "caption": "Dawn"}));
exec(&mut h, "photo.setMeta", json!({"ids": [ids[1]], "caption": "Dusk"}));
h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
exec(&mut h, "library.select", json!({"ids": ids, "active": ids[0]}));
h.settle(SETTLE);
ask(&mut h, "ui.clickWidget", json!({"id": "field:caption"}));
ask(&mut h, "ui.key", json!({"key": "End"}));
ask(&mut h, "ui.text", json!({"text": "Harbor"}));
ask(&mut h, "ui.key", json!({"key": "Tab"}));
h.settle(SETTLE);
assert_eq!((caption(&h, 0), caption(&h, 1)), ("Harbor".into(), "Harbor".into()));
}
/// The fields the Info panel marks as mixed: each text field, the copyright status and GPS
/// where a selected photo differs from the photo the panel shows, and none for one photo.
#[test]
+4 -4
View File
@@ -477,9 +477,9 @@ pub struct AppSettings {
pub photocraft_file: EditFile,
/// Memory the caches may hold together, in MB (0 = automatic; `app.memoryBudget`).
pub memory_mb: u32,
/// The Info panel's metadata fields with several photos selected: false shows the active
/// photo's values (Target Photo), true shows the values the selected photos share and marks
/// each field where they differ (Selected Photos) (`view.infoSelectedPhotos`).
/// The Info panel's metadata fields with several photos selected: true (the default) shows the
/// values the selected photos share and marks each field where they differ (Selected Photos),
/// false shows the active photo's values (Target Photo) (`view.infoSelectedPhotos`).
pub info_selected_photos: bool,
/// Filmstrip: file names above the thumbnails.
pub film_names: bool,
@@ -552,7 +552,7 @@ impl Default for AppSettings {
photocraft_path: String::new(),
photocraft_file: EditFile { format: EditFormat::Psd, ..EditFile::default() },
memory_mb: 0,
info_selected_photos: false,
info_selected_photos: true,
film_names: true,
film_badges: true,
grid_badges: GridBadges::Auto,
+1 -1
View File
@@ -137,7 +137,7 @@ file, on a background job (activity kind `focus`); `wait: true` returns the run'
With several photos selected, the Info panel's Show Metadata For is `view.infoSelectedPhotos {on?}` (no `on`, or
`on: null`, toggles; any other value that is not true or false is refused; replies `{on}`;
`settings.infoSelectedPhotos`): off (Target Photo, the default) shows the active photo's values, on (Selected Photos)
`settings.infoSelectedPhotos`): off (Target Photo) shows the active photo's values, on (Selected Photos, the default)
shows the values the selected photos share, leaves each text field and GPS where they differ empty with a Mixed hint,
and shows Mixed as the Copyright Status when the statuses differ.
+1 -1
View File
@@ -1,6 +1,6 @@
# Gaps: where LightCraft falls short of Lightroom
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF; gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-24 · **Change:** minor (gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and
how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is
+1 -1
View File
@@ -164,7 +164,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| Id | Feature | Tier | Status | Evidence | Missing / notes |
|---|---|---|---|---|---|
| LR-META-INFO | Info panel | P0 | ✅ | `cmd:panel.info`, `cmd:photo.setMeta`, `cmd:view.infoSelectedPhotos`, `crates/ui-egui/src/panels/right.rs` (`info`, `camera_card`, `mixed_meta`) | camera card (camera, lens, size, format, focal length / shutter / aperture / ISO); title, caption, alt text, extended description, copyright, copyright status, rights usage terms, copyright info URL, creator; file name (rename), file path (reveal), capture time (edit); location, city, state, country; GPS. With several photos selected, Show Metadata For picks Target Photo (the active photo's values, the default) or Selected Photos (the values they share; a text field or GPS where they differ is empty and says Mixed, Copyright Status shows Mixed, and only what is typed or picked there is saved, to every selected photo). No flash, map snippet or people; a raw with no lens name (Nikon DSLRs) shows its range from Exif LensSpecification or the Nikon maker note's Lens tag, written like Lightroom's `24.0-70.0 mm f/2.8` |
| LR-META-INFO | Info panel | P0 | ✅ | `cmd:panel.info`, `cmd:photo.setMeta`, `cmd:view.infoSelectedPhotos`, `crates/ui-egui/src/panels/right.rs` (`info`, `camera_card`, `mixed_meta`) | camera card (camera, lens, size, format, focal length / shutter / aperture / ISO); title, caption, alt text, extended description, copyright, copyright status, rights usage terms, copyright info URL, creator; file name (rename), file path (reveal), capture time (edit); location, city, state, country; GPS. With several photos selected, Show Metadata For picks Selected Photos (the default: the values they share; a text field or GPS where they differ is empty and says Mixed, Copyright Status shows Mixed, and only what is typed or picked there is saved, to every selected photo) or Target Photo (the active photo's values). No flash, map snippet or people; a raw with no lens name (Nikon DSLRs) shows its range from Exif LensSpecification or the Nikon maker note's Lens tag, written like Lightroom's `24.0-70.0 mm f/2.8` |
| LR-META-COPYRIGHT-DEFAULT | Default copyright on import | P1 | ✅ | `cmd:library.preferences` (`import.copyright`, `import.creator`), `crates/ui-egui/src/panels/settings.rs` | Settings → Import → Metadata; fills only photos without their own |
| LR-META-LOCATION | Location editing | P2 | 🟡 | `cmd:photo.setMeta` (`location`, `city`, `state`, `country`, `gps`), `cmd:metadata.reverseGeocode` | place fields and GPS (decimal or degrees / minutes / seconds) edited in Info, read/written as IPTC Core / Photoshop / EXIF XMP; Show on Map opens OpenStreetMap; Fill Location fills empty City / State / Country from GPS offline (GeoNames, downloaded on request: `cmd:metadata.downloadPlaceNames`); no embedded map |
| LR-META-COPYPASTE | Copy / paste metadata | P2 | ✅ | `cmd:photo.copyMetadata`, `cmd:photo.pasteMetadata` | title, caption, alt text, extended description, copyright (notice, status, usage terms, info URL), creator, place fields, keywords; `fields` picks a subset |