Drive develop, the grid, the sidebar and People from the keyboard
An audit of every action by view against the keys the handlers bind left develop without zoom, pan, fit or a way back to the grid, the grid without select-none, thumbnail size or keywording, People with no key at all, and the export and copy sheets without Enter. It also found the reverse gap FR-UI-5 forbids: pick and reject had no route but P, X and U, and the 2026-09-19 amendment's judging in develop had not been built. Develop: Ctrl+= and Ctrl+Plus zoom in and Ctrl+- out about the middle of the view, Ctrl+0 fits and Ctrl+1 goes to 1:1, Shift and an arrow pan a magnified view, G goes back to the grid, Ctrl+Y redoes, and Enter keeps a crop that hid a mask. 0-5, P, X and U rate and flag the open photograph without moving on, with stars and Pick/Reject in the top bar as the pointer and touch route. = and - nudge the control last moved by a hundredth of its travel; the framing sliders, perspective included, now count as "last moved", so R puts them back as well. J turns the selected mask part's join chip. Grid: Ctrl+D and Ctrl+Shift+A clear the selection, = and - resize the thumbnails, Ctrl+K opens keywording, and Flag in the selection bar gives pick and reject a pointer and touch route. Sidebar: Enter commits a collection's name, and Enter or Escape hands the keyboard back to the grid, where it used to go nowhere until something was clicked. People: Up and Down walk the rail, F2 puts the name field under the keys, and Escape or Back now leave the screen the way its back button does instead of doing nothing. Sheets: Enter does what the export or copy sheet's button does. The choices follow Lightroom where it has one. No new key steals typing: the grid's and People's keys live on focus holders that are not ancestors of any text field, and the sheets' Enter comes after a focused field has had it. Every binding is tagged beside its handler, and the gate added in the previous commit holds the two to each other.
This commit is contained in:
@@ -411,6 +411,46 @@ fn wire_adjustments(window: &AppWindow, w: &DevelopWiring) {
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
// TRACES: FR-DEV-16
|
||||
// The nudge keys, on the control last moved: a hundredth of its
|
||||
// travel, which is the step the slider's own increment takes. Read
|
||||
// off the row on screen rather than the descriptor, because the row
|
||||
// is what the photographer is looking at — with masks selected it is
|
||||
// the layer's value, not the global one.
|
||||
//
|
||||
// Scalars only. A switch or a choice has no "a little more", and a
|
||||
// curve's points are not one number.
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
let rows = rows.clone();
|
||||
window
|
||||
.global::<Adjustments>()
|
||||
.on_param_nudged(move |op, param, direction| {
|
||||
use slint::Model as _;
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let shown = w.global::<Adjustments>().get_rows();
|
||||
let Some(row) = shown
|
||||
.iter()
|
||||
.find(|r| r.op_index == op && r.param_index == param)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if row.kind != "scalar" || row.maximum <= row.minimum {
|
||||
return;
|
||||
}
|
||||
let step = (row.maximum - row.minimum) / 100.0;
|
||||
let value =
|
||||
(row.value + direction.signum() as f32 * step).clamp(row.minimum, row.maximum);
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_param(op, param, value);
|
||||
s.rebake_film_if_affected(op);
|
||||
}
|
||||
sync_rows(&w, &rows, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
// TRACES: FR-DEV-3
|
||||
// The local-adjustment panel. Wired as a block rather than inline because
|
||||
// it is a dozen callbacks that all say the same three things, and they
|
||||
|
||||
@@ -37,7 +37,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Develop",
|
||||
touch: "Pinch it with two fingers",
|
||||
pointer: "The scroll wheel over it",
|
||||
keys: "",
|
||||
keys: "Ctrl+= or Ctrl+Plus in, Ctrl+- out, about the middle of the view",
|
||||
manual: "looking-closer",
|
||||
},
|
||||
Gesture {
|
||||
@@ -45,7 +45,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Develop",
|
||||
touch: "Drag it",
|
||||
pointer: "Drag it",
|
||||
keys: "",
|
||||
keys: "Shift+←, Shift+→, Shift+↑ and Shift+↓, a fifth of the view at a time",
|
||||
manual: "looking-closer",
|
||||
},
|
||||
Gesture {
|
||||
@@ -69,7 +69,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Develop",
|
||||
touch: "Tap the step below the current one in the History list",
|
||||
pointer: "Click it, or press Redo in the History header",
|
||||
keys: "Ctrl+Shift+Z",
|
||||
keys: "Ctrl+Shift+Z, or Ctrl+Y",
|
||||
manual: "history-snapshots-presets",
|
||||
},
|
||||
Gesture {
|
||||
@@ -120,6 +120,30 @@ pub const GESTURES: &[Gesture] = &[
|
||||
keys: "Ctrl+E",
|
||||
manual: "export",
|
||||
},
|
||||
Gesture {
|
||||
title: "Keep a crop that leaves a mask outside",
|
||||
section: "Develop",
|
||||
touch: "Press \"Keep crop\" on the notice, or \"Undo crop\" to take it back",
|
||||
pointer: "Press \"Keep crop\" on the notice, or \"Undo crop\" to take it back",
|
||||
keys: "Enter keeps it; Ctrl+Z takes the crop back, like any other step",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Go back to the grid",
|
||||
section: "Develop",
|
||||
touch: "Press \"‹ Library\" in the top bar",
|
||||
pointer: "Press \"‹ Library\" in the top bar",
|
||||
keys: "G",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Nudge the control last moved",
|
||||
section: "Develop",
|
||||
touch: "Drag its track",
|
||||
pointer: "Drag its track",
|
||||
keys: "= or Plus up and - down, a hundredth of its travel at a time; hold for more",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Change which group of adjustments is on screen",
|
||||
section: "Develop",
|
||||
@@ -133,9 +157,25 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Develop",
|
||||
touch: "Double-tap the photograph",
|
||||
pointer: "Double-click it, or press the zoom readout floating over the canvas",
|
||||
keys: "Z",
|
||||
keys: "Z goes in and back out; Ctrl+1 goes to 1:1 and Ctrl+0 back to the whole frame",
|
||||
manual: "looking-closer",
|
||||
},
|
||||
Gesture {
|
||||
title: "Rate this photograph",
|
||||
section: "Develop",
|
||||
touch: "Tap a star in the top bar",
|
||||
pointer: "Click a star in the top bar",
|
||||
keys: "0–5",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Pick or reject this photograph",
|
||||
section: "Develop",
|
||||
touch: "Press Pick or Reject in the top bar; again to take the flag off",
|
||||
pointer: "Press Pick or Reject in the top bar; again to take the flag off",
|
||||
keys: "P picks, X rejects and U takes the flag off",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Give this photograph a colour label",
|
||||
section: "Develop",
|
||||
@@ -205,7 +245,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Develop",
|
||||
touch: "Tap the + / − / ∩ chip on the part's row",
|
||||
pointer: "Click the + / − / ∩ chip on the part's row",
|
||||
keys: "",
|
||||
keys: "J turns the selected part's chip, while masking",
|
||||
manual: "local-adjustments",
|
||||
},
|
||||
Gesture {
|
||||
@@ -224,6 +264,14 @@ pub const GESTURES: &[Gesture] = &[
|
||||
keys: "Escape, or Back where the device has one",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Do what a sheet offers",
|
||||
section: "Everywhere",
|
||||
touch: "Press its button — Export, or Copy",
|
||||
pointer: "Press its button — Export, or Copy",
|
||||
keys: "Enter, on the export and copy sheets",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Pick a collection up to rearrange the tree",
|
||||
section: "Collections sidebar",
|
||||
@@ -253,7 +301,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Collections sidebar",
|
||||
touch: "Hold the collection, then \"Rename\"",
|
||||
pointer: "Double-click its name, or right-click it and choose \"Rename\"",
|
||||
keys: "Escape while typing abandons the new name",
|
||||
keys: "Type the name and press Enter; Escape abandons it",
|
||||
manual: "collections",
|
||||
},
|
||||
Gesture {
|
||||
@@ -272,6 +320,22 @@ pub const GESTURES: &[Gesture] = &[
|
||||
keys: "",
|
||||
manual: "people",
|
||||
},
|
||||
Gesture {
|
||||
title: "Move between people",
|
||||
section: "People",
|
||||
touch: "Tap a person on the rail",
|
||||
pointer: "Click a person on the rail",
|
||||
keys: "↑ and ↓, along the rail",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Name a person",
|
||||
section: "People",
|
||||
touch: "Tap the name field over their faces, type, and press Enter",
|
||||
pointer: "Click the name field over their faces, type, and press Enter",
|
||||
keys: "F2 puts the name field under the keys",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "See a person's photographs",
|
||||
section: "People",
|
||||
@@ -368,6 +432,14 @@ pub const GESTURES: &[Gesture] = &[
|
||||
keys: "Ctrl+V",
|
||||
manual: "copying-settings",
|
||||
},
|
||||
Gesture {
|
||||
title: "Keyword the selection",
|
||||
section: "Library grid",
|
||||
touch: "Select them, then Keywords in the selection bar",
|
||||
pointer: "Select them, then Keywords in the selection bar",
|
||||
keys: "Ctrl+K",
|
||||
manual: "",
|
||||
},
|
||||
Gesture {
|
||||
title: "Show only photographs with some number of stars",
|
||||
section: "Library grid",
|
||||
@@ -387,8 +459,8 @@ pub const GESTURES: &[Gesture] = &[
|
||||
Gesture {
|
||||
title: "Pick or reject a photograph",
|
||||
section: "Library grid",
|
||||
touch: "",
|
||||
pointer: "",
|
||||
touch: "Select them, then Flag in the selection bar and Pick, Reject or No flag",
|
||||
pointer: "Select them, then Flag in the selection bar and Pick, Reject or No flag",
|
||||
keys: "P picks, X rejects and U takes the flag off, with the pointer over it or on the selection",
|
||||
manual: "",
|
||||
},
|
||||
@@ -429,7 +501,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Library grid",
|
||||
touch: "Pinch the grid with two fingers",
|
||||
pointer: "Ctrl and the scroll wheel",
|
||||
keys: "",
|
||||
keys: "= or Plus for larger, - for smaller",
|
||||
manual: "getting-about",
|
||||
},
|
||||
Gesture {
|
||||
@@ -469,7 +541,7 @@ pub const GESTURES: &[Gesture] = &[
|
||||
section: "Library grid",
|
||||
touch: "Press Clear in the selection strip",
|
||||
pointer: "Press Clear in the selection strip",
|
||||
keys: "",
|
||||
keys: "Ctrl+D or Ctrl+Shift+A",
|
||||
manual: "selecting-several",
|
||||
},
|
||||
Gesture {
|
||||
|
||||
@@ -798,6 +798,44 @@ fn wire_navigation(
|
||||
reset_name_field(&w);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-5
|
||||
// Up and down the rail from the keyboard. The rail's order is the model's
|
||||
// order, so the neighbour is found there and then picked exactly as a
|
||||
// click on it would pick it — one path for selecting a person, whichever
|
||||
// way it was asked for. Stops at either end rather than wrapping: a rail
|
||||
// of two thousand people that jumped from the last to the first would
|
||||
// lose the user's place.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
window.on_identity_person_stepped(move |delta| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let people = w.get_identity_people();
|
||||
let ids: Vec<i32> = people.iter().map(|p| p.id).collect();
|
||||
if let Some(id) = stepped_person(&ids, w.get_identity_selected(), delta) {
|
||||
w.invoke_identity_person_picked(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The person `delta` places along the rail from `selected`, or `None` where
|
||||
/// that is off either end or the rail is empty. With nobody selected, the
|
||||
/// first key down the rail takes the first person and up takes the last.
|
||||
fn stepped_person(ids: &[i32], selected: i32, delta: i32) -> Option<i32> {
|
||||
let last = ids.len().checked_sub(1)?;
|
||||
let next = match ids.iter().position(|id| *id == selected) {
|
||||
Some(at) => {
|
||||
let to = at as i64 + delta.signum() as i64;
|
||||
if to < 0 || to > last as i64 {
|
||||
return None;
|
||||
}
|
||||
to as usize
|
||||
}
|
||||
None if delta < 0 => last,
|
||||
None => 0,
|
||||
};
|
||||
ids.get(next).copied()
|
||||
}
|
||||
|
||||
/// Renaming a person, and the namesake merge offer a rename can raise.
|
||||
@@ -1600,6 +1638,21 @@ fn park_cover_timer(timer: Option<slint::Timer>) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::stepped_person;
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
#[test]
|
||||
fn stepping_the_rail_stops_at_either_end() {
|
||||
let rail = [7, 3, 9];
|
||||
assert_eq!(stepped_person(&rail, 3, 1), Some(9));
|
||||
assert_eq!(stepped_person(&rail, 3, -1), Some(7));
|
||||
assert_eq!(stepped_person(&rail, 9, 1), None);
|
||||
assert_eq!(stepped_person(&rail, 7, -1), None);
|
||||
assert_eq!(stepped_person(&rail, -1, 1), Some(7));
|
||||
assert_eq!(stepped_person(&rail, -1, -1), Some(9));
|
||||
assert_eq!(stepped_person(&[], -1, 1), None);
|
||||
}
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
||||
+9
-10
@@ -448,6 +448,7 @@ fn reset_view_state(window: &AppWindow) {
|
||||
// would land on whichever control happens to share them.
|
||||
window.global::<Adjustments>().set_touched_op(-1);
|
||||
window.global::<Adjustments>().set_touched_param(-1);
|
||||
window.global::<Framing>().set_touched(0);
|
||||
// TRACES: FR-DSP-7
|
||||
// Emptied rather than left standing: the previous photograph's histogram
|
||||
// beside the next one's filename is a confident, precise lie, and the gap
|
||||
@@ -3433,19 +3434,17 @@ fn apply_layout_class(
|
||||
/// the activity — the behaviour every application there has, and the reason
|
||||
/// this answers with a bool rather than swallowing the gesture.
|
||||
fn back_one_step(w: &AppWindow) -> bool {
|
||||
// TRACES: FR-UI-5 | FR-UI-4
|
||||
// The Identity screen has no representation in `NavState`: nothing here
|
||||
// records which view it replaced, only `identity_ui`'s own
|
||||
// `came_from_library`, which this function has no reason to reach for
|
||||
// over one screen. Before this refactor, back queued nothing behind
|
||||
// Identity opened from the library (show-library stayed true, unread by
|
||||
// anything) and queued `ToLibrary` behind Identity opened from develop —
|
||||
// but `ToLibrary` only ever set `show-library`, a property
|
||||
// `IdentityScreen`'s own `if` never read, so both cases looked the same
|
||||
// on screen: nothing happened. Swallowing the gesture here keeps that:
|
||||
// falling into the general case below would instead write `active-view`,
|
||||
// which `IdentityScreen`'s `if` *does* read, and close the screen as a
|
||||
// side effect nobody asked for.
|
||||
// `came_from_library`. So back leaves it the way its own "‹ Library" /
|
||||
// "‹ Develop" button does, through `identity-close`, which knows where it
|
||||
// came from — rather than through the general case below, which would
|
||||
// write `active-view` without that knowledge. It used to swallow the key
|
||||
// and do nothing, which left Escape and Android's Back dead on a whole
|
||||
// screen.
|
||||
if w.get_active_view() == View::Identity {
|
||||
w.invoke_identity_close();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -986,6 +986,29 @@ pub(super) fn wire_ratings_and_flags(
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-5 | FR-CULL-4
|
||||
// The flag of one photograph by row: develop's Pick and Reject, and its
|
||||
// P, X and U. The open photograph only, for the reason a star clicked in
|
||||
// the grid rates only its own cell — develop is working on one frame, and
|
||||
// a selection left behind in the grid is not what is in front of it.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window
|
||||
.global::<Library>()
|
||||
.on_library_cell_flagged(move |row, flag| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = ctl
|
||||
.image_ids
|
||||
.borrow()
|
||||
.get(row as usize)
|
||||
.map(|id| dr_types::ImageId(*id as u64));
|
||||
let Some(id) = id else { return };
|
||||
|
||||
apply_judgement(&w, &ctl, &[id], None, Some(flag_from_code(flag)));
|
||||
});
|
||||
}
|
||||
|
||||
// Fold a burst up, or open it out (FR-CULL-5). A reload rather than a repaint, because
|
||||
// it changes what the grid's query returns — see [`crate::bursts::toggle`].
|
||||
{
|
||||
|
||||
@@ -1136,6 +1136,47 @@ fn wire_layers_parts(
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
// TRACES: FR-DEV-16 | FR-DEV-19a
|
||||
// The keyboard's turn of the join chip: the part the tools point at,
|
||||
// on the one layer being edited, one step round the chip's own cycle.
|
||||
// Nothing with several layers selected — a part belongs to one — and
|
||||
// nothing on the base part, which joins nothing.
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window
|
||||
.global::<Masking>()
|
||||
.on_selected_part_join_cycled(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
{
|
||||
let mut slot = session.borrow_mut();
|
||||
let Some(s) = slot.as_mut() else { return };
|
||||
let [id] = s.active_masks() else { return };
|
||||
let id = id.clone();
|
||||
let index = s.active_part();
|
||||
if index == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(join) = s
|
||||
.masks()
|
||||
.get(&id)
|
||||
.and_then(|l| l.part(index))
|
||||
.map(|p| p.join)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let at = dr_pipeline::mask::Join::ALL
|
||||
.iter()
|
||||
.position(|j| *j == join)
|
||||
.unwrap_or(0);
|
||||
let next = (at + 1) % dr_pipeline::mask::Join::ALL.len();
|
||||
s.set_mask_part_join(&id, index, next);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
|
||||
@@ -647,6 +647,11 @@ export global Framing {
|
||||
in property <float> keystone-v: 0.0;
|
||||
in property <float> keystone-h: 0.0;
|
||||
in property <float> max-keystone: 100.0;
|
||||
/// TRACES: FR-DEV-16
|
||||
/// Which framing slider was moved last — 1 straighten, 2 vertical,
|
||||
/// 3 horizontal — or 0 when the last control moved was an adjustment
|
||||
/// (see `Adjustments.touched-op`). What `R` and the nudge keys act on.
|
||||
in-out property <int> touched: 0;
|
||||
/// Any of crop, angle, keystone, rotation or flips differs from neutral — what
|
||||
/// lights the section's dot and enables its reset. Distinct from the crop
|
||||
/// rect being non-full: a rotation or a flip is an edit with the crop
|
||||
@@ -813,7 +818,11 @@ export component ComposePanel inherits Rectangle {
|
||||
minimum: -Framing.max-straighten;
|
||||
maximum: Framing.max-straighten;
|
||||
unit: "°";
|
||||
changed(v) => { Framing.angle-changed(v); }
|
||||
changed(v) => {
|
||||
Framing.touched = 1;
|
||||
Adjustments.touched-op = -1;
|
||||
Framing.angle-changed(v);
|
||||
}
|
||||
committed(v) => { Framing.angle-committed(v); }
|
||||
reset => { Framing.angle-changed(0); }
|
||||
}
|
||||
@@ -831,7 +840,11 @@ export component ComposePanel inherits Rectangle {
|
||||
minimum: -Framing.max-keystone;
|
||||
maximum: Framing.max-keystone;
|
||||
unit: "";
|
||||
changed(v) => { Framing.keystone-v-changed(v); }
|
||||
changed(v) => {
|
||||
Framing.touched = 2;
|
||||
Adjustments.touched-op = -1;
|
||||
Framing.keystone-v-changed(v);
|
||||
}
|
||||
committed(v) => { Framing.keystone-committed(); }
|
||||
reset => {
|
||||
Framing.keystone-v-changed(0);
|
||||
@@ -845,7 +858,11 @@ export component ComposePanel inherits Rectangle {
|
||||
minimum: -Framing.max-keystone;
|
||||
maximum: Framing.max-keystone;
|
||||
unit: "";
|
||||
changed(v) => { Framing.keystone-h-changed(v); }
|
||||
changed(v) => {
|
||||
Framing.touched = 3;
|
||||
Adjustments.touched-op = -1;
|
||||
Framing.keystone-h-changed(v);
|
||||
}
|
||||
committed(v) => { Framing.keystone-committed(); }
|
||||
reset => {
|
||||
Framing.keystone-h-changed(0);
|
||||
@@ -1004,6 +1021,11 @@ export global Adjustments {
|
||||
callback param-changed(int, int, float);
|
||||
callback param-reset(int, int);
|
||||
/// TRACES: FR-DEV-16
|
||||
/// Move one scalar control a hundredth of its travel, up for a positive
|
||||
/// direction — the nudge keys, on the control last moved. Rust reads the
|
||||
/// range off the row, where it is.
|
||||
callback param-nudged(int, int, int);
|
||||
/// TRACES: FR-DEV-16
|
||||
/// The control last moved, so a key can put it back.
|
||||
///
|
||||
/// The generated rows have no focus: they are a model the repeater
|
||||
@@ -1531,6 +1553,7 @@ export component AdjustPanel inherits Rectangle {
|
||||
curve-channel: Adjustments.curve-channel;
|
||||
drag-changed(on) => { root.slider-dragging = on; }
|
||||
param-changed(op, param, v) => {
|
||||
Framing.touched = 0;
|
||||
Adjustments.touched-op = op;
|
||||
Adjustments.touched-param = param;
|
||||
Adjustments.param-changed(op, param, v);
|
||||
|
||||
+263
-16
@@ -384,6 +384,9 @@ export component AppWindow inherits Window {
|
||||
|
||||
callback identity-open();
|
||||
callback identity-close();
|
||||
/// TRACES: FR-UI-5
|
||||
/// Up or down the people rail by one, from the keyboard.
|
||||
callback identity-person-stepped(int);
|
||||
callback identity-person-picked(int);
|
||||
callback identity-rename(string);
|
||||
callback identity-merge-accept();
|
||||
@@ -651,6 +654,62 @@ export component AppWindow inherits Window {
|
||||
/// The copy sheet: which kinds of edit a copy carries, and Copy. Ctrl+Shift+C.
|
||||
in-out property <bool> copy-sheet-open: false;
|
||||
|
||||
/// TRACES: FR-EXP-7 | FR-UI-5
|
||||
/// Whether the export sheet's button would do anything — the develop
|
||||
/// view's photograph, or the grid's selection. One definition for the
|
||||
/// button and for Enter, so the key cannot export what the greyed button
|
||||
/// would not.
|
||||
property <bool> export-sheet-can-export: root.active-view == View.develop
|
||||
? !root.export-busy && root.total > 0
|
||||
: !Library.library-exporting && Library.library-selected-count > 0;
|
||||
|
||||
/// What the export sheet's button does.
|
||||
function export-from-sheet() {
|
||||
root.export-sheet-open = false;
|
||||
if (root.active-view == View.develop) {
|
||||
root.export-image();
|
||||
} else {
|
||||
Library.library-export-selection();
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-16 | FR-DEV-20
|
||||
/// The framing slider moved last, nudged a hundredth of its travel — the
|
||||
/// step its own increment takes — and committed, because one keystroke is
|
||||
/// a whole gesture and the refit of the crop hangs off the end of one.
|
||||
function framing-nudged(direction: int) {
|
||||
if (Framing.touched == 1) {
|
||||
Framing.angle-changed(clamp(
|
||||
Framing.angle + direction * Framing.max-straighten / 50,
|
||||
-Framing.max-straighten, Framing.max-straighten));
|
||||
Framing.angle-committed(Framing.angle);
|
||||
} else if (Framing.touched == 2) {
|
||||
Framing.keystone-v-changed(clamp(
|
||||
Framing.keystone-v + direction * Framing.max-keystone / 50,
|
||||
-Framing.max-keystone, Framing.max-keystone));
|
||||
Framing.keystone-committed();
|
||||
} else if (Framing.touched == 3) {
|
||||
Framing.keystone-h-changed(clamp(
|
||||
Framing.keystone-h + direction * Framing.max-keystone / 50,
|
||||
-Framing.max-keystone, Framing.max-keystone));
|
||||
Framing.keystone-committed();
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-DEV-16 | FR-DEV-20
|
||||
/// The framing slider moved last, back to level — what its own reset does.
|
||||
function framing-reset-touched() {
|
||||
if (Framing.touched == 1) {
|
||||
Framing.angle-changed(0);
|
||||
} else if (Framing.touched == 2) {
|
||||
Framing.keystone-v-changed(0);
|
||||
Framing.keystone-committed();
|
||||
} else if (Framing.touched == 3) {
|
||||
Framing.keystone-h-changed(0);
|
||||
Framing.keystone-committed();
|
||||
}
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
/// One photograph along, from the keyboard.
|
||||
///
|
||||
@@ -923,6 +982,12 @@ in property <bool> panel-visible: true;
|
||||
// view before the library. Nothing is left behind a
|
||||
// dialogue that the key walked straight past.
|
||||
//
|
||||
// GESTURE: Do what a sheet offers
|
||||
// where: Everywhere
|
||||
// touch: Press its button — Export, or Copy
|
||||
// pointer: Press its button — Export, or Copy
|
||||
// keys: `Enter`, on the export and copy sheets
|
||||
//
|
||||
// KEYMAP: Everywhere
|
||||
key-pressed(event) => {
|
||||
// Escape alongside Back: the same "go up one" on a keyboard, and
|
||||
@@ -971,6 +1036,21 @@ in property <bool> panel-visible: true;
|
||||
}
|
||||
return root.back-requested() ? accept : reject;
|
||||
}
|
||||
// TRACES: FR-UI-5 | FR-EXP-7 | FR-DEV-6
|
||||
// Enter does what the sheet's own button does. A text field in
|
||||
// the sheet that has focus takes Enter first, so typing a folder
|
||||
// name and pressing it does not export from under the typing.
|
||||
if (Keys.chord(event) == "Enter") {
|
||||
if (root.export-sheet-open && root.export-sheet-can-export) {
|
||||
root.export-from-sheet();
|
||||
return accept;
|
||||
}
|
||||
if (root.copy-sheet-open && root.total > 0 && root.load-error == "") {
|
||||
root.copy-sheet-open = false;
|
||||
Transfer.copy();
|
||||
return accept;
|
||||
}
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
|
||||
@@ -1195,6 +1275,7 @@ in property <bool> panel-visible: true;
|
||||
check-coverage() => { root.identity-check-coverage(); }
|
||||
delete-all-face-data() => { root.identity-delete-all(); }
|
||||
close() => { root.identity-close(); }
|
||||
person-stepped(d) => { root.identity-person-stepped(d); }
|
||||
}
|
||||
|
||||
// The launch screen replaces the whole window rather than overlaying it:
|
||||
@@ -1292,6 +1373,9 @@ in property <bool> panel-visible: true;
|
||||
Collections.collection-rename-commit(id, name);
|
||||
}
|
||||
rename-cancel() => { Collections.collection-rename-cancel(); }
|
||||
// The name field took the keyboard; when a key
|
||||
// finished the name, the grid gets it back.
|
||||
keys-released() => { library-grid.take-keys(); }
|
||||
trash-count: Collections.trash-count;
|
||||
trash-label: Collections.trash-label;
|
||||
selected-count: Library.library-selected-count;
|
||||
@@ -1305,7 +1389,7 @@ in property <bool> panel-visible: true;
|
||||
}
|
||||
}
|
||||
|
||||
LibraryGrid {
|
||||
library-grid := LibraryGrid {
|
||||
horizontal-stretch: 1;
|
||||
// FR-UI-1: the header collapses its actions behind a
|
||||
// disclosure below the breakpoint. A tablet in portrait is
|
||||
@@ -1526,6 +1610,15 @@ in property <bool> panel-visible: true;
|
||||
label-picked(code) => {
|
||||
Library.library-cell-labelled(Library.library-roll-current, code, false);
|
||||
}
|
||||
// TRACES: FR-UI-5 | FR-CULL-4
|
||||
// The open photograph's stars and flag, read from its cell as
|
||||
// the label is; -1 with no library to hold them.
|
||||
rating: Library.library-total > 0 && Library.library-roll-current >= 0
|
||||
? Library.library-cells[Library.library-roll-current].rating : -1;
|
||||
flag: Library.library-total > 0 && Library.library-roll-current >= 0
|
||||
? Library.library-cells[Library.library-roll-current].flag : 0;
|
||||
rated(n) => { Library.library-cell-rated(Library.library-roll-current, n); }
|
||||
flagged(f) => { Library.library-cell-flagged(Library.library-roll-current, f); }
|
||||
position: root.total > 0 ? root.index + 1 + " / " + root.total : "";
|
||||
// Only where a library was opened; command-line files have no
|
||||
// grid to return to.
|
||||
@@ -1793,6 +1886,8 @@ in property <bool> panel-visible: true;
|
||||
// where: Develop
|
||||
// touch: Pinch it with two fingers
|
||||
// pointer: The scroll wheel over it
|
||||
// keys: `Ctrl+=` or `Ctrl+Plus` in, `Ctrl+-` out,
|
||||
// about the middle of the view
|
||||
// why: Anchored on the fingers' midpoint, and on the
|
||||
// pointer, so the gesture reads as magnifying
|
||||
// the picture rather than sliding it about.
|
||||
@@ -1887,6 +1982,9 @@ in property <bool> panel-visible: true;
|
||||
// where: Develop
|
||||
// touch: Drag it
|
||||
// pointer: Drag it
|
||||
// keys: `Shift+Left`, `Shift+Right`, `Shift+Up`
|
||||
// and `Shift+Down`, a fifth of the view
|
||||
// at a time
|
||||
// why: Only once there is something outside the
|
||||
// viewport to reach, which is why the
|
||||
// cursor becomes a hand exactly then. The
|
||||
@@ -2225,15 +2323,17 @@ in property <bool> panel-visible: true;
|
||||
// the History list
|
||||
// pointer: Click it, or press Redo in the
|
||||
// History header
|
||||
// keys: `Ctrl+Shift+Z`
|
||||
// keys: `Ctrl+Shift+Z`, or `Ctrl+Y`
|
||||
// manual: history-snapshots-presets
|
||||
//
|
||||
// Ctrl+Z and Ctrl+Shift+Z (FR-DEV-5).
|
||||
// Ctrl+Z and Ctrl+Shift+Z (FR-DEV-5), and Ctrl+Y for
|
||||
// hands that learned redo on Windows.
|
||||
if (Keys.chord(event) == "Ctrl+Z") {
|
||||
Steps.undo();
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "Ctrl+Shift+Z") {
|
||||
if (Keys.chord(event) == "Ctrl+Shift+Z"
|
||||
|| Keys.chord(event) == "Ctrl+Y") {
|
||||
Steps.redo();
|
||||
return accept;
|
||||
}
|
||||
@@ -2349,6 +2449,121 @@ in property <bool> panel-visible: true;
|
||||
root.export-options-open();
|
||||
return accept;
|
||||
}
|
||||
// TRACES: FR-UI-4 | FR-DEV-16
|
||||
// Magnification from the keyboard: in and out about
|
||||
// the middle of the view, the fit and the 1:1 by
|
||||
// name. Photoshop's and Lightroom's chords. Not while
|
||||
// composing, which drops the zoom on the way in so
|
||||
// the crop handles stay on screen; the wheel and the
|
||||
// pinch stand down there too.
|
||||
if (!Develop.cropping) {
|
||||
if (Keys.chord(event) == "Ctrl+=" || Keys.chord(event) == "Ctrl+Plus") {
|
||||
root.zoom-at(1.25, 0.5, 0.5);
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "Ctrl+-") {
|
||||
root.zoom-at(0.8, 0.5, 0.5);
|
||||
return accept;
|
||||
}
|
||||
// The toggle goes out to fit from any zoom, so fit is
|
||||
// the toggle when zoomed and nothing when not; and
|
||||
// 1:1 is the toggle from fit — twice from a zoom
|
||||
// that is not 1:1, once out and once back in.
|
||||
if (Keys.chord(event) == "Ctrl+0") {
|
||||
if (root.zoomed) { root.inspect-toggled(-1, -1); }
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "Ctrl+1") {
|
||||
if (root.zoomed) { root.inspect-toggled(-1, -1); }
|
||||
root.inspect-toggled(-1, -1);
|
||||
return accept;
|
||||
}
|
||||
}
|
||||
// TRACES: FR-UI-4 | FR-DEV-16
|
||||
// Shift and an arrow move a magnified view by a fifth
|
||||
// of itself — see "Move a magnified photograph about"
|
||||
// above. Only while magnified, as the drag is: at fit
|
||||
// there is nothing outside the view to move to.
|
||||
if (root.zoomed) {
|
||||
if (Keys.chord(event) == "Shift+Left") { root.pan-by(-0.2, 0); return accept; }
|
||||
if (Keys.chord(event) == "Shift+Right") { root.pan-by(0.2, 0); return accept; }
|
||||
if (Keys.chord(event) == "Shift+Up") { root.pan-by(0, -0.2); return accept; }
|
||||
if (Keys.chord(event) == "Shift+Down") { root.pan-by(0, 0.2); return accept; }
|
||||
}
|
||||
// GESTURE: Keep a crop that leaves a mask outside
|
||||
// where: Develop
|
||||
// touch: Press "Keep crop" on the notice, or "Undo
|
||||
// crop" to take it back
|
||||
// pointer: Press "Keep crop" on the notice, or "Undo
|
||||
// crop" to take it back
|
||||
// keys: `Enter` keeps it; `Ctrl+Z` takes the crop
|
||||
// back, like any other step
|
||||
//
|
||||
// TRACES: FR-DEV-17 | FR-DEV-16
|
||||
// Only while the notice is up: Enter means nothing
|
||||
// else here, and a key that dismissed a notice nobody
|
||||
// could see would be a key that did nothing visible.
|
||||
if (Framing.hidden-masks > 0 && Keys.chord(event) == "Enter") {
|
||||
Framing.hidden-masks-kept();
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Go back to the grid
|
||||
// where: Develop
|
||||
// touch: Press "‹ Library" in the top bar
|
||||
// pointer: Press "‹ Library" in the top bar
|
||||
// keys: `G`
|
||||
// why: Lightroom's key for the grid. Escape gets
|
||||
// there too, but a step at a time — out of a
|
||||
// mode, then out of a zoom — where this goes
|
||||
// straight back.
|
||||
//
|
||||
// TRACES: FR-DEV-16
|
||||
// Only where there is a grid: files named on the
|
||||
// command line have none, which is also when the bar
|
||||
// hides its button.
|
||||
if (Library.library-total > 0 && Keys.chord(event) == "G") {
|
||||
root.back-to-library();
|
||||
return accept;
|
||||
}
|
||||
// TRACES: FR-DEV-19a | FR-DEV-16
|
||||
// J turns the selected part's join chip — see "Change
|
||||
// how a part joins its mask" in masks.slint.
|
||||
// The chip's own cycle, on the part the brush and
|
||||
// the edge controls point at. Rust resolves which that
|
||||
// is, and does nothing on a base part, which joins
|
||||
// nothing.
|
||||
if (Develop.local-mode && Keys.chord(event) == "J") {
|
||||
Masking.selected-part-join-cycled();
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Nudge the control last moved
|
||||
// where: Develop
|
||||
// touch: Drag its track
|
||||
// pointer: Drag its track
|
||||
// keys: `=` or `Plus` up and `-` down, a hundredth
|
||||
// of its travel at a time; hold for more
|
||||
// why: Lightroom's keys for the selected slider.
|
||||
// There is no focus ring on a slider here, so
|
||||
// "selected" is the last one moved — the same
|
||||
// control `R` puts back — which covers the
|
||||
// framing sliders, perspective included, as
|
||||
// well as the adjustments.
|
||||
//
|
||||
// TRACES: FR-DEV-16 | FR-DEV-20
|
||||
// A framing slider moved last takes the key; Rust
|
||||
// works out an adjustment's step from its row, where
|
||||
// the range is.
|
||||
if (Keys.chord(event) == "=" || Keys.chord(event) == "Plus"
|
||||
|| Keys.chord(event) == "-") {
|
||||
if (Framing.touched > 0) {
|
||||
root.framing-nudged(Keys.chord(event) == "-" ? -1 : 1);
|
||||
} else if (Adjustments.touched-op >= 0) {
|
||||
Adjustments.param-nudged(
|
||||
Adjustments.touched-op, Adjustments.touched-param,
|
||||
Keys.chord(event) == "-" ? -1 : 1);
|
||||
}
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Change which group of adjustments is on screen
|
||||
// where: Develop
|
||||
// touch: Tap a group in the rail down the left
|
||||
@@ -2390,7 +2605,8 @@ in property <bool> panel-visible: true;
|
||||
// touch: Double-tap the photograph
|
||||
// pointer: Double-click it, or press the zoom
|
||||
// readout floating over the canvas
|
||||
// keys: `Z`
|
||||
// keys: `Z` goes in and back out; `Ctrl+1` goes to
|
||||
// 1:1 and `Ctrl+0` back to the whole frame
|
||||
// why: Noise reduction and capture
|
||||
// sharpening are judgements about
|
||||
// single pixels, and a fitted view
|
||||
@@ -2421,6 +2637,10 @@ in property <bool> panel-visible: true;
|
||||
// been moved on this photograph until something
|
||||
// has, so the key is silent until then rather than
|
||||
// resetting a control nobody named.
|
||||
if (Keys.chord(event) == "R" && Framing.touched > 0) {
|
||||
root.framing-reset-touched();
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "R"
|
||||
&& Adjustments.touched-op >= 0) {
|
||||
Adjustments.param-reset(
|
||||
@@ -2437,6 +2657,42 @@ in property <bool> panel-visible: true;
|
||||
Masking.selected-toggled();
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Rate this photograph
|
||||
// where: Develop
|
||||
// touch: Tap a star in the top bar
|
||||
// pointer: Click a star in the top bar
|
||||
// keys: `0`–`5`
|
||||
//
|
||||
// GESTURE: Pick or reject this photograph
|
||||
// where: Develop
|
||||
// touch: Press Pick or Reject in the top bar; again
|
||||
// to take the flag off
|
||||
// pointer: Press Pick or Reject in the top bar; again
|
||||
// to take the flag off
|
||||
// keys: `P` picks, `X` rejects and `U` takes the
|
||||
// flag off
|
||||
// why: The grid's keys, on the photograph that is
|
||||
// open (FR-UI-5, 2026-09-19). Judging here does
|
||||
// not move on to the next frame: that belongs
|
||||
// to culling, and in develop the photograph in
|
||||
// front of you is the one being worked on.
|
||||
//
|
||||
// TRACES: FR-UI-5 | FR-CULL-4 | FR-DEV-16
|
||||
// Only with a library, like the labels below: a file
|
||||
// named on the command line has no catalog to hold a
|
||||
// rating.
|
||||
if (Library.library-total > 0
|
||||
&& Library.library-roll-current >= 0) {
|
||||
if (Keys.chord(event) == "0") { Library.library-cell-rated(Library.library-roll-current, 0); return accept; }
|
||||
if (Keys.chord(event) == "1") { Library.library-cell-rated(Library.library-roll-current, 1); return accept; }
|
||||
if (Keys.chord(event) == "2") { Library.library-cell-rated(Library.library-roll-current, 2); return accept; }
|
||||
if (Keys.chord(event) == "3") { Library.library-cell-rated(Library.library-roll-current, 3); return accept; }
|
||||
if (Keys.chord(event) == "4") { Library.library-cell-rated(Library.library-roll-current, 4); return accept; }
|
||||
if (Keys.chord(event) == "5") { Library.library-cell-rated(Library.library-roll-current, 5); return accept; }
|
||||
if (Keys.chord(event) == "P") { Library.library-cell-flagged(Library.library-roll-current, 1); return accept; }
|
||||
if (Keys.chord(event) == "X") { Library.library-cell-flagged(Library.library-roll-current, 2); return accept; }
|
||||
if (Keys.chord(event) == "U") { Library.library-cell-flagged(Library.library-roll-current, 0); return accept; }
|
||||
}
|
||||
// GESTURE: Give this photograph a colour label
|
||||
// where: Develop
|
||||
// touch: Tap Label in the top bar, then a
|
||||
@@ -3085,17 +3341,8 @@ in property <bool> panel-visible: true;
|
||||
: (root.export-to-server
|
||||
? "Export " + Library.library-selected-count + " to the library"
|
||||
: "Export " + Library.library-selected-count);
|
||||
can-export: root.active-view == View.develop
|
||||
? !root.export-busy && root.total > 0
|
||||
: !Library.library-exporting && Library.library-selected-count > 0;
|
||||
export => {
|
||||
root.export-sheet-open = false;
|
||||
if (root.active-view == View.develop) {
|
||||
root.export-image();
|
||||
} else {
|
||||
Library.library-export-selection();
|
||||
}
|
||||
}
|
||||
can-export: root.export-sheet-can-export;
|
||||
export => { root.export-from-sheet(); }
|
||||
dismiss => { root.export-sheet-open = false; }
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +106,11 @@ component TreeRow inherits Rectangle {
|
||||
callback rename-committed(string);
|
||||
/// Renaming abandoned with Escape; the old name stands.
|
||||
callback rename-cancelled();
|
||||
/// TRACES: FR-UI-5
|
||||
/// Enter or Escape finished the name, and the keyboard should go back to
|
||||
/// whatever had it. Not raised when the name is committed by clicking
|
||||
/// away: the click has already put focus where the user wanted it.
|
||||
callback keys-released();
|
||||
|
||||
height: Theme.row-height;
|
||||
|
||||
@@ -255,6 +260,15 @@ component TreeRow inherits Rectangle {
|
||||
// the input has focus and consumes it.
|
||||
if (Keys.chord(event) == "Escape") {
|
||||
root.rename-cancelled();
|
||||
root.keys-released();
|
||||
return accept;
|
||||
}
|
||||
// Enter keeps the name. The input's own `accepted`
|
||||
// would do the same; answering it here is what puts the
|
||||
// key where the checker can see it bound.
|
||||
if (Keys.chord(event) == "Enter") {
|
||||
root.rename-committed(self.text);
|
||||
root.keys-released();
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
@@ -375,7 +389,7 @@ component TreeRow inherits Rectangle {
|
||||
// where: Collections sidebar
|
||||
// touch: Hold the collection, then "Rename"
|
||||
// pointer: Double-click its name, or right-click it and choose "Rename"
|
||||
// keys: `Escape` while typing abandons the new name
|
||||
// keys: Type the name and press `Enter`; `Escape` abandons it
|
||||
// why: Double-click is what a file manager and a Lightroom panel use
|
||||
// for the same thing, so it needs no discovering — but nothing
|
||||
// on screen says so, which is what the menu item is for.
|
||||
@@ -506,6 +520,8 @@ export component CollectionsPanel inherits Rectangle {
|
||||
callback rename-commit(int, string);
|
||||
/// Renaming was abandoned.
|
||||
callback rename-cancel();
|
||||
/// A key finished a rename; see `CollectionRow.keys-released`.
|
||||
callback keys-released();
|
||||
|
||||
// --- trash (FR-CAT-15) ---
|
||||
//
|
||||
@@ -698,6 +714,7 @@ export component CollectionsPanel inherits Rectangle {
|
||||
rename-requested => { root.rename-start(row.id); }
|
||||
rename-committed(name) => { root.rename-commit(row.id, name); }
|
||||
rename-cancelled => { root.rename-cancel(); }
|
||||
keys-released => { root.keys-released(); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Develop } from "session.slint";
|
||||
import { Transfer } from "adjust.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel } from "widgets.slint";
|
||||
import { Labels, LabelMark, LabelPicker } from "labels.slint";
|
||||
import { StarStrip } from "library.slint";
|
||||
|
||||
// Status strip — surfaces the GPU backend and adapter, which matters during
|
||||
// v0.1 because assumption A1 is exactly "does this compositing path work on
|
||||
@@ -51,6 +52,13 @@ export component StatusBar inherits Rectangle {
|
||||
/// Whether the label choices are open in the strip.
|
||||
property <bool> picking-label: false;
|
||||
callback label-picked(int);
|
||||
/// TRACES: FR-UI-5 | FR-CULL-4
|
||||
/// The open photograph's stars, or -1 where there is no library to hold
|
||||
/// them, and its flag: 0 none, 1 pick, 2 reject.
|
||||
in property <int> rating: -1;
|
||||
in property <int> flag: 0;
|
||||
callback rated(int);
|
||||
callback flagged(int);
|
||||
|
||||
callback back-to-library();
|
||||
callback open-people();
|
||||
@@ -149,6 +157,40 @@ export component StatusBar inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-5 | FR-CULL-4 | NFR-A11Y-3
|
||||
// The judgement, shown and set on the photograph that is open — the
|
||||
// pointer and touch half of the rating and flag keys, which a tablet
|
||||
// has no number row for (FR-UI-5, amended 2026-09-19). The grid's own
|
||||
// stars, so a rating looks the same in both places; the star already
|
||||
// set clears it, as there.
|
||||
StarStrip {
|
||||
rating: max(0, root.rating);
|
||||
show-empty: true;
|
||||
can-trash: false;
|
||||
visible: root.rating >= 0;
|
||||
y: (parent.height - self.height) / 2;
|
||||
rate(n) => { root.rated(n); }
|
||||
}
|
||||
|
||||
// Pressed again, a flag comes off: two buttons that each toggle,
|
||||
// rather than a third for "none" that is only ever pressed to undo
|
||||
// one of the other two.
|
||||
Button {
|
||||
text: root.flag == 1 ? "Picked" : "Pick";
|
||||
active: root.flag == 1;
|
||||
visible: root.rating >= 0;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.flagged(root.flag == 1 ? 0 : 1); }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: root.flag == 2 ? "Rejected" : "Reject";
|
||||
active: root.flag == 2;
|
||||
visible: root.rating >= 0;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.flagged(root.flag == 2 ? 0 : 2); }
|
||||
}
|
||||
|
||||
Rectangle { horizontal-stretch: 1; }
|
||||
|
||||
// --- instrumentation lives in Settings now ------------------------
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
// the confirm/reject pair sits on the face itself rather than behind a menu.
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Keys } from "keys.slint";
|
||||
import { Panel, Button, IconButton, Field, ListView } from "widgets.slint";
|
||||
import { SliderRow } from "controls.slint";
|
||||
import { Icon } from "icons.slint";
|
||||
@@ -290,6 +291,11 @@ export component IdentityScreen inherits Rectangle {
|
||||
in property <string> back-label: "‹ Library";
|
||||
|
||||
callback person-picked(int);
|
||||
/// TRACES: FR-UI-5
|
||||
/// The person above (-1) or below (1) the selected one on the rail — the
|
||||
/// arrow keys. Rust walks the rail, because the selection is an id and
|
||||
/// only Rust can find it in the list.
|
||||
callback person-stepped(int);
|
||||
callback rename(string);
|
||||
callback confirm-face(int);
|
||||
callback reject-face(int);
|
||||
@@ -376,6 +382,50 @@ export component IdentityScreen inherits Rectangle {
|
||||
name-field.text = root.selected-name;
|
||||
}
|
||||
|
||||
// GESTURE: Move between people
|
||||
// where: People
|
||||
// touch: Tap a person on the rail
|
||||
// pointer: Click a person on the rail
|
||||
// keys: `Up` and `Down`, along the rail
|
||||
//
|
||||
// GESTURE: Name a person
|
||||
// where: People
|
||||
// touch: Tap the name field over their faces, type, and press Enter
|
||||
// pointer: Click the name field over their faces, type, and press Enter
|
||||
// keys: `F2` puts the name field under the keys
|
||||
// why: The rename key everywhere else. Enter finishes the name and
|
||||
// hands the keys back to the rail, so naming a run of people
|
||||
// is `Down`, `F2`, a name, Enter, over and over.
|
||||
//
|
||||
// TRACES: FR-UI-5
|
||||
// The screen's keys, on a holder of no size that takes focus on show, as
|
||||
// the grid's judgement keys do: without a focus item nothing on this
|
||||
// screen hears a key at all — Escape included, which bubbles from here to
|
||||
// the shell and leaves. Not an ancestor of the name field, so the arrows
|
||||
// move the caret there rather than the selection.
|
||||
screen-keys := FocusScope {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
init => { self.focus(); }
|
||||
|
||||
// KEYMAP: People
|
||||
key-pressed(event) => {
|
||||
if (Keys.chord(event) == "Up") {
|
||||
root.person-stepped(-1);
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "Down") {
|
||||
root.person-stepped(1);
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "F2" && root.selected-person >= 0) {
|
||||
name-field.take-focus();
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
// ── the people rail ───────────────────────────────────────────────
|
||||
Panel {
|
||||
@@ -570,7 +620,9 @@ export component IdentityScreen inherits Rectangle {
|
||||
// and the screen looks stuck.
|
||||
accepted(t) => {
|
||||
root.rename(t);
|
||||
name-field.release-focus();
|
||||
// To the screen's keys rather than to nothing, so
|
||||
// the arrows walk the rail again straight away.
|
||||
screen-keys.focus();
|
||||
}
|
||||
}
|
||||
if root.selected-person < 0: Text {
|
||||
|
||||
@@ -632,6 +632,10 @@ export global Library {
|
||||
// own rather than a zero score.
|
||||
/// A star was clicked on a cell: row, then the rating 0..5.
|
||||
callback library-cell-rated(int, int);
|
||||
/// TRACES: FR-UI-5 | FR-CULL-4
|
||||
/// Pick (1), reject (2) or clear the flag (0) of one photograph by row —
|
||||
/// the one open in develop.
|
||||
callback library-cell-flagged(int, int);
|
||||
/// The trash target was clicked on one cell, by row.
|
||||
callback library-cell-trashed(int);
|
||||
/// The burst mark was clicked on one cell, by row (FR-CULL-5).
|
||||
@@ -1556,6 +1560,22 @@ export component LibraryGrid inherits Rectangle {
|
||||
/// Whether the selection bar's label picker is open. Local, like
|
||||
/// `keywording`.
|
||||
property <bool> labelling: false;
|
||||
/// TRACES: FR-UI-5 | FR-CULL-4
|
||||
/// Whether the selection bar's pick / reject choices are open. Local,
|
||||
/// like `labelling`.
|
||||
property <bool> flagging: false;
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
/// Give the keyboard back to the grid.
|
||||
///
|
||||
/// For a field outside the grid that has just finished — a collection's
|
||||
/// name in the sidebar. The field took focus to be typed into and then
|
||||
/// went away with it, and from there no key reached anything: the arrows,
|
||||
/// the judgement keys and Escape were all dead until something was
|
||||
/// clicked.
|
||||
public function take-keys() {
|
||||
judge-keys.focus();
|
||||
}
|
||||
|
||||
/// TRACES: FR-UI-5
|
||||
/// The library ordinal of the cell under the pointer, -1 when none.
|
||||
@@ -3070,6 +3090,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
root.labelling = false;
|
||||
return accept;
|
||||
}
|
||||
if (root.flagging) {
|
||||
root.flagging = false;
|
||||
return accept;
|
||||
}
|
||||
if (root.filing) {
|
||||
root.filing = false;
|
||||
return accept;
|
||||
@@ -3125,6 +3149,48 @@ export component LibraryGrid inherits Rectangle {
|
||||
root.select-all();
|
||||
return accept;
|
||||
}
|
||||
// TRACES: FR-CAT-5 | FR-UI-5
|
||||
// Lightroom's two chords for "select none". Clears, and
|
||||
// stays in select mode, as the strip's Clear does.
|
||||
if (Keys.chord(event) == "Ctrl+D"
|
||||
|| Keys.chord(event) == "Ctrl+Shift+A") {
|
||||
root.clear-selection();
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Keyword the selection
|
||||
// where: Library grid
|
||||
// touch: Select them, then Keywords in the selection
|
||||
// bar
|
||||
// pointer: Select them, then Keywords in the selection
|
||||
// bar
|
||||
// keys: `Ctrl+K`
|
||||
// why: Lightroom's keywording chord. The sheet opens
|
||||
// with its field ready for typing, so the
|
||||
// keys that judge in the grid are out of the
|
||||
// way until it closes.
|
||||
//
|
||||
// TRACES: FR-CAT-5 | FR-CAT-6 | FR-UI-5
|
||||
// What the Keywords button does, and only when it is on
|
||||
// the bar: with nothing selected there is nothing to
|
||||
// keyword.
|
||||
if (Keys.chord(event) == "Ctrl+K") {
|
||||
if (root.selected-count > 0) {
|
||||
root.keywords-opened();
|
||||
root.keywording = true;
|
||||
}
|
||||
return accept;
|
||||
}
|
||||
// TRACES: FR-UI-4 | FR-UI-5
|
||||
// Lightroom's grid keys for the thumbnail size, the same
|
||||
// step as a notch of Ctrl and the wheel.
|
||||
if (Keys.chord(event) == "=" || Keys.chord(event) == "Plus") {
|
||||
root.zoom-cells(1);
|
||||
return accept;
|
||||
}
|
||||
if (Keys.chord(event) == "-") {
|
||||
root.zoom-cells(-1);
|
||||
return accept;
|
||||
}
|
||||
// GESTURE: Show only photographs with some number of stars
|
||||
// where: Library grid
|
||||
// touch: Tap a star chip in the filter bar
|
||||
@@ -3201,6 +3267,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
if (Keys.chord(event) == "9") { root.labelled(4, root.hover-ordinal); return accept; }
|
||||
// GESTURE: Pick or reject a photograph
|
||||
// where: Library grid
|
||||
// touch: Select them, then Flag in the selection bar
|
||||
// and Pick, Reject or No flag
|
||||
// pointer: Select them, then Flag in the selection bar
|
||||
// and Pick, Reject or No flag
|
||||
// keys: `P` picks, `X` rejects and `U` takes the flag
|
||||
// off, with the pointer over it or on the
|
||||
// selection
|
||||
@@ -3404,6 +3474,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// where: Library grid
|
||||
// touch: Pinch the grid with two fingers
|
||||
// pointer: Ctrl and the scroll wheel
|
||||
// keys: `=` or `Plus` for larger, `-` for smaller
|
||||
// why: There is no wheel on a tablet, so without the
|
||||
// pinch the cell size could only be changed by a
|
||||
// control a finger cannot reach.
|
||||
@@ -4832,6 +4903,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// where: Library grid
|
||||
// touch: Press Clear in the selection strip
|
||||
// pointer: Press Clear in the selection strip
|
||||
// keys: `Ctrl+D` or `Ctrl+Shift+A`
|
||||
// why: Distinct from Done, which leaves the mode
|
||||
// entirely. Clearing keeps it, so the next
|
||||
// selection can start straight away.
|
||||
@@ -4933,6 +5005,45 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-UI-5 | FR-CULL-4
|
||||
// Pick or reject the selection. Until this the flag had no route
|
||||
// but `P`, `X` and `U`, which a tablet does not have. Opened like
|
||||
// Label, so the three choices take room on the bar only while
|
||||
// they are being made.
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Flag";
|
||||
active: root.flagging;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.flagging = !root.flagging; }
|
||||
}
|
||||
|
||||
if !root.ranging && root.selected-count > 0 && root.flagging: Button {
|
||||
text: "Pick";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
root.judged(-1, 1, -1);
|
||||
root.flagging = false;
|
||||
}
|
||||
}
|
||||
|
||||
if !root.ranging && root.selected-count > 0 && root.flagging: Button {
|
||||
text: "Reject";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
root.judged(-1, 2, -1);
|
||||
root.flagging = false;
|
||||
}
|
||||
}
|
||||
|
||||
if !root.ranging && root.selected-count > 0 && root.flagging: Button {
|
||||
text: "No flag";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
root.judged(-1, 0, -1);
|
||||
root.flagging = false;
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// Paste the copied edit onto the selection. One of the things a
|
||||
// selection can have done to it, like filing it in a collection,
|
||||
|
||||
@@ -368,6 +368,8 @@ component MaskEntry inherits Rectangle {
|
||||
// where: Develop
|
||||
// touch: Tap the + / − / ∩ chip on the part's row
|
||||
// pointer: Click the + / − / ∩ chip on the part's row
|
||||
// keys: `J` turns the selected part's chip, while
|
||||
// masking
|
||||
// why: A chip that cycles rather than a menu, because
|
||||
// a photographer flips a join while looking at
|
||||
// the picture, not at the panel: add, take
|
||||
@@ -879,6 +881,10 @@ export global Masking {
|
||||
/// TRACES: FR-DEV-19a
|
||||
callback part-selected(string, int);
|
||||
callback part-join-picked(string, int, int);
|
||||
/// TRACES: FR-DEV-16
|
||||
/// The selected part's join chip, turned once — the keyboard's copy of
|
||||
/// tapping it. Rust knows which part the tools point at.
|
||||
callback selected-part-join-cycled();
|
||||
/// Whether the part at this index is left out of the build.
|
||||
callback part-hidden-toggled(string, int, bool);
|
||||
callback part-removed(string, int);
|
||||
|
||||
Reference in New Issue
Block a user