mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-25 18:47:42 -06:00
LRC-LIB-KEYWORDS: what the check of the review fixes found
- An offer taken asked the box to start a new edit on the next frame; a box not drawn then (its panel switched away at once) took the cursor whenever it was next drawn. The request holds for one frame. - Return on the frame the box scrolled out of sight still took the highlighted offer, which was no longer drawn: it gives nothing. - In a window with room for the offers on neither side of the box they covered it: they go to the side with more room and show as many lines as fit there, the highlighted one among them. - Ukrainian says "suggestions" as the panel does; four clearer wordings (uk, ru, de, ja). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c2c85f5d97
commit
6bb52f09f6
@@ -1976,7 +1976,7 @@
|
||||
"Filter Keywords": "Stichwörter filtern",
|
||||
"No keywords yet": "Noch keine Stichwörter",
|
||||
"Show fewer": "Weniger anzeigen",
|
||||
"Whole names": "Ganze Namen",
|
||||
"Whole names": "Vollständige Namen",
|
||||
"Grouped by parent": "Nach übergeordnetem Stichwort gruppiert",
|
||||
"Keywords of the selection": "Stichwörter der Auswahl",
|
||||
"Grouped: keywords inside the same keyword share a line naming it, and long lists and the suggestions are shortened.": "Gruppiert: Stichwörter im selben Stichwort teilen sich eine Zeile mit dessen Namen; lange Listen und die Vorschläge werden gekürzt.",
|
||||
|
||||
@@ -1822,7 +1822,7 @@
|
||||
"Filter Keywords": "キーワードを絞り込む",
|
||||
"No keywords yet": "キーワードはまだありません",
|
||||
"Show fewer": "表示を減らす",
|
||||
"Whole names": "完全な名前",
|
||||
"Whole names": "パス全体",
|
||||
"Grouped by parent": "親キーワードでグループ化",
|
||||
"Keywords of the selection": "選択した写真のキーワード",
|
||||
"Grouped: keywords inside the same keyword share a line naming it, and long lists and the suggestions are shortened.": "グループ化:同じキーワードの中にあるキーワードは、その名前を示す行にまとめられ、長いリストと候補は短く表示されます。",
|
||||
|
||||
@@ -1530,7 +1530,7 @@
|
||||
"No keywords yet": "Ключевых слов пока нет",
|
||||
"Show fewer": "Показать меньше",
|
||||
"Whole names": "Полные имена",
|
||||
"Grouped by parent": "Сгруппированы по родительскому слову",
|
||||
"Grouped by parent": "По родительскому ключевому слову",
|
||||
"Keywords of the selection": "Ключевые слова выбранных фото",
|
||||
"Grouped: keywords inside the same keyword share a line naming it, and long lists and the suggestions are shortened.": "Сгруппированы: ключевые слова внутри одного ключевого слова стоят под строкой с его именем; длинные списки и подсказки сокращаются.",
|
||||
"Show every keyword of the selected photos": "Показать все ключевые слова выбранных фото",
|
||||
|
||||
@@ -2005,9 +2005,9 @@
|
||||
"No keywords yet": "Ключових слів ще немає",
|
||||
"Show fewer": "Показати менше",
|
||||
"Whole names": "Повні назви",
|
||||
"Grouped by parent": "Згруповано за батьківським словом",
|
||||
"Grouped by parent": "За батьківським ключовим словом",
|
||||
"Keywords of the selection": "Ключові слова вибраних фото",
|
||||
"Grouped: keywords inside the same keyword share a line naming it, and long lists and the suggestions are shortened.": "Згруповано: ключові слова всередині одного ключового слова стоять під рядком із його назвою; довгі списки та підказки скорочуються.",
|
||||
"Grouped: keywords inside the same keyword share a line naming it, and long lists and the suggestions are shortened.": "Згруповано: ключові слова всередині одного ключового слова стоять під рядком із його назвою; довгі списки та пропозиції скорочуються.",
|
||||
"Show every keyword of the selected photos": "Показати всі ключові слова вибраних фото",
|
||||
"new keyword": "нове ключове слово",
|
||||
"Return creates this keyword and gives it to the selected photos": "Клавіша Enter створить це ключове слово й додасть його вибраним фото",
|
||||
|
||||
@@ -417,11 +417,23 @@ pub(crate) fn offers_below(ui: &egui::Ui, below: egui::Rect, offers: &Offers, at
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut picked = None;
|
||||
let width = (below.width() - 10.0).max(60.0);
|
||||
// as tall as its lines: under the box, or over it where that is the only side with room
|
||||
let lines = offers.keywords.len().saturating_add(usize::from(offers.create.is_some()));
|
||||
let height = lines as f32 * 24.0 + if offers.create.is_some() && !offers.keywords.is_empty() { 17.0 } else { 10.0 };
|
||||
// under the box, or over it where the window has more room there; as many lines as fit that
|
||||
// side, so that the offers never cover the box (the highlighted one is always among them)
|
||||
let window = ui.ctx().content_rect();
|
||||
let over = below.bottom() + 2.0 + height > window.bottom() && below.top() - window.top() > window.bottom() - below.bottom();
|
||||
let create = usize::from(offers.create.is_some());
|
||||
let fits = |room: f32| {
|
||||
let lines = (room - 2.0 - if create > 0 && !offers.keywords.is_empty() { 17.0 } else { 10.0 }) / 24.0;
|
||||
if lines.is_finite() && lines >= 1.0 { lines as usize } else { 0 }
|
||||
};
|
||||
let wanted = offers.keywords.len().saturating_add(create);
|
||||
let (under, above) = (fits(window.bottom() - below.bottom()), fits(below.top() - window.top()));
|
||||
let over = under < wanted && above > under;
|
||||
let room = (if over { above } else { under }).clamp(1, wanted.max(1));
|
||||
// the keywords shown: those up to the highlighted one when not all fit
|
||||
let count = room.saturating_sub(create).min(offers.keywords.len());
|
||||
let lit = at.and_then(|k| offers.keywords.iter().position(|x| x == k));
|
||||
let first = lit.map_or(0, |a| a.saturating_add(1).saturating_sub(count)).min(offers.keywords.len().saturating_sub(count));
|
||||
let listed = offers.keywords.iter().skip(first).take(count);
|
||||
let area = egui::Area::new(egui::Id::new("keyword-offers")).order(egui::Order::Foreground).constrain(true);
|
||||
let area = if over {
|
||||
area.pivot(Align2::LEFT_BOTTOM).fixed_pos(below.left_top() - vec2(0.0, 2.0))
|
||||
@@ -453,7 +465,7 @@ pub(crate) fn offers_below(ui: &egui::Ui, below: egui::Rect, offers: &Offers, at
|
||||
}
|
||||
resp
|
||||
};
|
||||
for k in &offers.keywords {
|
||||
for k in listed {
|
||||
let (place, name) = k.rsplit_once(SEP).unwrap_or(("", k));
|
||||
let whole = k.replace(SEP, " › ");
|
||||
let lit = at == Some(k.as_str());
|
||||
@@ -467,7 +479,7 @@ pub(crate) fn offers_below(ui: &egui::Ui, below: egui::Rect, offers: &Offers, at
|
||||
}
|
||||
}
|
||||
if let Some(new) = &offers.create {
|
||||
if !offers.keywords.is_empty() {
|
||||
if count > 0 {
|
||||
ui.add_space(3.0);
|
||||
let (r, _) = ui.allocate_exact_size(vec2(width, 1.0), Sense::hover());
|
||||
ui.painter().rect_filled(r, 0.0, t.field_border);
|
||||
|
||||
@@ -914,8 +914,10 @@ fn keywording(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
// (after an offer was taken the box starts a new edit, empty: Esc has nothing to bring back)
|
||||
// (asked for on the frame before this one only: a box that wasn't drawn then, its panel
|
||||
// switched away, doesn't claim the cursor whenever it is next drawn)
|
||||
let bid = egui::Id::new("kw-begin");
|
||||
let begin = ui.data_mut(|d| d.remove_temp::<bool>(bid)).unwrap_or(false);
|
||||
let begin = ui.data_mut(|d| d.remove_temp::<u64>(bid)).is_some_and(|asked| asked.checked_add(1) == Some(pass));
|
||||
let r = crate::text_field::TextField::singleline("field:keyword", &mut text)
|
||||
.hint(crate::i18n::tr("Add keyword"))
|
||||
.width(f32::INFINITY)
|
||||
@@ -923,6 +925,10 @@ fn keywording(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
.show(ui);
|
||||
let in_sight = ui.clip_rect().contains(r.response.rect.center());
|
||||
let mut drawn = Rect::NOTHING;
|
||||
// (what isn't drawn can't be taken: Return on the frame the box scrolls out gives nothing)
|
||||
if !in_sight {
|
||||
picked = None;
|
||||
}
|
||||
if offers.is_none() {
|
||||
// (not over the offers)
|
||||
r.response.clone().on_hover_text(crate::i18n::tr("Separate several keywords with commas. Return gives them to the selected photos."));
|
||||
@@ -958,7 +964,7 @@ fn keywording(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
text.clear();
|
||||
// an offer taken leaves the cursor in the box, for the next keyword
|
||||
if !returned {
|
||||
ui.data_mut(|d| d.insert_temp(bid, true));
|
||||
ui.data_mut(|d| d.insert_temp(bid, pass));
|
||||
}
|
||||
}
|
||||
Err(e) => app.toast_error(ui.ctx(), e),
|
||||
|
||||
@@ -720,3 +720,75 @@ fn wrong_values_for_the_new_settings_keep_the_rest() {
|
||||
let ui: crate::UiState = serde_json::from_str(r#"{"settings": {"keywordChips": "grouped"}}"#).unwrap();
|
||||
assert_eq!(ui.settings.keyword_chips, crate::state::KeywordChips::Grouped);
|
||||
}
|
||||
|
||||
// Given an offer taken with ↓ and Return,
|
||||
// when the panel is switched away at once and comes back much later,
|
||||
// then the box does not take the cursor unasked: keys typed then are not the box's.
|
||||
#[test]
|
||||
fn an_offer_taken_does_not_claim_the_cursor_later() {
|
||||
let (mut h, ids) = with_places();
|
||||
typed(&mut h, "lis");
|
||||
ask(&mut h, "ui.key", json!({"key": "ArrowDown"}));
|
||||
// (Return and the switch on consecutive frames, as a script does them)
|
||||
let r = h.request("ui.key", json!({"key": "Enter"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
ask(&mut h, "ui.set", json!({"right": "info"}));
|
||||
assert!(all_have(&h, &ids, "Places|Portugal|Lisbon"));
|
||||
ask(&mut h, "ui.set", json!({"right": "keywords"}));
|
||||
ask(&mut h, "ui.text", json!({"text": "zz"}));
|
||||
ask(&mut h, "ui.key", json!({"key": "Enter"}));
|
||||
assert!(none_has(&h, &ids, "zz"), "typed with the cursor nowhere: {:?}", keywords_of(&h, ids[0]));
|
||||
}
|
||||
|
||||
// Given a highlighted offer and the box scrolled out of sight, Return gives nothing: what isn't
|
||||
// drawn can't be taken, not even on the frame the box leaves.
|
||||
#[test]
|
||||
fn an_offer_out_of_sight_is_not_taken() {
|
||||
let (mut h, ids) = with_places();
|
||||
give(&mut h, &ids, &sixteen());
|
||||
typed(&mut h, "lis");
|
||||
ask(&mut h, "ui.key", json!({"key": "ArrowDown"}));
|
||||
let panel = rect(&h, "panel:right_panel");
|
||||
ask(&mut h, "ui.move", json!({"x": panel.center().x, "y": panel.center().y}));
|
||||
for _ in 0..8 {
|
||||
// the wheel and Return together, frame after frame
|
||||
let r = h.request("ui.scroll", json!({"dy": -400.0}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
if rect(&h, "field:keyword").bottom() < panel.top() {
|
||||
let r = h.request("ui.key", json!({"key": "Enter"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
}
|
||||
}
|
||||
h.settle(SETTLE);
|
||||
ask(&mut h, "ui.key", json!({"key": "Enter"}));
|
||||
assert!(none_has(&h, &ids, "Places|Portugal|Lisbon"), "{:?}", keywords_of(&h, ids[0]));
|
||||
}
|
||||
|
||||
// Given a window too short for the offers under the box, they never cover the box being typed
|
||||
// in: they go where there is more room and show as many lines as fit there.
|
||||
#[test]
|
||||
fn offers_never_cover_the_box() {
|
||||
for height in [1000.0, 420.0, 300.0, 230.0, 180.0] {
|
||||
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
let mut h = Headless::new(app, [1400.0, height], 1.0);
|
||||
let ids: Vec<u64> = h.app.session.visible_cloned().iter().take(12).map(|p| p.0).collect();
|
||||
ask(&mut h, "ui.set", json!({"view": "photoGrid", "right": "keywords"}));
|
||||
// nine keywords to offer for "pa", on photos that are not selected
|
||||
let many: Vec<String> = (1..=9).map(|i| format!("Parks|park {i}")).collect();
|
||||
ask(&mut h, "engine.execute", json!({"command": "photo.setMeta", "params": {"ids": &ids[2..], "keywords": many}}));
|
||||
ask(&mut h, "engine.execute", json!({"command": "library.select", "params": {"ids": &ids[..2]}}));
|
||||
typed(&mut h, "pa");
|
||||
let field = rect(&h, "field:keyword");
|
||||
let offers: Vec<egui::Rect> = h.app.widgets.iter().filter(|(w, _)| w.starts_with("kwComplete")).map(|(_, r)| *r).collect();
|
||||
assert!(!offers.is_empty(), "{height}: something is offered");
|
||||
for r in &offers {
|
||||
assert!(!r.intersects(field.shrink(0.5)), "{height}: an offer at {r:?} covers the box at {field:?}");
|
||||
assert!(r.top() >= -0.5 && r.bottom() <= height + 0.5, "{height}: an offer at {r:?} is off the window");
|
||||
}
|
||||
// ↓ still reaches every offer drawn, and the highlighted one is among them
|
||||
for _ in 0..12 {
|
||||
ask(&mut h, "ui.key", json!({"key": "ArrowDown"}));
|
||||
assert_eq!(lit(&h).len(), 1, "{height}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user