Merge PR #1195
# Conflicts: # docs/gaps.md # docs/parity-checklist.md # docs/ui-parity.md
This commit is contained in:
commit
ac40fa3882
|
|
@ -26,8 +26,10 @@ const QUIT: &str = "app.quit";
|
|||
const SETTINGS: &str = "app.settings";
|
||||
const SETTINGS_KEY: &str = "Cmd+,";
|
||||
/// Keys whose meaning depends on the panel (X = reject or swap crop aspect, ⌫ = delete the photo
|
||||
/// or the active mask): left to the egui handler, which knows the context.
|
||||
const CONTEXTUAL: &[&str] = &["X", "Delete"];
|
||||
/// or the active mask): left to the egui handler, which knows the context. Shift+Tab (Hide All
|
||||
/// Panels) stays there too: it has no native accelerator, and the egui handler cancels egui's own
|
||||
/// Shift+Tab focus traversal when it fires.
|
||||
const CONTEXTUAL: &[&str] = &["X", "Delete", "Shift+Tab"];
|
||||
/// Shortcuts text fields need while they have focus.
|
||||
const TEXT_EDIT: &[&str] = &["Cmd+A", "Cmd+C", "Cmd+V", "Cmd+X", "Cmd+Z", "Cmd+Shift+Z"];
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ mod tests_offline;
|
|||
#[cfg(test)]
|
||||
mod tests_open_lightroom;
|
||||
#[cfg(test)]
|
||||
mod tests_panel_keys;
|
||||
#[cfg(test)]
|
||||
mod tests_panels;
|
||||
#[cfg(test)]
|
||||
mod tests_preset_import;
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ const LAYOUT: &[(&str, &[&str])] = &[
|
|||
"view.people",
|
||||
"---",
|
||||
"view.leftPanel",
|
||||
"view.rightPanel",
|
||||
"@Sidebar Sections",
|
||||
"view.photoCounts",
|
||||
"view.faceBoxes",
|
||||
|
|
@ -138,6 +139,7 @@ const LAYOUT: &[(&str, &[&str])] = &[
|
|||
"view.infoOverlay",
|
||||
"@Appearance",
|
||||
"---",
|
||||
"view.allPanels",
|
||||
"view.fullScreenPreview",
|
||||
"view.lightsOut",
|
||||
"view.enterFullScreen",
|
||||
|
|
@ -314,6 +316,8 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option<bool> {
|
|||
"view.people" => Some(u.view == ViewMode::People),
|
||||
"view.reference" => Some(u.view == ViewMode::Reference),
|
||||
"view.leftPanel" => Some(u.left_panel),
|
||||
"view.rightPanel" => Some(u.right != RightPanel::None),
|
||||
"view.allPanels" => Some(u.all_panels_hidden()),
|
||||
"view.faceBoxes" => Some(u.face_boxes),
|
||||
"view.filmstrip" => Some(u.filmstrip),
|
||||
"view.frameRate" => Some(u.frame_rate),
|
||||
|
|
|
|||
|
|
@ -73,6 +73,10 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
|||
("view.autoAdvance", "Auto Advance", None, "Photo"),
|
||||
("view.filmstrip", "Filmstrip", Some("/"), "View"),
|
||||
("view.leftPanel", "My Photos Panel", Some("Cmd+Shift+L"), "View"),
|
||||
// F8 / Shift+Tab as in Lightroom Classic (F7 and F6 are PR #1078; Tab toggles the side panels there).
|
||||
// On a Mac keyboard F8 is Play/Pause unless fn is held or "Use F1, F2, … as standard function keys" is on.
|
||||
("view.rightPanel", "Right Panel", Some("F8"), "View"),
|
||||
("view.allPanels", "Hide All Panels", Some("Shift+Tab"), "View"),
|
||||
("view.beforeAfter", "Compare Before and After", Some("Y"), "View"),
|
||||
("view.beforeAfterSplit", "Before/After Split", Some("Shift+Y"), "View"),
|
||||
("view.beforeAfterTopBottom", "Before/After Top/Bottom", Some("Alt+Y"), "View"),
|
||||
|
|
@ -225,6 +229,14 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
|||
("app.exportPrevious", "Export with Previous", Some("Cmd+Alt+Shift+E"), "File"),
|
||||
];
|
||||
|
||||
/// A tool panel going away ends its gesture, as closing it from its own key does.
|
||||
fn end_panel_tool(app: &mut LightcraftApp) {
|
||||
if app.ui.tool != "wbPicker" {
|
||||
app.ui.tool.clear();
|
||||
}
|
||||
let _ = app.session.end_interaction();
|
||||
}
|
||||
|
||||
fn panel(app: &mut LightcraftApp, ctx: &egui::Context, p: RightPanel, name: &str) {
|
||||
if app.ui.right == p && crate::docking::exposed(app, crate::docking::Panel::Tools) {
|
||||
app.ui.right = RightPanel::None;
|
||||
|
|
@ -684,6 +696,24 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
|||
app.ui.left_panel = !app.ui.left_panel;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
// F8: `show` (true / false) says which; without it the panel toggles
|
||||
"view.rightPanel" => {
|
||||
let was = app.ui.right;
|
||||
let shown = app.ui.toggle_right_panel(p.get("show").and_then(Value::as_bool));
|
||||
if was != RightPanel::None && !shown {
|
||||
end_panel_tool(app);
|
||||
}
|
||||
Ok(json!({"shown": shown, "right": app.ui.right}))
|
||||
}
|
||||
// Shift+Tab: `hide` (true / false) says which; without it the panels hide, then come back
|
||||
"view.allPanels" => {
|
||||
let was = app.ui.right;
|
||||
let hidden = app.ui.toggle_all_panels(p.get("hide").and_then(Value::as_bool));
|
||||
if was != RightPanel::None && app.ui.right == RightPanel::None {
|
||||
end_panel_tool(app);
|
||||
}
|
||||
Ok(json!({"hidden": hidden}))
|
||||
}
|
||||
"view.beforeAfter" => {
|
||||
app.ui.before_after = if app.ui.before_after == BeforeAfter::SideBySide { BeforeAfter::Off } else { BeforeAfter::SideBySide };
|
||||
Ok(Value::Null)
|
||||
|
|
|
|||
|
|
@ -392,6 +392,13 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
});
|
||||
// Shift+Tab is also egui's "focus the previous widget", decided when the pass began and without
|
||||
// consuming the key: with nothing focused it would put the keyboard in the last text field
|
||||
// that took part in focus traversal. Hiding the panels must not do that.
|
||||
if fire.iter().any(|f| f == "view.allPanels") {
|
||||
ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::Tab));
|
||||
ctx.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
|
||||
}
|
||||
use crate::panels::compare;
|
||||
// rating/flag/label keys: in Compare/Survey they act on the active photo only; Shift+key or
|
||||
// Auto Advance then moves on (next candidate in Compare, next photo elsewhere)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,26 @@ impl RightPanel {
|
|||
pub fn is_edit_tool(self) -> bool {
|
||||
matches!(self, RightPanel::Edit | RightPanel::Profiles | RightPanel::Crop | RightPanel::Remove | RightPanel::Masking | RightPanel::RedEye)
|
||||
}
|
||||
|
||||
/// What comes back when the panel is shown again (F8, Shift+Tab). The tools that work on the
|
||||
/// photo (crop, remove, masking, red eye) end when their panel is hidden, so they return as
|
||||
/// Edit; every other panel returns as it was.
|
||||
pub fn restored(self) -> RightPanel {
|
||||
match self {
|
||||
RightPanel::None | RightPanel::Crop | RightPanel::Remove | RightPanel::Masking | RightPanel::RedEye => RightPanel::Edit,
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The panels Shift+Tab (Hide All Panels) hid, to bring back exactly those.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct HiddenPanels {
|
||||
pub left_panel: bool,
|
||||
pub right: RightPanel,
|
||||
pub presets: bool,
|
||||
pub filmstrip: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -503,6 +523,11 @@ pub struct UiState {
|
|||
pub view: ViewMode,
|
||||
pub left_panel: bool,
|
||||
pub right: RightPanel,
|
||||
/// The panel the right side showed before F8 (or Hide All Panels) hid it; F8 brings it back.
|
||||
pub right_hidden: RightPanel,
|
||||
/// Set while Hide All Panels (Shift+Tab) is in effect: what to bring back. Showing any panel
|
||||
/// by hand makes it stale (see [`UiState::all_panels_hidden`]).
|
||||
pub hidden_panels: Option<HiddenPanels>,
|
||||
/// Widths of the left sidebar and of the right panel in points (dragging their inner edge
|
||||
/// resizes them; within [`LEFT_WIDTH`] / [`RIGHT_WIDTH`]).
|
||||
pub left_width: f32,
|
||||
|
|
@ -1050,6 +1075,8 @@ impl Default for UiState {
|
|||
left_width: LEFT_WIDTH.default,
|
||||
right_width: RIGHT_WIDTH.default,
|
||||
right: RightPanel::Edit,
|
||||
right_hidden: RightPanel::None,
|
||||
hidden_panels: None,
|
||||
presets: false,
|
||||
preset_thumbs: false,
|
||||
filmstrip: true,
|
||||
|
|
@ -1234,3 +1261,70 @@ fn deserialize_favorite_themes<'de, D: serde::Deserializer<'de>>(d: D) -> Result
|
|||
}
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
impl UiState {
|
||||
/// Every side panel and the filmstrip are hidden.
|
||||
fn no_panel_shown(&self) -> bool {
|
||||
!self.left_panel && self.right == RightPanel::None && !self.presets && !self.filmstrip
|
||||
}
|
||||
|
||||
/// Hide All Panels is in effect: its snapshot is kept and nothing has been shown since. (Showing
|
||||
/// a panel by hand drops it out of this state, so the next Shift+Tab hides again instead of
|
||||
/// bringing back panels the user had put away.)
|
||||
pub fn all_panels_hidden(&self) -> bool {
|
||||
self.hidden_panels.is_some() && self.no_panel_shown()
|
||||
}
|
||||
|
||||
/// F8: hide the right panel, or show the one that was there (Edit when there was none).
|
||||
/// `show` forces the state. Returns whether the panel is shown now.
|
||||
pub fn toggle_right_panel(&mut self, show: Option<bool>) -> bool {
|
||||
let show = show.unwrap_or(self.right == RightPanel::None);
|
||||
if show && self.right == RightPanel::None {
|
||||
self.right = self.right_hidden.restored();
|
||||
} else if !show && self.right != RightPanel::None {
|
||||
self.right_hidden = self.right.restored();
|
||||
self.right = RightPanel::None;
|
||||
}
|
||||
self.right != RightPanel::None
|
||||
}
|
||||
|
||||
/// Shift+Tab: hide the side panels, the Presets column and the filmstrip, remembering what was
|
||||
/// shown; again, bring back exactly that. When nothing is shown and nothing was remembered, it
|
||||
/// shows the usual panels (left sidebar, the last right panel, filmstrip). `hide` forces the
|
||||
/// direction. Returns whether the panels are hidden now.
|
||||
pub fn toggle_all_panels(&mut self, hide: Option<bool>) -> bool {
|
||||
let hide = hide.unwrap_or_else(|| !self.all_panels_hidden() && !(self.no_panel_shown() && self.hidden_panels.is_none()));
|
||||
if hide {
|
||||
if self.no_panel_shown() {
|
||||
return true;
|
||||
}
|
||||
self.hidden_panels =
|
||||
Some(HiddenPanels { left_panel: self.left_panel, right: self.right, presets: self.presets, filmstrip: self.filmstrip });
|
||||
if self.right != RightPanel::None {
|
||||
self.right_hidden = self.right.restored();
|
||||
}
|
||||
self.left_panel = false;
|
||||
self.right = RightPanel::None;
|
||||
self.presets = false;
|
||||
self.filmstrip = false;
|
||||
} else {
|
||||
match self.hidden_panels.take() {
|
||||
Some(was) if self.no_panel_shown() => {
|
||||
self.left_panel = was.left_panel;
|
||||
self.right = if was.right == RightPanel::None { RightPanel::None } else { was.right.restored() };
|
||||
self.presets = was.presets;
|
||||
self.filmstrip = was.filmstrip;
|
||||
}
|
||||
_ => {
|
||||
self.hidden_panels = None;
|
||||
self.left_panel = true;
|
||||
if self.right == RightPanel::None {
|
||||
self.right = self.right_hidden.restored();
|
||||
}
|
||||
self.filmstrip = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.all_panels_hidden()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
//! Headless tests of the panel keys: F8 (Right Panel) and Shift+Tab (Hide All Panels), which hides
|
||||
//! the side panels, the Presets column and the filmstrip, and brings back exactly what it hid.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::headless::Headless;
|
||||
use crate::menubar::{MenuNode, checked, menu_bar};
|
||||
use crate::state::RightPanel;
|
||||
use crate::{LightcraftApp, Services};
|
||||
|
||||
const T: Duration = Duration::from_secs(20);
|
||||
const SETTLE: Duration = Duration::from_secs(120);
|
||||
|
||||
fn loupe() -> Headless {
|
||||
let services = Services { png: None, ..Default::default() };
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
app.ui.settings.gpu = false;
|
||||
let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
|
||||
let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
h
|
||||
}
|
||||
|
||||
fn key(h: &mut Headless, k: &str, shift: bool) {
|
||||
let r = h.request("ui.key", json!({"key": k, "shift": shift}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
}
|
||||
|
||||
fn run(h: &mut Headless, command: &str, params: Value) -> Value {
|
||||
let r = h.request("ui.menu.invoke", json!({"id": command, "params": params}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
r
|
||||
}
|
||||
|
||||
/// (left panel, right panel, presets, filmstrip)
|
||||
fn shown(h: &Headless) -> (bool, RightPanel, bool, bool) {
|
||||
(h.app.ui.left_panel, h.app.ui.right, h.app.ui.presets, h.app.ui.filmstrip)
|
||||
}
|
||||
|
||||
fn arrange(h: &mut Headless, left: bool, right: RightPanel, presets: bool, filmstrip: bool) {
|
||||
h.app.ui.left_panel = left;
|
||||
h.app.ui.right = right;
|
||||
h.app.ui.presets = presets;
|
||||
h.app.ui.filmstrip = filmstrip;
|
||||
h.step();
|
||||
h.step();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f8_hides_the_right_panel_and_brings_back_the_tool_that_was_there() {
|
||||
let mut h = loupe();
|
||||
for tool in [RightPanel::Edit, RightPanel::Keywords, RightPanel::Info, RightPanel::Versions, RightPanel::Activity, RightPanel::Profiles] {
|
||||
arrange(&mut h, true, tool, false, true);
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(shown(&h), (true, RightPanel::None, false, true), "F8 hides {tool:?} and nothing else");
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(shown(&h), (true, tool, false, true), "F8 shows {tool:?} again");
|
||||
}
|
||||
// the tools that act on the photo end when hidden and come back as Edit
|
||||
arrange(&mut h, true, RightPanel::Crop, false, true);
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(h.app.ui.right, RightPanel::None);
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(h.app.ui.right, RightPanel::Edit);
|
||||
// closed some other way, with nothing remembered: Edit
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, true, RightPanel::None, false, true);
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(h.app.ui.right, RightPanel::Edit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_tab_hides_all_panels_and_again_brings_back_exactly_those() {
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, true, RightPanel::Keywords, true, true);
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (false, RightPanel::None, false, false));
|
||||
assert!(h.app.ui.all_panels_hidden());
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (true, RightPanel::Keywords, true, true));
|
||||
assert!(!h.app.ui.all_panels_hidden());
|
||||
// a panel that was already hidden stays hidden
|
||||
arrange(&mut h, false, RightPanel::Info, false, true);
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (false, RightPanel::None, false, false));
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (false, RightPanel::Info, false, true));
|
||||
arrange(&mut h, true, RightPanel::None, false, false);
|
||||
key(&mut h, "Tab", true);
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (true, RightPanel::None, false, false), "a hidden right panel stays hidden too");
|
||||
// F8 afterwards still finds the tool it had
|
||||
arrange(&mut h, true, RightPanel::Versions, false, true);
|
||||
key(&mut h, "Tab", true);
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(h.app.ui.right, RightPanel::Versions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_all_with_nothing_shown_and_nothing_remembered_shows_the_usual_panels() {
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, false, RightPanel::None, false, false);
|
||||
assert!(h.app.ui.hidden_panels.is_none());
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (true, RightPanel::Edit, false, true));
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (false, RightPanel::None, false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn showing_one_panel_while_all_are_hidden_makes_the_next_shift_tab_hide_again() {
|
||||
for show in ["view.leftPanel", "view.filmstrip", "view.rightPanel", "panel.presets", "panel.info"] {
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, true, RightPanel::Edit, false, true);
|
||||
key(&mut h, "Tab", true);
|
||||
assert!(h.app.ui.all_panels_hidden());
|
||||
run(&mut h, show, json!({}));
|
||||
assert!(!h.app.ui.all_panels_hidden(), "{show} shows a panel");
|
||||
let one = shown(&h);
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(
|
||||
shown(&h),
|
||||
(false, RightPanel::None, false, false),
|
||||
"{show}: Shift+Tab hides what is shown ({one:?}), it doesn't resurrect the old state"
|
||||
);
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), one, "{show}: and the next one brings back what was shown");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_tab_leaves_keyboard_focus_alone() {
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, true, RightPanel::Edit, false, true);
|
||||
assert_eq!(h.view.ctx.memory(|m| m.focused()), None, "nothing has focus to begin with");
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(h.view.ctx.memory(|m| m.focused()), None, "Shift+Tab doesn't focus the last widget");
|
||||
assert!(!h.view.ctx.egui_wants_keyboard_input());
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(h.view.ctx.memory(|m| m.focused()), None);
|
||||
assert!(!h.view.ctx.egui_wants_keyboard_input());
|
||||
assert_eq!(shown(&h), (true, RightPanel::Edit, false, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_keys_belong_to_a_focused_text_field() {
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, true, RightPanel::Info, false, true);
|
||||
let r = h.request("ui.clickWidget", json!({"id": "field:search"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
assert!(h.view.ctx.egui_wants_keyboard_input(), "the search field has the keyboard");
|
||||
let field = h.view.ctx.memory(|m| m.focused());
|
||||
key(&mut h, "F8", false);
|
||||
key(&mut h, "Tab", true);
|
||||
assert_eq!(shown(&h), (true, RightPanel::Info, false, true), "F8 and Shift+Tab are typed into, not shortcuts");
|
||||
assert_ne!(h.view.ctx.memory(|m| m.focused()), field, "Shift+Tab still moves focus out of the field");
|
||||
// with the field left, the keys work again
|
||||
key(&mut h, "Escape", false);
|
||||
h.view.ctx.memory_mut(|m| m.stop_text_input());
|
||||
h.step();
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(h.app.ui.right, RightPanel::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_panel_commands_work_without_the_keys() {
|
||||
let mut h = loupe();
|
||||
arrange(&mut h, true, RightPanel::Keywords, false, true);
|
||||
let r = run(&mut h, "view.rightPanel", json!({}));
|
||||
assert_eq!(r["result"]["shown"], false, "{r}");
|
||||
assert_eq!(h.app.ui.right, RightPanel::None);
|
||||
// `show` says which way, whatever the state
|
||||
run(&mut h, "view.rightPanel", json!({"show": false}));
|
||||
assert_eq!(h.app.ui.right, RightPanel::None);
|
||||
run(&mut h, "view.rightPanel", json!({"show": true}));
|
||||
assert_eq!(h.app.ui.right, RightPanel::Keywords);
|
||||
run(&mut h, "view.rightPanel", json!({"show": true}));
|
||||
assert_eq!(h.app.ui.right, RightPanel::Keywords, "showing a shown panel keeps it");
|
||||
|
||||
let r = run(&mut h, "view.allPanels", json!({}));
|
||||
assert_eq!(r["result"]["hidden"], true, "{r}");
|
||||
assert_eq!(shown(&h), (false, RightPanel::None, false, false));
|
||||
run(&mut h, "view.allPanels", json!({"hide": true}));
|
||||
assert_eq!(shown(&h), (false, RightPanel::None, false, false), "hiding hidden panels keeps what was remembered");
|
||||
let r = run(&mut h, "view.allPanels", json!({}));
|
||||
assert_eq!(r["result"]["hidden"], false, "{r}");
|
||||
assert_eq!(shown(&h), (true, RightPanel::Keywords, false, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hiding_the_panels_ends_the_tool_in_use() {
|
||||
let mut h = loupe();
|
||||
run(&mut h, "panel.crop", json!({}));
|
||||
assert_eq!(h.app.ui.right, RightPanel::Crop);
|
||||
key(&mut h, "F8", false);
|
||||
assert_eq!(h.app.ui.right, RightPanel::None);
|
||||
assert!(h.app.ui.tool.is_empty());
|
||||
run(&mut h, "panel.masking", json!({}));
|
||||
run(&mut h, "view.allPanels", json!({}));
|
||||
assert_eq!(h.app.ui.right, RightPanel::None);
|
||||
run(&mut h, "view.allPanels", json!({}));
|
||||
assert_eq!(h.app.ui.right, RightPanel::Edit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_view_menu_lists_the_panel_commands_with_their_state() {
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default());
|
||||
let item = |app: &LightcraftApp, id: &str| -> Option<(String, Option<String>, Option<bool>)> {
|
||||
fn find(nodes: &[MenuNode], id: &str) -> Option<(String, Option<String>, Option<bool>)> {
|
||||
nodes.iter().find_map(|n| match n {
|
||||
MenuNode::Item { id: i, label, shortcut, checked, .. } if i == id => Some((label.clone(), shortcut.clone(), *checked)),
|
||||
MenuNode::Submenu { children, .. } => find(children, id),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
let bar = menu_bar(app);
|
||||
find(&bar.iter().find(|(t, _)| t == "View")?.1, id)
|
||||
};
|
||||
app.ui.left_panel = true;
|
||||
app.ui.right = RightPanel::Edit;
|
||||
app.ui.filmstrip = true;
|
||||
let (label, shortcut, on) = item(&app, "view.rightPanel").expect("View ▸ Right Panel");
|
||||
assert_eq!((label.as_str(), shortcut.as_deref(), on), ("Right Panel", Some("F8"), Some(true)));
|
||||
let (label, shortcut, on) = item(&app, "view.allPanels").expect("View ▸ Hide All Panels");
|
||||
assert_eq!((label.as_str(), shortcut.as_deref(), on), ("Hide All Panels", Some("Shift+Tab"), Some(false)));
|
||||
app.run("view.allPanels", json!({})).unwrap();
|
||||
assert_eq!(checked(&app, "view.rightPanel"), Some(false));
|
||||
assert_eq!(checked(&app, "view.allPanels"), Some(true));
|
||||
assert_eq!(checked(&app, "view.leftPanel"), Some(false));
|
||||
app.run("view.rightPanel", json!({})).unwrap();
|
||||
assert_eq!(checked(&app, "view.rightPanel"), Some(true));
|
||||
assert_eq!(checked(&app, "view.allPanels"), Some(false), "a panel is shown again, so the panels are no longer all hidden");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_keys_are_in_the_keymap_and_f1_opens_help() {
|
||||
use crate::shortcuts::{parse, shortcut_of};
|
||||
let keymap = Default::default();
|
||||
assert_eq!(shortcut_of(&keymap, "view.rightPanel"), Some("F8"));
|
||||
assert_eq!(shortcut_of(&keymap, "view.allPanels"), Some("Shift+Tab"));
|
||||
assert!(parse("F8").is_some() && parse("Shift+Tab").is_some());
|
||||
// F1 (LightCraft Help) goes to the docs
|
||||
let mut h = loupe();
|
||||
let opened = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let o = opened.clone();
|
||||
h.app.services.open_url = Some(Box::new(move |u: &str| {
|
||||
o.lock().unwrap().push(u.to_string());
|
||||
Ok(())
|
||||
}));
|
||||
key(&mut h, "F1", false);
|
||||
assert_eq!(opened.lock().unwrap().as_slice(), [crate::links::HELP]);
|
||||
}
|
||||
|
|
@ -48,6 +48,8 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app
|
|||
| 26 | **Performance at scale**: 85k-photo library opens in 1.7–4.7 s; 100k grid test pending; colour NR at half resolution | ROADMAP log, gpu-pipeline.md | large-catalog users | 15–30 | [target-app-parity](target-app-parity.md#by-dimension) |
|
||||
| 27 | **Windows / Linux runtime coverage**: Windows installer UI unverified on Windows (#79); GPU path proven on Apple and on one AMD GPU on Linux (Radeon 8060S, Vulkan); NVIDIA, Intel and DX12 unverified; the Linux riscv64 tarball (new in 0.6.0, #844) is cross-compiled and never run beyond a QEMU smoke test | ROADMAP, issues, `release.yml` | non-Mac users find bugs first | 13–27 | [hardware-parity](hardware-parity.md) |
|
||||
| 28 | **Small Classic shortcuts and keys**: rating `[` `]`, flag cycling, filter-bar keys, F1 help, panel keys (Tab, F5–F8) | [ui-parity](ui-parity.md#shortcuts) | Classic muscle memory | 4–8 | [ui-parity](ui-parity.md) |
|
||||
| 27 | **Windows / Linux runtime coverage**: Windows installer UI unverified on Windows (#79); GPU path proven on Apple only; the Linux riscv64 tarball (new in 0.6.0, #844) is cross-compiled and never run beyond a QEMU smoke test | ROADMAP, issues, `release.yml` | non-Mac users find bugs first | 15–30 | [hardware-parity](hardware-parity.md) |
|
||||
| 28 | **Small Classic shortcuts and keys**: flag cycling (backtick), the filter-bar `\` key, panel keys Tab and F5 (F6 / F7 are in open PR #1078; F8 and ⇧Tab landed) | [ui-parity](ui-parity.md#shortcuts) | Classic muscle memory | 2–4 | [ui-parity](ui-parity.md) |
|
||||
|
||||
Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform work they need around them,
|
||||
**~340–600 h** to beta (incl. 40–80 h for the ~92 open core-path user issues) (see [target-app-parity.md](target-app-parity.md#stage)).
|
||||
|
|
@ -87,6 +89,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
|
|||
| 2026-10-12 | minor | Gap 10 shrinks: the Hasselblad Lunar (a Sony NEX-7 ARW under its own Make) decodes and is verified, 236 models (#1275) |
|
||||
| 2026-10-12 | minor | Gap 14: HLG AVIF export (`hdrTransfer`) and a tested SDR/HDR display decision |
|
||||
| 2026-10-12 | minor | Gap 13 narrowed: smart collections and stacks now import from `.lrcat` |
|
||||
| 2026-10-12 | minor | Gap 28 shrinks: F8 and Shift+Tab land; F1 and `[` `]` were already implemented; gap not closed |
|
||||
| 2026-10-11 | minor | Reviewed against main after 0.6.0: CR3 entry names the CRX variants `crx.rs` still refuses and the R7 / R6 Mark II C-RAW state (decoding, not verified, so the measured numbers don't move); CI and Windows / Linux entries note the riscv64 release build (#844); R7 fix added to Closed recently. Panel docking (UI.9, #869) closes no listed gap |
|
||||
| 2026-10-11 | minor | Camera colour entry: raws with a black-and-white camera JPEG take its tone curve (#577); gap not closed |
|
||||
| 2026-10-11 | minor | Gap 4: Compare and Reference views get zoom windows per pane (1:1 was a 2,560 px render magnified); overlays and soft proofing remain, 8–15 h → 4–8 h |
|
||||
|
|
|
|||
|
|
@ -50,12 +50,16 @@ cited here still exists. Formerly docs/parity.md.
|
|||
| V. Preferences (PREF) | 6 | 2 | 2 | 2 | 1/1 (100%) | 4/5 (80%) |
|
||||
| W. Cloud & AI infrastructure (CLOUD / AI) | 1 | 0 | 1 | 2 | — | — |
|
||||
| X. Cross-cutting behaviours (BEHAV) | 23 | 6 | 1 | 1 | 8/8 (100%) | 7/10 (70%) |
|
||||
| Y. Menus | 84 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) |
|
||||
| Y. Menus | 86 | 0 | 4 | 8 | 47/47 (100%) | 25/25 (100%) |
|
||||
| Z. Keyboard shortcuts (desktop) | 76 | 3 | 3 | 1 | 49/52 (94%) | 23/24 (96%) |
|
||||
| Lightroom Classic extras | 34 | 16 | 35 | 8 | — | 24/26 (92%) |
|
||||
| **Total** | 408 | 52 | 72 | 35 | 193/201 (96%) | 145/157 (92%) |
|
||||
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.6%** of 532 in-scope rows — P0 98.0% of 201 · P1 95.2% of 157 · P2 50.0% of 173.
|
||||
| Lightroom Classic extras | 35 | 16 | 34 | 8 | — | 24/26 (92%) |
|
||||
| **Total** | 411 | 51 | 71 | 35 | 193/201 (96%) | 147/159 (92%) |
|
||||
|
||||
Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **81.9%** of 533 in-scope rows — P0 98.0% of 201 · P1 95.3% of 159 · P2 50.6% of 172.
|
||||
<!-- /parity:summary -->
|
||||
|
||||
## Top gaps and shortcut notes
|
||||
|
|
@ -529,6 +533,8 @@ visible menu bar yet: the menu model is only exposed through the control channel
|
|||
| MENU-VIEW-ENTERFULLSCREEN | Enter Full Screen | P1 | ✅ | `cmd:view.enterFullScreen` | |
|
||||
| MENU-VIEW-PHOTOSPANEL | Show/Hide photos panel | P0 | ✅ | `cmd:view.leftPanel` | |
|
||||
| MENU-VIEW-FILMSTRIP | Show/Hide filmstrip | P0 | ✅ | `cmd:view.filmstrip` | |
|
||||
| MENU-VIEW-RIGHTPANEL | Show/Hide right panel — F8 | P1 | ✅ | `cmd:view.rightPanel`, `crates/ui-egui/src/menus.rs` | checked while a tool panel is shown; shows the panel that was hidden (Edit when there was none) |
|
||||
| MENU-VIEW-HIDEALL | Toggle All Panels (hide / show every panel) — ⇧Tab | P1 | ✅ | `cmd:view.allPanels`, `crates/ui-egui/src/menus.rs` | named Hide All Panels here, checked while they are hidden |
|
||||
| MENU-VIEW-INFO | Show/Hide info | P0 | ✅ | `cmd:panel.info` | |
|
||||
| MENU-VIEW-KEYWORDS | Show/Hide keywords | P0 | ✅ | `cmd:panel.keywords` | |
|
||||
| MENU-VIEW-ACTIVITY | Show/Hide activity (comments) | OOS | 🚫 | | our History panel is `panel.activity` |
|
||||
|
|
@ -764,18 +770,18 @@ 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 | 🟡 | `cmd:view.rightPanel`, `cmd:view.allPanels`, `crates/ui-egui/src/menus.rs` | F8 toggles the right panel (the tool that was there comes back); ⇧Tab hides the side panels, Presets and filmstrip and a second press restores exactly what was shown, and does nothing in a text field (`tests_panel_keys.rs`). On a Mac keyboard F8 is Play/Pause unless fn is held or "Use F1, F2, … as standard function keys" is on. Missing: Tab (side panels), T (toolbar), F5, solo mode (F6, F7 in open PR #1078) |
|
||||
| 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 | ⬜ | | |
|
||||
| KEYC-CATALOG | Classic photo/catalog keys (⇧⌘I, ⌘', ⌘R, F2, ⌫, ⇧⌘E…) | P2 | ✅ | `cmd:library.import`, `cmd:photo.delete`, `cmd:photo.virtualCopy`, `cmd:app.showInFinder`, `cmd:dialog.rename`, `cmd:photo.editInExternal` | ⇧⌘I add, ⌘' virtual copy, ⌘R show in Finder, F2 rename, ⌫ delete, ⇧⌘E external editor |
|
||||
| KEYC-COMPARE | Classic grid/compare keys (Z, Home/End, =/−, ⌘⇧D, S…) | P2 | 🟡 | `cmd:view.zoomToggle` | Z toggles zoom; no compare, stacks, thumbnail-size keys |
|
||||
| KEYC-RATING | Classic rating/flag keys (1–5, ⇧1–5, 6–9, P, X, U, ⇧X, ⇧U, `[` `]`, \`) | P2 | 🟡 | `cmd:photo.rate`, `cmd:photo.pick`, `cmd:photo.flag` | 0–5 / Shift+0–5 and 6–9 / Shift+6–9; P/X/U, Shift+X/U and grid Shift+P work; tests cover batch/undo, active Compare candidate and text focus. Missing: Shift+P outside grids (Presets), rating `[` / `]`, flag cycle and filter-bar keys |
|
||||
| KEYC-RATING | Classic rating/flag keys (1–5, ⇧1–5, 6–9, P, X, U, ⇧X, ⇧U, `[` `]`, \`) | P2 | 🟡 | `cmd:photo.rate`, `cmd:photo.pick`, `cmd:photo.flag` | 0–5 / Shift+0–5 and 6–9 / Shift+6–9; P/X/U, Shift+X/U and grid Shift+P work; tests cover batch/undo, active Compare candidate and text focus. `[` / `]` change the rating in grids (`tests_relative_ratings.rs`). Missing: flag cycle (backtick) and the filter-bar `\` key |
|
||||
| KEYC-COLLECTIONS | Classic collection keys (⌘N, B…) | P2 | 🟡 | `cmd:dialog.newAlbum` | ⌘N new album; no quick collection |
|
||||
| KEYC-METADATA | Classic keyword/metadata keys (⌘K, ⌘S, ⌘⌥⇧C/V…) | P2 | 🟡 | `cmd:photo.saveMetadataToFile` | ⌘S saves metadata; no keyword sets, metadata copy/paste |
|
||||
| KEYC-DEVELOP | Classic develop keys (V, ⌘U, ⇧⌘U, R, Q, K, M, ⇧M, ⇧W, ⇧J, ⇧Q…) | P2 | 🟡 | `cmd:develop.treatment`, `cmd:develop.reset`, `cmd:crop.reset` | V, ⇧⌘R, ⌥⌘R, W, J, Y, ⇧Y, \ match; R/K/M/⇧M differ; no Classic keymap layer |
|
||||
| KEYC-MODULE-OUTPUT | Book / slideshow / print / map / web keys | P2 | ⬜ | | modules not implemented |
|
||||
| KEYC-HELP | Classic help keys (⌘/, F1) | P2 | 🟡 | `cmd:app.shortcuts` | ⌘/ only |
|
||||
| KEYC-HELP | Classic help keys (⌘/, F1) | P2 | ✅ | `cmd:app.shortcuts`, `cmd:app.help` | ⌘/ opens Keyboard Shortcuts, F1 LightCraft Help (`tests_panel_keys.rs`) |
|
||||
|
||||
### Japanese text watermarks
|
||||
|
||||
|
|
@ -809,6 +815,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove
|
|||
| 2026-10-11 | patch | LR-LIB-SMARTALBUM: sidebar counts key on the library revision, so develop edits recount them only while a smart album reads develop state (issue #1057); status unchanged |
|
||||
| 2026-10-11 | minor | MENU-HELP-REPORTBUG: Help ▸ Report a Bug… writes a report file and opens a pre-filled issue (issue #1015) |
|
||||
| 2026-10-12 | minor | LRC-LIB-CATALOG-IMPORT: smart collections import as smart albums where every rule maps; Lightroom folder stacks import as stacks |
|
||||
| 2026-10-12 | minor | KEYC-PANELS: F8 toggles the right panel and Shift+Tab hides and restores all panels (🟡, was ⬜); KEYC-HELP: F1 opens help (✅); KEYC-RATING: `[` `]` are implemented, notes corrected; two View menu rows |
|
||||
| 2026-10-11 | minor | KEY-SLIDER-TYPE: honor immediate Return/Escape and keep typed keys in the value editor; add regression references |
|
||||
| 2026-10-11 | minor | LR-PROF-CAMERACOLOR: raws with a black-and-white camera JPEG take its tone curve (issue #577) |
|
||||
| 2026-10-11 | minor | LR-VIEW-ZOOM and LR-VIEW-COMPARE: Compare and Reference are sharp at 1:1 (zoom windows per pane, two originals kept decoded while they are open); statuses unchanged |
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ assessment and the notes that don't fit a row. Visual reference for agents: `pla
|
|||
|
||||
## Measured counts (2026-10-10)
|
||||
|
||||
- 226 engine commands (`crates/engine/src/cmd/`, 30 modules) and 143 UI commands (`UI_COMMANDS` in
|
||||
`crates/ui-egui/src/menus.rs`, 73 with a shortcut) plus 10 language commands; 23 secondary
|
||||
- 226 engine commands (`crates/engine/src/cmd/`, 30 modules) and 158 UI commands (`UI_COMMANDS` in
|
||||
`crates/ui-egui/src/menus.rs`, 77 with a shortcut; recounted 2026-10-12) plus 10 language commands; 23 secondary
|
||||
shortcut bindings (`ALIASES`, `crates/ui-egui/src/shortcuts.rs`); a keymap editor.
|
||||
- 122 develop controls plus 10 indexed templates (`crates/develop/src/controls.rs`).
|
||||
- Checklist: menus 95.3% (86 in-scope rows), desktop shortcuts 94.4% (81 rows), Classic extras
|
||||
|
|
@ -65,11 +65,12 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
|||
| Create Version | ⌘⇧S (+ ⇧M) | ⇧M (Windows: Ctrl+⇧S) | — |
|
||||
| Select None | ⌘⇧A (+ ⌘D) | ⌘D | — |
|
||||
|
||||
**Library culling (KEYC-RATING):** `0–5`, `6–9` and `P/X/U` reach egui on macOS even when displayed in the native menu (adapted from PR #261; issue #283). `Shift+0–9` applies and advances once; `Shift+P` picks and advances in Photo Grid / Square Grid and retains Presets elsewhere. See [library shortcuts](library-shortcuts.md) for the verified Classic mapping and regression coverage. Remaining Classic gaps: rating `[` / `]`, flag cycling and filter-bar keys.
|
||||
**Library culling (KEYC-RATING):** `0–5`, `6–9` and `P/X/U` reach egui on macOS even when displayed in the native menu (adapted from PR #261; issue #283). `Shift+0–9` applies and advances once; `Shift+P` picks and advances in Photo Grid / Square Grid and retains Presets elsewhere. See [library shortcuts](library-shortcuts.md) for the verified Classic mapping and regression coverage. Remaining Classic gaps: flag cycling (backtick), the filter-bar `\` key, Tab and F5.
|
||||
|
||||
**Still missing / broken:**
|
||||
- No command yet: F1 help (verify the rest of the old list: full screen, settings, stacks,
|
||||
visualize spots and merges have commands now).
|
||||
- No Tab (side panels) or F5 yet; F6 and F7 are in PR #1078. F8 (right panel) and ⇧Tab (hide / show all
|
||||
panels) work: ⇧Tab hides the side panels, Presets and filmstrip and brings back exactly those; on a Mac
|
||||
keyboard F8 is Play/Pause unless fn is held.
|
||||
- `H` opens Remove; Lightroom also uses it (Classic) to hide pins — pins toggle from View → Show Mask Pins.
|
||||
- ⌘M / ⌘H / ⌘Q / ⌘W rely on the platform window defaults (unverified).
|
||||
|
||||
|
|
@ -106,5 +107,6 @@ left panel, canvas, right panel, which is sensible but not tuned).
|
|||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Accessibility: an AccessKit tree audit test over every view, Develop tool and 13 dialogs; unnamed fields, slider ranges and the Auto Stack slider fixed; 30% → 55%; UI remaining 40–70 h → 34–58 h |
|
||||
| 2026-10-12 | minor | Shortcuts: F8 and Shift+Tab panel keys; F1 help and `[` `]` rating were already implemented; UI command counts recounted |
|
||||
| 2026-10-11 | minor | Zoom row: Compare and Reference get zoom windows; what remains of LR-VIEW-ZOOM is overlays and soft proofing |
|
||||
| 2026-10-10 | major | Created: measured command and shortcut counts, assessment by area, estimates; moved the shortcut conflicts section here from the checklist |
|
||||
|
|
|
|||
Loading…
Reference in New Issue