From 69c91013e0c53aeeb99ef882ba367b9b3e1786dc Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Fri, 9 Oct 2026 15:32:40 -0400 Subject: [PATCH] LRC-LIB-KEYWORDS: the active photo alone is the selection Keyword commands act on the selected photos, or on the active photo when the selection is empty (`Session::targets`), but the Keywording box, the keyword set's buttons, the suggestions and the Keyword List's tick boxes read the selection only: with just an active photo the box showed nothing while typed keywords went to it. They now all use the photos the commands use. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/panels/keyword_list.rs | 2 +- crates/ui-egui/src/panels/keywording.rs | 4 ++-- crates/ui-egui/src/panels/right.rs | 4 ++-- crates/ui-egui/src/tests_keywording.rs | 14 ++++++++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/panels/keyword_list.rs b/crates/ui-egui/src/panels/keyword_list.rs index fb51a14cd..0dcf40d0c 100644 --- a/crates/ui-egui/src/panels/keyword_list.rs +++ b/crates/ui-egui/src/panels/keyword_list.rs @@ -72,7 +72,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { ui.label(egui::RichText::new(crate::i18n::tr(none)).color(t.text_dim)); }); } - let selection = app.session.selection.ids.clone(); + let selection = app.session.targets(&serde_json::Value::Null); let ticks = app.caches.keyword_ticks(&app.session.catalog, &selection); for r in &rows { row(app, ui, r, &selection, &ticks, filter.trim().is_empty()); diff --git a/crates/ui-egui/src/panels/keywording.rs b/crates/ui-egui/src/panels/keywording.rs index aacc342ae..ce3b60fc2 100644 --- a/crates/ui-egui/src/panels/keywording.rs +++ b/crates/ui-egui/src/panels/keywording.rs @@ -26,7 +26,7 @@ pub fn view_switch(app: &mut LightcraftApp, ui: &mut egui::Ui) { /// carry is marked with an asterisk. pub fn export_row(app: &mut LightcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); - let selection = app.session.selection.ids.clone(); + let selection = app.session.targets(&serde_json::Value::Null); let names = app.caches.keyword_export(&app.session.catalog, &selection); if names.is_empty() { ui.label(egui::RichText::new(crate::i18n::tr("No keywords are exported")).color(t.text_dim)); @@ -105,7 +105,7 @@ fn menu(app: &mut LightcraftApp, ui: &mut egui::Ui, chip: &Chip) { /// The selection's keywords as chips: the name (right-click for its menu), and × to take it off /// every selected photo. One only some of them have is marked with an asterisk. pub fn chip_row(app: &mut LightcraftApp, ui: &mut egui::Ui) { - let selection = app.session.selection.ids.clone(); + let selection = app.session.targets(&serde_json::Value::Null); let chips = app.caches.keyword_chips(&app.session.catalog, &selection); ui.horizontal_wrapped(|ui| { for c in chips.iter() { diff --git a/crates/ui-egui/src/panels/right.rs b/crates/ui-egui/src/panels/right.rs index a649d8b5a..f9a2ce9e5 100644 --- a/crates/ui-egui/src/panels/right.rs +++ b/crates/ui-egui/src/panels/right.rs @@ -831,7 +831,7 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { let typed = ui.data(|d| d.get_temp::(kid).unwrap_or_default()); let last = typed.rsplit(',').next().unwrap_or("").trim().to_string(); // the keywords every selected photo has: those only some have are still suggested - let selection = app.session.selection.ids.clone(); + let selection = app.session.targets(&serde_json::Value::Null); let have: Vec = app.caches.keyword_chips(&app.session.catalog, &selection).iter().filter(|c| c.on_all()).map(|c| c.path.clone()).collect(); let suggestions = (*app.caches.suggestions(&app.session.catalog, &have, &last, 12)).clone(); @@ -859,7 +859,7 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) { /// selected photos; "Save as Set…" keeps the current nine under a name. fn keyword_set(app: &mut LightcraftApp, ui: &mut egui::Ui) { // on: every selected photo has it (⌥1–⌥9 then take it off them); partly on: only some do - let selection = app.session.selection.ids.clone(); + let selection = app.session.targets(&serde_json::Value::Null); let chips = app.caches.keyword_chips(&app.session.catalog, &selection); let t = Tokens::get(ui.ctx()); let sets = lightcraft_engine::cmd::keywords::keyword_sets_json(&app.session); diff --git a/crates/ui-egui/src/tests_keywording.rs b/crates/ui-egui/src/tests_keywording.rs index e142c24a6..a3caabe51 100644 --- a/crates/ui-egui/src/tests_keywording.rs +++ b/crates/ui-egui/src/tests_keywording.rs @@ -176,3 +176,17 @@ fn chips_stay_on_one_line() { assert!((x.center().y - r.center().y).abs() < 4.0, "{id}: its × is on its line"); } } + +/// With an active photo and nothing else selected, the box is that photo's, as the commands are: +/// its chips show (keywords typed go to it), and the Keyword List ticks what it has. +#[test] +fn the_active_photo_alone_is_the_selection() { + let (mut h, ids) = two_selected(); + ask(&mut h, "engine.execute", json!({"command": "library.select", "params": {"ids": [], "active": ids[0]}})); + assert_eq!(h.app.session.selection.active.map(|p| p.0), Some(ids[0])); + assert!(h.app.session.selection.ids.is_empty()); + assert!(has(&h, "keywordChip:Weddings") && !has(&h, "keywordChipPartial:Weddings"), "the active photo's keywords"); + let ticks = crate::panels::keyword_list::Ticks::of(&h.app.session.catalog, &h.app.session.targets(&json!({}))); + assert_eq!(ticks.tick("Weddings"), crate::panels::keyword_list::Tick::All); + assert!(has(&h, "keywordCheck:beach")); +}