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:
2026-09-24 23:42:26 -04:00
parent 9d1e31ffbb
commit d489a34190
15 changed files with 975 additions and 160 deletions
+26 -3
View File
@@ -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
View File
@@ -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; }
}
+18 -1
View File
@@ -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(); }
}
}
+42
View File
@@ -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 ------------------------
+53 -1
View File
@@ -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 {
+111
View File
@@ -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,
+6
View File
@@ -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);