diff --git a/crates/ui-egui/locales/it.json b/crates/ui-egui/locales/it.json index 67fc43753..e159fd47d 100644 --- a/crates/ui-egui/locales/it.json +++ b/crates/ui-egui/locales/it.json @@ -2336,6 +2336,7 @@ "View Options": "Opzioni di visualizzazione", "Loupe View": "Vista lente", "Show Info Overlay": "Mostra sovrapposizione informazioni", + "Show message when loading or rendering photos": "Mostra un messaggio durante il caricamento o il rendering delle foto", "Loupe Info 1": "Info lente 1", "Loupe Info 2": "Info lente 2", "Use Defaults": "Usa predefiniti", diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index c9e6a5ae0..30373a406 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -5017,6 +5017,69 @@ mod tests { assert!(!h.app.renderer.is_pending(Slot::Main)); } + /// View Options' Show message when loading or rendering photos (on by default): turned off in + /// the window, the loupe does not say "Rendering…" while an edit renders; `view.viewOptions + /// {loupeMessages}` turns it back on for an agent (refusing a value that is not true or false). + #[test] + fn view_options_turn_the_rendering_message_off() { + use crate::render::Slot; + use std::{cell::Cell, rc::Rc}; + let pill = |h: &Headless| h.app.widgets.iter().any(|(w, _)| w == "loupe:rendering"); + let mut h = demo([1000.0, 700.0]); + let t = Duration::from_secs(10); + h.request("ui.set", json!({"view": "detail", "right": "none"}), t); + h.settle(SETTLE); + assert!(h.app.ui.settings.loupe_messages, "on by default"); + assert_eq!(h.request("ui.key", json!({"key": "j", "cmd": true}), t)["ok"], true); + h.step(); + assert_eq!(h.request("ui.clickWidget", json!({"id": "check:viewOptions.loupeMessages"}), t)["ok"], true); + h.step(); + assert!(!h.app.ui.settings.loupe_messages, "the tick box turns it off"); + h.app.ui.dialog = None; + let open = Rc::new(Cell::new(false)); + h.app.renderer.set_offload(Box::new(super::RenderGate::new(open.clone()))); + h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), t); + for _ in 0..45 { + h.step(); + } + assert!(h.app.renderer.is_pending(Slot::Main), "the edit's render is held"); + assert!(!pill(&h), "no message while it is off"); + + let r = h.request("engine.execute", json!({"command": "view.viewOptions", "params": {"loupeMessages": "yes"}}), t); + assert_eq!(r["ok"], false, "{r}"); + assert!(!h.app.ui.settings.loupe_messages, "a refused change changes nothing"); + let r = h.request("engine.execute", json!({"command": "view.viewOptions", "params": {"loupeMessages": true}}), t); + assert_eq!(r["result"]["loupeMessages"], true, "{r}"); + assert_eq!(h.app.ui.dialog, None, "a value sets it without opening the window"); + assert!(h.step_until(SETTLE, pill), "the message shows again while the render is held"); + open.set(true); + assert!(h.step_until(SETTLE, |h| !pill(h)), "gone once the render lands"); + } + + /// With nothing to show yet, the loupe says "Rendering…" in its middle only while View Options' + /// Show message when loading or rendering photos is on. + #[test] + fn an_empty_loupe_says_rendering_only_with_the_message_on() { + use std::{cell::Cell, rc::Rc}; + for on in [true, false] { + let mut h = demo([1000.0, 700.0]); + let t = Duration::from_secs(10); + h.app.ui.settings.loupe_messages = on; + h.app.renderer.set_offload(Box::new(super::RenderGate::new(Rc::new(Cell::new(false))))); + h.request("ui.set", json!({"view": "detail", "right": "none"}), t); + // drop every texture (quick previews and thumbnails land outside the gate) until a + // frame has nothing to show + let empty = (0..60).any(|_| { + h.app.renderer.textures.clear(); + h.step(); + h.app.loupe_shown.map(|l| l.1) == Some("none") + }); + assert!(empty, "a frame with nothing rendered"); + let said = h.painted_text().iter().any(|text| text == "Rendering…"); + assert_eq!(said, on, "message on: {on}"); + } + } + /// A render that fails while the loupe shows a stand-in (the embedded JPEG, a thumbnail) left /// the stand-in up as if it were the photo: the loupe says the photo can't be opened, with the /// error on hover, and keeps saying so while a library change retries it. diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 8a4aa3649..86bb3217f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -751,10 +751,22 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { - // `info1` / `info2` set the lines, and `briefly` sets the Info that Show Briefly When Photo - // Changes shows, for an agent; without them the window opens + // `info1` / `info2` set the lines, `briefly` the Info that Show Briefly When Photo Changes + // shows and `loupeMessages` Show message when loading or rendering photos, for an agent; + // without any of them the window opens let mut info = app.ui.settings.loupe_info; let mut set = false; + // `loupeMessages` sets Show message when loading or rendering photos + let messages = match p.get("loupeMessages") { + None => app.ui.settings.loupe_messages, + Some(v) => match v.as_bool() { + Some(on) => { + set = true; + on + } + None => return Some(Err("`loupeMessages`: true|false".into())), + }, + }; for (key, lines) in [("info1", &mut info.info1), ("info2", &mut info.info2)] { let Some(v) = p.get(key) else { continue }; match serde_json::from_value(v.clone()) { @@ -774,10 +786,11 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option { let path = match p.get("path").and_then(Value::as_str) { diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 509a1c4f1..c77c96f42 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -844,9 +844,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { ); p.text(canvas.center() + vec2(0.0, 12.0), Align2::CENTER_CENTER, e, t.font(12.0), t.text_dim); } - None => { + None if app.ui.settings.loupe_messages => { p.text(canvas.center(), Align2::CENTER_CENTER, crate::i18n::tr("Rendering…"), t.font(13.0), t.text_dim); } + None => {} } } } @@ -998,9 +999,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let over = if over.is_positive() { over } else { canvas }; match failed { Some(e) => failed_pill(ui, app, &photo, canvas, over, nav, "loupe:renderFailed", e), - None => { + // View Options' Show message when loading or rendering photos + None if app.ui.settings.loupe_messages => { loupe_pill(ui, canvas, over, nav, "loupe:rendering", crate::i18n::tr("Rendering…"), false); } + None => {} } } if let Some(why) = photo.preview_only.as_deref().filter(|_| !fullscreen) { diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index fb01b0dab..d93aa0688 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -870,8 +870,9 @@ fn interface_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) { } /// View ▸ View Options (⌘J), Lightroom Classic's Loupe View tab: whether the info overlay shows and -/// which, the three lines each of Info 1 and Info 2 shows, and which of them shows briefly when the -/// photo changes (`view.viewOptions`). Changes apply as they are made. +/// which, the three lines each of Info 1 and Info 2 shows, which of them shows briefly when the +/// photo changes, and whether the loupe shows its loading and rendering message +/// (`view.viewOptions`). Changes apply as they are made. pub(super) fn view_options(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) { use crate::state::{InfoOverlay as I, LoupeInfo, LoupeInfoField as F}; heading(ui, t, crate::i18n::tr("Loupe View")); @@ -913,6 +914,8 @@ pub(super) fn view_options(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Token app.ui.settings.loupe_info.briefly = if briefly { info } else { I::Off }; } } + heading(ui, t, crate::i18n::tr("General")); + check(ui, "viewOptions.loupeMessages", &mut app.ui.settings.loupe_messages, "Show message when loading or rendering photos"); } // ----------------------------------------------------------------------------------- Slideshow diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index db9644a2f..4ce077354 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -483,6 +483,9 @@ pub struct AppSettings { pub film_badges: bool, /// Grid: when to show the rating / flag / edited badges. pub grid_badges: GridBadges, + /// View ▸ View Options ▸ Show message when loading or rendering photos: the loupe says + /// "Rendering…" while the photo it shows is not the finished render. + pub loupe_messages: bool, /// Shortcuts the user changed (Help ▸ Keyboard Shortcuts): command id → shortcut, `""` = none. pub keymap: crate::shortcuts::Keymap, /// Auto (follow the system), Dark or Light. @@ -548,6 +551,7 @@ impl Default for AppSettings { film_names: true, film_badges: true, grid_badges: GridBadges::Auto, + loupe_messages: true, keymap: Default::default(), appearance_mode: AppearanceMode::Dark, dark_theme: DarkTheme::Charcoal, @@ -1747,4 +1751,18 @@ mod loupe_info_tests { assert_eq!(newer.loupe_info, LoupeInfo::default()); assert!(!newer.film_names, "the rest of the settings are still read"); } + + /// Show message when loading or rendering photos is on until it is turned off, and is saved + /// with the app's settings. + #[test] + fn the_loading_message_choice_is_saved_with_the_settings() { + assert!(AppSettings::default().loupe_messages); + let old: AppSettings = serde_json::from_value(serde_json::json!({"filmNames": false})).unwrap(); + assert!(old.loupe_messages, "a settings file from before the choice existed keeps the message"); + let settings = AppSettings { loupe_messages: false, ..AppSettings::default() }; + let saved = serde_json::to_value(&settings).unwrap(); + assert_eq!(saved["loupeMessages"], false); + let read: AppSettings = serde_json::from_value(saved).unwrap(); + assert!(!read.loupe_messages); + } } diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 5a9af1ee7..098087a1d 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -730,7 +730,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06- | LRC-DEV-TAT | Targeted adjustment tools | P1 | ✅ | `cmd:develop.targeted` (`target`: curve / hue / sat / lum) | | | LRC-DEV-DEFAULTS | Per-camera raw defaults | P1 | ✅ | `cmd:library.preferences` (`camera`, `import.perCamera`), `crates/ui-egui/src/panels/settings.rs` | Settings → Import: raw default and per-camera presets | | LRC-DEV-PROCESS | Process versions, Update to Current Process | P1 | ✅ | `cmd:develop.updateProcess`, `cmd:develop.reset`, `cmd:develop.get`, `crates/develop/src/settings.rs`, `crates/pipeline/src/finish.rs`, `docs/process-versions.md` | each photo records the rendering process its settings are interpreted by: settings saved before this existed load as V1, new photos and Reset get the latest, virtual copies inherit, copy / paste / sync / presets keep each photo's own; Photo ▸ Update to Current Process (one undo step, enabled while a selected photo is on an older process); a number from a newer LightCraft is kept and renders with the newest known process. Only V1 exists so far: this is what lets a retuned default tone (#146) or stronger Light sliders (#196) ship without changing existing edits | -| LRC-DEV-VIEWOPTIONS | Develop view options | P2 | 🟡 | `cmd:view.viewOptions`, `crates/ui-egui/src/panels/settings.rs` (`view_options`) | View ▸ View Options… (⌘J), the Loupe View part: Show Info Overlay with Info 1 / Info 2, and the three lines of each (none, file name, capture date, dimensions, exposure, camera, lens, camera + lens) with Use Defaults; Show Briefly When Photo Changes under each Info (one at most): with the overlay off, that Info shows for 3 s each time the loupe changes photo; applied as they change, saved with the app settings; `view.viewOptions {info1, info2, briefly}` sets them for agents. Not yet: the Grid View tab, more fields (ratings, metadata, copy name), the loading-message and video-frame options | +| LRC-DEV-VIEWOPTIONS | Develop view options | P2 | 🟡 | `cmd:view.viewOptions`, `crates/ui-egui/src/panels/settings.rs` (`view_options`) | View ▸ View Options… (⌘J), the Loupe View part: Show Info Overlay with Info 1 / Info 2, and the three lines of each (none, file name, capture date, dimensions, exposure, camera, lens, camera + lens) with Use Defaults; Show Briefly When Photo Changes under each Info (one at most): with the overlay off, that Info shows for 3 s each time the loupe changes photo; General's Show message when loading or rendering photos (on by default; off, the loupe does not say "Rendering…"); applied as they change, saved with the app settings; `view.viewOptions {info1, info2, briefly, loupeMessages}` sets them for agents. Not yet: the Grid View tab, more fields (ratings, metadata, copy name), the video-frame option | | LRC-DEV-LOUPEOVERLAY | Loupe overlay: layout image | P2 | 🟡 | `cmd:view.chooseLayoutImage`, `cmd:view.layoutImage`, `cmd:view.layoutImageOpacity`, `cmd:view.layoutImageMatte`, `crates/ui-egui/src/panels/layout_overlay.rs` | View ▸ Loupe Overlay: a PNG with transparency (a cover layout, a banner's safe areas) fitted and centred over the loupe photo, following zoom and pan; opacity and matte (the photo darkened outside the graphic's box) in steps; ⌥⌘O shows / hides it; read off the UI thread; saved with the UI state. Not yet: the Grid and Guides overlays, moving and resizing the layout with ⌘ held, free opacity / matte sliders | | LRC-DEV-VIDEO | Video frame capture | P2 | ⬜ | | | | LRC-DEV-HISTOGRAM-DRAG | Drag on the histogram to adjust tone regions | P2 | ✅ | `crates/ui-egui/src/panels/histogram.rs`, `crates/ui-egui/src/panels/edit.rs`, `crates/develop/src/controls.rs`, `crates/ui-egui/src/tests_histogram_drag.rs`, `cmd:develop.set`, `cmd:develop.beginInteraction`, `cmd:develop.endInteraction`, `ctl:light.blacks`, `ctl:light.shadows`, `ctl:light.exposure`, `ctl:light.highlights`, `ctl:light.whites` | five zones across the plot's width, each moving the Basic control it names: Blacks 0.00–0.15, Shadows 0.15–0.35, Exposure 0.35–0.65, Highlights 0.65–0.85, Whites 0.85–1.00; every step is a `develop.set` between `develop.beginInteraction` and `develop.endInteraction` (the sliders' own path), so the draft render follows the drag live and the whole drag lands as one undo step; the zone the press landed in stays locked for the drag, so dragging across the others changes nothing; sideways travel of the plot's width sweeps that control's whole range (Shift a tenth, as on the sliders), clamped to its spec; hovering a zone tints it and shows its value in the strip under the plot ("Exposure: +0.45 EV"), with the sideways-resize cursor; a double-click resets that zone's control to its default in one undo step; the strip under the plot (readout / EXIF) and the clipping buttons in the plot's top corners are not zones |