mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
Merge PR #1308
# Conflicts: # crates/ui-egui/src/lib.rs # docs/parity-checklist.md
This commit is contained in:
@@ -1434,6 +1434,7 @@
|
||||
"Soft Pastel": "Sanftes Pastell",
|
||||
"Soft Proofing": "Softproof",
|
||||
"Soft Skin": "Weiche Haut",
|
||||
"Solo Mode": "Solo-Modus",
|
||||
"Sony ARWs from before about 2017 (RX100, RX100 II–V, RX10, NEX, SLT, ILCE-6000, A7 / A7 II / A7R II and their": "Sony-ARWs von vor etwa 2017, RX100, RX100 II–V, RX10, NEX, SLT, ILCE-6000, A7, A7 II, A7R II und verwandte",
|
||||
"Sony ILCE-7M4 downsized lossless ARWs now decode subsampled YCbCr tiles into linear RGB,": "Verkleinerte verlustfreie ARW-Dateien der Sony ILCE-7M4 werden jetzt aus unterabgetasteten YCbCr-Kacheln in lineares RGB decodiert.",
|
||||
"Sort": "Sortieren",
|
||||
|
||||
@@ -1444,6 +1444,7 @@
|
||||
"Soft Pastel": "Pastel suave",
|
||||
"Soft Proofing": "Prueba en pantalla",
|
||||
"Soft Skin": "Piel suave",
|
||||
"Solo Mode": "Modo solo",
|
||||
"Sort": "Ordenar",
|
||||
"Sort Albums by Name": "Ordenar álbumes por nombre",
|
||||
"Sort Contents A–Z": "Ordenar contenido A–Z",
|
||||
|
||||
@@ -1367,6 +1367,7 @@
|
||||
"Soft Pastel": "Pastel doux",
|
||||
"Soft Proofing": "Épreuvage à l'écran",
|
||||
"Soft Skin": "Peau douce",
|
||||
"Solo Mode": "Mode solo",
|
||||
"Sort": "Trier",
|
||||
"Sort by": "Trier par",
|
||||
"Source": "Source",
|
||||
|
||||
@@ -1312,6 +1312,7 @@
|
||||
"Soft Pastel": "ソフトパステル",
|
||||
"Soft Proofing": "ソフト校正",
|
||||
"Soft Skin": "ソフトスキン",
|
||||
"Solo Mode": "ソロモード",
|
||||
"Sort": "並べ替え",
|
||||
"Sort by": "並べ替え",
|
||||
"Source": "読み込み元",
|
||||
|
||||
@@ -1390,6 +1390,7 @@
|
||||
"Soft Pastel": "Pastel suave",
|
||||
"Soft Proofing": "Prova suave",
|
||||
"Soft Skin": "Pele suave",
|
||||
"Solo Mode": "Modo solo",
|
||||
"Sort": "Classificar",
|
||||
"Sort by": "Classificar por",
|
||||
"Source": "Origem",
|
||||
|
||||
@@ -1154,6 +1154,7 @@
|
||||
"Soft Pastel": "Мягкий пастельный",
|
||||
"Soft Proofing": "Экранная цветопроба",
|
||||
"Soft Skin": "Мягкая кожа",
|
||||
"Solo Mode": "Режим соло",
|
||||
"Sort": "Сортировка",
|
||||
"Sort by": "Сортировать по",
|
||||
"Source": "Источник",
|
||||
|
||||
@@ -1376,6 +1376,7 @@
|
||||
"Soft Pastel": "М’яка пастель",
|
||||
"Soft Proofing": "Екранна кольоропроба",
|
||||
"Soft Skin": "М’яка шкіра",
|
||||
"Solo Mode": "Режим соло",
|
||||
"Sort by": "Сортувати за",
|
||||
"Spherical": "Сферична",
|
||||
"Split Stack": "Розділити стос",
|
||||
|
||||
@@ -1375,6 +1375,7 @@
|
||||
"Soft Pastel": "柔和粉彩",
|
||||
"Soft Proofing": "软打样",
|
||||
"Soft Skin": "柔和肤质",
|
||||
"Solo Mode": "单独模式",
|
||||
"Sort": "排序",
|
||||
"Sort by": "排序方式",
|
||||
"Source": "来源",
|
||||
|
||||
@@ -1375,6 +1375,7 @@
|
||||
"Soft Pastel": "柔和粉彩",
|
||||
"Soft Proofing": "軟式校樣",
|
||||
"Soft Skin": "柔和膚質",
|
||||
"Solo Mode": "單獨模式",
|
||||
"Sort": "排序",
|
||||
"Sort by": "排序依據",
|
||||
"Source": "來源",
|
||||
|
||||
@@ -126,6 +126,8 @@ mod tests_scroll;
|
||||
#[cfg(test)]
|
||||
mod tests_slideshow;
|
||||
#[cfg(test)]
|
||||
mod tests_solo_sections;
|
||||
#[cfg(test)]
|
||||
mod tests_switch_library;
|
||||
#[cfg(test)]
|
||||
mod tests_sync;
|
||||
|
||||
@@ -328,6 +328,7 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option<bool> {
|
||||
"view.beforeAfterSplit" => Some(u.before_after == crate::state::BeforeAfter::Split),
|
||||
"view.autoAdvance" => Some(u.auto_advance),
|
||||
"view.filterBar" => Some(u.filter_bar),
|
||||
"view.soloSections" => Some(u.single_panel),
|
||||
"view.navigator" => Some(u.navigator),
|
||||
"view.fullScreenPreview" => Some(u.fullscreen),
|
||||
"view.lightsOut" => Some(u.lights_out != crate::state::LightsOut::On),
|
||||
|
||||
@@ -129,11 +129,15 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
("panel.versions", "Versions", Some("Shift+V"), "Window"),
|
||||
("panel.activity", "History", None, "Window"),
|
||||
("panel.close", "Close Panel", None, ""),
|
||||
// {solo?}: true opens just that section (⌥-click on its header); otherwise opens or closes it
|
||||
("section.light", "Light", Some("Cmd+1"), "Window>Edit Sections"),
|
||||
("section.color", "Color", Some("Cmd+2"), "Window>Edit Sections"),
|
||||
("section.effects", "Effects", Some("Cmd+3"), "Window>Edit Sections"),
|
||||
("section.detail", "Detail", Some("Cmd+4"), "Window>Edit Sections"),
|
||||
("section.optics", "Optics", Some("Cmd+5"), "Window>Edit Sections"),
|
||||
("section.calibration", "Calibration", None, ""),
|
||||
// {on?}; no params toggles. Opening one Edit section closes the others.
|
||||
("view.soloSections", "Solo Mode", None, "Window>Edit Sections"),
|
||||
("tool.brush", "Brush", Some("B"), "Window>Tools"),
|
||||
("tool.linear", "Linear Gradient", Some("L"), "Window>Tools"),
|
||||
("tool.radial", "Radial Gradient", Some("R"), "Window>Tools"),
|
||||
@@ -971,6 +975,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
}
|
||||
Ok(json!({"hidden": app.ui.hidden_locations}))
|
||||
}
|
||||
"view.soloSections" => bool_param(id, p, "on").map(|on| {
|
||||
app.ui.set_single_panel(on.unwrap_or(!app.ui.single_panel));
|
||||
json!({"soloSections": app.ui.single_panel})
|
||||
}),
|
||||
"view.filterBar" => {
|
||||
app.ui.filter_bar = !app.ui.filter_bar;
|
||||
if app.ui.filter_bar && !matches!(app.ui.view, ViewMode::PhotoGrid | ViewMode::SquareGrid) {
|
||||
@@ -1051,14 +1059,22 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
app.ui.presets = false;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
s if s.starts_with("section.") => {
|
||||
s if s.starts_with("section.") => bool_param(s, p, "solo").and_then(|solo| {
|
||||
// only the Edit sections listed above: an unknown one would close the real ones
|
||||
if !UI_COMMANDS.iter().any(|c| c.0 == s) {
|
||||
return Err(format!("{s}: no such Edit section"));
|
||||
}
|
||||
let sec = &s["section.".len()..];
|
||||
if app.ui.right != RightPanel::Edit {
|
||||
app.ui.right = RightPanel::Edit;
|
||||
}
|
||||
app.ui.toggle_section(sec);
|
||||
if solo == Some(true) {
|
||||
app.ui.solo_section(sec);
|
||||
} else {
|
||||
app.ui.toggle_section(sec);
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
}),
|
||||
s if s.starts_with("tool.") => {
|
||||
let tool = &s["tool.".len()..];
|
||||
match tool {
|
||||
|
||||
@@ -533,8 +533,23 @@ fn section(
|
||||
if let Some(on) = toggled {
|
||||
let _ = app.run("develop.sectionEnabled", json!({"section": id, "enabled": on}));
|
||||
} else if resp.clicked() {
|
||||
app.ui.toggle_section(id);
|
||||
// ⌥-click opens just this section, as in Lightroom Classic
|
||||
let solo = ui.input(|i| i.modifiers.alt);
|
||||
if let Err(e) = app.run(&format!("section.{id}"), json!({"solo": solo})) {
|
||||
app.toast(ui.ctx(), e);
|
||||
}
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
let mut solo = app.ui.single_panel;
|
||||
let item = ui.checkbox(&mut solo, crate::i18n::tr("Solo Mode"));
|
||||
register(ui.ctx(), "sectionMenu:solo", item.rect);
|
||||
if item.clicked() {
|
||||
if let Err(e) = app.run("view.soloSections", json!({"on": solo})) {
|
||||
app.toast(ui.ctx(), e);
|
||||
}
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
if open {
|
||||
body(app, ui, d);
|
||||
}
|
||||
|
||||
@@ -547,7 +547,7 @@ pub struct UiState {
|
||||
pub open_sections: Vec<String>,
|
||||
/// Open flyouts (curve, mixer, grading…).
|
||||
pub open_flyouts: Vec<String>,
|
||||
/// Single-panel mode: opening one section closes the others.
|
||||
/// Solo Mode (`view.soloSections`): opening one Edit section closes the others.
|
||||
pub single_panel: bool,
|
||||
pub show_clipping: bool,
|
||||
pub histogram: bool,
|
||||
@@ -1266,13 +1266,25 @@ impl UiState {
|
||||
pub fn toggle_section(&mut self, id: &str) {
|
||||
if self.section_open(id) {
|
||||
self.open_sections.retain(|s| s != id);
|
||||
} else if self.single_panel {
|
||||
self.solo_section(id);
|
||||
} else {
|
||||
if self.single_panel {
|
||||
self.open_sections.clear();
|
||||
}
|
||||
self.open_sections.push(id.to_string());
|
||||
}
|
||||
}
|
||||
/// Opens `id` and closes every other Edit section (⌥-click on a section header).
|
||||
pub fn solo_section(&mut self, id: &str) {
|
||||
self.open_sections.clear();
|
||||
self.open_sections.push(id.to_string());
|
||||
}
|
||||
/// Turns Solo Mode on or off. Turning it on keeps only the section opened last.
|
||||
pub fn set_single_panel(&mut self, on: bool) {
|
||||
self.single_panel = on;
|
||||
if on {
|
||||
let keep = self.open_sections.len().saturating_sub(1);
|
||||
self.open_sections.drain(..keep);
|
||||
}
|
||||
}
|
||||
/// Whether the sidebar section with this id is folded shut (an unknown id: no).
|
||||
pub fn sidebar_section_collapsed(&self, id: &str) -> bool {
|
||||
crate::sidebar::SidebarSection::from_id(id).is_some_and(|s| self.sidebar.is_collapsed(s))
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! Headless tests of Solo Mode: opening one Edit section closes the others (Lightroom Classic's
|
||||
//! panel Solo Mode), from the Window menu command, the section header's menu and ⌥-click.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::headless::Headless;
|
||||
use crate::{LightcraftApp, Services};
|
||||
|
||||
const T: Duration = Duration::from_secs(20);
|
||||
|
||||
/// Detail view with the Edit panel open and Light (the default) expanded.
|
||||
fn edit_open() -> Headless {
|
||||
let services = Services { png: None, ..Default::default() };
|
||||
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
let mut h = Headless::new(app, [1200.0, 1400.0], 1.0);
|
||||
let r = h.request("ui.set", json!({"view": "detail"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.app.ui.right = crate::state::RightPanel::Edit;
|
||||
h.app.ui.open_sections = vec!["light".into()];
|
||||
h.step();
|
||||
h.step();
|
||||
h
|
||||
}
|
||||
|
||||
fn exec(h: &mut Headless, command: &str, params: serde_json::Value) -> serde_json::Value {
|
||||
let r = h.request("engine.execute", json!({"command": command, "params": params}), T);
|
||||
assert_eq!(r["ok"], true, "{command}: {r}");
|
||||
r["result"].clone()
|
||||
}
|
||||
|
||||
/// A real click through the control channel: `{id, button?, alt?}` as `ui.clickWidget` takes them.
|
||||
fn click(h: &mut Headless, params: serde_json::Value) {
|
||||
let r = h.request("ui.clickWidget", params.clone(), T);
|
||||
assert_eq!(r["ok"], true, "click {params}: {r}");
|
||||
h.step();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solo_mode_closes_the_other_sections_when_one_opens() {
|
||||
let mut h = edit_open();
|
||||
click(&mut h, json!({"id": "section:color"}));
|
||||
assert_eq!(h.app.ui.open_sections, ["light", "color"], "off: sections open side by side");
|
||||
|
||||
assert_eq!(exec(&mut h, "view.soloSections", json!({})), json!({"soloSections": true}));
|
||||
assert_eq!(h.app.ui.open_sections, ["color"], "turning it on keeps the section opened last");
|
||||
assert_eq!(crate::menubar::checked(&h.app, "view.soloSections"), Some(true));
|
||||
h.step();
|
||||
|
||||
click(&mut h, json!({"id": "section:detail"}));
|
||||
assert_eq!(h.app.ui.open_sections, ["detail"]);
|
||||
// the Window > Edit Sections shortcuts follow it too
|
||||
exec(&mut h, "section.optics", json!({}));
|
||||
assert_eq!(h.app.ui.open_sections, ["optics"]);
|
||||
// closing the open one leaves none open
|
||||
exec(&mut h, "section.optics", json!({}));
|
||||
assert!(h.app.ui.open_sections.is_empty());
|
||||
|
||||
assert_eq!(exec(&mut h, "view.soloSections", json!({"on": false})), json!({"soloSections": false}));
|
||||
exec(&mut h, "section.light", json!({}));
|
||||
exec(&mut h, "section.color", json!({}));
|
||||
assert_eq!(h.app.ui.open_sections, ["light", "color"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_section_header_menu_turns_solo_mode_on() {
|
||||
let mut h = edit_open();
|
||||
click(&mut h, json!({"id": "section:color"}));
|
||||
click(&mut h, json!({"id": "section:effects", "button": "right"}));
|
||||
click(&mut h, json!({"id": "sectionMenu:solo"}));
|
||||
assert!(h.app.ui.single_panel, "the header's Solo Mode item turns it on");
|
||||
assert_eq!(h.app.ui.open_sections, ["color"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_click_opens_just_that_section() {
|
||||
let mut h = edit_open();
|
||||
click(&mut h, json!({"id": "section:color"}));
|
||||
click(&mut h, json!({"id": "section:detail", "alt": true}));
|
||||
assert_eq!(h.app.ui.open_sections, ["detail"], "⌥-click closes the others");
|
||||
assert!(!h.app.ui.single_panel, "⌥-click doesn't turn Solo Mode on");
|
||||
// agents do the same with the section's command (Calibration has one, though not in the menu)
|
||||
exec(&mut h, "section.calibration", json!({"solo": true}));
|
||||
assert_eq!(h.app.ui.open_sections, ["calibration"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_flags_and_unknown_sections_are_refused() {
|
||||
let mut h = edit_open();
|
||||
for (command, params) in
|
||||
[("view.soloSections", json!({"on": "yes"})), ("section.color", json!({"solo": 1})), ("section.typo", json!({"solo": true}))]
|
||||
{
|
||||
let r = h.request("engine.execute", json!({"command": command, "params": params}), T);
|
||||
assert_eq!(r["ok"], false, "{command} {params}: {r}");
|
||||
}
|
||||
assert!(!h.app.ui.single_panel);
|
||||
assert_eq!(h.app.ui.open_sections, ["light"]);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ cited here still exists. Formerly docs/parity.md.
|
||||
| C. Views & navigation (VIEW) | 15 | 1 | 1 | 0 | 8/9 (89%) | 4/4 (100%) |
|
||||
| D. Search & filter (FILT) | 11 | 1 | 1 | 0 | 4/4 (100%) | 4/4 (100%) |
|
||||
| E. Metadata (META) | 5 | 1 | 0 | 0 | 2/2 (100%) | 2/2 (100%) |
|
||||
| F. Edit panel — global adjustments (EDIT) | 43 | 2 | 3 | 1 | 28/28 (100%) | 13/14 (93%) |
|
||||
| F. Edit panel — global adjustments (EDIT) | 44 | 2 | 3 | 1 | 28/28 (100%) | 13/14 (93%) |
|
||||
| G. Profiles (PROF) | 7 | 1 | 3 | 0 | 3/4 (75%) | 3/3 (100%) |
|
||||
| H. Crop & rotate (CROP) | 9 | 0 | 0 | 1 | 6/6 (100%) | 3/3 (100%) |
|
||||
| I. Remove / healing (REM) | 8 | 1 | 2 | 2 | 4/4 (100%) | 2/3 (67%) |
|
||||
@@ -226,6 +226,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
|
||||
| LR-EDIT-LENSBLUR | Lens blur | P2 | ⬜ | | settings field reserved, not rendered |
|
||||
| LR-EDIT-CALIB | Calibration [Classic] | P1 | ✅ | `ctl:calibration.*` | shadows tint, red/green/blue primary hue and saturation; read/written in XMP |
|
||||
| LR-EDIT-SECTION-TOGGLE | Section on/off | P1 | ✅ | `cmd:develop.sectionEnabled`, `crates/develop/src/lib.rs` (`effective`) | the eye on a section header hides its adjustments: Light (with the curve), Color (white balance, presence, mixers, grading, point colour), Effects, Detail, Optics, Geometry, Calibration; masks stay on (issue #316 fixed Light, Color and Detail, which had no effect) |
|
||||
| LR-EDIT-SECTION-SOLO | Solo Mode for Edit sections | P2 | ✅ | `cmd:view.soloSections`, `cmd:section.calibration`, `crates/ui-egui/src/state.rs` (`toggle_section`, `set_single_panel`, `solo_section`), `crates/ui-egui/src/panels/edit.rs` (`section`), `crates/ui-egui/src/tests_solo_sections.rs` | Lightroom Classic's panel Solo Mode: while it is on, opening one Edit section (a header click or ⌘1–⌘5) closes the others. Window ▸ Edit Sections ▸ Solo Mode (checked while on) or a right-click on any section header (`sectionMenu:solo`) turns it on and off; turning it on keeps only the section opened last. ⌥-click on a header opens just that section once, without turning the mode on (agents: `section.<id>` `{solo: true}`, refused for a section the Edit panel has not got; header clicks run `section.<id>` too, Calibration included). `view.soloSections` `{on?}` (no params toggles) answers `{soloSections}`. Kept with the UI state (`singlePanel`) |
|
||||
| LR-EDIT-RESET | Reset all / section / slider | P0 | ✅ | `cmd:develop.reset`, `cmd:develop.resetSection`, `cmd:develop.resetControl`, `crates/ui-egui/src/widgets.rs` (double-click) | no "reset to open" |
|
||||
| LR-EDIT-SHOWORIG | Show original | P0 | ✅ | `cmd:view.showOriginal` | |
|
||||
|
||||
@@ -770,7 +771,7 @@ From `08-lightroom-classic-extras.md` (Classic-only features) and part 2 of `06-
|
||||
| LRC-WEB-IMAGEINFO | Web gallery image info | OOS | 🚫 | | |
|
||||
| LRC-WEB-OUTPUT | Web gallery output | OOS | 🚫 | | |
|
||||
| LRC-WEB-UPLOAD | Web gallery upload | OOS | 🚫 | | |
|
||||
| KEYC-PANELS | Classic panel keys (Tab, ⇧Tab, T, F5–F8, solo) | P2 | ⬜ | | |
|
||||
| KEYC-PANELS | Classic panel keys (Tab, ⇧Tab, T, F5–F8, solo) | P2 | ⬜ | | solo itself (⌥-click a header, Solo Mode) is done: see LR-EDIT-SECTION-SOLO |
|
||||
| KEYC-MODULES | Classic module switching (⌘⌥1–7) | P2 | 🚫 | | no modules in LightCraft |
|
||||
| KEYC-VIEWS | Classic view keys (E, G, C, N, L, F, I, ⇧R, ⌘⌥0) | P2 | 🟡 | `cmd:view.photoGrid`, `cmd:view.zoom100` | G works; E opens Edit (not loupe); no compare/survey/lights-out/screen modes |
|
||||
| KEYC-SECONDWINDOW | Classic secondary-window keys | P2 | ⬜ | | |
|
||||
@@ -796,6 +797,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | LR-IMP-CAMERA-COVERAGE: 17 uncompressed Olympus and Samsung Adobe models verified on one CC0 file each; status unchanged (🟡) |
|
||||
| 2026-10-13 | minor | LR-EDIT-SECTION-SOLO: Solo Mode for Edit sections (Window ▸ Edit Sections, section-header menu, ⌥-click); KEYC-PANELS notes it |
|
||||
| 2026-10-13 | patch | LR-VIEW-COMPARE, LRC-LIB-COMPARE: ⌘/Ctrl- and Shift-clicks on the filmstrip in Compare change the selection as in the grid; the two photos shown stay selected and the active photo is unchanged (#187); statuses unchanged |
|
||||
| 2026-10-13 | minor | KEYC-SELECTONLYACTIVE: Edit → Select Only Active Photo (⌘⇧D) lands as a new row; status 🟡 (headless-tested, native macOS menu path unverified) |
|
||||
| 2026-10-13 | patch | LRC-LIB-CATALOG-IMPORT: a catalog whose tables are all still in its `-wal` file (main file schema format 0) opens instead of failing as an unsupported schema format; status unchanged |
|
||||
|
||||
Reference in New Issue
Block a user