mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-26 19:17:02 -06:00
LRC-LIB-KEYWORDS: a Keywords & Containing Keywords view
Lightroom Classic's Keywording box has three views; the box had two. The switch now offers Keywords, & Containing (the selection's keywords with the keywords containing them, flat and read only) and Will Export, the two read-only views cached per selection. The label check covers the box's other labels passed through variables too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fe02cfe2e1
commit
4b5c7ccd3c
@@ -1914,5 +1914,6 @@
|
||||
"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",
|
||||
"Will Export": "Wird exportiert",
|
||||
"No keywords are exported": "Keine Stichwörter werden exportiert"
|
||||
"No keywords are exported": "Keine Stichwörter werden exportiert",
|
||||
"& Containing": "& Übergeordnete"
|
||||
}
|
||||
|
||||
@@ -1759,5 +1759,6 @@
|
||||
"Select photos to give them keywords.": "Selecciona fotos para darles palabras clave.",
|
||||
"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"
|
||||
"No keywords are exported": "No se exporta ninguna palabra clave",
|
||||
"& Containing": "y contenedoras"
|
||||
}
|
||||
|
||||
@@ -1759,5 +1759,6 @@
|
||||
"Select photos to give them keywords.": "写真を選択するとキーワードを付けられます。",
|
||||
"Add to All Selected Photos": "選択したすべての写真に追加",
|
||||
"Will Export": "書き出される内容",
|
||||
"No keywords are exported": "書き出されるキーワードはありません"
|
||||
"No keywords are exported": "書き出されるキーワードはありません",
|
||||
"& Containing": "と親キーワード"
|
||||
}
|
||||
|
||||
@@ -1759,5 +1759,6 @@
|
||||
"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",
|
||||
"Will Export": "Será exportado",
|
||||
"No keywords are exported": "Nenhuma palavra-chave é exportada"
|
||||
"No keywords are exported": "Nenhuma palavra-chave é exportada",
|
||||
"& Containing": "e as que as contêm"
|
||||
}
|
||||
|
||||
@@ -1433,5 +1433,6 @@
|
||||
"Person": "Человек",
|
||||
"Add to All Selected Photos": "Добавить ко всем выбранным фото",
|
||||
"Will Export": "Будет экспортировано",
|
||||
"No keywords are exported": "Ключевые слова не экспортируются"
|
||||
"No keywords are exported": "Ключевые слова не экспортируются",
|
||||
"& Containing": "и родительские"
|
||||
}
|
||||
|
||||
@@ -1759,5 +1759,6 @@
|
||||
"Select photos to give them keywords.": "选择照片以为其添加关键字。",
|
||||
"Add to All Selected Photos": "添加到所有所选照片",
|
||||
"Will Export": "将导出",
|
||||
"No keywords are exported": "没有要导出的关键字"
|
||||
"No keywords are exported": "没有要导出的关键字",
|
||||
"& Containing": "及上级"
|
||||
}
|
||||
|
||||
@@ -1759,5 +1759,6 @@
|
||||
"Select photos to give them keywords.": "選取照片以加入關鍵字。",
|
||||
"Add to All Selected Photos": "加入所有選取的照片",
|
||||
"Will Export": "將匯出",
|
||||
"No keywords are exported": "沒有要匯出的關鍵字"
|
||||
"No keywords are exported": "沒有要匯出的關鍵字",
|
||||
"& Containing": "及上層"
|
||||
}
|
||||
|
||||
@@ -1080,8 +1080,9 @@ 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 read-only names (with containing keywords, or Will Export), by library
|
||||
/// revision, selection and view.
|
||||
keyword_names: 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.
|
||||
@@ -1172,18 +1173,24 @@ impl Caches {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// What exported files carry for the selection (the Keywording box's Will Export).
|
||||
pub(crate) fn keyword_export(
|
||||
/// The Keywording box's read-only names for the selection: what exported files carry
|
||||
/// (`export`), or the keywords with those containing them.
|
||||
pub(crate) fn keyword_names(
|
||||
&mut self,
|
||||
cat: &lightcraft_catalog::Catalog,
|
||||
selection: &[lightcraft_catalog::PhotoId],
|
||||
export: bool,
|
||||
) -> std::sync::Arc<Vec<panels::keywording::Chip>> {
|
||||
let key = key_of(selection);
|
||||
match &self.keyword_export {
|
||||
let key = key_of((selection, export));
|
||||
match &self.keyword_names {
|
||||
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()));
|
||||
let c = std::sync::Arc::new(if export {
|
||||
panels::keywording::will_export(cat, selection)
|
||||
} else {
|
||||
panels::keywording::with_containing(cat, selection)
|
||||
});
|
||||
self.keyword_names = Some((cat.revision, key, c.clone()));
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,7 +594,10 @@ mod tests {
|
||||
"Add to Selected Photos",
|
||||
"Remove from Selected Photos",
|
||||
"Keywords",
|
||||
"& Containing",
|
||||
"Will Export",
|
||||
"No keywords are exported",
|
||||
"Add to All Selected Photos",
|
||||
];
|
||||
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"] {
|
||||
|
||||
@@ -9,33 +9,39 @@ 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.
|
||||
/// The Keywording box's view: the keywords (chips), them with the keywords containing them, 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));
|
||||
use crate::state::KeywordingView as V;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (view, id, label) in
|
||||
[(V::Keywords, "keywords", "Keywords"), (V::Containing, "containing", "& Containing"), (V::WillExport, "willExport", "Will Export")]
|
||||
{
|
||||
let r = ui.selectable_label(app.ui.keywording_view == view, crate::i18n::tr(label));
|
||||
register(ui.ctx(), format!("keywordView:{id}"), r.rect);
|
||||
if r.clicked() {
|
||||
app.ui.keywording_will_export = export;
|
||||
app.ui.keywording_view = view;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
/// A read-only view of the selection's keywords: with the keywords containing them, or what
|
||||
/// exported files will carry. One only some of the photos have is marked with an asterisk.
|
||||
pub fn names_row(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let selection = app.session.targets(&serde_json::Value::Null);
|
||||
let names = app.caches.keyword_export(&app.session.catalog, &selection);
|
||||
let export = app.ui.keywording_view == crate::state::KeywordingView::WillExport;
|
||||
let names = app.caches.keyword_names(&app.session.catalog, &selection, export);
|
||||
if names.is_empty() {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("No keywords are exported")).color(t.text_dim));
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(if export { "No keywords are exported" } else { "No keywords yet" })).color(t.text_dim));
|
||||
return;
|
||||
}
|
||||
let prefix = if export { "keywordExport" } else { "keywordContaining" };
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for n in names.iter() {
|
||||
let (body, _) = chip(ui, &n.path, !n.on_all(), false);
|
||||
register(ui.ctx(), format!("keywordExport:{}", n.path), body.rect);
|
||||
register(ui.ctx(), format!("{prefix}:{}", n.path), body.rect);
|
||||
if n.on_all() {
|
||||
body.on_hover_text(&n.path);
|
||||
} else {
|
||||
@@ -197,6 +203,27 @@ pub(crate) fn will_export(catalog: &Catalog, photos: &[PhotoId]) -> Vec<Chip> {
|
||||
have.into_values().map(|(path, have)| Chip { path, have, of: photos.len() }).collect()
|
||||
}
|
||||
|
||||
/// "Keywords & Containing Keywords": the names of the selection's keywords and of the keywords
|
||||
/// containing them, each with how many of the selected photos have it (itself or below it); by
|
||||
/// name.
|
||||
pub(crate) fn with_containing(catalog: &Catalog, photos: &[PhotoId]) -> Vec<Chip> {
|
||||
use lightcraft_catalog::keywords::{SEP, clean};
|
||||
let photos = distinct(catalog, photos);
|
||||
let mut have: std::collections::BTreeMap<String, (String, usize)> = Default::default();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for p in &photos {
|
||||
seen.clear();
|
||||
for k in &p.meta.keywords {
|
||||
for name in clean(k).split(SEP) {
|
||||
if !name.is_empty() && seen.insert(name.to_lowercase()) {
|
||||
have.entry(name.to_lowercase()).or_insert_with(|| (name.to_string(), 0)).1 += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
have.into_values().map(|(path, have)| Chip { path, have, of: photos.len() }).collect()
|
||||
}
|
||||
|
||||
/// The selected photos, each once, those the library has: what "N of M" counts.
|
||||
pub(crate) fn distinct<'a>(catalog: &'a Catalog, photos: &[PhotoId]) -> Vec<&'a lightcraft_catalog::Photo> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
@@ -342,4 +369,12 @@ mod tests {
|
||||
assert!(has("Button", "Places › Portugal › Lisbon › Belém tower"), "{found:?}");
|
||||
assert!(has("Button", "Remove “Places › Portugal › Lisbon › Belém tower”"), "{found:?}");
|
||||
}
|
||||
|
||||
/// "Keywords & Containing Keywords": every level of the selection's keywords, flat, each
|
||||
/// counted once per photo.
|
||||
#[test]
|
||||
fn containing_keywords_are_every_level() {
|
||||
let (c, ids) = library(&[&["travel|Italy|Rome", "travel|Spain"], &["travel|Italy"]]);
|
||||
assert_eq!(shown(&with_containing(&c, &ids)), [("Italy", 2, 2), ("Rome", 1, 2), ("Spain", 1, 2), ("travel", 2, 2)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,10 +790,10 @@ fn keywords(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
ui.add_space(8.0);
|
||||
super::keywording::view_switch(app, ui);
|
||||
ui.add_space(4.0);
|
||||
if app.ui.keywording_will_export {
|
||||
super::keywording::export_row(app, ui);
|
||||
} else {
|
||||
if app.ui.keywording_view == crate::state::KeywordingView::Keywords {
|
||||
super::keywording::chip_row(app, ui);
|
||||
} else {
|
||||
super::keywording::names_row(app, ui);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
keyword_set(app, ui);
|
||||
|
||||
@@ -262,6 +262,17 @@ pub const RIGHT_WIDTH: PanelWidth = PanelWidth { min: 250.0, default: 270.0, max
|
||||
/// The photo area the side panels always leave free (as far as their minimum widths allow).
|
||||
pub const MIN_PHOTO_WIDTH: f32 = 360.0;
|
||||
|
||||
/// What the Keywording box shows: the keywords (chips to edit), them with the keywords containing
|
||||
/// them, or what exported files will carry (read only).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum KeywordingView {
|
||||
#[default]
|
||||
Keywords,
|
||||
Containing,
|
||||
WillExport,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct UiState {
|
||||
@@ -449,9 +460,8 @@ 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,
|
||||
/// What the Keywording box shows (Lightroom Classic's Keyword Tags views).
|
||||
pub keywording_view: KeywordingView,
|
||||
/// 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).
|
||||
@@ -798,7 +808,7 @@ impl Default for UiState {
|
||||
slideshow: None,
|
||||
second_window: false,
|
||||
keyword_painter: None,
|
||||
keywording_will_export: false,
|
||||
keywording_view: KeywordingView::default(),
|
||||
keyword_list_open: Vec::new(),
|
||||
keyword_list_selected: None,
|
||||
dragging_keyword: None,
|
||||
|
||||
@@ -213,3 +213,17 @@ fn escape_gives_back_the_text_from_before_the_edit() {
|
||||
let k = keywords_of(&h, ids[0]);
|
||||
assert!(k.contains(&"harbour".to_string()) && !k.contains(&"sea".to_string()), "{k:?}");
|
||||
}
|
||||
|
||||
/// "Keywords & Containing Keywords" lists the selection's keywords with the keywords containing
|
||||
/// them, flat (read only).
|
||||
#[test]
|
||||
fn containing_keywords_view_lists_parents_too() {
|
||||
let (mut h, ids) = two_selected();
|
||||
let r = h.request("engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [ids[0]], "addKeywords": ["Events|Birthdays"]}}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
ask(&mut h, "ui.clickWidget", json!({"id": "keywordView:containing"}));
|
||||
for name in ["Events", "Birthdays", "beach"] {
|
||||
assert!(has(&h, &format!("keywordContaining:{name}")), "{name}");
|
||||
}
|
||||
assert!(!has(&h, "keywordChip:beach"), "the chips give way");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user