LRC-LIB-KEYWORDS: Will Export shows what exported files carry

Lightroom Classic's Keywording box can show the keywords that will be
exported rather than those entered. A Keywords / Will Export switch
under the box now does the same for the selection: the names exported
files carry as the keyword tag options say (a keyword left out of
export goes, parents and synonyms come), one only some of the photos
carry marked with an asterisk. Read only; cached per selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nathaniel Ledford 2026-10-09 15:21:13 -04:00
parent 3e0c412249
commit f577ccf828
No known key found for this signature in database
13 changed files with 141 additions and 8 deletions

View File

@ -1912,5 +1912,7 @@
"Drop here for the top level": "Hier ablegen für die oberste Ebene",
"Their photos and the keywords below them come together under one keyword.": "Ihre Fotos und die Stichwörter darunter kommen unter einem Stichwort zusammen.",
"Select photos to give them keywords.": "Wählen Sie Fotos aus, um ihnen Stichwörter zu geben.",
"Add to All Selected Photos": "Allen ausgewählten Fotos hinzufügen"
"Add to All Selected Photos": "Allen ausgewählten Fotos hinzufügen",
"Will Export": "Wird exportiert",
"No keywords are exported": "Keine Stichwörter werden exportiert"
}

View File

@ -1757,5 +1757,7 @@
"Drop here for the top level": "Soltar aquí para el nivel superior",
"Their photos and the keywords below them come together under one keyword.": "Sus fotos y las palabras clave que contienen quedan bajo una sola palabra clave.",
"Select photos to give them keywords.": "Selecciona fotos para darles palabras clave.",
"Add to All Selected Photos": "Añadir a todas las fotos seleccionadas"
"Add to All Selected Photos": "Añadir a todas las fotos seleccionadas",
"Will Export": "Se exportará",
"No keywords are exported": "No se exporta ninguna palabra clave"
}

View File

@ -1757,5 +1757,7 @@
"Drop here for the top level": "ここにドロップすると最上位に移動",
"Their photos and the keywords below them come together under one keyword.": "写真と下位のキーワードが1つのキーワードにまとめられます。",
"Select photos to give them keywords.": "写真を選択するとキーワードを付けられます。",
"Add to All Selected Photos": "選択したすべての写真に追加"
"Add to All Selected Photos": "選択したすべての写真に追加",
"Will Export": "書き出される内容",
"No keywords are exported": "書き出されるキーワードはありません"
}

View File

@ -1757,5 +1757,7 @@
"Drop here for the top level": "Solte aqui para o nível superior",
"Their photos and the keywords below them come together under one keyword.": "Suas fotos e as palavras-chave abaixo delas ficam sob uma só palavra-chave.",
"Select photos to give them keywords.": "Selecione fotos para dar palavras-chave a elas.",
"Add to All Selected Photos": "Adicionar a todas as fotos selecionadas"
"Add to All Selected Photos": "Adicionar a todas as fotos selecionadas",
"Will Export": "Será exportado",
"No keywords are exported": "Nenhuma palavra-chave é exportada"
}

View File

@ -1431,5 +1431,7 @@
"Their photos and the keywords below them come together under one keyword.": "Их фото и вложенные ключевые слова объединятся под одним ключевым словом.",
"Select photos to give them keywords.": "Выберите фото, чтобы назначить им ключевые слова.",
"Person": "Человек",
"Add to All Selected Photos": "Добавить ко всем выбранным фото"
"Add to All Selected Photos": "Добавить ко всем выбранным фото",
"Will Export": "Будет экспортировано",
"No keywords are exported": "Ключевые слова не экспортируются"
}

View File

@ -1757,5 +1757,7 @@
"Drop here for the top level": "拖放到此处以移至顶层",
"Their photos and the keywords below them come together under one keyword.": "它们的照片和下级关键字将合并到同一个关键字下。",
"Select photos to give them keywords.": "选择照片以为其添加关键字。",
"Add to All Selected Photos": "添加到所有所选照片"
"Add to All Selected Photos": "添加到所有所选照片",
"Will Export": "将导出",
"No keywords are exported": "没有要导出的关键字"
}

View File

@ -1757,5 +1757,7 @@
"Drop here for the top level": "拖放到此處以移至最上層",
"Their photos and the keywords below them come together under one keyword.": "它們的照片與下層關鍵字將合併到同一個關鍵字下。",
"Select photos to give them keywords.": "選取照片以加入關鍵字。",
"Add to All Selected Photos": "加入所有選取的照片"
"Add to All Selected Photos": "加入所有選取的照片",
"Will Export": "將匯出",
"No keywords are exported": "沒有要匯出的關鍵字"
}

View File

@ -1080,6 +1080,8 @@ mod drop_tests {
#[derive(Default)]
pub struct Caches {
keyword_tree: Option<(u64, std::sync::Arc<Vec<lightcraft_catalog::KeywordNode>>)>,
/// The Keywording box's Will Export names, by library revision and selection.
keyword_export: Option<(u64, u64, std::sync::Arc<Vec<panels::keywording::Chip>>)>,
/// The Keywording box's chips, by library revision and selection.
keyword_chips: Option<(u64, u64, std::sync::Arc<Vec<panels::keywording::Chip>>)>,
/// The Keyword List's tick boxes, by library revision and selection.
@ -1170,6 +1172,22 @@ impl Caches {
}
}
}
/// What exported files carry for the selection (the Keywording box's Will Export).
pub(crate) fn keyword_export(
&mut self,
cat: &lightcraft_catalog::Catalog,
selection: &[lightcraft_catalog::PhotoId],
) -> std::sync::Arc<Vec<panels::keywording::Chip>> {
let key = key_of(selection);
match &self.keyword_export {
Some((r, k, c)) if *r == cat.revision && *k == key => c.clone(),
_ => {
let c = std::sync::Arc::new(panels::keywording::will_export(cat, selection));
self.keyword_export = Some((cat.revision, key, c.clone()));
c
}
}
}
/// The selection's keywords (the Keywording box's chips).
pub(crate) fn keyword_chips(
&mut self,

View File

@ -593,6 +593,8 @@ mod tests {
"No keywords match",
"Add to Selected Photos",
"Remove from Selected Photos",
"Keywords",
"Will Export",
];
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("locales");
for code in ["de", "es", "pt-br", "ru", "ja", "zh-hans", "zh-hant"] {

View File

@ -9,6 +9,47 @@ use crate::LightcraftApp;
use crate::theme::Tokens;
use crate::widgets::register;
/// The Keywording box's view: the keywords (chips), or what exported files will carry.
pub fn view_switch(app: &mut LightcraftApp, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
for (export, id, label) in [(false, "keywords", "Keywords"), (true, "willExport", "Will Export")] {
let r = ui.selectable_label(app.ui.keywording_will_export == export, crate::i18n::tr(label));
register(ui.ctx(), format!("keywordView:{id}"), r.rect);
if r.clicked() {
app.ui.keywording_will_export = export;
}
}
});
}
/// What exported files will carry for the selection, read only: one name only some of the photos
/// 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 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));
return;
}
ui.horizontal_wrapped(|ui| {
for n in names.iter() {
let partial = !n.on_all();
let text = format!("{}{}", n.path, if partial { " *" } else { "" });
let r = egui::Frame::NONE
.stroke(egui::Stroke::new(1.0, t.field_border))
.corner_radius(10.0)
.inner_margin(egui::Margin::symmetric(8, 2))
.show(ui, |ui| ui.label(egui::RichText::new(text).color(if partial { t.text_dim } else { t.text_label })))
.response;
register(ui.ctx(), format!("keywordExport:{}", n.path), r.rect);
if partial {
r.on_hover_text(crate::i18n::tr_format!("On {have} of {of} selected photos", have = n.have, of = n.of));
}
}
});
}
/// A chip's menu: it acts on the selected photos (deleting a keyword from the whole library is the
/// Keyword List's).
fn menu(app: &mut LightcraftApp, ui: &mut egui::Ui, chip: &Chip) {
@ -93,6 +134,20 @@ impl Chip {
/// The keywords of the selected `photos`, each once whatever its case, with how many of them have
/// it; by name.
/// "Will Export" (Lightroom Classic's Keyword Tags ▸ Will Export): the names exported files carry
/// for the selected `photos`, as the keyword tag options say, each with how many of them carry it;
/// by name.
pub(crate) fn will_export(catalog: &Catalog, photos: &[PhotoId]) -> Vec<Chip> {
let mut have: std::collections::BTreeMap<String, (String, usize)> = Default::default();
for id in photos {
let Some(p) = catalog.photo(*id) else { continue };
for name in catalog.export_keywords(&p.meta.keywords).flat {
have.entry(name.to_lowercase()).or_insert_with(|| (name.clone(), 0)).1 += 1;
}
}
have.into_values().map(|(path, have)| Chip { path, have, of: photos.len() }).collect()
}
pub(crate) fn chips(catalog: &Catalog, photos: &[PhotoId]) -> Vec<Chip> {
use lightcraft_catalog::keywords::clean;
// by lower-case keyword: the photos with it (each once)
@ -152,4 +207,23 @@ mod tests {
let (c, ids) = library(&[&["Beach"], &["beach"], &["BEACH", "beach"]]);
assert_eq!(shown(&chips(&c, &ids)), [("Beach", 3, 3)]);
}
/// "Will Export": the names exported files carry for the selection, as the keyword tag options
/// say (a keyword left out goes, its parents and synonyms come), each with how many of the
/// selected photos carry it.
#[test]
fn will_export_shows_what_exported_files_carry() {
use lightcraft_catalog::keywords::KeywordInfo;
let (mut c, ids) = library(&[&["Places|Lisbon", "draft"], &["Places|Lisbon"]]);
let out = KeywordInfo { include_on_export: false, ..KeywordInfo::default() };
c.apply(Op::SetKeyword { path: "Places".into(), info: Some(out.clone()) }).unwrap();
c.apply(Op::SetKeyword { path: "draft".into(), info: Some(out) }).unwrap();
c.apply(Op::SetKeyword {
path: "Places|Lisbon".into(),
info: Some(KeywordInfo { synonyms: vec!["Lisboa".into()], ..KeywordInfo::default() }),
})
.unwrap();
assert_eq!(shown(&will_export(&c, &ids)), [("Lisboa", 2, 2), ("Lisbon", 2, 2)]);
assert!(will_export(&c, &[]).is_empty());
}
}

View File

@ -787,7 +787,13 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
}
ui.data_mut(|d| d.insert_temp(kid, text));
ui.add_space(8.0);
super::keywording::chip_row(app, ui);
super::keywording::view_switch(app, ui);
ui.add_space(4.0);
if app.ui.keywording_will_export {
super::keywording::export_row(app, ui);
} else {
super::keywording::chip_row(app, ui);
}
ui.add_space(10.0);
keyword_set(app, ui);
ui.add_space(8.0);

View File

@ -449,6 +449,9 @@ pub struct UiState {
/// The keyword painter: clicking a photo in the grid toggles this keyword on it.
#[serde(skip)]
pub keyword_painter: Option<String>,
/// The Keywording box shows what exported files will carry (Will Export) instead of the
/// keywords.
pub keywording_will_export: bool,
/// The Keyword List's open levels (lower-case paths).
pub keyword_list_open: Vec<String>,
/// The keyword picked in the Keyword List (− deletes it, Edit edits it).
@ -795,6 +798,7 @@ impl Default for UiState {
slideshow: None,
second_window: false,
keyword_painter: None,
keywording_will_export: false,
keyword_list_open: Vec::new(),
keyword_list_selected: None,
dragging_keyword: None,

View File

@ -119,3 +119,18 @@ fn keyword_set_buttons_reflect_the_selection() {
assert!(has(&h, &format!("kwSetSome:{}", at("Weddings"))), "only the active one has Weddings");
assert!(!has(&h, &format!("kwSetOn:{}", at("Weddings"))));
}
/// "Will Export" shows the names exported files will carry for the selection instead of the chips:
/// a keyword left out of export isn't there; "Keywords" brings the chips back.
#[test]
fn will_export_shows_what_exports_carry() {
let (mut h, _) = two_selected();
let r = h.request("engine.execute", json!({"command": "keyword.edit", "params": {"keyword": "Weddings", "includeOnExport": false}}), T);
assert_eq!(r["ok"], true, "{r}");
ask(&mut h, "ui.clickWidget", json!({"id": "keywordView:willExport"}));
assert!(has(&h, "keywordExport:beach"));
assert!(!has(&h, "keywordExport:Weddings"), "left out of export");
assert!(!has(&h, "keywordChip:beach"), "the chips give way");
ask(&mut h, "ui.clickWidget", json!({"id": "keywordView:keywords"}));
assert!(has(&h, "keywordChip:beach") && !has(&h, "keywordExport:beach"));
}