mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
LR-BEHAV-TEXTFIELD: Return confirms the one-field dialogs
New Album, Rename Album, New Smart Album, Merge Keywords and the text prompts (rename a preset or a version, …) had Rename Keyword's bug: their field took the focus back on every frame, so egui never reported the focus Return gives up, and Return did nothing; only OK confirmed. All six now use one `dialog_field`: a shared text field that keeps the focus except while its own menu has it, opens with its text selected and reports Return. They get the Cut / Copy / Paste / Select All menu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9f001c5a53
commit
e92095a57a
@@ -1600,6 +1600,50 @@ mod tests {
|
||||
assert!(keywords(&h).contains(&"weddings".to_string()), "renamed nothing: {:?}", keywords(&h));
|
||||
}
|
||||
|
||||
/// Return confirms the one-field dialogs: New Album, Rename Album, New Smart Album, Merge
|
||||
/// Keywords and a text prompt (here: renaming an album). Their name opens selected, so typing
|
||||
/// replaces it. Return did nothing: the field took the focus back on every frame, so egui never
|
||||
/// reported the focus Return gives up.
|
||||
#[test]
|
||||
fn return_confirms_the_one_field_dialogs() {
|
||||
use crate::state::Dialog;
|
||||
let mut h = demo([1200.0, 800.0]);
|
||||
let t = Duration::from_secs(10);
|
||||
let first = h.app.session.visible_cloned()[0].0;
|
||||
h.request("engine.execute", json!({"command": "photo.setMeta", "params": {"ids": [first], "addKeywords": ["holiday"]}}), t);
|
||||
let album = |h: &Headless, name: &str| h.app.session.catalog.albums().find(|a| a.name == name).map(|a| a.id.0);
|
||||
let garden = album(&h, "Garden").expect("the demo's Garden album");
|
||||
let confirm = |h: &mut Headless, dialog: Dialog, typed: &str| {
|
||||
h.app.ui.dialog = Some(dialog);
|
||||
h.settle(SETTLE);
|
||||
for (method, params) in [("ui.text", json!({"text": typed})), ("ui.key", json!({"key": "Enter"}))] {
|
||||
let r = h.request(method, params, t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
}
|
||||
assert_eq!(h.app.ui.dialog, None, "Return confirmed the dialog ({typed})");
|
||||
};
|
||||
confirm(&mut h, Dialog::NewAlbum { name: String::new(), folder: false, parent: None }, "Weddings");
|
||||
assert!(album(&h, "Weddings").is_some(), "New Album");
|
||||
confirm(&mut h, Dialog::RenameAlbum { id: garden, name: "Garden".into() }, "Flowers");
|
||||
assert_eq!(album(&h, "Flowers"), Some(garden), "Rename Album");
|
||||
confirm(&mut h, Dialog::NewSmartAlbum { name: "Smart Album".into(), parent: None }, "Everything");
|
||||
assert!(album(&h, "Everything").is_some(), "New Smart Album");
|
||||
confirm(&mut h, Dialog::MergeKeywords { from: vec!["holiday".into()], into: String::new() }, "travel");
|
||||
let keywords = h.app.session.catalog.photo(lightcraft_catalog::PhotoId(first)).unwrap().meta.keywords.clone();
|
||||
assert!(keywords.contains(&"travel".to_string()) && !keywords.contains(&"holiday".to_string()), "Merge Keywords: {keywords:?}");
|
||||
let prompt = Dialog::TextPrompt {
|
||||
title: "Rename Album".into(),
|
||||
hint: "Name".into(),
|
||||
value: "Flowers".into(),
|
||||
command: "album.rename".into(),
|
||||
params: json!({"id": garden}),
|
||||
key: "name".into(),
|
||||
};
|
||||
confirm(&mut h, prompt, "Botanical");
|
||||
assert_eq!(album(&h, "Botanical"), Some(garden), "the text prompt");
|
||||
}
|
||||
|
||||
/// The sidebar's file-system checks run on worker threads and add rows when they land, which
|
||||
/// moves every row below them: `busy()` counts them, so `settle` waits for them before a test
|
||||
/// reads widget positions (a click aimed at a stale rect hits the neighbouring row).
|
||||
|
||||
@@ -260,9 +260,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
);
|
||||
}
|
||||
Dialog::NewSmartAlbum { name, .. } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text(crate::i18n::tr("Name")).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
if dialog_field(ui, "field:smartAlbumName", name, "Name") {
|
||||
confirm = true;
|
||||
}
|
||||
let rules = app.session.view_rules();
|
||||
@@ -410,17 +408,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
Dialog::RenameKeyword { from, to } => {
|
||||
let n = app.session.catalog.photos().filter(|p| p.meta.keywords.iter().any(|k| lightcraft_catalog::keywords::is_under(k, from))).count();
|
||||
// the name, selected so typing replaces it; Esc cancels the dialog (below)
|
||||
let r = crate::text_field::TextField::singleline("field:keywordName", to)
|
||||
.hint(crate::i18n::tr("New name"))
|
||||
.width(f32::INFINITY)
|
||||
.select_on_focus(true)
|
||||
.show(ui);
|
||||
// the field keeps the focus, but leaves it to its own menu
|
||||
if !r.editing {
|
||||
r.response.request_focus();
|
||||
}
|
||||
if r.ending == Some(crate::text_field::Ending::Return) {
|
||||
if dialog_field(ui, "field:keywordName", to, "New name") {
|
||||
confirm = true;
|
||||
}
|
||||
ui.label(
|
||||
@@ -431,10 +419,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
Dialog::MergeKeywords { from, into } => {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr_format!("Replace {} with:", from.iter().map(|f| format!("“{f}”")).collect::<Vec<_>>().join(", "))).color(t.text_label));
|
||||
let r = ui.add(egui::TextEdit::singleline(into).hint_text(crate::i18n::tr("Keyword")).desired_width(f32::INFINITY));
|
||||
crate::widgets::register(ui.ctx(), "field:keywordInto", r.rect);
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
if dialog_field(ui, "field:keywordInto", into, "Keyword") {
|
||||
confirm = true;
|
||||
}
|
||||
let options: Vec<String> = app
|
||||
@@ -453,16 +438,12 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
}
|
||||
Dialog::TextPrompt { value, hint, .. } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(value).hint_text(crate::i18n::tr(hint.as_str())).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
if dialog_field(ui, "field:prompt", value, hint) {
|
||||
confirm = true;
|
||||
}
|
||||
}
|
||||
Dialog::NewAlbum { name, .. } | Dialog::RenameAlbum { name, .. } => {
|
||||
let r = ui.add(egui::TextEdit::singleline(name).hint_text(crate::i18n::tr("Name")).desired_width(f32::INFINITY));
|
||||
r.request_focus();
|
||||
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
if dialog_field(ui, "field:albumName", name, "Name") {
|
||||
confirm = true;
|
||||
}
|
||||
}
|
||||
@@ -1302,3 +1283,15 @@ fn choices<V: PartialEq + Copy>(ui: &mut egui::Ui, label: &str, id: &str, option
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A dialog's one text field: it keeps the focus (except while its own menu has it) and opens
|
||||
/// with its text selected, so typing replaces it. True when Return confirms the dialog; Esc
|
||||
/// cancels the dialog (`show`). Taking the focus back with a bare `TextEdit` every frame hid the
|
||||
/// focus Return gives up, and Return did nothing.
|
||||
fn dialog_field(ui: &mut egui::Ui, widget: &str, text: &mut String, hint: &str) -> bool {
|
||||
let r = crate::text_field::TextField::singleline(widget, text).hint(crate::i18n::tr(hint)).width(f32::INFINITY).select_on_focus(true).show(ui);
|
||||
if !r.editing {
|
||||
r.response.request_focus();
|
||||
}
|
||||
r.ending == Some(crate::text_field::Ending::Return)
|
||||
}
|
||||
|
||||
+1
-1
@@ -525,7 +525,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-BEHAV-PANEL-RESIZE | Resizable side panels | P1 | ✅ | `crates/ui-egui/src/panels/mod.rs` (`resizable_side`), `crates/ui-egui/src/state.rs` (`LEFT_WIDTH`, `RIGHT_WIDTH`), `crates/ui-egui/src/tests_panels.rs` | drag the left sidebar's right edge (200–480 pt) or the right panel's left edge (250–520 pt); the photo area keeps ≥ 360 pt; widths are kept across panels, views and restarts (`leftWidth` / `rightWidth` in the UI state); the Presets column stays fixed |
|
||||
| LR-BEHAV-EMPTY-STATES | Empty states | P1 | ✅ | `crates/ui-egui/src/panels/mod.rs` (`empty_message`) | |
|
||||
| LR-BEHAV-TOOLTIPS | Tooltips with shortcuts | P0 | ✅ | `crates/ui-egui/src/panels/bottombar.rs` | |
|
||||
| LR-BEHAV-TEXTFIELD | Text fields behave alike | P1 | 🟡 | `crates/ui-egui/src/text_field.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/right.rs` (`meta_field`), `crates/ui-egui/src/panels/dialogs.rs`, `crates/ui-egui/src/panels/unnamed.rs` | one shared field: right-click Cut / Copy / Paste / Select All (no Paste in the browser), Esc gives back the text from before the edit, Return / leaving / Esc reported to the caller, optional select-on-focus; agents use `ui.clipboard` (MCP `clipboard`) and `ui.inspect` → `copied`. Used by Search Photos, slider values, the Info panel's metadata fields (multi-line captions), Rename Keyword and the unnamed faces' name field so far; the other text fields (most dialogs, import, settings, masking, keywords …) still use egui's own and have no menu |
|
||||
| LR-BEHAV-TEXTFIELD | Text fields behave alike | P1 | 🟡 | `crates/ui-egui/src/text_field.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/right.rs` (`meta_field`), `crates/ui-egui/src/panels/dialogs.rs`, `crates/ui-egui/src/panels/unnamed.rs` | one shared field: right-click Cut / Copy / Paste / Select All (no Paste in the browser), Esc gives back the text from before the edit, Return / leaving / Esc reported to the caller, optional select-on-focus; agents use `ui.clipboard` (MCP `clipboard`) and `ui.inspect` → `copied`. Used by Search Photos, slider values, the Info panel's metadata fields (multi-line captions), the one-field dialogs (New / Rename Album, New Smart Album, Rename / Merge Keywords, text prompts) and the unnamed faces' name field so far; the other text fields (most dialogs, import, settings, masking, keywords …) still use egui's own and have no menu |
|
||||
| LR-BEHAV-HEADLESS | Headless UI snapshots | P2 | ✅ | `apps/lightcraft-cli/src/main.rs`, `crates/ui-egui/src/headless.rs`, `docs/control-protocol.md` | `snapshot` rasterizes the UI and defaults photo development to the CPU before the first frame, without adapter discovery; scaled dimensions are validated before allocation, and UI zoom preserves the requested physical viewport; JSON-lines control scripts are supported |
|
||||
| LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/panels/grid.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control and value, buttons / icon buttons / dropdowns / section headers / sources their labels and state, grid thumbnails file name, rating, flag and label; the canvas tools (crop, masks) are pointer-only; not audited with a screen reader |
|
||||
| LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese`, `cmd:app.language.spanish`, `cmd:app.language.german`, `cmd:app.language.russian` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) |
|
||||
|
||||
+5
-3
@@ -3,9 +3,11 @@
|
||||
## October 2026
|
||||
|
||||
### Text fields
|
||||
- Right-click Search Photos, a slider's typed value, the Info panel's fields, Rename Keyword or the name field under
|
||||
unnamed faces for Cut, Copy, Paste and Select All. Esc in them gives back the text from before you started typing (in
|
||||
the Info panel it no longer saves it). Return renames a keyword. More fields will follow.
|
||||
- Right-click Search Photos, a slider's typed value, the Info panel's fields, the name field of New Album, Rename
|
||||
Album, New Smart Album, Rename Keyword, Merge Keywords and similar dialogs, or the name field under unnamed faces for
|
||||
Cut, Copy, Paste and Select All. Esc in them gives back the text from before you started typing (in the Info panel it
|
||||
no longer saves it). Return in those dialogs now confirms them, and their name opens selected. More fields will
|
||||
follow.
|
||||
- On Windows and Linux, Ctrl+C outside a text field copies edit settings and Ctrl+V pastes them (⇧Ctrl+V: Paste
|
||||
Selected Settings). Ctrl+V only reaches LightCraft while the system clipboard holds text; otherwise use Edit ▸ Paste Edit
|
||||
Settings.
|
||||
|
||||
Reference in New Issue
Block a user