LR-META-INFO: an Info field saves to the photos selected when its edit started

The text fields and GPS saved with no `ids`, so `photo.setMeta` wrote to
whatever was selected when the edit ended. A selection change during an
edit then gave the typed text to the new selection. Each edit now keeps
the selected photos from its start and saves to them.
This commit is contained in:
dotCooCoo
2026-10-24 14:51:15 -07:00
parent e25cc3ca0b
commit 7a4e842dd5
2 changed files with 52 additions and 7 deletions
+41
View File
@@ -3078,6 +3078,47 @@ mod tests {
assert_eq!((title(&h, 0), title(&h, 1), title(&h, 2)), ("Same!".into(), "Same!".into(), "Other".into()));
}
/// An Info field saves to the photos selected when its edit started, also when the selection
/// changes while the field is being edited.
#[test]
fn info_field_saves_to_the_photos_selected_when_the_edit_started() {
// (tall enough for the GPS field to show without scrolling)
let mut h = demo([1300.0, 1800.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(4).map(|p| p.0).collect();
let meta = |h: &Headless, i: usize| h.app.session.catalog.photo(lightcraft_catalog::PhotoId(ids[i])).unwrap().meta.clone();
exec(&mut h, "photo.setMeta", json!({"ids": ids, "title": "Same", "gps": ""}));
h.request("ui.set", json!({"view": "detail", "right": "info"}), t);
exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));
h.settle(SETTLE);
ask(&mut h, "ui.clickWidget", json!({"id": "field:title"}));
ask(&mut h, "ui.key", json!({"key": "End"}));
ask(&mut h, "ui.text", json!({"text": "!"}));
exec(&mut h, "library.select", json!({"ids": [ids[2]]}));
ask(&mut h, "ui.key", json!({"key": "Tab"}));
h.settle(SETTLE);
assert_eq!((meta(&h, 0).title, meta(&h, 1).title, meta(&h, 2).title), ("Same!".into(), "Same!".into(), "Same".into()));
// GPS too (the new selection is two photos as well, so the panel keeps its layout)
exec(&mut h, "library.select", json!({"ids": [ids[0], ids[1]], "active": ids[0]}));
h.settle(SETTLE);
ask(&mut h, "ui.clickWidget", json!({"id": "field:gps"}));
ask(&mut h, "ui.text", json!({"text": "41.15, -8.61"}));
exec(&mut h, "library.select", json!({"ids": [ids[2], ids[3]], "active": ids[2]}));
ask(&mut h, "ui.key", json!({"key": "Tab"}));
h.settle(SETTLE);
let gps: Vec<bool> = (0..4).map(|i| meta(&h, i).gps.is_some()).collect();
assert_eq!(gps, [true, true, false, false]);
}
/// Local: a folder's photos show without joining the library; the breadcrumb, Include
/// subfolders and Add to My Photos work from the grid header.
#[test]
+11 -7
View File
@@ -756,7 +756,7 @@ fn info(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
let gps = if gps_mixed { String::new() } else { m.gps.map(|(la, lo)| format!("{la:.6}, {lo:.6}")).unwrap_or_default() };
let gid = egui::Id::new("info-gps");
ui.label(egui::RichText::new("GPS").size(11.5).color(t.text_dim));
let mut edit: FieldEdit = ui.data(|d| d.get_temp(gid)).unwrap_or_else(|| FieldEdit::new(&gps, gps_mixed));
let mut edit: FieldEdit = ui.data(|d| d.get_temp(gid)).unwrap_or_else(|| FieldEdit::new(&gps, gps_mixed, &selected));
let hint = crate::i18n::tr(if gps_mixed { MIXED } else { "latitude, longitude" });
let r = ui.add(egui::TextEdit::singleline(&mut edit.text).hint_text(hint).desired_width(f32::INFINITY));
crate::widgets::screen_reader_name(ui.ctx(), r.id, "GPS");
@@ -770,7 +770,7 @@ fn info(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
let text = edit.text.trim();
if r.lost_focus()
&& (text != edit.start || edit.clears())
&& let Err(e) = app.run("photo.setMeta", json!({"gps": text}))
&& let Err(e) = app.run("photo.setMeta", json!({"ids": edit.ids, "gps": text}))
{
app.toast(ui.ctx(), e);
}
@@ -915,11 +915,14 @@ const MIXED: &str = "Mixed";
/// An Info panel field's edit under way, kept in egui memory between frames. A field saves when
/// its text is not the text the edit started from, so a field left as it was saves nothing, even
/// when what the panel shows changed during the edit.
/// when what the panel shows changed during the edit. It saves to the photos selected when the
/// edit started, also when the selection changed since.
#[derive(Clone)]
struct FieldEdit {
text: String,
start: String,
/// The selected photos when the edit started (`photo.setMeta` `ids`).
ids: Vec<u64>,
/// The field said Mixed when the edit started.
mixed: bool,
/// Something was typed or deleted since.
@@ -927,8 +930,9 @@ struct FieldEdit {
}
impl FieldEdit {
fn new(value: &str, mixed: bool) -> Self {
FieldEdit { text: value.to_string(), start: value.to_string(), mixed, typed: false }
fn new(value: &str, mixed: bool, selected: &[PhotoId]) -> Self {
let ids = selected.iter().map(|id| id.0).collect();
FieldEdit { text: value.to_string(), start: value.to_string(), ids, mixed, typed: false }
}
/// A Mixed field that was typed in and left empty: it clears the field on every selected photo.
@@ -1053,7 +1057,7 @@ fn meta_field(app: &mut LightcraftApp, ui: &mut egui::Ui, label: &str, key: &str
ui.label(egui::RichText::new(crate::i18n::tr(label)).size(11.5).color(t.text_dim));
let id = egui::Id::new(("info-field", key));
let value = if mixed { "" } else { value };
let mut edit: FieldEdit = ui.data(|d| d.get_temp(id)).unwrap_or_else(|| FieldEdit::new(value, mixed));
let mut edit: FieldEdit = ui.data(|d| d.get_temp(id)).unwrap_or_else(|| FieldEdit::new(value, mixed, &app.session.selected()));
let widget = format!("field:{key}");
let field = if lines > 1 {
crate::text_field::TextField::multiline(&widget, &mut edit.text).rows(lines)
@@ -1071,7 +1075,7 @@ fn meta_field(app: &mut LightcraftApp, ui: &mut egui::Ui, label: &str, key: &str
}
// Return (one line) or leaving saves; Esc gives the edit up
if r.committed() && (edit.text != edit.start || edit.clears()) {
let _ = app.run("photo.setMeta", json!({key: edit.text}));
let _ = app.run("photo.setMeta", json!({"ids": edit.ids, key: edit.text}));
}
ui.add_space(6.0);
}