Fail CI when a key is bound but not in the gesture book, or listed but not bound
The gesture book is generated from GESTURE tags, so it could not describe a gesture nobody tagged, but nothing made anyone tag one. The arrow keys, Enter, P, X, U, Delete, F1 and F2 all worked in the grid with no line in the help sheet, and a tag could name a key whose handler had gone. Key handlers now compare one canonical string, Keys.chord(event) == "Ctrl+Z", instead of reading event.text and the modifiers themselves. keys.slint folds the key and its modifiers into that spelling, so the literal in the handler is the whole binding and the checker reads exactly what the handler dispatches on. Each handler carries a KEYMAP comment naming the gesture-book section its keys belong to, and a tag's keys field names its keys between backticks. gestures-check now fails when a handler binds a key no tag in that section names, when a tag names a key no handler there binds, when any .slint file other than keys.slint reads event.text, when a compared literal is not canonical, and when keys.slint's named keys drift from the Rust list. Spellings are normalised in one place, chord.rs: Ctrl+z, Control+Z and LeftArrow all mean what the handler's "Ctrl+Z" and "Left" mean. Shift and Alt count only for letters and named keys, because on the French layout every digit needs shift and a 6 has to be a 6 however it was typed. A Rust keymap that both dispatched and was read by the generator was the alternative. It would have moved the handlers' decisions away from the Slint state they depend on, and a window that forgot to install it would have had no working keys at all. The keys that were already bound and undocumented are now tagged.
This commit is contained in:
@@ -91,7 +91,9 @@ jobs:
|
|||||||
# they will conclude the application is broken rather than the page.
|
# they will conclude the application is broken rather than the page.
|
||||||
#
|
#
|
||||||
# This also fails on a malformed tag, so a typo costs a gesture its
|
# This also fails on a malformed tag, so a typo costs a gesture its
|
||||||
# desktop half loudly rather than silently.
|
# desktop half loudly rather than silently — and on a key a Slint
|
||||||
|
# handler binds that no tag names, or a key a tag names that no handler
|
||||||
|
# binds (tools/traceability/src/keymap.rs).
|
||||||
- name: Regenerate the gesture vocabulary and check it is committed
|
- name: Regenerate the gesture vocabulary and check it is committed
|
||||||
run: cargo run -q -p traceability -- gestures-check
|
run: cargo run -q -p traceability -- gestures-check
|
||||||
|
|
||||||
|
|||||||
+45
-45
File diff suppressed because one or more lines are too long
+133
-66
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
Every entry here is extracted from the comment beside the code that implements it, so this file cannot describe a gesture the application does not have. Add one by writing a `GESTURE:` block next to the implementation; there is nowhere else to write it.
|
Every entry here is extracted from the comment beside the code that implements it, so this file cannot describe a gesture the application does not have. Add one by writing a `GESTURE:` block next to the implementation; there is nowhere else to write it.
|
||||||
|
|
||||||
51 gestures, in 4 places.
|
58 gestures, in 5 places.
|
||||||
|
|
||||||
## Develop
|
## Develop
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ Sampling a neutral is the first move of the tonal pass — every colour judgemen
|
|||||||
|
|
||||||
Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as magnifying the picture rather than sliding it about. Double-tap is the way to an exact 1:1; this is the way to everything in between. Past 1:1 the pixels are shown as they are, square and unsmoothed; below it, filtered.
|
Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as magnifying the picture rather than sliding it about. Double-tap is the way to an exact 1:1; this is the way to everything in between. Past 1:1 the pixels are shown as they are, square and unsmoothed; below it, filtered.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:1780`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:1792`</sub>
|
||||||
|
|
||||||
### Move a magnified photograph about
|
### Move a magnified photograph about
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as m
|
|||||||
|
|
||||||
Only once there is something outside the viewport to reach, which is why the cursor becomes a hand exactly then. The view is clamped to the frame: panning past the edge would show undefined area beside the photograph, and that reads as a rendering fault rather than as the end of the picture.
|
Only once there is something outside the viewport to reach, which is why the cursor becomes a hand exactly then. The view is clamped to the frame: panning past the edge would show undefined area beside the photograph, and that reads as a rendering fault rather than as the end of the picture.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:1874`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:1886`</sub>
|
||||||
|
|
||||||
### Paint a mask by hand
|
### Paint a mask by hand
|
||||||
|
|
||||||
@@ -47,143 +47,151 @@ Only once there is something outside the viewport to reach, which is why the cur
|
|||||||
|
|
||||||
A model's mask stops inside a shoulder and leaks into the hair, and no single edge control fixes two errors that go opposite ways. The whole stroke is one step in the history, so taking a mark back costs one press however long it took to make.
|
A model's mask stops inside a shoulder and leaks into the hair, and no single edge control fixes two errors that go opposite ways. The whole stroke is one step in the history, so taking a mark back costs one press however long it took to make.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:1962`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:1974`</sub>
|
||||||
|
|
||||||
### Take back the last change
|
### Take back the last change
|
||||||
|
|
||||||
- **Touch** — Tap the step above the current one in the History list
|
- **Touch** — Tap the step above the current one in the History list
|
||||||
- **Pointer** — Click it, or press Undo in the History header
|
- **Pointer** — Click it, or press Undo in the History header
|
||||||
- **Keyboard** — Ctrl+Z
|
- **Keyboard** — `Ctrl+Z`
|
||||||
- **See it** — [in the manual](manual/README.md#history-snapshots-presets)
|
- **See it** — [in the manual](manual/README.md#history-snapshots-presets)
|
||||||
|
|
||||||
A whole drag is one step, so undo takes back a decision rather than a frame of a gesture. The list is there because arriving six steps back costs what arriving from one does.
|
A whole drag is one step, so undo takes back a decision rather than a frame of a gesture. The list is there because arriving six steps back costs what arriving from one does.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2194`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2208`</sub>
|
||||||
|
|
||||||
### Do it again after taking it back
|
### Do it again after taking it back
|
||||||
|
|
||||||
- **Touch** — Tap the step below the current one in the History list
|
- **Touch** — Tap the step below the current one in the History list
|
||||||
- **Pointer** — Click it, or press Redo in the History header
|
- **Pointer** — Click it, or press Redo in the History header
|
||||||
- **Keyboard** — Ctrl+Shift+Z
|
- **Keyboard** — `Ctrl+Shift+Z`
|
||||||
- **See it** — [in the manual](manual/README.md#history-snapshots-presets)
|
- **See it** — [in the manual](manual/README.md#history-snapshots-presets)
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2208`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2222`</sub>
|
||||||
|
|
||||||
|
### Remove a repair
|
||||||
|
|
||||||
|
- **Touch** — Tap it, then Delete Repair
|
||||||
|
- **Pointer** — Click it, then Delete Repair
|
||||||
|
- **Keyboard** — `Delete` or `Backspace`, while repairing
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/app.slint:2240`</sub>
|
||||||
|
|
||||||
### Copy the settings from this photograph
|
### Copy the settings from this photograph
|
||||||
|
|
||||||
- **Touch** — Press Copy in the top bar
|
- **Touch** — Press Copy in the top bar
|
||||||
- **Pointer** — Press Copy in the top bar
|
- **Pointer** — Press Copy in the top bar
|
||||||
- **Keyboard** — Ctrl+C
|
- **Keyboard** — `Ctrl+C`
|
||||||
- **See it** — [in the manual](manual/README.md#copying-settings)
|
- **See it** — [in the manual](manual/README.md#copying-settings)
|
||||||
|
|
||||||
The button is the copy that has to work: a tablet has no modifier key to hold and no menu bar to hang the action from. The shortcut is an accelerator for a control that is on screen either way.
|
The button is the copy that has to work: a tablet has no modifier key to hold and no menu bar to hang the action from. The shortcut is an accelerator for a control that is on screen either way.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2242`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2259`</sub>
|
||||||
|
|
||||||
### Paste the settings onto this photograph
|
### Paste the settings onto this photograph
|
||||||
|
|
||||||
- **Touch** — Press Paste in the top bar
|
- **Touch** — Press Paste in the top bar
|
||||||
- **Pointer** — Press Paste in the top bar
|
- **Pointer** — Press Paste in the top bar
|
||||||
- **Keyboard** — Ctrl+V
|
- **Keyboard** — `Ctrl+V`
|
||||||
- **See it** — [in the manual](manual/README.md#copying-settings)
|
- **See it** — [in the manual](manual/README.md#copying-settings)
|
||||||
|
|
||||||
The button names what would be pasted — "3 adjustments", and whether the crop is coming with it — which the shortcut cannot say. Both paste the same scope.
|
The button names what would be pasted — "3 adjustments", and whether the crop is coming with it — which the shortcut cannot say. Both paste the same scope.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2255`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2272`</sub>
|
||||||
|
|
||||||
### Choose which kinds of edit a copy carries
|
### Choose which kinds of edit a copy carries
|
||||||
|
|
||||||
- **Touch** — Open Presets and toggle the kinds
|
- **Touch** — Open Presets and toggle the kinds
|
||||||
- **Pointer** — Open Presets and toggle the kinds
|
- **Pointer** — Open Presets and toggle the kinds
|
||||||
- **Keyboard** — Ctrl+Shift+C, which offers Copy beside them
|
- **Keyboard** — `Ctrl+Shift+C`, which offers Copy beside them
|
||||||
- **See it** — [in the manual](manual/README.md#copying-settings)
|
- **See it** — [in the manual](manual/README.md#copying-settings)
|
||||||
|
|
||||||
Lightroom's Copy Settings. Pasting a look across a shoot usually means leaving each frame's crop and rotation alone, and that is a choice to make at the moment of copying.
|
Lightroom's Copy Settings. Pasting a look across a shoot usually means leaving each frame's crop and rotation alone, and that is a choice to make at the moment of copying.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2273`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2290`</sub>
|
||||||
|
|
||||||
### Export this photograph as the last one was
|
### Export this photograph as the last one was
|
||||||
|
|
||||||
- **Touch** — Press Export in the top bar
|
- **Touch** — Press Export in the top bar
|
||||||
- **Pointer** — Press Export in the top bar
|
- **Pointer** — Press Export in the top bar
|
||||||
- **Keyboard** — Ctrl+Shift+E
|
- **Keyboard** — `Ctrl+Shift+E`
|
||||||
- **See it** — [in the manual](manual/README.md#export)
|
- **See it** — [in the manual](manual/README.md#export)
|
||||||
|
|
||||||
Every export runs on the defaults in Settings, so "as the last one was" is what the button already does. The chord is Lightroom's and darktable's, kept so hands that learned it there need not learn it again.
|
Every export runs on the defaults in Settings, so "as the last one was" is what the button already does. The chord is Lightroom's and darktable's, kept so hands that learned it there need not learn it again.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2303`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2315`</sub>
|
||||||
|
|
||||||
### Choose how to export, then export
|
### Choose how to export, then export
|
||||||
|
|
||||||
- **Touch** — Open Settings, then Export defaults
|
- **Touch** — Open Settings, then Export defaults
|
||||||
- **Pointer** — Open Settings, then Export defaults
|
- **Pointer** — Open Settings, then Export defaults
|
||||||
- **Keyboard** — Ctrl+E
|
- **Keyboard** — `Ctrl+E`
|
||||||
- **See it** — [in the manual](manual/README.md#export)
|
- **See it** — [in the manual](manual/README.md#export)
|
||||||
|
|
||||||
The export sheet is the export defaults alone with an Export button. What is chosen there is kept, so it is also what the next Ctrl+Shift+E uses.
|
The export sheet is the export defaults alone with an Export button. What is chosen there is kept, so it is also what the next Ctrl+Shift+E uses.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2316`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2328`</sub>
|
||||||
|
|
||||||
### Change which group of adjustments is on screen
|
### Change which group of adjustments is on screen
|
||||||
|
|
||||||
- **Touch** — Tap a group in the rail down the left
|
- **Touch** — Tap a group in the rail down the left
|
||||||
- **Pointer** — Click a group in the strip above the develop column
|
- **Pointer** — Click a group in the strip above the develop column
|
||||||
- **Keyboard** — [ and ] step through them, wrapping round through "everything"
|
- **Keyboard** — `[` and `]` step through them, wrapping round through "everything"
|
||||||
- **See it** — [in the manual](manual/README.md#developing-a-photograph)
|
- **See it** — [in the manual](manual/README.md#developing-a-photograph)
|
||||||
|
|
||||||
The groups are whatever the operation set declares itself to be about, so there are as many as the pipeline has and no key can be assigned to one of them by name. Stepping is the binding that survives a node being added.
|
The groups are whatever the operation set declares itself to be about, so there are as many as the pipeline has and no key can be assigned to one of them by name. Stepping is the binding that survives a node being added.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2342`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2352`</sub>
|
||||||
|
|
||||||
### Look at the photograph at 1:1
|
### Look at the photograph at 1:1
|
||||||
|
|
||||||
- **Touch** — Double-tap the photograph
|
- **Touch** — Double-tap the photograph
|
||||||
- **Pointer** — Double-click it, or press the zoom readout floating over the canvas
|
- **Pointer** — Double-click it, or press the zoom readout floating over the canvas
|
||||||
- **Keyboard** — Z
|
- **Keyboard** — `Z`
|
||||||
- **See it** — [in the manual](manual/README.md#looking-closer)
|
- **See it** — [in the manual](manual/README.md#looking-closer)
|
||||||
|
|
||||||
Noise reduction and capture sharpening are judgements about single pixels, and a fitted view averages several of the file's into each one on screen — so the frame looks softer than it is and the correction goes too far. The point and the magnification survive opening the next photograph, which is what makes checking the same eye across forty portraits forty keystrokes rather than forty pans. From 1:1 on the photograph is drawn as its own pixels, each a hard-edged square, rather than smoothed into a blur.
|
Noise reduction and capture sharpening are judgements about single pixels, and a fitted view averages several of the file's into each one on screen — so the frame looks softer than it is and the correction goes too far. The point and the magnification survive opening the next photograph, which is what makes checking the same eye across forty portraits forty keystrokes rather than forty pans. From 1:1 on the photograph is drawn as its own pixels, each a hard-edged square, rather than smoothed into a blur.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2378`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2388`</sub>
|
||||||
|
|
||||||
### Give this photograph a colour label
|
### Give this photograph a colour label
|
||||||
|
|
||||||
- **Touch** — Tap Label in the top bar, then a colour
|
- **Touch** — Tap Label in the top bar, then a colour
|
||||||
- **Pointer** — Click Label in the top bar, then a colour
|
- **Pointer** — Click Label in the top bar, then a colour
|
||||||
- **Keyboard** — 6 red, 7 yellow, 8 green, 9 blue; the same key again takes it off
|
- **Keyboard** — `6` red, `7` yellow, `8` green, `9` blue; the same key again takes it off
|
||||||
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
||||||
|
|
||||||
The grid's keys, on the photograph that is open, so labelling while stepping through a folder is one hand's work. The bar names the label in words beside its mark.
|
The grid's keys, on the photograph that is open, so labelling while stepping through a folder is one hand's work. The bar names the label in words beside its mark.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2434`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2440`</sub>
|
||||||
|
|
||||||
### Move to the next or previous photograph
|
### Move to the next or previous photograph
|
||||||
|
|
||||||
- **Touch** — Tap a frame in the roll along the foot of the canvas
|
- **Touch** — Tap a frame in the roll along the foot of the canvas
|
||||||
- **Pointer** — Click a frame in the roll
|
- **Pointer** — Click a frame in the roll
|
||||||
- **Keyboard** — Right arrow, D or space for the next; left arrow or A for the one before
|
- **Keyboard** — `→`, `D` or `Space` for the next; `←` or `A` for the one before
|
||||||
- **See it** — [in the manual](manual/README.md#moving-between-photographs)
|
- **See it** — [in the manual](manual/README.md#moving-between-photographs)
|
||||||
|
|
||||||
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing. A and D as well as the arrows, so the left hand steps along the roll while the right stays on the mouse. Unmodified only: Ctrl+D and Ctrl+A are not this.
|
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing. A and D as well as the arrows, so the left hand steps along the roll while the right stays on the mouse.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2460`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2465`</sub>
|
||||||
|
|
||||||
### See the photograph before you edited it
|
### See the photograph before you edited it
|
||||||
|
|
||||||
- **Touch** — Press and hold "Before"
|
- **Touch** — Press and hold "Before"
|
||||||
- **Pointer** — Press and hold "Before"
|
- **Pointer** — Press and hold "Before"
|
||||||
- **Keyboard** — Hold \
|
- **Keyboard** — Hold `\`
|
||||||
- **See it** — [in the manual](manual/README.md#light)
|
- **See it** — [in the manual](manual/README.md#light)
|
||||||
|
|
||||||
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
|
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2594`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2595`</sub>
|
||||||
|
|
||||||
### Put one control back to its default
|
### Put one control back to its default
|
||||||
|
|
||||||
- **Touch** — Double-tap its track
|
- **Touch** — Double-tap its track
|
||||||
- **Pointer** — Double-click its track, or right-click it
|
- **Pointer** — Double-click its track, or right-click it
|
||||||
- **Keyboard** — R, for the control last moved
|
- **Keyboard** — `R`, for the control last moved
|
||||||
|
|
||||||
The column is 280px wide and the colour mixer alone puts thirty-six of these in it, so a reset button per row would be most of the width. Two ways in with a pointer because right-click is the one a hand already reaches for and double-click is the one that needs no second button. A group's own reset is in its heading; this is the single control.
|
The column is 280px wide and the colour mixer alone puts thirty-six of these in it, so a reset button per row would be most of the width. Two ways in with a pointer because right-click is the one a hand already reaches for and double-click is the one that needs no second button. A group's own reset is in its heading; this is the single control.
|
||||||
|
|
||||||
@@ -213,10 +221,10 @@ The history is forgotten with the sitting, on purpose; a snapshot is the photogr
|
|||||||
|
|
||||||
- **Touch** — Tap the ring at the head of its row
|
- **Touch** — Tap the ring at the head of its row
|
||||||
- **Pointer** — Click the ring at the head of its row
|
- **Pointer** — Click the ring at the head of its row
|
||||||
- **Keyboard** — H, for the selected layer history step, unlike holding "Before" — the layer really is off until it is switched back on.
|
- **Keyboard** — `H`, for the selected layer
|
||||||
- **See it** — [in the manual](manual/README.md#local-adjustments)
|
- **See it** — [in the manual](manual/README.md#local-adjustments)
|
||||||
|
|
||||||
Disabling a layer is the before-and-after a local edit constantly wants, so it is one press away rather than inside the row. It is an edit and does take a
|
Disabling a layer is the before-and-after a local edit constantly wants, so it is one press away rather than inside the row. It is an edit and does take a history step, unlike holding "Before" — the layer really is off until it is switched back on.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/masks.slint:213`</sub>
|
<sub>`ui/dr-ui/ui/masks.slint:213`</sub>
|
||||||
|
|
||||||
@@ -228,7 +236,7 @@ Disabling a layer is the before-and-after a local edit constantly wants, so it i
|
|||||||
|
|
||||||
A mask is judged by seeing where it falls, and two are judged by seeing where they meet — so each row has its own eye rather than the panel having one, and the eye is drawn in the colour the mask shows in, so the row says which shape on the picture is its. Nothing about the edit changes: this is how the photograph is looked at, and takes no history step.
|
A mask is judged by seeing where it falls, and two are judged by seeing where they meet — so each row has its own eye rather than the panel having one, and the eye is drawn in the colour the mask shows in, so the row says which shape on the picture is its. Nothing about the edit changes: this is how the photograph is looked at, and takes no history step.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/masks.slint:281`</sub>
|
<sub>`ui/dr-ui/ui/masks.slint:280`</sub>
|
||||||
|
|
||||||
### Change how a part joins its mask
|
### Change how a part joins its mask
|
||||||
|
|
||||||
@@ -238,7 +246,7 @@ A mask is judged by seeing where it falls, and two are judged by seeing where th
|
|||||||
|
|
||||||
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 away, keep only where both agree, and round.
|
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 away, keep only where both agree, and round.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/masks.slint:368`</sub>
|
<sub>`ui/dr-ui/ui/masks.slint:367`</sub>
|
||||||
|
|
||||||
### Leave one part out of a mask, and put it back
|
### Leave one part out of a mask, and put it back
|
||||||
|
|
||||||
@@ -248,7 +256,19 @@ A chip that cycles rather than a menu, because a photographer flips a join while
|
|||||||
|
|
||||||
The question a correction raises is whether it did what it was for — whether the stroke filled the shoulder, whether the subtracted gradient took only the sky. Removing it answers that and loses it. The same ring the layer wears, one row down, because it is the same question about a smaller thing.
|
The question a correction raises is whether it did what it was for — whether the stroke filled the shoulder, whether the subtracted gradient took only the sky. Removing it answers that and loses it. The same ring the layer wears, one row down, because it is the same question about a smaller thing.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/masks.slint:408`</sub>
|
<sub>`ui/dr-ui/ui/masks.slint:407`</sub>
|
||||||
|
|
||||||
|
## Everywhere
|
||||||
|
|
||||||
|
### Close what is open, or go back a step
|
||||||
|
|
||||||
|
- **Touch** — The system Back gesture, or the Back button
|
||||||
|
- **Pointer** — The Close or Back button on whatever is open
|
||||||
|
- **Keyboard** — `Escape`, or `Back` where the device has one
|
||||||
|
|
||||||
|
One key for "up one", innermost first: a question before the sheet under it, a sheet before the view, a view before the library. Nothing is left behind a dialogue that the key walked straight past.
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/app.slint:916`</sub>
|
||||||
|
|
||||||
## Collections sidebar
|
## Collections sidebar
|
||||||
|
|
||||||
@@ -260,7 +280,7 @@ The question a correction raises is whether it did what it was for — whether t
|
|||||||
|
|
||||||
The tree is inside a Flickable, which claims any drag beginning inside it — so with a finger a drag on a row is a scroll until something says otherwise. The hold is that something, and it is what every mobile list already uses to pick a row up. The row lifts the moment it fires, so the gesture says it has been understood before anything moves.
|
The tree is inside a Flickable, which claims any drag beginning inside it — so with a finger a drag on a row is a scroll until something says otherwise. The hold is that something, and it is what every mobile list already uses to pick a row up. The row lifts the moment it fires, so the gesture says it has been understood before anything moves.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/collections.slint:335`</sub>
|
<sub>`ui/dr-ui/ui/collections.slint:337`</sub>
|
||||||
|
|
||||||
### Act on a collection — rename, nest, un-nest, delete
|
### Act on a collection — rename, nest, un-nest, delete
|
||||||
|
|
||||||
@@ -270,7 +290,7 @@ The tree is inside a Flickable, which claims any drag beginning inside it — so
|
|||||||
|
|
||||||
The hold arms a drag and opens this menu, and which one you get is decided by whether you moved — the same fork the grid uses. One menu for everything done to a row, because there is one hold per row: while the hold opened the offline question by itself, nothing else the tree can do had a touch route.
|
The hold arms a drag and opens this menu, and which one you get is decided by whether you moved — the same fork the grid uses. One menu for everything done to a row, because there is one hold per row: while the hold opened the offline question by itself, nothing else the tree can do had a touch route.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/collections.slint:347`</sub>
|
<sub>`ui/dr-ui/ui/collections.slint:349`</sub>
|
||||||
|
|
||||||
### Take a collection back out of the one it is nested in
|
### Take a collection back out of the one it is nested in
|
||||||
|
|
||||||
@@ -280,17 +300,18 @@ The hold arms a drag and opens this menu, and which one you get is decided by wh
|
|||||||
|
|
||||||
Nesting is a drag of one row onto another, and its inverse had no gesture at all: "All photographs" refused every drop, which is right for a photograph — it is already in the library — and wrong for a collection, which has a top level to be returned to. Without it a collection dragged into another was in there permanently.
|
Nesting is a drag of one row onto another, and its inverse had no gesture at all: "All photographs" refused every drop, which is right for a photograph — it is already in the library — and wrong for a collection, which has a top level to be returned to. Without it a collection dragged into another was in there permanently.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/collections.slint:358`</sub>
|
<sub>`ui/dr-ui/ui/collections.slint:360`</sub>
|
||||||
|
|
||||||
### Rename a collection
|
### Rename a collection
|
||||||
|
|
||||||
- **Touch** — Hold the collection, then "Rename"
|
- **Touch** — Hold the collection, then "Rename"
|
||||||
- **Pointer** — Double-click its name, or right-click it and choose "Rename"
|
- **Pointer** — Double-click its name, or right-click it and choose "Rename"
|
||||||
|
- **Keyboard** — `Escape` while typing abandons the new name
|
||||||
- **See it** — [in the manual](manual/README.md#collections)
|
- **See it** — [in the manual](manual/README.md#collections)
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/collections.slint:372`</sub>
|
<sub>`ui/dr-ui/ui/collections.slint:374`</sub>
|
||||||
|
|
||||||
## People
|
## People
|
||||||
|
|
||||||
@@ -344,7 +365,7 @@ The right match confidence is a property of your library, not of the model. "Wha
|
|||||||
|
|
||||||
Touch has no ctrl, so without a mode there is no way to select a second photograph — the first tap would open it. The hold is the fast way in and the button is the one that can be found.
|
Touch has no ctrl, so without a mode there is no way to select a second photograph — the first tap would open it. The hold is the fast way in and the button is the one that can be found.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:1629`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1630`</sub>
|
||||||
|
|
||||||
### Add or remove one photograph
|
### Add or remove one photograph
|
||||||
|
|
||||||
@@ -354,16 +375,16 @@ Touch has no ctrl, so without a mode there is no way to select a second photogra
|
|||||||
|
|
||||||
While selecting, a tap never opens. That is the whole point of the mode: one meaning per gesture at a time. Press Done to get tap-to-open back.
|
While selecting, a tap never opens. That is the whole point of the mode: one meaning per gesture at a time. Press Done to get tap-to-open back.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:1639`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1640`</sub>
|
||||||
|
|
||||||
### Leave selecting
|
### Leave selecting
|
||||||
|
|
||||||
- **Touch** — Press Done in the header
|
- **Touch** — Press Done in the header
|
||||||
- **Pointer** — Press Done in the header
|
- **Pointer** — Press Done in the header
|
||||||
- **Keyboard** — Escape
|
- **Keyboard** — `Escape`, or `Back`; an open sheet closes first
|
||||||
- **See it** — [in the manual](manual/README.md#selecting-several)
|
- **See it** — [in the manual](manual/README.md#selecting-several)
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:1648`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1649`</sub>
|
||||||
|
|
||||||
### Pick a photograph up to drag it
|
### Pick a photograph up to drag it
|
||||||
|
|
||||||
@@ -373,17 +394,18 @@ While selecting, a tap never opens. That is the whole point of the mode: one mea
|
|||||||
|
|
||||||
A finger on a photograph might be starting a scroll, and for the first half-second the grid assumes it is. Holding says otherwise, and the ring is the grid saying it heard — from there the drag cannot be lost to a scroll. A mouse never waits: the cursor is precise enough that a sideways drag is unambiguous from the first pixel.
|
A finger on a photograph might be starting a scroll, and for the first half-second the grid assumes it is. Holding says otherwise, and the ring is the grid saying it heard — from there the drag cannot be lost to a scroll. A mouse never waits: the cursor is precise enough that a sideways drag is unambiguous from the first pixel.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:1679`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1680`</sub>
|
||||||
|
|
||||||
### Select a range
|
### Select a range
|
||||||
|
|
||||||
- **Touch** — While selecting, press "Select to…", then tap the last photograph of the run
|
- **Touch** — While selecting, press "Select to…", then tap the last photograph of the run
|
||||||
- **Pointer** — Shift-click the last photograph of the run
|
- **Pointer** — Shift-click the last photograph of the run
|
||||||
|
- **Keyboard** — Shift with any key that walks the grid — `Shift+←`, `Shift+→`, `Shift+↑`, `Shift+↓`, `Shift+Page Up`, `Shift+Page Down`, `Shift+Home`, `Shift+End`
|
||||||
- **See it** — [in the manual](manual/README.md#selecting-several)
|
- **See it** — [in the manual](manual/README.md#selecting-several)
|
||||||
|
|
||||||
This replaced a double tap, which had no visible state and could take forty photographs by accident. The run is resolved by the catalog rather than by what is on screen, so the grid can scroll between the two taps — the ranges that hurt on a tablet are longer than a screenful, which is exactly where a finger sweep runs out.
|
This replaced a double tap, which had no visible state and could take forty photographs by accident. The run is resolved by the catalog rather than by what is on screen, so the grid can scroll between the two taps — the ranges that hurt on a tablet are longer than a screenful, which is exactly where a finger sweep runs out.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:1745`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1746`</sub>
|
||||||
|
|
||||||
### Take the blinks out of a burst
|
### Take the blinks out of a burst
|
||||||
|
|
||||||
@@ -393,7 +415,7 @@ This replaced a double tap, which had no visible state and could take forty phot
|
|||||||
|
|
||||||
Face indexing reads each face's eyes. The chip drops frames where the chosen people are caught blinking, and leaves sunglasses and eyes it could not read alone.
|
Face indexing reads each face's eyes. The chip drops frames where the chosen people are caught blinking, and leaves sunglasses and eyes it could not read alone.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:2459`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:2463`</sub>
|
||||||
|
|
||||||
### Find photographs with two people in them
|
### Find photographs with two people in them
|
||||||
|
|
||||||
@@ -403,7 +425,7 @@ Face indexing reads each face's eyes. The chip drops frames where the chosen peo
|
|||||||
|
|
||||||
"Any of them" is a union and "all of them" is an intersection. The tray is where both terms and the choice between them live, because a filter belongs on the filter bar.
|
"Any of them" is a union and "all of them" is an intersection. The tray is where both terms and the choice between them live, because a filter belongs on the filter bar.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:2489`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:2493`</sub>
|
||||||
|
|
||||||
### Show only photographs with one colour label
|
### Show only photographs with one colour label
|
||||||
|
|
||||||
@@ -413,49 +435,93 @@ Face indexing reads each face's eyes. The chip drops frames where the chosen peo
|
|||||||
|
|
||||||
Each chip is the label's mark and its name, so the one you want is found by reading it; tap the lit chip again to show every label.
|
Each chip is the label's mark and its name, so the one you want is found by reading it; tap the lit chip again to show every label.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:2613`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:2617`</sub>
|
||||||
|
|
||||||
### Export the selection as the last export was
|
### Export the selection as the last export was
|
||||||
|
|
||||||
- **Touch** — Select them, then Export in the selection bar
|
- **Touch** — Select them, then Export in the selection bar
|
||||||
- **Pointer** — Select them, then Export in the selection bar
|
- **Pointer** — Select them, then Export in the selection bar
|
||||||
- **Keyboard** — Ctrl+Shift+E, or Ctrl+E to see the export settings first
|
- **Keyboard** — `Ctrl+Shift+E`, or `Ctrl+E` to see the export settings first
|
||||||
- **See it** — [in the manual](manual/README.md#export)
|
- **See it** — [in the manual](manual/README.md#export)
|
||||||
|
|
||||||
Lightroom's and darktable's chords. Every export runs on the saved defaults, so the plain chord opens them beside an Export button and the shifted one skips straight to exporting.
|
Lightroom's and darktable's chords. Every export runs on the saved defaults, so the plain chord opens them beside an Export button and the shifted one skips straight to exporting.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3077`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3083`</sub>
|
||||||
|
|
||||||
### Paste copied settings onto the selection
|
### Paste copied settings onto the selection
|
||||||
|
|
||||||
- **Touch** — Select them, then "Paste to N" in the selection bar
|
- **Touch** — Select them, then "Paste to N" in the selection bar
|
||||||
- **Pointer** — Select them, then "Paste to N"
|
- **Pointer** — Select them, then "Paste to N"
|
||||||
- **Keyboard** — Ctrl+V
|
- **Keyboard** — `Ctrl+V`
|
||||||
- **See it** — [in the manual](manual/README.md#copying-settings)
|
- **See it** — [in the manual](manual/README.md#copying-settings)
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3102`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3107`</sub>
|
||||||
|
|
||||||
### Show only photographs with some number of stars
|
### Show only photographs with some number of stars
|
||||||
|
|
||||||
- **Touch** — Tap a star chip in the filter bar
|
- **Touch** — Tap a star chip in the filter bar
|
||||||
- **Pointer** — Click a star chip in the filter bar
|
- **Pointer** — Click a star chip in the filter bar
|
||||||
- **Keyboard** — Hold F and tap a digit for exactly that many stars, or two digits for everything between them; tap F alone to show every rating again
|
- **Keyboard** — Hold `F` and tap a digit, `0`–`5`, for exactly that many stars, or two digits for everything between them; tap `F` alone to show every rating again
|
||||||
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
||||||
|
|
||||||
The chips say "this many or more". A range with a ceiling — the twos and threes still to be decided — is the keyboard's alone, and the bar says so in words while it holds.
|
The chips say "this many or more". A range with a ceiling — the twos and threes still to be decided — is the keyboard's alone, and the bar says so in words while it holds.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3125`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3128`</sub>
|
||||||
|
|
||||||
### Give photographs a colour label
|
### Give photographs a colour label
|
||||||
|
|
||||||
- **Touch** — Select them, then Label in the selection bar and tap a colour
|
- **Touch** — Select them, then Label in the selection bar and tap a colour
|
||||||
- **Pointer** — Select them, then Label in the selection bar and click a colour
|
- **Pointer** — Select them, then Label in the selection bar and click a colour
|
||||||
- **Keyboard** — 6 red, 7 yellow, 8 green, 9 blue, with the pointer over it or on the selection; the same key again takes the label off
|
- **Keyboard** — `6` red, `7` yellow, `8` green, `9` blue, with the pointer over it or on the selection; the same key again takes the label off
|
||||||
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
||||||
|
|
||||||
Lightroom's keys, so hands that learned them there need not learn them again. Purple has no key there either, and is on the bar. Every mark carries its label's initial, so the label is read without telling the colours apart.
|
Lightroom's keys, so hands that learned them there need not learn them again. Purple has no key there either, and is on the bar. Every mark carries its label's initial, so the label is read without telling the colours apart.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3175`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3178`</sub>
|
||||||
|
|
||||||
|
### Pick or reject a photograph
|
||||||
|
|
||||||
|
- **Keyboard** — `P` picks, `X` rejects and `U` takes the flag off, with the pointer over it or on the selection
|
||||||
|
|
||||||
|
The keys every culling tool uses, so muscle memory built elsewhere works here.
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/library.slint:3202`</sub>
|
||||||
|
|
||||||
|
### Move photographs to the trash
|
||||||
|
|
||||||
|
- **Touch** — Tap the bin at the start of a cell's stars
|
||||||
|
- **Pointer** — Hover the cell and click the bin before its stars
|
||||||
|
- **Keyboard** — `Delete` or `Backspace`, on the selection
|
||||||
|
|
||||||
|
The bin acts on one photograph, so a stray click cannot trash a selection; the key acts on the selection because that is what every file manager's Delete does. Both are undone from the trash view.
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/library.slint:3225`</sub>
|
||||||
|
|
||||||
|
### Open this list
|
||||||
|
|
||||||
|
- **Touch** — Press Help in the header
|
||||||
|
- **Pointer** — Press Help in the header
|
||||||
|
- **Keyboard** — `F1`
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/library.slint:3250`</sub>
|
||||||
|
|
||||||
|
### Rename the collection the grid is showing
|
||||||
|
|
||||||
|
- **Touch** — Hold it in the sidebar, then "Rename"
|
||||||
|
- **Pointer** — Double-click it in the sidebar
|
||||||
|
- **Keyboard** — `F2`
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/library.slint:3256`</sub>
|
||||||
|
|
||||||
|
### Move through the grid
|
||||||
|
|
||||||
|
- **Touch** — Scroll, and tap a photograph
|
||||||
|
- **Pointer** — Scroll, and click a photograph
|
||||||
|
- **Keyboard** — `←`, `→`, `↑` and `↓` move one photograph; `Page Up` and `Page Down` a screenful; `Home` and `End` to the first and the last
|
||||||
|
|
||||||
|
The cursor selects what it lands on, so walking and judging are one hand's work.
|
||||||
|
|
||||||
|
<sub>`ui/dr-ui/ui/library.slint:3276`</sub>
|
||||||
|
|
||||||
### Resize the thumbnails
|
### Resize the thumbnails
|
||||||
|
|
||||||
@@ -465,7 +531,7 @@ Lightroom's keys, so hands that learned them there need not learn them again. Pu
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3356`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3403`</sub>
|
||||||
|
|
||||||
### File photographs in a collection
|
### File photographs in a collection
|
||||||
|
|
||||||
@@ -475,28 +541,29 @@ There is no wheel on a tablet, so without the pinch the cell size could only be
|
|||||||
|
|
||||||
The selection is what the drag carries, which is why selecting several is worth the mode: forty photographs file in one gesture.
|
The selection is what the drag carries, which is why selecting several is worth the mode: forty photographs file in one gesture.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3554`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3601`</sub>
|
||||||
|
|
||||||
### Open a photograph
|
### Open a photograph
|
||||||
|
|
||||||
- **Touch** — Tap it — a single tap, any length
|
- **Touch** — Tap it — a single tap, any length
|
||||||
- **Pointer** — Click it
|
- **Pointer** — Click it
|
||||||
|
- **Keyboard** — `Enter`, on the photograph the arrow keys have walked to
|
||||||
- **See it** — [in the manual](manual/README.md#developing-a-photograph)
|
- **See it** — [in the manual](manual/README.md#developing-a-photograph)
|
||||||
|
|
||||||
A tap opens; a tap that *moved* does not. Travel is what separates a deliberate tap from a hand brushing past, and it is the only thing that does: the two are the same length. An earlier version required the finger to dwell 120 ms instead, and that rejected ordinary taps — a real tap is often quicker than a brush.
|
A tap opens; a tap that *moved* does not. Travel is what separates a deliberate tap from a hand brushing past, and it is the only thing that does: the two are the same length. An earlier version required the finger to dwell 120 ms instead, and that rejected ordinary taps — a real tap is often quicker than a brush.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3851`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3898`</sub>
|
||||||
|
|
||||||
### Rate a photograph without opening it
|
### Rate a photograph without opening it
|
||||||
|
|
||||||
- **Touch** — Tap a star on the cell
|
- **Touch** — Tap a star on the cell
|
||||||
- **Pointer** — Hover the cell, then click a star
|
- **Pointer** — Hover the cell, then click a star
|
||||||
- **Keyboard** — 0 to 5 with the pointer over it, or on the selection
|
- **Keyboard** — `0`–`5` with the pointer over it, or on the selection
|
||||||
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
- **See it** — [in the manual](manual/README.md#rating-and-flagging)
|
||||||
|
|
||||||
A star has to take the press without it also reaching the cell, or every rating throws the user into develop.
|
A star has to take the press without it also reaching the cell, or every rating throws the user into develop.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3972`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4021`</sub>
|
||||||
|
|
||||||
### Choose the frame a folded burst shows
|
### Choose the frame a folded burst shows
|
||||||
|
|
||||||
@@ -506,7 +573,7 @@ A star has to take the press without it also reaching the cell, or every rating
|
|||||||
|
|
||||||
A folded burst draws its earliest frame, which is a fact about the clock and not a judgement about the photograph — nothing in this application ranks a frame (FR-CULL-5). But the point of a burst is that one of the twelve is better than the other eleven, and the photographer is the only one who knows which. So the choice is offered on the frames themselves, while they are open and side by side, which is the one moment the alternatives are on screen to be compared.
|
A folded burst draws its earliest frame, which is a fact about the clock and not a judgement about the photograph — nothing in this application ranks a frame (FR-CULL-5). But the point of a burst is that one of the twelve is better than the other eleven, and the photographer is the only one who knows which. So the choice is offered on the frames themselves, while they are open and side by side, which is the one moment the alternatives are on screen to be compared.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:4105`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4154`</sub>
|
||||||
|
|
||||||
### Drop the selection but keep selecting
|
### Drop the selection but keep selecting
|
||||||
|
|
||||||
@@ -516,18 +583,18 @@ A folded burst draws its earliest frame, which is a fact about the clock and not
|
|||||||
|
|
||||||
Distinct from Done, which leaves the mode entirely. Clearing keeps it, so the next selection can start straight away.
|
Distinct from Done, which leaves the mode entirely. Clearing keeps it, so the next selection can start straight away.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:4782`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4831`</sub>
|
||||||
|
|
||||||
### Select everything the grid is showing
|
### Select everything the grid is showing
|
||||||
|
|
||||||
- **Touch** — While selecting, press "Select all"
|
- **Touch** — While selecting, press "Select all"
|
||||||
- **Pointer** — While selecting, press "Select all"
|
- **Pointer** — While selecting, press "Select all"
|
||||||
- **Keyboard** — Ctrl+A
|
- **Keyboard** — `Ctrl+A`
|
||||||
- **See it** — [in the manual](manual/README.md#selecting-several)
|
- **See it** — [in the manual](manual/README.md#selecting-several)
|
||||||
|
|
||||||
A scoped grid of two hundred frames is two hundred taps otherwise, and "all of them, except those three" is a far more common shape than the taps it took to say it.
|
A scoped grid of two hundred frames is two hundred taps otherwise, and "all of them, except those three" is a far more common shape than the taps it took to say it.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:4800`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4849`</sub>
|
||||||
|
|
||||||
### Take photographs out of a collection
|
### Take photographs out of a collection
|
||||||
|
|
||||||
@@ -537,4 +604,4 @@ A scoped grid of two hundred frames is two hundred taps otherwise, and "all of t
|
|||||||
|
|
||||||
The badge on a cell says a photograph is filed in three collections and never which. This is the sheet that names them, and the only way out of one the grid is not currently scoped to.
|
The badge on a cell says a photograph is filed in three collections and never which. This is the sheet that names them, and the only way out of one the grid is not currently scoped to.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:4942`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4991`</sub>
|
||||||
|
|||||||
@@ -0,0 +1,515 @@
|
|||||||
|
//! TRACES: FR-UI-5 | FR-DEV-16
|
||||||
|
//! The one place a key is spelled.
|
||||||
|
//!
|
||||||
|
//! A key reaches this repository in three spellings: the text Slint hands a
|
||||||
|
//! `key-pressed` handler (`"\u{F702}"` for the left arrow, `"Z"` for a shifted
|
||||||
|
//! z), the literal a handler compares against, and the words a `GESTURE:` tag
|
||||||
|
//! uses to tell a person which key to press. The checker in
|
||||||
|
//! [`crate::keymap`] only works if all three meet in one vocabulary, and this
|
||||||
|
//! is it.
|
||||||
|
//!
|
||||||
|
//! # The canonical form
|
||||||
|
//!
|
||||||
|
//! `Ctrl+Alt+Shift+Meta+Key`, modifiers in that order, each present only when
|
||||||
|
//! held. The key is one of:
|
||||||
|
//!
|
||||||
|
//! - a **named key** from [`NAMED`] — `Left`, `Enter`, `Escape`, `F1`, `Space`
|
||||||
|
//! and the rest;
|
||||||
|
//! - a **letter**, upper case: `Z`. Case is not a key — caps lock during a long
|
||||||
|
//! cull must not stop `P` picking — so `z` and `Z` are one key, and shift is
|
||||||
|
//! read from the modifier rather than from the case;
|
||||||
|
//! - any **other printable character**, as itself: `[`, `6`, `\`, `=`, with
|
||||||
|
//! `+` spelled `Plus` because it is the separator.
|
||||||
|
//!
|
||||||
|
//! **Shift and Alt count only for letters and named keys.** For every other
|
||||||
|
//! character the layout has already applied them: `}` is what shift does to
|
||||||
|
//! `]` on one keyboard and not another, and on the French layout this
|
||||||
|
//! application's author uses, *every digit* needs shift. A binding of `6` has
|
||||||
|
//! to answer the key that produces a 6, whatever it took to produce it, so the
|
||||||
|
//! canonical form of a shifted 6 is `6` — and a spelling like `Shift+6` is
|
||||||
|
//! refused rather than accepted as a binding no keyboard can reach.
|
||||||
|
//!
|
||||||
|
//! The same rules are implemented once more, in `ui/dr-ui/ui/keys.slint`,
|
||||||
|
//! because the application compares keys in Slint and cannot call this crate.
|
||||||
|
//! That copy is held to this one by the checker: it reads the names the Slint
|
||||||
|
//! function can produce and fails if they are not exactly [`NAMED`], and it
|
||||||
|
//! fails if a handler compares against a literal that is not canonical here.
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// A named key: its canonical spelling, what a person sees, and the other
|
||||||
|
/// spellings accepted for it in a tag.
|
||||||
|
///
|
||||||
|
/// `shown` is what the help sheet prints. The arrows get arrows; everything
|
||||||
|
/// else is already the word on the keycap.
|
||||||
|
pub struct Named {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub shown: &'static str,
|
||||||
|
pub aliases: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every named key a binding may use. Mirrored in `keys.slint`'s `named`.
|
||||||
|
pub const NAMED: &[Named] = &[
|
||||||
|
Named {
|
||||||
|
name: "Left",
|
||||||
|
shown: "←",
|
||||||
|
aliases: &["leftarrow", "left arrow", "←"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Right",
|
||||||
|
shown: "→",
|
||||||
|
aliases: &["rightarrow", "right arrow", "→"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Up",
|
||||||
|
shown: "↑",
|
||||||
|
aliases: &["uparrow", "up arrow", "↑"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Down",
|
||||||
|
shown: "↓",
|
||||||
|
aliases: &["downarrow", "down arrow", "↓"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Home",
|
||||||
|
shown: "Home",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "End",
|
||||||
|
shown: "End",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "PageUp",
|
||||||
|
shown: "Page Up",
|
||||||
|
aliases: &["page up", "pgup"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "PageDown",
|
||||||
|
shown: "Page Down",
|
||||||
|
aliases: &["page down", "pgdn"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Enter",
|
||||||
|
shown: "Enter",
|
||||||
|
aliases: &["return"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Escape",
|
||||||
|
shown: "Escape",
|
||||||
|
aliases: &["esc"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Back",
|
||||||
|
shown: "Back",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Tab",
|
||||||
|
shown: "Tab",
|
||||||
|
aliases: &["backtab"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Backspace",
|
||||||
|
shown: "Backspace",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Delete",
|
||||||
|
shown: "Delete",
|
||||||
|
aliases: &["del"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Insert",
|
||||||
|
shown: "Insert",
|
||||||
|
aliases: &["ins"],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "Space",
|
||||||
|
shown: "Space",
|
||||||
|
aliases: &["spacebar", " "],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F1",
|
||||||
|
shown: "F1",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F2",
|
||||||
|
shown: "F2",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F3",
|
||||||
|
shown: "F3",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F4",
|
||||||
|
shown: "F4",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F5",
|
||||||
|
shown: "F5",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F6",
|
||||||
|
shown: "F6",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F7",
|
||||||
|
shown: "F7",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F8",
|
||||||
|
shown: "F8",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F9",
|
||||||
|
shown: "F9",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F10",
|
||||||
|
shown: "F10",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F11",
|
||||||
|
shown: "F11",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
Named {
|
||||||
|
name: "F12",
|
||||||
|
shown: "F12",
|
||||||
|
aliases: &[],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/// One key with the modifiers held for it.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Chord {
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub shift: bool,
|
||||||
|
pub meta: bool,
|
||||||
|
/// Canonical: a [`NAMED`] name, an upper-case letter, `Plus`, or the
|
||||||
|
/// printable character itself.
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Chord {
|
||||||
|
/// Parse any accepted spelling: `Ctrl+z`, `Control+Z`, `ctrl+shift+z`,
|
||||||
|
/// `LeftArrow`, `←`, `Esc`, `Ctrl++`.
|
||||||
|
pub fn parse(spelling: &str) -> Result<Chord, String> {
|
||||||
|
let s = spelling.trim();
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err("an empty key".into());
|
||||||
|
}
|
||||||
|
// `+` is both the separator and a key. A spelling ending in `++`, or
|
||||||
|
// that is `+` alone, has the plus key as its last part.
|
||||||
|
let (mods, key) = if s == "+" {
|
||||||
|
("", "+")
|
||||||
|
} else if let Some(head) = s.strip_suffix("++") {
|
||||||
|
(head, "+")
|
||||||
|
} else {
|
||||||
|
match s.rsplit_once('+') {
|
||||||
|
Some((head, key)) => (head, key),
|
||||||
|
None => ("", s),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut c = Chord {
|
||||||
|
ctrl: false,
|
||||||
|
alt: false,
|
||||||
|
shift: false,
|
||||||
|
meta: false,
|
||||||
|
key: String::new(),
|
||||||
|
};
|
||||||
|
if !mods.is_empty() {
|
||||||
|
for m in mods.split('+') {
|
||||||
|
let slot = match m.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"ctrl" | "control" | "ctl" => &mut c.ctrl,
|
||||||
|
"alt" | "option" | "opt" => &mut c.alt,
|
||||||
|
"shift" => &mut c.shift,
|
||||||
|
"meta" | "super" | "win" => &mut c.meta,
|
||||||
|
other => return Err(format!("`{other}` is not a modifier in `{spelling}`")),
|
||||||
|
};
|
||||||
|
if *slot {
|
||||||
|
return Err(format!("a modifier named twice in `{spelling}`"));
|
||||||
|
}
|
||||||
|
*slot = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (key, counts_shift) = canonical_key(key)
|
||||||
|
.ok_or_else(|| format!("`{}` is not a key this vocabulary knows", key.trim()))?;
|
||||||
|
if !counts_shift && (c.shift || c.alt) {
|
||||||
|
return Err(format!(
|
||||||
|
"`{spelling}`: shift and alt are part of the character on a printable key \
|
||||||
|
(the layout decides what they make), so they cannot be bound — write the \
|
||||||
|
character itself"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
c.key = key;
|
||||||
|
Ok(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The canonical spelling: what `Keys.chord` in `keys.slint` produces and
|
||||||
|
/// what a handler's literal must be.
|
||||||
|
pub fn canonical(&self) -> String {
|
||||||
|
self.with(&self.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the help sheet prints: canonical, with arrows drawn as arrows.
|
||||||
|
pub fn shown(&self) -> String {
|
||||||
|
let key = NAMED
|
||||||
|
.iter()
|
||||||
|
.find(|n| n.name == self.key)
|
||||||
|
.map_or(self.key.as_str(), |n| n.shown);
|
||||||
|
self.with(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with(&self, key: &str) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
for (on, word) in [
|
||||||
|
(self.ctrl, "Ctrl+"),
|
||||||
|
(self.alt, "Alt+"),
|
||||||
|
(self.shift, "Shift+"),
|
||||||
|
(self.meta, "Meta+"),
|
||||||
|
] {
|
||||||
|
if on {
|
||||||
|
s.push_str(word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.push_str(key);
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Chord {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.canonical())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The canonical key for one spelling, and whether shift and alt count for it.
|
||||||
|
fn canonical_key(spelling: &str) -> Option<(String, bool)> {
|
||||||
|
// A lone space is a key and must not be trimmed away; anything longer is
|
||||||
|
// a word and its padding is not part of it.
|
||||||
|
let raw = if spelling == " " {
|
||||||
|
spelling
|
||||||
|
} else {
|
||||||
|
spelling.trim()
|
||||||
|
};
|
||||||
|
let lower = raw.to_lowercase();
|
||||||
|
for n in NAMED {
|
||||||
|
if n.name.to_ascii_lowercase() == lower || n.aliases.contains(&lower.as_str()) {
|
||||||
|
return Some((n.name.to_string(), true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lower == "plus" {
|
||||||
|
return Some(("Plus".into(), false));
|
||||||
|
}
|
||||||
|
let mut chars = raw.chars();
|
||||||
|
let c = chars.next()?;
|
||||||
|
if chars.next().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if c == '+' {
|
||||||
|
return Some(("Plus".into(), false));
|
||||||
|
}
|
||||||
|
if c.is_control() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let upper: String = c.to_uppercase().collect();
|
||||||
|
let lower: String = c.to_lowercase().collect();
|
||||||
|
if upper != lower {
|
||||||
|
Some((upper, true))
|
||||||
|
} else {
|
||||||
|
Some((c.to_string(), false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every chord a `keys:` field names, in order, or why one would not parse.
|
||||||
|
///
|
||||||
|
/// A chord is written between backticks — `` `Ctrl+Z` `` — which is what lets
|
||||||
|
/// the field stay a sentence for a person ("the same key again takes it off")
|
||||||
|
/// while every key in it is still read by a machine. Two single-character
|
||||||
|
/// chords joined by an en dash or " to " are a range: `` `0`–`5` `` names all
|
||||||
|
/// six digits, because writing out six backticked digits helps nobody.
|
||||||
|
pub fn chords_in(keys: &str) -> Result<Vec<Chord>, String> {
|
||||||
|
let parts: Vec<&str> = keys.split('`').collect();
|
||||||
|
if parts.len().is_multiple_of(2) {
|
||||||
|
return Err("an unclosed backtick".into());
|
||||||
|
}
|
||||||
|
let tokens: Vec<(usize, &str)> = parts
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(i, _)| i % 2 == 1)
|
||||||
|
.map(|(i, t)| (i, *t))
|
||||||
|
.collect();
|
||||||
|
if tokens.is_empty() {
|
||||||
|
return Err(
|
||||||
|
"names no key — write each key between backticks, e.g. `Ctrl+Z`, so the \
|
||||||
|
checker can hold it to a handler"
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut out: Vec<Chord> = Vec::new();
|
||||||
|
let mut prev: Option<(usize, Chord)> = None;
|
||||||
|
for (i, tok) in tokens {
|
||||||
|
let chord = Chord::parse(tok)?;
|
||||||
|
if let Some((pi, p)) = &prev {
|
||||||
|
let between = parts[pi + 1];
|
||||||
|
if *pi + 2 == i && (between == "–" || between == " to ") {
|
||||||
|
for mid in range_between(p, &chord)? {
|
||||||
|
out.push(mid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(chord.clone());
|
||||||
|
prev = Some((i, chord));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The chords strictly between two ends of a written range.
|
||||||
|
fn range_between(a: &Chord, b: &Chord) -> Result<Vec<Chord>, String> {
|
||||||
|
let (ca, cb) = (single(&a.key), single(&b.key));
|
||||||
|
let (Some(ca), Some(cb)) = (ca, cb) else {
|
||||||
|
return Err(format!(
|
||||||
|
"`{a}`–`{b}` is not a range: only single characters make one"
|
||||||
|
));
|
||||||
|
};
|
||||||
|
if !(ca.is_ascii_alphanumeric() && cb.is_ascii_alphanumeric()) || ca >= cb {
|
||||||
|
return Err(format!("`{a}`–`{b}` is not an ascending range"));
|
||||||
|
}
|
||||||
|
Ok(((ca as u8 + 1)..(cb as u8))
|
||||||
|
.map(|m| Chord {
|
||||||
|
key: (m as char).to_string(),
|
||||||
|
..a.clone()
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn single(s: &str) -> Option<char> {
|
||||||
|
let mut c = s.chars();
|
||||||
|
let first = c.next()?;
|
||||||
|
c.next().is_none().then_some(first)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `keys:` field as a person reads it: every chord in its shown form.
|
||||||
|
///
|
||||||
|
/// `markdown` keeps the backticks, so the document sets keys as code; the
|
||||||
|
/// application's sheet drops them. A chord that does not parse is left as
|
||||||
|
/// written — the checker reports it, and rendering is not the place to fail.
|
||||||
|
pub fn render_keys(keys: &str, markdown: bool) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
for (i, part) in keys.split('`').enumerate() {
|
||||||
|
if i % 2 == 0 {
|
||||||
|
out.push_str(part);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let shown = Chord::parse(part).map_or_else(|_| part.to_string(), |c| c.shown());
|
||||||
|
if markdown {
|
||||||
|
out.push('`');
|
||||||
|
out.push_str(&shown);
|
||||||
|
out.push('`');
|
||||||
|
} else {
|
||||||
|
out.push_str(&shown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn canon(s: &str) -> String {
|
||||||
|
Chord::parse(s).unwrap().canonical()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The spellings a person writes for one key all meet in one place.
|
||||||
|
#[test]
|
||||||
|
fn spellings_of_one_chord_agree() {
|
||||||
|
for s in ["Ctrl+Z", "Control+z", "ctrl+z", " Ctrl + Z "] {
|
||||||
|
assert_eq!(canon(s), "Ctrl+Z", "{s}");
|
||||||
|
}
|
||||||
|
for s in ["Left", "LeftArrow", "←", "left arrow"] {
|
||||||
|
assert_eq!(canon(s), "Left", "{s}");
|
||||||
|
}
|
||||||
|
assert_eq!(canon("Shift+Ctrl+z"), "Ctrl+Shift+Z");
|
||||||
|
assert_eq!(canon("Esc"), "Escape");
|
||||||
|
assert_eq!(canon("Return"), "Enter");
|
||||||
|
assert_eq!(canon("PgUp"), "PageUp");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plus_is_a_key_as_well_as_the_separator() {
|
||||||
|
assert_eq!(canon("Ctrl++"), "Ctrl+Plus");
|
||||||
|
assert_eq!(canon("+"), "Plus");
|
||||||
|
assert_eq!(canon("Ctrl+Plus"), "Ctrl+Plus");
|
||||||
|
assert_eq!(canon("Ctrl+-"), "Ctrl+-");
|
||||||
|
assert_eq!(canon("Ctrl+="), "Ctrl+=");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The French layout needs shift for every digit, so a binding of `6` is
|
||||||
|
/// the character, and `Shift+6` is a binding no handler could receive.
|
||||||
|
#[test]
|
||||||
|
fn shift_on_a_printable_character_is_refused() {
|
||||||
|
assert!(Chord::parse("Shift+6").is_err());
|
||||||
|
assert!(Chord::parse("Alt+[").is_err());
|
||||||
|
assert_eq!(canon("Shift+Left"), "Shift+Left");
|
||||||
|
assert_eq!(canon("Shift+a"), "Shift+A");
|
||||||
|
assert_eq!(canon("Ctrl+0"), "Ctrl+0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonsense_is_refused() {
|
||||||
|
assert!(Chord::parse("").is_err());
|
||||||
|
assert!(Chord::parse("Hyper+Z").is_err());
|
||||||
|
assert!(Chord::parse("Ctrl+Ctrl+Z").is_err());
|
||||||
|
assert!(Chord::parse("Banana").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_sheet_draws_arrows() {
|
||||||
|
assert_eq!(Chord::parse("Shift+Left").unwrap().shown(), "Shift+←");
|
||||||
|
assert_eq!(
|
||||||
|
render_keys("`Right` or `D` for the next", false),
|
||||||
|
"→ or D for the next"
|
||||||
|
);
|
||||||
|
assert_eq!(render_keys("`ctrl+z`", true), "`Ctrl+Z`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_field_names_its_chords_between_backticks() {
|
||||||
|
let c = chords_in("`Right`, `D` or `Space` for the next").unwrap();
|
||||||
|
let names: Vec<String> = c.iter().map(Chord::canonical).collect();
|
||||||
|
assert_eq!(names, ["Right", "D", "Space"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_dash_between_two_digits_is_a_range() {
|
||||||
|
let c = chords_in("`0`–`5` with the pointer over it").unwrap();
|
||||||
|
let names: Vec<String> = c.iter().map(Chord::canonical).collect();
|
||||||
|
assert_eq!(names, ["0", "1", "2", "3", "4", "5"]);
|
||||||
|
let c = chords_in("`6` red, `9` blue").unwrap();
|
||||||
|
assert_eq!(c.len(), 2, "a comma is not a range");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prose naming a key without backticks would be a key nobody checks.
|
||||||
|
#[test]
|
||||||
|
fn a_field_with_no_backticked_key_is_refused() {
|
||||||
|
assert!(chords_in("Hold backslash").is_err());
|
||||||
|
assert!(chords_in("`Ctrl+Z").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -372,6 +372,11 @@ pub fn render_markdown(gestures: &[Gesture]) -> String {
|
|||||||
for g in list {
|
for g in list {
|
||||||
m.push_str(&format!("\n### {}\n\n", g.title));
|
m.push_str(&format!("\n### {}\n\n", g.title));
|
||||||
for (modality, how) in g.routes() {
|
for (modality, how) in g.routes() {
|
||||||
|
let how = if modality == "Keyboard" {
|
||||||
|
crate::chord::render_keys(how, true)
|
||||||
|
} else {
|
||||||
|
how.to_string()
|
||||||
|
};
|
||||||
m.push_str(&format!("- **{modality}** — {how}\n"));
|
m.push_str(&format!("- **{modality}** — {how}\n"));
|
||||||
}
|
}
|
||||||
if let Some(anchor) = &g.manual {
|
if let Some(anchor) = &g.manual {
|
||||||
@@ -442,7 +447,10 @@ pub fn render_rust(gestures: &[Gesture]) -> String {
|
|||||||
));
|
));
|
||||||
r.push_str(&format!(
|
r.push_str(&format!(
|
||||||
" keys: {},\n",
|
" keys: {},\n",
|
||||||
quote(g.keys.as_deref().unwrap_or(""))
|
quote(&crate::chord::render_keys(
|
||||||
|
g.keys.as_deref().unwrap_or(""),
|
||||||
|
false
|
||||||
|
))
|
||||||
));
|
));
|
||||||
r.push_str(&format!(
|
r.push_str(&format!(
|
||||||
" manual: {},\n",
|
" manual: {},\n",
|
||||||
|
|||||||
@@ -0,0 +1,605 @@
|
|||||||
|
//! TRACES: FR-UI-5 | FR-DEV-16 | FR-UI-4
|
||||||
|
//! Every key the application answers is in the gesture book, and every key the
|
||||||
|
//! book names is answered.
|
||||||
|
//!
|
||||||
|
//! # The failure this closes
|
||||||
|
//!
|
||||||
|
//! The gesture book ([`crate::gestures`]) is generated from `GESTURE:` tags,
|
||||||
|
//! so it cannot describe a gesture nobody tagged — but nothing made anyone tag
|
||||||
|
//! one. Arrow keys walked the grid, `P` picked, `Delete` trashed and `F2`
|
||||||
|
//! renamed for months with no line in the book, and a tag could just as well
|
||||||
|
//! name a key whose handler had been deleted. The tags were trusted in both
|
||||||
|
//! directions, and they were right about half the keys.
|
||||||
|
//!
|
||||||
|
//! # Why the handlers compare a canonical string, and not a Rust keymap
|
||||||
|
//!
|
||||||
|
//! There were two ways to make the bound keys machine-readable.
|
||||||
|
//!
|
||||||
|
//! **A keymap table in Rust**, which both dispatches and is read by the
|
||||||
|
//! generator. It is the tidier data structure and the wrong home for the
|
||||||
|
//! dispatch. The handlers' state lives in Slint: the F chord's held flag, which
|
||||||
|
//! sheet is open, which adjustment group is on screen, whether a text field has
|
||||||
|
//! focus. Moving the decision to Rust means either mirroring all of that into
|
||||||
|
//! Rust or passing it across on every key, and a key handler has to answer
|
||||||
|
//! `accept` or `reject` synchronously — which is the part that decides whether
|
||||||
|
//! Escape reaches the shell. And the table would be a callback every window
|
||||||
|
//! must remember to install: forget it once, in a test harness or on Android,
|
||||||
|
//! and every key in the application is silently dead.
|
||||||
|
//!
|
||||||
|
//! **A static extractor over the Slint handlers**, which is what this is — but
|
||||||
|
//! over a shape designed to be extracted, not over whatever a handler happened
|
||||||
|
//! to be written as. Reading `event.text == "z"` and inferring its modifiers
|
||||||
|
//! from the `if` around it is the fragile version: `event.modifiers.shift`
|
||||||
|
//! appears as a guard, as a negated guard, as an argument, and in a nested
|
||||||
|
//! `if` that splits Ctrl+E from Ctrl+Shift+E. So the handlers no longer read
|
||||||
|
//! `event.text` at all. They compare one canonical string:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! if (Keys.chord(event) == "Ctrl+Shift+Z") { … }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! `Keys.chord` (`ui/dr-ui/ui/keys.slint`) folds the text and every modifier
|
||||||
|
//! into the spelling [`crate::chord`] defines, so the literal *is* the whole
|
||||||
|
//! binding — key and modifiers — and the handler dispatches on exactly the
|
||||||
|
//! string this module reads. There is nothing to infer.
|
||||||
|
//!
|
||||||
|
//! What keeps that honest:
|
||||||
|
//!
|
||||||
|
//! 1. `event.text` may appear in no `.slint` file but `keys.slint`. A handler
|
||||||
|
//! that went back to reading it would be a binding this cannot see.
|
||||||
|
//! 2. Every literal compared against `Keys.chord(…)` must already be
|
||||||
|
//! canonical. `"ctrl+z"` would parse, and would never match.
|
||||||
|
//! 3. The names `keys.slint` can produce must be exactly [`crate::chord::NAMED`]:
|
||||||
|
//! the Slint half of the vocabulary cannot grow or lose a key on its own.
|
||||||
|
//! 4. A handler that binds anything carries a `// KEYMAP: <where>` comment
|
||||||
|
//! above it, naming the `where:` its keys are documented under. That is how
|
||||||
|
//! a key in the develop handler is held to the Develop section while a tag
|
||||||
|
//! for it sits in `controls.slint`, beside the control it resets.
|
||||||
|
//!
|
||||||
|
//! Then the two directions, on (place, canonical chord):
|
||||||
|
//!
|
||||||
|
//! - **(a)** a key some handler binds that no `GESTURE:` tag in that place
|
||||||
|
//! names;
|
||||||
|
//! - **(b)** a key some tag names that no handler in that place binds.
|
||||||
|
//!
|
||||||
|
//! A tag's `keys:` names its keys between backticks — see
|
||||||
|
//! [`crate::chord::chords_in`] — so the field stays a sentence for a person.
|
||||||
|
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
use crate::chord::{self, Chord};
|
||||||
|
use crate::gestures::{Gesture, GestureProblem};
|
||||||
|
|
||||||
|
/// The file that owns the raw key text, and the only one allowed to read it.
|
||||||
|
pub const KEYS_FILE: &str = "keys.slint";
|
||||||
|
|
||||||
|
/// One key a handler answers.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Binding {
|
||||||
|
/// The `KEYMAP:` of the handler — a gesture section.
|
||||||
|
pub section: String,
|
||||||
|
/// Canonical chord.
|
||||||
|
pub chord: String,
|
||||||
|
pub file: String,
|
||||||
|
pub line: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn problem(file: &str, line: usize, what: String) -> GestureProblem {
|
||||||
|
GestureProblem {
|
||||||
|
file: file.to_string(),
|
||||||
|
line,
|
||||||
|
what,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every binding in one `.slint` file, and whatever breaks the rules above.
|
||||||
|
pub fn extract_bindings(text: &str, path: &str) -> (Vec<Binding>, Vec<GestureProblem>) {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
let is_keys_file = path.ends_with(KEYS_FILE);
|
||||||
|
let code = code_only(text);
|
||||||
|
let lines: Vec<&str> = text.lines().collect();
|
||||||
|
let code_lines: Vec<&str> = code.lines().collect();
|
||||||
|
|
||||||
|
if !is_keys_file {
|
||||||
|
for (i, l) in code_lines.iter().enumerate() {
|
||||||
|
if l.contains("event.text") {
|
||||||
|
problems.push(problem(
|
||||||
|
path,
|
||||||
|
i + 1,
|
||||||
|
"reads `event.text` — compare `Keys.chord(event)` against a canonical \
|
||||||
|
chord instead, or the key checker cannot see this binding"
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, l) in code_lines.iter().enumerate() {
|
||||||
|
let is_handler =
|
||||||
|
(l.contains("key-pressed(") || l.contains("key-released(")) && l.contains("=>");
|
||||||
|
if !is_handler {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let section = keymap_above(&lines, i);
|
||||||
|
let body_end = block_end(&code_lines, i);
|
||||||
|
let mut found_any = false;
|
||||||
|
for (j, cl) in code_lines.iter().enumerate().take(body_end + 1).skip(i) {
|
||||||
|
for lit in compared_literals(cl, lines[j]) {
|
||||||
|
found_any = true;
|
||||||
|
match Chord::parse(&lit) {
|
||||||
|
Ok(c) if c.canonical() == lit => {
|
||||||
|
if let Some(s) = §ion {
|
||||||
|
out.push(Binding {
|
||||||
|
section: s.clone(),
|
||||||
|
chord: lit,
|
||||||
|
file: path.to_string(),
|
||||||
|
line: j + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(c) => problems.push(problem(
|
||||||
|
path,
|
||||||
|
j + 1,
|
||||||
|
format!(
|
||||||
|
"compares against \"{lit}\", which `Keys.chord` never produces — \
|
||||||
|
write \"{}\"",
|
||||||
|
c.canonical()
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
Err(e) => problems.push(problem(path, j + 1, format!("\"{lit}\": {e}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found_any && section.is_none() {
|
||||||
|
problems.push(problem(
|
||||||
|
path,
|
||||||
|
i + 1,
|
||||||
|
"a key handler with bindings and no `// KEYMAP: <where>` comment above it, \
|
||||||
|
so its keys cannot be held to a place in the gesture book"
|
||||||
|
.into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(out, problems)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `KEYMAP:` in the run of comment lines directly above line `at`.
|
||||||
|
fn keymap_above(lines: &[&str], at: usize) -> Option<String> {
|
||||||
|
let mut i = at;
|
||||||
|
while i > 0 {
|
||||||
|
i -= 1;
|
||||||
|
let t = lines[i].trim_start();
|
||||||
|
let Some(body) = t.strip_prefix("//") else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if let Some(rest) = body.trim_start().strip_prefix("KEYMAP:") {
|
||||||
|
return Some(rest.trim().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The line on which the brace block opened on line `at` closes.
|
||||||
|
///
|
||||||
|
/// Works on text with comments and strings already blanked, so braces inside
|
||||||
|
/// either do not count.
|
||||||
|
fn block_end(code_lines: &[&str], at: usize) -> usize {
|
||||||
|
let mut depth = 0i32;
|
||||||
|
let mut opened = false;
|
||||||
|
for (i, l) in code_lines.iter().enumerate().skip(at) {
|
||||||
|
for ch in l.chars() {
|
||||||
|
match ch {
|
||||||
|
'{' => {
|
||||||
|
depth += 1;
|
||||||
|
opened = true;
|
||||||
|
}
|
||||||
|
'}' => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if opened && depth == 0 {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code_lines.len().saturating_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The literals compared against `Keys.chord(…)` on one line.
|
||||||
|
///
|
||||||
|
/// `code` is the line with strings blanked, used to find the call; `raw` is
|
||||||
|
/// the original, used to read the literal. The two have the same byte offsets.
|
||||||
|
fn compared_literals(code: &str, raw: &str) -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut from = 0;
|
||||||
|
while let Some(at) = code[from..].find("Keys.chord(") {
|
||||||
|
let start = from + at;
|
||||||
|
let Some(close) = code[start..].find(')') else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let after = start + close + 1;
|
||||||
|
from = after;
|
||||||
|
let rest = code[after..].trim_start();
|
||||||
|
let Some(op_rest) = rest.strip_prefix("==").or_else(|| rest.strip_prefix("!=")) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let lit_at = code.len() - op_rest.trim_start().len();
|
||||||
|
if let Some(lit) = read_literal(&raw[lit_at..]) {
|
||||||
|
out.push(lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Slint string literal at the start of `s`, unescaped.
|
||||||
|
fn read_literal(s: &str) -> Option<String> {
|
||||||
|
let mut chars = s.chars();
|
||||||
|
if chars.next()? != '"' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut out = String::new();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
match c {
|
||||||
|
'"' => return Some(out),
|
||||||
|
'\\' => match chars.next()? {
|
||||||
|
'n' => out.push('\n'),
|
||||||
|
't' => out.push('\t'),
|
||||||
|
other => out.push(other),
|
||||||
|
},
|
||||||
|
c => out.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The text with every comment and the inside of every string literal
|
||||||
|
/// replaced by spaces, keeping byte offsets and line breaks.
|
||||||
|
fn code_only(text: &str) -> String {
|
||||||
|
let mut out = String::with_capacity(text.len());
|
||||||
|
let mut in_str = false;
|
||||||
|
let mut in_line_comment = false;
|
||||||
|
let mut in_block_comment = false;
|
||||||
|
let mut escape = false;
|
||||||
|
let bytes: Vec<char> = text.chars().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
let blank = |c: char, out: &mut String| {
|
||||||
|
if c == '\n' {
|
||||||
|
out.push('\n');
|
||||||
|
} else {
|
||||||
|
for _ in 0..c.len_utf8() {
|
||||||
|
out.push(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
while i < bytes.len() {
|
||||||
|
let c = bytes[i];
|
||||||
|
let next = bytes.get(i + 1).copied();
|
||||||
|
if in_line_comment {
|
||||||
|
if c == '\n' {
|
||||||
|
in_line_comment = false;
|
||||||
|
}
|
||||||
|
blank(c, &mut out);
|
||||||
|
} else if in_block_comment {
|
||||||
|
if c == '*' && next == Some('/') {
|
||||||
|
in_block_comment = false;
|
||||||
|
out.push_str(" ");
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
blank(c, &mut out);
|
||||||
|
} else if in_str {
|
||||||
|
if escape {
|
||||||
|
escape = false;
|
||||||
|
blank(c, &mut out);
|
||||||
|
} else if c == '\\' {
|
||||||
|
escape = true;
|
||||||
|
blank(c, &mut out);
|
||||||
|
} else if c == '"' {
|
||||||
|
in_str = false;
|
||||||
|
out.push('"');
|
||||||
|
} else {
|
||||||
|
blank(c, &mut out);
|
||||||
|
}
|
||||||
|
} else if c == '/' && next == Some('/') {
|
||||||
|
in_line_comment = true;
|
||||||
|
out.push_str(" ");
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
} else if c == '/' && next == Some('*') {
|
||||||
|
in_block_comment = true;
|
||||||
|
out.push_str(" ");
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if c == '"' {
|
||||||
|
in_str = true;
|
||||||
|
}
|
||||||
|
out.push(c);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The named keys `keys.slint`'s `named` function can return.
|
||||||
|
///
|
||||||
|
/// Read as the literals of its `return "…";` lines, between `function named`
|
||||||
|
/// and the next function.
|
||||||
|
pub fn slint_named_keys(text: &str) -> BTreeSet<String> {
|
||||||
|
let mut out = BTreeSet::new();
|
||||||
|
let mut inside = false;
|
||||||
|
for l in text.lines() {
|
||||||
|
let t = l.trim_start();
|
||||||
|
if t.starts_with("//") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if t.contains("function ") {
|
||||||
|
inside = t.contains("function named(");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !inside {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(rest) = t.split("return ").nth(1) {
|
||||||
|
if let Some(lit) = read_literal(rest.trim_start()) {
|
||||||
|
if !lit.is_empty() {
|
||||||
|
out.insert(lit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hold the Slint half of the vocabulary to the Rust half.
|
||||||
|
pub fn check_vocabulary(slint_names: &BTreeSet<String>, path: &str) -> Vec<GestureProblem> {
|
||||||
|
let rust: BTreeSet<String> = chord::NAMED.iter().map(|n| n.name.to_string()).collect();
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
for missing in rust.difference(slint_names) {
|
||||||
|
problems.push(problem(
|
||||||
|
path,
|
||||||
|
1,
|
||||||
|
format!("`Keys.named` never returns \"{missing}\", which chord.rs names"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for extra in slint_names.difference(&rust) {
|
||||||
|
problems.push(problem(
|
||||||
|
path,
|
||||||
|
1,
|
||||||
|
format!("`Keys.named` returns \"{extra}\", which chord.rs does not know"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
problems
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Both directions: bound but undocumented, documented but unbound.
|
||||||
|
pub fn cross_check(gestures: &[Gesture], bindings: &[Binding]) -> Vec<GestureProblem> {
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
|
||||||
|
// (section, chord) → where the tag naming it is.
|
||||||
|
let mut documented: BTreeMap<(String, String), (String, usize, String)> = BTreeMap::new();
|
||||||
|
for g in gestures {
|
||||||
|
let Some(keys) = &g.keys else { continue };
|
||||||
|
match chord::chords_in(keys) {
|
||||||
|
Ok(chords) => {
|
||||||
|
for c in chords {
|
||||||
|
documented
|
||||||
|
.entry((g.section.clone(), c.canonical()))
|
||||||
|
.or_insert_with(|| (g.file.clone(), g.line, g.title.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => problems.push(problem(
|
||||||
|
&g.file,
|
||||||
|
g.line,
|
||||||
|
format!("gesture `{}`: `keys:` {e}", g.title),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bound: BTreeMap<(String, String), (String, usize)> = BTreeMap::new();
|
||||||
|
for b in bindings {
|
||||||
|
bound
|
||||||
|
.entry((b.section.clone(), b.chord.clone()))
|
||||||
|
.or_insert_with(|| (b.file.clone(), b.line));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((section, chord), (file, line)) in &bound {
|
||||||
|
if !documented.contains_key(&(section.clone(), chord.clone())) {
|
||||||
|
problems.push(problem(
|
||||||
|
file,
|
||||||
|
*line,
|
||||||
|
format!(
|
||||||
|
"`{chord}` is bound in {section} but no GESTURE tag with \
|
||||||
|
`where: {section}` names it in its `keys:`"
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ((section, chord), (file, line, title)) in &documented {
|
||||||
|
if !bound.contains_key(&(section.clone(), chord.clone())) {
|
||||||
|
problems.push(problem(
|
||||||
|
file,
|
||||||
|
*line,
|
||||||
|
format!(
|
||||||
|
"gesture `{title}` names `{chord}`, but no key handler marked \
|
||||||
|
`KEYMAP: {section}` binds it"
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
problems
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::gestures::extract_from_text;
|
||||||
|
|
||||||
|
/// A handler in the shape the application writes, and a tag for each key.
|
||||||
|
const HANDLER: &str = r#"
|
||||||
|
FocusScope {
|
||||||
|
// KEYMAP: Grid
|
||||||
|
key-pressed(event) => {
|
||||||
|
if (Keys.chord(event) == "Ctrl+Z") { undo(); return accept; }
|
||||||
|
if (Keys.chord(event) == "Left" || Keys.chord(event) == "Shift+Left") {
|
||||||
|
step(-1);
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
return reject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const TAGS: &str = "
|
||||||
|
// GESTURE: Undo
|
||||||
|
// where: Grid
|
||||||
|
// pointer: Click Undo
|
||||||
|
// keys: `Ctrl+Z`
|
||||||
|
// GESTURE: Step back
|
||||||
|
// where: Grid
|
||||||
|
// pointer: Click the previous frame
|
||||||
|
// keys: `Left`, or `Shift+Left` to take the selection with you
|
||||||
|
";
|
||||||
|
|
||||||
|
fn run(handler: &str, tags: &str) -> Vec<GestureProblem> {
|
||||||
|
let (b, mut p) = extract_bindings(handler, "grid.slint");
|
||||||
|
let (g, gp) = extract_from_text(tags, "tags.slint");
|
||||||
|
p.extend(gp);
|
||||||
|
p.extend(cross_check(&g, &b));
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn documented_keys_pass() {
|
||||||
|
let p = run(HANDLER, TAGS);
|
||||||
|
assert!(p.is_empty(), "{p:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_bound_key_with_no_tag_fails() {
|
||||||
|
let handler = HANDLER.replace(
|
||||||
|
"return reject;",
|
||||||
|
"if (Keys.chord(event) == \"Delete\") { trash(); return accept; }\n return reject;",
|
||||||
|
);
|
||||||
|
let p = run(&handler, TAGS);
|
||||||
|
assert_eq!(p.len(), 1, "{p:?}");
|
||||||
|
assert!(
|
||||||
|
p[0].what.contains("`Delete` is bound in Grid"),
|
||||||
|
"{}",
|
||||||
|
p[0].what
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_tagged_key_no_handler_binds_fails() {
|
||||||
|
let tags = TAGS.replace("`Ctrl+Z`", "`Ctrl+Z` or `Ctrl+Y`");
|
||||||
|
let p = run(HANDLER, &tags);
|
||||||
|
assert_eq!(p.len(), 1, "{p:?}");
|
||||||
|
assert!(p[0].what.contains("names `Ctrl+Y`"), "{}", p[0].what);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A key documented in one place does not cover the same key in another.
|
||||||
|
#[test]
|
||||||
|
fn a_key_documented_elsewhere_does_not_count() {
|
||||||
|
let tags = TAGS.replace(
|
||||||
|
"where: Grid\n// pointer: Click Undo",
|
||||||
|
"where: Develop\n// pointer: Click Undo",
|
||||||
|
);
|
||||||
|
let p = run(HANDLER, &tags);
|
||||||
|
assert_eq!(p.len(), 2, "{p:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spelling differences between the tag and the handler are not drift.
|
||||||
|
#[test]
|
||||||
|
fn the_tag_may_spell_a_key_any_accepted_way() {
|
||||||
|
let tags = TAGS
|
||||||
|
.replace("`Ctrl+Z`", "`control+z`")
|
||||||
|
.replace("`Left`", "`←`");
|
||||||
|
let p = run(HANDLER, &tags);
|
||||||
|
assert!(p.is_empty(), "{p:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_event_text_is_refused() {
|
||||||
|
let handler = HANDLER.replace(
|
||||||
|
"return reject;",
|
||||||
|
"if (event.text == \"q\") { quit(); }\n return reject;",
|
||||||
|
);
|
||||||
|
let p = run(&handler, TAGS);
|
||||||
|
assert!(p.iter().any(|x| x.what.contains("event.text")), "{p:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A comment mentioning it is not a read.
|
||||||
|
#[test]
|
||||||
|
fn event_text_in_a_comment_is_fine() {
|
||||||
|
let handler = HANDLER.replace(
|
||||||
|
"return reject;",
|
||||||
|
"// not event.text any more\n return reject;",
|
||||||
|
);
|
||||||
|
assert!(run(&handler, TAGS).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_literal_that_is_not_canonical_is_refused() {
|
||||||
|
let handler = HANDLER.replace("\"Ctrl+Z\"", "\"ctrl+z\"");
|
||||||
|
let p = run(&handler, TAGS);
|
||||||
|
assert!(
|
||||||
|
p.iter().any(|x| x.what.contains("write \"Ctrl+Z\"")),
|
||||||
|
"{p:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_handler_without_keymap_is_refused() {
|
||||||
|
let handler = HANDLER.replace("// KEYMAP: Grid", "// the grid's keys");
|
||||||
|
let p = run(&handler, TAGS);
|
||||||
|
assert!(p.iter().any(|x| x.what.contains("KEYMAP")), "{p:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The handler ends where its braces do: a comparison after it belongs to
|
||||||
|
/// nothing, and a `}` in a string does not end it early.
|
||||||
|
#[test]
|
||||||
|
fn the_block_is_found_by_its_braces() {
|
||||||
|
let handler = r#"
|
||||||
|
// KEYMAP: Grid
|
||||||
|
key-pressed(event) => {
|
||||||
|
if (label == "}") { }
|
||||||
|
if (Keys.chord(event) == "Ctrl+Z") { return accept; }
|
||||||
|
return reject;
|
||||||
|
}
|
||||||
|
function other() { if (Keys.chord(e) == "Q") {} }
|
||||||
|
"#;
|
||||||
|
let (b, p) = extract_bindings(handler, "x.slint");
|
||||||
|
assert!(p.is_empty(), "{p:?}");
|
||||||
|
assert_eq!(b.len(), 1, "{b:?}");
|
||||||
|
assert_eq!(b[0].chord, "Ctrl+Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_backslash_literal_is_unescaped() {
|
||||||
|
let handler =
|
||||||
|
"// KEYMAP: Grid\nkey-pressed(event) => {\n if (Keys.chord(event) == \"\\\\\") {}\n}\n";
|
||||||
|
let (b, p) = extract_bindings(handler, "x.slint");
|
||||||
|
assert!(p.is_empty(), "{p:?}");
|
||||||
|
assert_eq!(b[0].chord, "\\");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_slint_vocabulary_is_read_from_named() {
|
||||||
|
let slint = r#"
|
||||||
|
export global Keys {
|
||||||
|
pure function named(text: string) -> string {
|
||||||
|
if (text == Key.LeftArrow) { return "Left"; }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
public pure function chord(event: KeyEvent) -> string {
|
||||||
|
return "Nope";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
let names = slint_named_keys(slint);
|
||||||
|
assert_eq!(names.into_iter().collect::<Vec<_>>(), ["Left"]);
|
||||||
|
let p = check_vocabulary(&slint_named_keys(slint), "keys.slint");
|
||||||
|
assert!(p.iter().any(|x| x.what.contains("\"Right\"")), "{p:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,9 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub mod chord;
|
||||||
pub mod gestures;
|
pub mod gestures;
|
||||||
|
pub mod keymap;
|
||||||
pub mod manual;
|
pub mod manual;
|
||||||
|
|
||||||
/// Requirement ID prefixes that participate in coverage.
|
/// Requirement ID prefixes that participate in coverage.
|
||||||
@@ -705,6 +707,8 @@ pub fn scan() {}
|
|||||||
for (name, src) in [
|
for (name, src) in [
|
||||||
("lib.rs", include_str!("lib.rs")),
|
("lib.rs", include_str!("lib.rs")),
|
||||||
("gestures.rs", include_str!("gestures.rs")),
|
("gestures.rs", include_str!("gestures.rs")),
|
||||||
|
("chord.rs", include_str!("chord.rs")),
|
||||||
|
("keymap.rs", include_str!("keymap.rs")),
|
||||||
("main.rs", include_str!("main.rs")),
|
("main.rs", include_str!("main.rs")),
|
||||||
] {
|
] {
|
||||||
let fixtures_begin = src
|
let fixtures_begin = src
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! The gesture half scans a different tag out of the same files — see
|
//! The gesture half scans a different tag out of the same files — see
|
||||||
//! [`traceability::gestures`] for what it is and why it lives here.
|
//! [`traceability::gestures`] for what it is and why it lives here — and holds
|
||||||
|
//! the keys the Slint handlers bind to the keys those tags name, in both
|
||||||
|
//! directions ([`traceability::keymap`]).
|
||||||
//!
|
//!
|
||||||
//! The gate fails hard on a *misconfigured run* — zero requirements parsed, or
|
//! The gate fails hard on a *misconfigured run* — zero requirements parsed, or
|
||||||
//! zero source files scanned — rather than reporting a plausible-looking 0%.
|
//! zero source files scanned — rather than reporting a plausible-looking 0%.
|
||||||
@@ -142,6 +144,8 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
|
|||||||
|
|
||||||
let mut found = Vec::new();
|
let mut found = Vec::new();
|
||||||
let mut problems = Vec::new();
|
let mut problems = Vec::new();
|
||||||
|
let mut bindings = Vec::new();
|
||||||
|
let mut keys_file_seen = false;
|
||||||
for file in &files {
|
for file in &files {
|
||||||
let text = std::fs::read_to_string(file).unwrap_or_default();
|
let text = std::fs::read_to_string(file).unwrap_or_default();
|
||||||
let rel = file
|
let rel = file
|
||||||
@@ -159,6 +163,18 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
|
|||||||
let (g, p) = gestures::extract_from_text(&text, &rel);
|
let (g, p) = gestures::extract_from_text(&text, &rel);
|
||||||
found.extend(g);
|
found.extend(g);
|
||||||
problems.extend(p);
|
problems.extend(p);
|
||||||
|
if rel.ends_with(".slint") {
|
||||||
|
let (b, p) = keymap::extract_bindings(&text, &rel);
|
||||||
|
bindings.extend(b);
|
||||||
|
problems.extend(p);
|
||||||
|
if rel.ends_with(&format!("/{}", keymap::KEYS_FILE)) {
|
||||||
|
keys_file_seen = true;
|
||||||
|
problems.extend(keymap::check_vocabulary(
|
||||||
|
&keymap::slint_named_keys(&text),
|
||||||
|
&rel,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A `manual:` must name a section the manual has, checked against the
|
// A `manual:` must name a section the manual has, checked against the
|
||||||
@@ -173,6 +189,7 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
|
|||||||
println!("files scanned {}", files.len());
|
println!("files scanned {}", files.len());
|
||||||
println!("gestures found {}", found.len());
|
println!("gestures found {}", found.len());
|
||||||
println!("places {}", gestures::by_section(&found).len());
|
println!("places {}", gestures::by_section(&found).len());
|
||||||
|
println!("keys bound {}", bindings.len());
|
||||||
|
|
||||||
if !problems.is_empty() {
|
if !problems.is_empty() {
|
||||||
for p in &problems {
|
for p in &problems {
|
||||||
@@ -181,6 +198,26 @@ fn run_gestures(base: &Path, check: bool) -> Result<()> {
|
|||||||
bail!("{} malformed gesture tag(s)", problems.len());
|
bail!("{} malformed gesture tag(s)", problems.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The key half, after the tags parse: a key cannot be held to a tag that
|
||||||
|
// did not. A run that found no bindings, or no `keys.slint`, read the
|
||||||
|
// wrong tree — the application has keys, so zero is a misconfiguration.
|
||||||
|
if !keys_file_seen || bindings.is_empty() {
|
||||||
|
bail!(
|
||||||
|
"no {} or no key bindings found — misconfigured, not an application without keys",
|
||||||
|
keymap::KEYS_FILE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let key_problems = keymap::cross_check(&found, &bindings);
|
||||||
|
if !key_problems.is_empty() {
|
||||||
|
for p in &key_problems {
|
||||||
|
println!(" {p}");
|
||||||
|
}
|
||||||
|
bail!(
|
||||||
|
"{} key(s) bound and undocumented, or documented and unbound",
|
||||||
|
key_problems.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let doc = gestures::render_markdown(&found);
|
let doc = gestures::render_markdown(&found);
|
||||||
let table = gestures::render_rust(&found);
|
let table = gestures::render_rust(&found);
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
keys: "Ctrl+Shift+Z",
|
keys: "Ctrl+Shift+Z",
|
||||||
manual: "history-snapshots-presets",
|
manual: "history-snapshots-presets",
|
||||||
},
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Remove a repair",
|
||||||
|
section: "Develop",
|
||||||
|
touch: "Tap it, then Delete Repair",
|
||||||
|
pointer: "Click it, then Delete Repair",
|
||||||
|
keys: "Delete or Backspace, while repairing",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
title: "Copy the settings from this photograph",
|
title: "Copy the settings from this photograph",
|
||||||
section: "Develop",
|
section: "Develop",
|
||||||
@@ -141,7 +149,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Develop",
|
section: "Develop",
|
||||||
touch: "Tap a frame in the roll along the foot of the canvas",
|
touch: "Tap a frame in the roll along the foot of the canvas",
|
||||||
pointer: "Click a frame in the roll",
|
pointer: "Click a frame in the roll",
|
||||||
keys: "Right arrow, D or space for the next; left arrow or A for the one before",
|
keys: "→, D or Space for the next; ← or A for the one before",
|
||||||
manual: "moving-between-photographs",
|
manual: "moving-between-photographs",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -181,7 +189,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Develop",
|
section: "Develop",
|
||||||
touch: "Tap the ring at the head of its row",
|
touch: "Tap the ring at the head of its row",
|
||||||
pointer: "Click the ring at the head of its row",
|
pointer: "Click the ring at the head of its row",
|
||||||
keys: "H, for the selected layer history step, unlike holding \"Before\" — the layer really is off until it is switched back on.",
|
keys: "H, for the selected layer",
|
||||||
manual: "local-adjustments",
|
manual: "local-adjustments",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -208,6 +216,14 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
keys: "",
|
keys: "",
|
||||||
manual: "local-adjustments",
|
manual: "local-adjustments",
|
||||||
},
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Close what is open, or go back a step",
|
||||||
|
section: "Everywhere",
|
||||||
|
touch: "The system Back gesture, or the Back button",
|
||||||
|
pointer: "The Close or Back button on whatever is open",
|
||||||
|
keys: "Escape, or Back where the device has one",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
title: "Pick a collection up to rearrange the tree",
|
title: "Pick a collection up to rearrange the tree",
|
||||||
section: "Collections sidebar",
|
section: "Collections sidebar",
|
||||||
@@ -237,7 +253,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Collections sidebar",
|
section: "Collections sidebar",
|
||||||
touch: "Hold the collection, then \"Rename\"",
|
touch: "Hold the collection, then \"Rename\"",
|
||||||
pointer: "Double-click its name, or right-click it and choose \"Rename\"",
|
pointer: "Double-click its name, or right-click it and choose \"Rename\"",
|
||||||
keys: "",
|
keys: "Escape while typing abandons the new name",
|
||||||
manual: "collections",
|
manual: "collections",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -293,7 +309,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Library grid",
|
section: "Library grid",
|
||||||
touch: "Press Done in the header",
|
touch: "Press Done in the header",
|
||||||
pointer: "Press Done in the header",
|
pointer: "Press Done in the header",
|
||||||
keys: "Escape",
|
keys: "Escape, or Back; an open sheet closes first",
|
||||||
manual: "selecting-several",
|
manual: "selecting-several",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -309,7 +325,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Library grid",
|
section: "Library grid",
|
||||||
touch: "While selecting, press \"Select to…\", then tap the last photograph of the run",
|
touch: "While selecting, press \"Select to…\", then tap the last photograph of the run",
|
||||||
pointer: "Shift-click the last photograph of the run",
|
pointer: "Shift-click the last photograph of the run",
|
||||||
keys: "",
|
keys: "Shift with any key that walks the grid — Shift+←, Shift+→, Shift+↑, Shift+↓, Shift+Page Up, Shift+Page Down, Shift+Home, Shift+End",
|
||||||
manual: "selecting-several",
|
manual: "selecting-several",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -357,7 +373,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Library grid",
|
section: "Library grid",
|
||||||
touch: "Tap a star chip in the filter bar",
|
touch: "Tap a star chip in the filter bar",
|
||||||
pointer: "Click a star chip in the filter bar",
|
pointer: "Click a star chip in the filter bar",
|
||||||
keys: "Hold F and tap a digit for exactly that many stars, or two digits for everything between them; tap F alone to show every rating again",
|
keys: "Hold F and tap a digit, 0–5, for exactly that many stars, or two digits for everything between them; tap F alone to show every rating again",
|
||||||
manual: "rating-and-flagging",
|
manual: "rating-and-flagging",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -368,6 +384,46 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
keys: "6 red, 7 yellow, 8 green, 9 blue, with the pointer over it or on the selection; the same key again takes the label off",
|
keys: "6 red, 7 yellow, 8 green, 9 blue, with the pointer over it or on the selection; the same key again takes the label off",
|
||||||
manual: "rating-and-flagging",
|
manual: "rating-and-flagging",
|
||||||
},
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Pick or reject a photograph",
|
||||||
|
section: "Library grid",
|
||||||
|
touch: "",
|
||||||
|
pointer: "",
|
||||||
|
keys: "P picks, X rejects and U takes the flag off, with the pointer over it or on the selection",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Move photographs to the trash",
|
||||||
|
section: "Library grid",
|
||||||
|
touch: "Tap the bin at the start of a cell's stars",
|
||||||
|
pointer: "Hover the cell and click the bin before its stars",
|
||||||
|
keys: "Delete or Backspace, on the selection",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Open this list",
|
||||||
|
section: "Library grid",
|
||||||
|
touch: "Press Help in the header",
|
||||||
|
pointer: "Press Help in the header",
|
||||||
|
keys: "F1",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Rename the collection the grid is showing",
|
||||||
|
section: "Library grid",
|
||||||
|
touch: "Hold it in the sidebar, then \"Rename\"",
|
||||||
|
pointer: "Double-click it in the sidebar",
|
||||||
|
keys: "F2",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
|
Gesture {
|
||||||
|
title: "Move through the grid",
|
||||||
|
section: "Library grid",
|
||||||
|
touch: "Scroll, and tap a photograph",
|
||||||
|
pointer: "Scroll, and click a photograph",
|
||||||
|
keys: "←, →, ↑ and ↓ move one photograph; Page Up and Page Down a screenful; Home and End to the first and the last",
|
||||||
|
manual: "",
|
||||||
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
title: "Resize the thumbnails",
|
title: "Resize the thumbnails",
|
||||||
section: "Library grid",
|
section: "Library grid",
|
||||||
@@ -389,7 +445,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Library grid",
|
section: "Library grid",
|
||||||
touch: "Tap it — a single tap, any length",
|
touch: "Tap it — a single tap, any length",
|
||||||
pointer: "Click it",
|
pointer: "Click it",
|
||||||
keys: "",
|
keys: "Enter, on the photograph the arrow keys have walked to",
|
||||||
manual: "developing-a-photograph",
|
manual: "developing-a-photograph",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
@@ -397,7 +453,7 @@ pub const GESTURES: &[Gesture] = &[
|
|||||||
section: "Library grid",
|
section: "Library grid",
|
||||||
touch: "Tap a star on the cell",
|
touch: "Tap a star on the cell",
|
||||||
pointer: "Hover the cell, then click a star",
|
pointer: "Hover the cell, then click a star",
|
||||||
keys: "0 to 5 with the pointer over it, or on the selection",
|
keys: "0–5 with the pointer over it, or on the selection",
|
||||||
manual: "rating-and-flagging",
|
manual: "rating-and-flagging",
|
||||||
},
|
},
|
||||||
Gesture {
|
Gesture {
|
||||||
|
|||||||
+74
-73
@@ -22,6 +22,7 @@ import { ImportPage } from "import.slint";
|
|||||||
import { MergePage, MergeFrameRow } from "merge.slint";
|
import { MergePage, MergeFrameRow } from "merge.slint";
|
||||||
import { Capture, StatusBar, InfoPanel } from "develop.slint";
|
import { Capture, StatusBar, InfoPanel } from "develop.slint";
|
||||||
import { ToolRail } from "toolrail.slint";
|
import { ToolRail } from "toolrail.slint";
|
||||||
|
import { Keys } from "keys.slint";
|
||||||
|
|
||||||
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView, PersonChip, MergeFrameRow }
|
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView, PersonChip, MergeFrameRow }
|
||||||
export { GestureRow }
|
export { GestureRow }
|
||||||
@@ -912,11 +913,22 @@ in property <bool> panel-visible: true;
|
|||||||
// `init` runs before or after its children's, so on the wrong ordering
|
// `init` runs before or after its children's, so on the wrong ordering
|
||||||
// this would take focus back off the grid and silently break culling.
|
// this would take focus back off the grid and silently break culling.
|
||||||
|
|
||||||
|
// GESTURE: Close what is open, or go back a step
|
||||||
|
// where: Everywhere
|
||||||
|
// touch: The system Back gesture, or the Back button
|
||||||
|
// pointer: The Close or Back button on whatever is open
|
||||||
|
// keys: `Escape`, or `Back` where the device has one
|
||||||
|
// why: One key for "up one", innermost first: a question
|
||||||
|
// before the sheet under it, a sheet before the view, a
|
||||||
|
// view before the library. Nothing is left behind a
|
||||||
|
// dialogue that the key walked straight past.
|
||||||
|
//
|
||||||
|
// KEYMAP: Everywhere
|
||||||
key-pressed(event) => {
|
key-pressed(event) => {
|
||||||
// Escape alongside Back: the same "go up one" on a keyboard, and
|
// Escape alongside Back: the same "go up one" on a keyboard, and
|
||||||
// the inner scopes that use it for something more specific — a
|
// the inner scopes that use it for something more specific — a
|
||||||
// rename in the sidebar — see it first and accept it there.
|
// rename in the sidebar — see it first and accept it there.
|
||||||
if (event.text == Key.Back || event.text == Key.Escape) {
|
if (Keys.chord(event) == "Back" || Keys.chord(event) == "Escape") {
|
||||||
// A dialogue takes "go up one" first, and it is the *only*
|
// A dialogue takes "go up one" first, and it is the *only*
|
||||||
// thing that goes up. Without this, Android's Back button
|
// thing that goes up. Without this, Android's Back button
|
||||||
// would leave the library from behind an open question — the
|
// would leave the library from behind an open question — the
|
||||||
@@ -2157,6 +2169,7 @@ in property <bool> panel-visible: true;
|
|||||||
// which reports no modifiers of its own. Tracked on
|
// which reports no modifiers of its own. Tracked on
|
||||||
// both edges so releasing it while the pointer is over
|
// both edges so releasing it while the pointer is over
|
||||||
// the canvas is noticed.
|
// the canvas is noticed.
|
||||||
|
// KEYMAP: Develop
|
||||||
key-released(event) => {
|
key-released(event) => {
|
||||||
root.shift-held = event.modifiers.shift;
|
root.shift-held = event.modifiers.shift;
|
||||||
// TRACES: FR-DEV-7
|
// TRACES: FR-DEV-7
|
||||||
@@ -2165,13 +2178,14 @@ in property <bool> panel-visible: true;
|
|||||||
// from the next press, or a photographer who holds
|
// from the next press, or a photographer who holds
|
||||||
// it and then reaches for a slider would be
|
// it and then reaches for a slider would be
|
||||||
// adjusting an image they cannot see.
|
// adjusting an image they cannot see.
|
||||||
if (event.text == "\\") {
|
if (Keys.chord(event) == "\\") {
|
||||||
root.compare-original(false);
|
root.compare-original(false);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
return reject;
|
return reject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYMAP: Develop
|
||||||
key-pressed(event) => {
|
key-pressed(event) => {
|
||||||
root.shift-held = event.modifiers.shift;
|
root.shift-held = event.modifiers.shift;
|
||||||
// A sheet is up over the photograph, and these
|
// A sheet is up over the photograph, and these
|
||||||
@@ -2187,7 +2201,7 @@ in property <bool> panel-visible: true;
|
|||||||
// Rust ignores a repeat that says what it already
|
// Rust ignores a repeat that says what it already
|
||||||
// knows, so the key repeating under a long look
|
// knows, so the key repeating under a long look
|
||||||
// costs nothing.
|
// costs nothing.
|
||||||
if (event.text == "\\") {
|
if (Keys.chord(event) == "\\") {
|
||||||
root.compare-original(true);
|
root.compare-original(true);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -2197,7 +2211,7 @@ in property <bool> panel-visible: true;
|
|||||||
// the History list
|
// the History list
|
||||||
// pointer: Click it, or press Undo in the
|
// pointer: Click it, or press Undo in the
|
||||||
// History header
|
// History header
|
||||||
// keys: Ctrl+Z
|
// keys: `Ctrl+Z`
|
||||||
// why: A whole drag is one step, so undo
|
// why: A whole drag is one step, so undo
|
||||||
// takes back a decision rather than a
|
// takes back a decision rather than a
|
||||||
// frame of a gesture. The list is there
|
// frame of a gesture. The list is there
|
||||||
@@ -2211,22 +2225,25 @@ in property <bool> panel-visible: true;
|
|||||||
// the History list
|
// the History list
|
||||||
// pointer: Click it, or press Redo in the
|
// pointer: Click it, or press Redo in the
|
||||||
// History header
|
// History header
|
||||||
// keys: Ctrl+Shift+Z
|
// keys: `Ctrl+Shift+Z`
|
||||||
// manual: history-snapshots-presets
|
// manual: history-snapshots-presets
|
||||||
//
|
//
|
||||||
// Ctrl+Z and Ctrl+Shift+Z (FR-DEV-5). Both cases
|
// Ctrl+Z and Ctrl+Shift+Z (FR-DEV-5).
|
||||||
// of the letter, because the logical key that
|
if (Keys.chord(event) == "Ctrl+Z") {
|
||||||
// reaches us carries the shift: holding it for
|
|
||||||
// redo turns "z" into "Z".
|
|
||||||
if (event.modifiers.control
|
|
||||||
&& (event.text == "z" || event.text == "Z")) {
|
|
||||||
if (event.modifiers.shift) {
|
|
||||||
Steps.redo();
|
|
||||||
} else {
|
|
||||||
Steps.undo();
|
Steps.undo();
|
||||||
}
|
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
|
if (Keys.chord(event) == "Ctrl+Shift+Z") {
|
||||||
|
Steps.redo();
|
||||||
|
return accept;
|
||||||
|
}
|
||||||
|
// GESTURE: Remove a repair
|
||||||
|
// where: Develop
|
||||||
|
// touch: Tap it, then Delete Repair
|
||||||
|
// pointer: Click it, then Delete Repair
|
||||||
|
// keys: `Delete` or `Backspace`, while
|
||||||
|
// repairing
|
||||||
|
//
|
||||||
// TRACES: FR-DEV-8
|
// TRACES: FR-DEV-8
|
||||||
// Delete removes the selected repair, and only in
|
// Delete removes the selected repair, and only in
|
||||||
// repair mode: the same key in the grid judges
|
// repair mode: the same key in the grid judges
|
||||||
@@ -2234,8 +2251,8 @@ in property <bool> panel-visible: true;
|
|||||||
// depending on a mode the user has forgotten they
|
// depending on a mode the user has forgotten they
|
||||||
// are in is how work disappears.
|
// are in is how work disappears.
|
||||||
if (Develop.repairing && Repair.selected != ""
|
if (Develop.repairing && Repair.selected != ""
|
||||||
&& (event.text == Key.Delete
|
&& (Keys.chord(event) == "Delete"
|
||||||
|| event.text == Key.Backspace)) {
|
|| Keys.chord(event) == "Backspace")) {
|
||||||
Repair.removed(Repair.selected);
|
Repair.removed(Repair.selected);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -2243,7 +2260,7 @@ in property <bool> panel-visible: true;
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Press Copy in the top bar
|
// touch: Press Copy in the top bar
|
||||||
// pointer: Press Copy in the top bar
|
// pointer: Press Copy in the top bar
|
||||||
// keys: Ctrl+C
|
// keys: `Ctrl+C`
|
||||||
// why: The button is the copy that has to
|
// why: The button is the copy that has to
|
||||||
// work: a tablet has no modifier key
|
// work: a tablet has no modifier key
|
||||||
// to hold and no menu bar to hang the
|
// to hold and no menu bar to hang the
|
||||||
@@ -2256,7 +2273,7 @@ in property <bool> panel-visible: true;
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Press Paste in the top bar
|
// touch: Press Paste in the top bar
|
||||||
// pointer: Press Paste in the top bar
|
// pointer: Press Paste in the top bar
|
||||||
// keys: Ctrl+V
|
// keys: `Ctrl+V`
|
||||||
// why: The button names what would be
|
// why: The button names what would be
|
||||||
// pasted — "3 adjustments", and
|
// pasted — "3 adjustments", and
|
||||||
// whether the crop is coming with it —
|
// whether the crop is coming with it —
|
||||||
@@ -2274,7 +2291,7 @@ in property <bool> panel-visible: true;
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Open Presets and toggle the kinds
|
// touch: Open Presets and toggle the kinds
|
||||||
// pointer: Open Presets and toggle the kinds
|
// pointer: Open Presets and toggle the kinds
|
||||||
// keys: Ctrl+Shift+C, which offers Copy
|
// keys: `Ctrl+Shift+C`, which offers Copy
|
||||||
// beside them
|
// beside them
|
||||||
// why: Lightroom's Copy Settings. Pasting a
|
// why: Lightroom's Copy Settings. Pasting a
|
||||||
// look across a shoot usually means
|
// look across a shoot usually means
|
||||||
@@ -2283,20 +2300,15 @@ in property <bool> panel-visible: true;
|
|||||||
// to make at the moment of copying.
|
// to make at the moment of copying.
|
||||||
// manual: copying-settings
|
// manual: copying-settings
|
||||||
//
|
//
|
||||||
// Shift first: the logical key is "C" under shift,
|
if (Keys.chord(event) == "Ctrl+Shift+C") {
|
||||||
// which the plain copy below would otherwise take.
|
|
||||||
if (event.modifiers.control && event.modifiers.shift
|
|
||||||
&& (event.text == "c" || event.text == "C")) {
|
|
||||||
root.copy-sheet-open = true;
|
root.copy-sheet-open = true;
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.modifiers.control
|
if (Keys.chord(event) == "Ctrl+C") {
|
||||||
&& (event.text == "c" || event.text == "C")) {
|
|
||||||
Transfer.copy();
|
Transfer.copy();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.modifiers.control && Transfer.armed
|
if (Transfer.armed && Keys.chord(event) == "Ctrl+V") {
|
||||||
&& (event.text == "v" || event.text == "V")) {
|
|
||||||
Transfer.paste();
|
Transfer.paste();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -2304,7 +2316,7 @@ in property <bool> panel-visible: true;
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Press Export in the top bar
|
// touch: Press Export in the top bar
|
||||||
// pointer: Press Export in the top bar
|
// pointer: Press Export in the top bar
|
||||||
// keys: Ctrl+Shift+E
|
// keys: `Ctrl+Shift+E`
|
||||||
// why: Every export runs on the defaults in
|
// why: Every export runs on the defaults in
|
||||||
// Settings, so "as the last one was" is
|
// Settings, so "as the last one was" is
|
||||||
// what the button already does. The
|
// what the button already does. The
|
||||||
@@ -2317,7 +2329,7 @@ in property <bool> panel-visible: true;
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Open Settings, then Export defaults
|
// touch: Open Settings, then Export defaults
|
||||||
// pointer: Open Settings, then Export defaults
|
// pointer: Open Settings, then Export defaults
|
||||||
// keys: Ctrl+E
|
// keys: `Ctrl+E`
|
||||||
// why: The export sheet is the export
|
// why: The export sheet is the export
|
||||||
// defaults alone with an Export button.
|
// defaults alone with an Export button.
|
||||||
// What is chosen there is kept, so it
|
// What is chosen there is kept, so it
|
||||||
@@ -2326,17 +2338,15 @@ in property <bool> panel-visible: true;
|
|||||||
// manual: export
|
// manual: export
|
||||||
//
|
//
|
||||||
// TRACES: FR-UI-5 | FR-EXP-7
|
// TRACES: FR-UI-5 | FR-EXP-7
|
||||||
// Both cases of the letter, as for Z: holding
|
// Rust turns away a second export while one is
|
||||||
// shift turns "e" into "E". Rust turns away a
|
// running, so the chord needs no busy guard of its
|
||||||
// second export while one is running, so the
|
// own.
|
||||||
// chord needs no busy guard of its own.
|
if (Keys.chord(event) == "Ctrl+Shift+E") {
|
||||||
if (event.modifiers.control
|
|
||||||
&& (event.text == "e" || event.text == "E")) {
|
|
||||||
if (event.modifiers.shift) {
|
|
||||||
root.export-image();
|
root.export-image();
|
||||||
} else {
|
return accept;
|
||||||
root.export-options-open();
|
|
||||||
}
|
}
|
||||||
|
if (Keys.chord(event) == "Ctrl+E") {
|
||||||
|
root.export-options-open();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
// GESTURE: Change which group of adjustments is on screen
|
// GESTURE: Change which group of adjustments is on screen
|
||||||
@@ -2344,7 +2354,7 @@ in property <bool> panel-visible: true;
|
|||||||
// touch: Tap a group in the rail down the left
|
// touch: Tap a group in the rail down the left
|
||||||
// pointer: Click a group in the strip above the
|
// pointer: Click a group in the strip above the
|
||||||
// develop column
|
// develop column
|
||||||
// keys: [ and ] step through them, wrapping
|
// keys: `[` and `]` step through them, wrapping
|
||||||
// round through "everything"
|
// round through "everything"
|
||||||
// why: The groups are whatever the operation
|
// why: The groups are whatever the operation
|
||||||
// set declares itself to be about, so
|
// set declares itself to be about, so
|
||||||
@@ -2360,7 +2370,7 @@ in property <bool> panel-visible: true;
|
|||||||
// panel starts, so a photographer stepping round
|
// panel starts, so a photographer stepping round
|
||||||
// the groups arrives back at the whole set instead
|
// the groups arrives back at the whole set instead
|
||||||
// of being trapped in the last one.
|
// of being trapped in the last one.
|
||||||
if (event.text == "]") {
|
if (Keys.chord(event) == "]") {
|
||||||
Adjustments.tab-picked(
|
Adjustments.tab-picked(
|
||||||
(Adjustments.active-tab + 1
|
(Adjustments.active-tab + 1
|
||||||
>= Adjustments.tabs.length)
|
>= Adjustments.tabs.length)
|
||||||
@@ -2368,7 +2378,7 @@ in property <bool> panel-visible: true;
|
|||||||
: (Adjustments.active-tab + 1));
|
: (Adjustments.active-tab + 1));
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == "[") {
|
if (Keys.chord(event) == "[") {
|
||||||
Adjustments.tab-picked(
|
Adjustments.tab-picked(
|
||||||
(Adjustments.active-tab - 1 < -1)
|
(Adjustments.active-tab - 1 < -1)
|
||||||
? (Adjustments.tabs.length - 1)
|
? (Adjustments.tabs.length - 1)
|
||||||
@@ -2380,7 +2390,7 @@ in property <bool> panel-visible: true;
|
|||||||
// touch: Double-tap the photograph
|
// touch: Double-tap the photograph
|
||||||
// pointer: Double-click it, or press the zoom
|
// pointer: Double-click it, or press the zoom
|
||||||
// readout floating over the canvas
|
// readout floating over the canvas
|
||||||
// keys: Z
|
// keys: `Z`
|
||||||
// why: Noise reduction and capture
|
// why: Noise reduction and capture
|
||||||
// sharpening are judgements about
|
// sharpening are judgements about
|
||||||
// single pixels, and a fitted view
|
// single pixels, and a fitted view
|
||||||
@@ -2401,21 +2411,17 @@ in property <bool> panel-visible: true;
|
|||||||
// TRACES: FR-UI-4 | FR-UI-5
|
// TRACES: FR-UI-4 | FR-UI-5
|
||||||
// Fit and 1:1 from the keyboard, so inspecting
|
// Fit and 1:1 from the keyboard, so inspecting
|
||||||
// forty portraits at the same eye is forty
|
// forty portraits at the same eye is forty
|
||||||
// keystrokes rather than forty pans. Bare, with
|
// keystrokes rather than forty pans.
|
||||||
// no modifier: the control-Z case above has
|
if (Keys.chord(event) == "Z") {
|
||||||
// already returned, so there is nothing left for
|
|
||||||
// this to shadow.
|
|
||||||
if (event.text == "z" || event.text == "Z") {
|
|
||||||
root.inspect-toggled(-1, -1);
|
root.inspect-toggled(-1, -1);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
// TRACES: FR-DEV-16
|
// TRACES: FR-DEV-16
|
||||||
// R puts back the control last moved. Bare, like
|
// R puts back the control last moved. Nothing has
|
||||||
// Z: the modifier chords above have all returned.
|
// been moved on this photograph until something
|
||||||
// Nothing has been moved on this photograph until
|
// has, so the key is silent until then rather than
|
||||||
// something has, so the key is silent until then
|
// resetting a control nobody named.
|
||||||
// rather than resetting a control nobody named.
|
if (Keys.chord(event) == "R"
|
||||||
if ((event.text == "r" || event.text == "R")
|
|
||||||
&& Adjustments.touched-op >= 0) {
|
&& Adjustments.touched-op >= 0) {
|
||||||
Adjustments.param-reset(
|
Adjustments.param-reset(
|
||||||
Adjustments.touched-op,
|
Adjustments.touched-op,
|
||||||
@@ -2427,7 +2433,7 @@ in property <bool> panel-visible: true;
|
|||||||
// ring at the head of its row, from the keyboard.
|
// ring at the head of its row, from the keyboard.
|
||||||
// With nothing selected there is nothing to hide,
|
// With nothing selected there is nothing to hide,
|
||||||
// and Rust says so by doing nothing.
|
// and Rust says so by doing nothing.
|
||||||
if (event.text == "h" || event.text == "H") {
|
if (Keys.chord(event) == "H") {
|
||||||
Masking.selected-toggled();
|
Masking.selected-toggled();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -2437,7 +2443,7 @@ in property <bool> panel-visible: true;
|
|||||||
// colour
|
// colour
|
||||||
// pointer: Click Label in the top bar, then a
|
// pointer: Click Label in the top bar, then a
|
||||||
// colour
|
// colour
|
||||||
// keys: 6 red, 7 yellow, 8 green, 9 blue; the
|
// keys: `6` red, `7` yellow, `8` green, `9` blue; the
|
||||||
// same key again takes it off
|
// same key again takes it off
|
||||||
// why: The grid's keys, on the photograph
|
// why: The grid's keys, on the photograph
|
||||||
// that is open, so labelling while
|
// that is open, so labelling while
|
||||||
@@ -2450,20 +2456,19 @@ in property <bool> panel-visible: true;
|
|||||||
// Only with a library: a file named on the command
|
// Only with a library: a file named on the command
|
||||||
// line has no catalog to hold a label.
|
// line has no catalog to hold a label.
|
||||||
if (Library.library-total > 0
|
if (Library.library-total > 0
|
||||||
&& Library.library-roll-current >= 0
|
&& Library.library-roll-current >= 0) {
|
||||||
&& !event.modifiers.control) {
|
if (Keys.chord(event) == "6") { Library.library-cell-labelled(Library.library-roll-current, 1, true); return accept; }
|
||||||
if (event.text == "6") { Library.library-cell-labelled(Library.library-roll-current, 1, true); return accept; }
|
if (Keys.chord(event) == "7") { Library.library-cell-labelled(Library.library-roll-current, 2, true); return accept; }
|
||||||
if (event.text == "7") { Library.library-cell-labelled(Library.library-roll-current, 2, true); return accept; }
|
if (Keys.chord(event) == "8") { Library.library-cell-labelled(Library.library-roll-current, 3, true); return accept; }
|
||||||
if (event.text == "8") { Library.library-cell-labelled(Library.library-roll-current, 3, true); return accept; }
|
if (Keys.chord(event) == "9") { Library.library-cell-labelled(Library.library-roll-current, 4, true); return accept; }
|
||||||
if (event.text == "9") { Library.library-cell-labelled(Library.library-roll-current, 4, true); return accept; }
|
|
||||||
}
|
}
|
||||||
// GESTURE: Move to the next or previous photograph
|
// GESTURE: Move to the next or previous photograph
|
||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Tap a frame in the roll along the
|
// touch: Tap a frame in the roll along the
|
||||||
// foot of the canvas
|
// foot of the canvas
|
||||||
// pointer: Click a frame in the roll
|
// pointer: Click a frame in the roll
|
||||||
// keys: Right arrow, D or space for the
|
// keys: `Right`, `D` or `Space` for the
|
||||||
// next; left arrow or A for the one
|
// next; `Left` or `A` for the one
|
||||||
// before
|
// before
|
||||||
// why: The edit on screen is saved on the
|
// why: The edit on screen is saved on the
|
||||||
// way out, so stepping through a folder
|
// way out, so stepping through a folder
|
||||||
@@ -2471,18 +2476,14 @@ in property <bool> panel-visible: true;
|
|||||||
// to the grid and loses nothing.
|
// to the grid and loses nothing.
|
||||||
// A and D as well as the arrows, so the left hand
|
// A and D as well as the arrows, so the left hand
|
||||||
// steps along the roll while the right stays on
|
// steps along the roll while the right stays on
|
||||||
// the mouse. Unmodified only: Ctrl+D and Ctrl+A
|
// the mouse.
|
||||||
// are not this.
|
|
||||||
// manual: moving-between-photographs
|
// manual: moving-between-photographs
|
||||||
if (event.text == Key.RightArrow || event.text == " "
|
if (Keys.chord(event) == "Right" || Keys.chord(event) == "Space"
|
||||||
|| (!event.modifiers.control
|
|| Keys.chord(event) == "D") {
|
||||||
&& (event.text == "d" || event.text == "D"))) {
|
|
||||||
root.step-photo(1);
|
root.step-photo(1);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.LeftArrow
|
if (Keys.chord(event) == "Left" || Keys.chord(event) == "A") {
|
||||||
|| (!event.modifiers.control
|
|
||||||
&& (event.text == "a" || event.text == "A"))) {
|
|
||||||
root.step-photo(-1);
|
root.step-photo(-1);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -2595,7 +2596,7 @@ in property <bool> panel-visible: true;
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Press and hold "Before"
|
// touch: Press and hold "Before"
|
||||||
// pointer: Press and hold "Before"
|
// pointer: Press and hold "Before"
|
||||||
// keys: Hold \
|
// keys: Hold `\`
|
||||||
// why: Held rather than toggled, and no split
|
// why: Held rather than toggled, and no split
|
||||||
// screen: a split halves the working image
|
// screen: a split halves the working image
|
||||||
// on the tablet the column was sized for,
|
// on the tablet the column was sized for,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
// release rather than the drop being silently discarded after it.
|
// release rather than the drop being silently discarded after it.
|
||||||
|
|
||||||
import { Theme } from "theme.slint";
|
import { Theme } from "theme.slint";
|
||||||
|
import { Keys } from "keys.slint";
|
||||||
import { Button, Icon } from "widgets.slint";
|
import { Button, Icon } from "widgets.slint";
|
||||||
|
|
||||||
// One row of the collection tree.
|
// One row of the collection tree.
|
||||||
@@ -247,11 +248,12 @@ component TreeRow inherits Rectangle {
|
|||||||
|
|
||||||
accepted => { root.rename-committed(self.text); }
|
accepted => { root.rename-committed(self.text); }
|
||||||
|
|
||||||
|
// KEYMAP: Collections sidebar
|
||||||
key-pressed(event) => {
|
key-pressed(event) => {
|
||||||
// Escape abandons. Handled here rather than on a
|
// Escape abandons. Handled here rather than on a
|
||||||
// surrounding FocusScope, which would never see the key —
|
// surrounding FocusScope, which would never see the key —
|
||||||
// the input has focus and consumes it.
|
// the input has focus and consumes it.
|
||||||
if (event.text == Key.Escape) {
|
if (Keys.chord(event) == "Escape") {
|
||||||
root.rename-cancelled();
|
root.rename-cancelled();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -373,6 +375,7 @@ component TreeRow inherits Rectangle {
|
|||||||
// where: Collections sidebar
|
// where: Collections sidebar
|
||||||
// touch: Hold the collection, then "Rename"
|
// touch: Hold the collection, then "Rename"
|
||||||
// pointer: Double-click its name, or right-click it and choose "Rename"
|
// pointer: Double-click its name, or right-click it and choose "Rename"
|
||||||
|
// keys: `Escape` while typing abandons the new name
|
||||||
// why: Double-click is what a file manager and a Lightroom panel use
|
// why: Double-click is what a file manager and a Lightroom panel use
|
||||||
// for the same thing, so it needs no discovering — but nothing
|
// for the same thing, so it needs no discovering — but nothing
|
||||||
// on screen says so, which is what the menu item is for.
|
// on screen says so, which is what the menu item is for.
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ export component SliderTrack inherits Rectangle {
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Double-tap its track
|
// touch: Double-tap its track
|
||||||
// pointer: Double-click its track, or right-click it
|
// pointer: Double-click its track, or right-click it
|
||||||
// keys: R, for the control last moved
|
// keys: `R`, for the control last moved
|
||||||
// why: The column is 280px wide and the colour mixer alone
|
// why: The column is 280px wide and the colour mixer alone
|
||||||
// puts thirty-six of these in it, so a reset button per
|
// puts thirty-six of these in it, so a reset button per
|
||||||
// row would be most of the width. Two ways in with a
|
// row would be most of the width. Two ways in with a
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// TRACES: FR-UI-5 | FR-DEV-16
|
||||||
|
//
|
||||||
|
// One name for every key press, and the only code that reads the raw key.
|
||||||
|
//
|
||||||
|
// A handler compares `Keys.chord(event)` against a canonical spelling —
|
||||||
|
// "Ctrl+Shift+Z", "Shift+Left", "6", "Escape" — rather than reading
|
||||||
|
// `event.text` and `event.modifiers` itself. Two reasons, one for each reader:
|
||||||
|
//
|
||||||
|
// **For the handler**, the literal is the whole binding. Undo used to be
|
||||||
|
// `control && (text == "z" || text == "Z")` with a nested `if` on shift to
|
||||||
|
// split it from redo; now it is "Ctrl+Z", and redo is "Ctrl+Shift+Z", and
|
||||||
|
// neither can accidentally answer the other.
|
||||||
|
//
|
||||||
|
// **For the checker**, the literal is the whole binding too. `traces
|
||||||
|
// gestures-check` reads every `Keys.chord(event) == "…"` in a key handler and
|
||||||
|
// fails CI if the key is not named by a GESTURE tag for that place, or if a
|
||||||
|
// tag names a key no handler binds (tools/traceability/src/keymap.rs). It can
|
||||||
|
// only do that because nothing else reads `event.text` — which it also checks.
|
||||||
|
//
|
||||||
|
// The canonical form is defined in tools/traceability/src/chord.rs, and this
|
||||||
|
// file must agree with it; the checker compares the names `named` can return
|
||||||
|
// against the list there. The rules, briefly:
|
||||||
|
//
|
||||||
|
// - Modifiers in the order Ctrl, Alt, Shift, Meta, each only when held.
|
||||||
|
// - A letter is upper case whatever its case arrived in — caps lock must not
|
||||||
|
// stop `P` picking — and shift is read from the modifier.
|
||||||
|
// - **Shift and Alt count only for letters and named keys.** For any other
|
||||||
|
// character the layout has already spent them: on the French layout every
|
||||||
|
// digit needs shift, and a 6 is a 6 however it was made.
|
||||||
|
// - `+` is "Plus", because it is the separator.
|
||||||
|
|
||||||
|
export global Keys {
|
||||||
|
// The canonical name of a key press. What every key handler compares.
|
||||||
|
public pure function chord(event: KeyEvent) -> string {
|
||||||
|
return self.name(event.text, event.modifiers.control, event.modifiers.alt,
|
||||||
|
event.modifiers.shift, event.modifiers.meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same, from its parts.
|
||||||
|
public pure function name(text: string, control: bool, alt: bool, shift: bool,
|
||||||
|
meta: bool) -> string {
|
||||||
|
return (control ? "Ctrl+" : "")
|
||||||
|
+ (alt && self.modifiable(text) ? "Alt+" : "")
|
||||||
|
+ (shift && self.modifiable(text) ? "Shift+" : "")
|
||||||
|
+ (meta ? "Meta+" : "")
|
||||||
|
+ self.base(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether shift and alt are part of the chord rather than of the character.
|
||||||
|
pure function modifiable(text: string) -> bool {
|
||||||
|
return self.named(text) != "" || text.to-uppercase() != text.to-lowercase();
|
||||||
|
}
|
||||||
|
|
||||||
|
pure function base(text: string) -> string {
|
||||||
|
if (self.named(text) != "") { return self.named(text); }
|
||||||
|
if (text == "+") { return "Plus"; }
|
||||||
|
return text.to-uppercase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys with a name rather than a character. Mirrors `NAMED` in chord.rs.
|
||||||
|
pure function named(text: string) -> string {
|
||||||
|
if (text == Key.LeftArrow) { return "Left"; }
|
||||||
|
if (text == Key.RightArrow) { return "Right"; }
|
||||||
|
if (text == Key.UpArrow) { return "Up"; }
|
||||||
|
if (text == Key.DownArrow) { return "Down"; }
|
||||||
|
if (text == Key.Home) { return "Home"; }
|
||||||
|
if (text == Key.End) { return "End"; }
|
||||||
|
if (text == Key.PageUp) { return "PageUp"; }
|
||||||
|
if (text == Key.PageDown) { return "PageDown"; }
|
||||||
|
if (text == Key.Return) { return "Enter"; }
|
||||||
|
if (text == Key.Escape) { return "Escape"; }
|
||||||
|
if (text == Key.Back) { return "Back"; }
|
||||||
|
if (text == Key.Tab || text == Key.Backtab) { return "Tab"; }
|
||||||
|
if (text == Key.Backspace) { return "Backspace"; }
|
||||||
|
if (text == Key.Delete) { return "Delete"; }
|
||||||
|
if (text == Key.Insert) { return "Insert"; }
|
||||||
|
if (text == Key.Space) { return "Space"; }
|
||||||
|
if (text == Key.F1) { return "F1"; }
|
||||||
|
if (text == Key.F2) { return "F2"; }
|
||||||
|
if (text == Key.F3) { return "F3"; }
|
||||||
|
if (text == Key.F4) { return "F4"; }
|
||||||
|
if (text == Key.F5) { return "F5"; }
|
||||||
|
if (text == Key.F6) { return "F6"; }
|
||||||
|
if (text == Key.F7) { return "F7"; }
|
||||||
|
if (text == Key.F8) { return "F8"; }
|
||||||
|
if (text == Key.F9) { return "F9"; }
|
||||||
|
if (text == Key.F10) { return "F10"; }
|
||||||
|
if (text == Key.F11) { return "F11"; }
|
||||||
|
if (text == Key.F12) { return "F12"; }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
-54
@@ -9,6 +9,7 @@
|
|||||||
// must not look identical (FR-NC-6c).
|
// must not look identical (FR-NC-6c).
|
||||||
|
|
||||||
import { Theme } from "theme.slint";
|
import { Theme } from "theme.slint";
|
||||||
|
import { Keys } from "keys.slint";
|
||||||
import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip, ProgressBar, Icon, Field } from "widgets.slint";
|
import { Button, IconButton, Label, Value, Caption, EmptyState, FilterChip, ProgressBar, Icon, Field } from "widgets.slint";
|
||||||
// The filing sheet lists the same rows the sidebar draws, from the same model:
|
// The filing sheet lists the same rows the sidebar draws, from the same model:
|
||||||
// two lists of collections that could disagree about what exists is one list
|
// two lists of collections that could disagree about what exists is one list
|
||||||
@@ -1649,7 +1650,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: Press Done in the header
|
// touch: Press Done in the header
|
||||||
// pointer: Press Done in the header
|
// pointer: Press Done in the header
|
||||||
// keys: Escape
|
// keys: `Escape`, or `Back`; an open sheet closes first
|
||||||
// manual: selecting-several
|
// manual: selecting-several
|
||||||
in property <bool> select-mode: false;
|
in property <bool> select-mode: false;
|
||||||
callback toggle-select-mode();
|
callback toggle-select-mode();
|
||||||
@@ -1747,6 +1748,9 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// touch: While selecting, press "Select to…", then tap the last
|
// touch: While selecting, press "Select to…", then tap the last
|
||||||
// photograph of the run
|
// photograph of the run
|
||||||
// pointer: Shift-click the last photograph of the run
|
// pointer: Shift-click the last photograph of the run
|
||||||
|
// keys: Shift with any key that walks the grid — `Shift+Left`,
|
||||||
|
// `Shift+Right`, `Shift+Up`, `Shift+Down`, `Shift+PageUp`,
|
||||||
|
// `Shift+PageDown`, `Shift+Home`, `Shift+End`
|
||||||
// why: This replaced a double tap, which had no visible state and
|
// why: This replaced a double tap, which had no visible state and
|
||||||
// could take forty photographs by accident. The run is resolved
|
// could take forty photographs by accident. The run is resolved
|
||||||
// by the catalog rather than by what is on screen, so the grid
|
// by the catalog rather than by what is on screen, so the grid
|
||||||
@@ -3024,10 +3028,11 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
property <int> f-first: -1;
|
property <int> f-first: -1;
|
||||||
property <bool> f-used: false;
|
property <bool> f-used: false;
|
||||||
|
|
||||||
|
// KEYMAP: Library grid
|
||||||
key-released(event) => {
|
key-released(event) => {
|
||||||
// Tapping F on its own clears the star filter; having
|
// Tapping F on its own clears the star filter; having
|
||||||
// tapped digits under it, letting go only ends the chord.
|
// tapped digits under it, letting go only ends the chord.
|
||||||
if (self.f-held && (event.text == "f" || event.text == "F")) {
|
if (self.f-held && Keys.chord(event) == "F") {
|
||||||
if (!self.f-used) {
|
if (!self.f-used) {
|
||||||
root.filter-rating-range(0, 5);
|
root.filter-rating-range(0, 5);
|
||||||
}
|
}
|
||||||
@@ -3043,6 +3048,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// the first keystroke of a culling session is swallowed.
|
// the first keystroke of a culling session is swallowed.
|
||||||
init => { self.focus(); }
|
init => { self.focus(); }
|
||||||
|
|
||||||
|
// KEYMAP: Library grid
|
||||||
key-pressed(event) => {
|
key-pressed(event) => {
|
||||||
// Declined, not swallowed, so Escape reaches the shell
|
// Declined, not swallowed, so Escape reaches the shell
|
||||||
// and closes the sheet rather than judging under it.
|
// and closes the sheet rather than judging under it.
|
||||||
@@ -3055,7 +3061,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// "leave the library". On Android that is the system Back
|
// "leave the library". On Android that is the system Back
|
||||||
// button, and a sheet it walked straight past would leave
|
// button, and a sheet it walked straight past would leave
|
||||||
// the user out of the grid with their selection gone.
|
// the user out of the grid with their selection gone.
|
||||||
if (event.text == Key.Back || event.text == Key.Escape) {
|
if (Keys.chord(event) == "Back" || Keys.chord(event) == "Escape") {
|
||||||
if (root.keywording) {
|
if (root.keywording) {
|
||||||
root.keywording = false;
|
root.keywording = false;
|
||||||
return accept;
|
return accept;
|
||||||
@@ -3078,7 +3084,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: Select them, then Export in the selection bar
|
// touch: Select them, then Export in the selection bar
|
||||||
// pointer: Select them, then Export in the selection bar
|
// pointer: Select them, then Export in the selection bar
|
||||||
// keys: Ctrl+Shift+E, or Ctrl+E to see the export
|
// keys: `Ctrl+Shift+E`, or `Ctrl+E` to see the export
|
||||||
// settings first
|
// settings first
|
||||||
// why: Lightroom's and darktable's chords. Every
|
// why: Lightroom's and darktable's chords. Every
|
||||||
// export runs on the saved defaults, so the
|
// export runs on the saved defaults, so the
|
||||||
@@ -3090,13 +3096,12 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// TRACES: FR-EXP-7 | FR-UI-5
|
// TRACES: FR-EXP-7 | FR-UI-5
|
||||||
// Before the bare keys below: a chord must not also be read
|
// Before the bare keys below: a chord must not also be read
|
||||||
// as the letter it is spelled with.
|
// as the letter it is spelled with.
|
||||||
if (event.modifiers.control
|
if (Keys.chord(event) == "Ctrl+Shift+E") {
|
||||||
&& (event.text == "e" || event.text == "E")) {
|
|
||||||
if (event.modifiers.shift) {
|
|
||||||
root.export-selection();
|
root.export-selection();
|
||||||
} else {
|
return accept;
|
||||||
root.open-export-options();
|
|
||||||
}
|
}
|
||||||
|
if (Keys.chord(event) == "Ctrl+E") {
|
||||||
|
root.open-export-options();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
// GESTURE: Paste copied settings onto the selection
|
// GESTURE: Paste copied settings onto the selection
|
||||||
@@ -3104,21 +3109,19 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// touch: Select them, then "Paste to N" in the
|
// touch: Select them, then "Paste to N" in the
|
||||||
// selection bar
|
// selection bar
|
||||||
// pointer: Select them, then "Paste to N"
|
// pointer: Select them, then "Paste to N"
|
||||||
// keys: Ctrl+V
|
// keys: `Ctrl+V`
|
||||||
// manual: copying-settings
|
// manual: copying-settings
|
||||||
//
|
//
|
||||||
// TRACES: FR-DEV-6 | FR-UI-5
|
// TRACES: FR-DEV-6 | FR-UI-5
|
||||||
// Silent with nothing copied, as the button is disabled.
|
// Silent with nothing copied, as the button is disabled.
|
||||||
if (event.modifiers.control && root.settings-armed
|
if (root.settings-armed && Keys.chord(event) == "Ctrl+V") {
|
||||||
&& (event.text == "v" || event.text == "V")) {
|
|
||||||
root.paste-settings-to-selection();
|
root.paste-settings-to-selection();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
// TRACES: FR-CAT-5 | FR-UI-5
|
// TRACES: FR-CAT-5 | FR-UI-5
|
||||||
// Ctrl+A takes everything the grid shows, as "Select all"
|
// Ctrl+A takes everything the grid shows, as "Select all"
|
||||||
// in the selection strip does.
|
// in the selection strip does.
|
||||||
if (event.modifiers.control
|
if (Keys.chord(event) == "Ctrl+A") {
|
||||||
&& (event.text == "a" || event.text == "A")) {
|
|
||||||
root.select-all();
|
root.select-all();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -3126,9 +3129,10 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: Tap a star chip in the filter bar
|
// touch: Tap a star chip in the filter bar
|
||||||
// pointer: Click a star chip in the filter bar
|
// pointer: Click a star chip in the filter bar
|
||||||
// keys: Hold F and tap a digit for exactly that many
|
// keys: Hold `F` and tap a digit, `0`–`5`, for exactly
|
||||||
// stars, or two digits for everything between
|
// that many stars, or two digits for everything
|
||||||
// them; tap F alone to show every rating again
|
// between them; tap `F` alone to show every
|
||||||
|
// rating again
|
||||||
// why: The chips say "this many or more". A range
|
// why: The chips say "this many or more". A range
|
||||||
// with a ceiling — the twos and threes still
|
// with a ceiling — the twos and threes still
|
||||||
// to be decided — is the keyboard's alone, and
|
// to be decided — is the keyboard's alone, and
|
||||||
@@ -3140,8 +3144,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// digit filters as it lands. Any other key ends the chord,
|
// digit filters as it lands. Any other key ends the chord,
|
||||||
// in case its release went to a window that had focus by
|
// in case its release went to a window that had focus by
|
||||||
// then and the grid never heard it.
|
// then and the grid never heard it.
|
||||||
if (!event.modifiers.control
|
if (Keys.chord(event) == "F") {
|
||||||
&& (event.text == "f" || event.text == "F")) {
|
|
||||||
// Key repeat while held says nothing new.
|
// Key repeat while held says nothing new.
|
||||||
if (!self.f-held) {
|
if (!self.f-held) {
|
||||||
self.f-held = true;
|
self.f-held = true;
|
||||||
@@ -3151,34 +3154,34 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (self.f-held) {
|
if (self.f-held) {
|
||||||
if (event.text == "0" || event.text == "1"
|
if (Keys.chord(event) == "0" || Keys.chord(event) == "1"
|
||||||
|| event.text == "2" || event.text == "3"
|
|| Keys.chord(event) == "2" || Keys.chord(event) == "3"
|
||||||
|| event.text == "4" || event.text == "5") {
|
|| Keys.chord(event) == "4" || Keys.chord(event) == "5") {
|
||||||
if (self.f-first < 0) {
|
if (self.f-first < 0) {
|
||||||
self.f-first = event.text.to-float();
|
self.f-first = Keys.chord(event).to-float();
|
||||||
root.filter-rating-range(self.f-first, self.f-first);
|
root.filter-rating-range(self.f-first, self.f-first);
|
||||||
} else {
|
} else {
|
||||||
root.filter-rating-range(self.f-first,
|
root.filter-rating-range(self.f-first,
|
||||||
event.text.to-float());
|
Keys.chord(event).to-float());
|
||||||
}
|
}
|
||||||
self.f-used = true;
|
self.f-used = true;
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
self.f-held = false;
|
self.f-held = false;
|
||||||
}
|
}
|
||||||
if (event.text == "0") { root.judged(0, -1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "0") { root.judged(0, -1, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "1") { root.judged(1, -1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "1") { root.judged(1, -1, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "2") { root.judged(2, -1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "2") { root.judged(2, -1, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "3") { root.judged(3, -1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "3") { root.judged(3, -1, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "4") { root.judged(4, -1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "4") { root.judged(4, -1, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "5") { root.judged(5, -1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "5") { root.judged(5, -1, root.hover-ordinal); return accept; }
|
||||||
// GESTURE: Give photographs a colour label
|
// GESTURE: Give photographs a colour label
|
||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: Select them, then Label in the selection bar
|
// touch: Select them, then Label in the selection bar
|
||||||
// and tap a colour
|
// and tap a colour
|
||||||
// pointer: Select them, then Label in the selection bar
|
// pointer: Select them, then Label in the selection bar
|
||||||
// and click a colour
|
// and click a colour
|
||||||
// keys: 6 red, 7 yellow, 8 green, 9 blue, with the
|
// keys: `6` red, `7` yellow, `8` green, `9` blue, with the
|
||||||
// pointer over it or on the selection; the
|
// pointer over it or on the selection; the
|
||||||
// same key again takes the label off
|
// same key again takes the label off
|
||||||
// why: Lightroom's keys, so hands that learned them
|
// why: Lightroom's keys, so hands that learned them
|
||||||
@@ -3192,30 +3195,51 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// TRACES: FR-CAT-5 | FR-UI-5 | NFR-A11Y-3
|
// TRACES: FR-CAT-5 | FR-UI-5 | NFR-A11Y-3
|
||||||
// After the F chord above, which has returned for its own
|
// After the F chord above, which has returned for its own
|
||||||
// digits (0-5 only), so these never filter.
|
// digits (0-5 only), so these never filter.
|
||||||
if (event.text == "6") { root.labelled(1, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "6") { root.labelled(1, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "7") { root.labelled(2, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "7") { root.labelled(2, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "8") { root.labelled(3, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "8") { root.labelled(3, root.hover-ordinal); return accept; }
|
||||||
if (event.text == "9") { root.labelled(4, root.hover-ordinal); return accept; }
|
if (Keys.chord(event) == "9") { root.labelled(4, root.hover-ordinal); return accept; }
|
||||||
// Case-insensitive: caps lock during a long cull must not
|
// GESTURE: Pick or reject a photograph
|
||||||
// silently stop the keys working.
|
// where: Library grid
|
||||||
if (event.text == "p" || event.text == "P") {
|
// keys: `P` picks, `X` rejects and `U` takes the flag
|
||||||
|
// off, with the pointer over it or on the
|
||||||
|
// selection
|
||||||
|
// why: The keys every culling tool uses, so muscle
|
||||||
|
// memory built elsewhere works here.
|
||||||
|
//
|
||||||
|
// Case-insensitive, because `Keys.chord` is: caps lock
|
||||||
|
// during a long cull must not silently stop the keys
|
||||||
|
// working.
|
||||||
|
if (Keys.chord(event) == "P") {
|
||||||
root.judged(-1, 1, root.hover-ordinal);
|
root.judged(-1, 1, root.hover-ordinal);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == "x" || event.text == "X") {
|
if (Keys.chord(event) == "X") {
|
||||||
root.judged(-1, 2, root.hover-ordinal);
|
root.judged(-1, 2, root.hover-ordinal);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == "u" || event.text == "U") {
|
if (Keys.chord(event) == "U") {
|
||||||
root.judged(-1, 0, root.hover-ordinal);
|
root.judged(-1, 0, root.hover-ordinal);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
|
// GESTURE: Move photographs to the trash
|
||||||
|
// where: Library grid
|
||||||
|
// touch: Tap the bin at the start of a cell's stars
|
||||||
|
// pointer: Hover the cell and click the bin before its
|
||||||
|
// stars
|
||||||
|
// keys: `Delete` or `Backspace`, on the selection
|
||||||
|
// why: The bin acts on one photograph, so a stray
|
||||||
|
// click cannot trash a selection; the key acts
|
||||||
|
// on the selection because that is what every
|
||||||
|
// file manager's Delete does. Both are undone
|
||||||
|
// from the trash view.
|
||||||
|
//
|
||||||
// Delete moves the selection to the trash folder on the
|
// Delete moves the selection to the trash folder on the
|
||||||
// server. Unlike every other key here it is not metadata —
|
// server. Unlike every other key here it is not metadata —
|
||||||
// it relocates files — but it is also the key every file
|
// it relocates files — but it is also the key every file
|
||||||
// manager binds to exactly this, and the operation is
|
// manager binds to exactly this, and the operation is
|
||||||
// reversible from the trash view.
|
// reversible from the trash view.
|
||||||
if (event.text == Key.Delete || event.text == Key.Backspace) {
|
if (Keys.chord(event) == "Delete" || Keys.chord(event) == "Backspace") {
|
||||||
root.trash-selection();
|
root.trash-selection();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -3223,20 +3247,43 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// rename key everywhere else, and the reason it is bound
|
// rename key everywhere else, and the reason it is bound
|
||||||
// here is that this scope is what holds focus in library
|
// here is that this scope is what holds focus in library
|
||||||
// mode. Rust ignores it when nothing is scoped.
|
// mode. Rust ignores it when nothing is scoped.
|
||||||
|
// GESTURE: Open this list
|
||||||
|
// where: Library grid
|
||||||
|
// touch: Press Help in the header
|
||||||
|
// pointer: Press Help in the header
|
||||||
|
// keys: `F1`
|
||||||
|
//
|
||||||
|
// GESTURE: Rename the collection the grid is showing
|
||||||
|
// where: Library grid
|
||||||
|
// touch: Hold it in the sidebar, then "Rename"
|
||||||
|
// pointer: Double-click it in the sidebar
|
||||||
|
// keys: `F2`
|
||||||
|
//
|
||||||
// TRACES: FR-UI-4 | FR-UI-5
|
// TRACES: FR-UI-4 | FR-UI-5
|
||||||
// F1 opens the reference the Help button does — the help
|
// F1 opens the reference the Help button does — the help
|
||||||
// key everywhere else.
|
// key everywhere else.
|
||||||
if (event.text == Key.F1) {
|
if (Keys.chord(event) == "F1") {
|
||||||
root.helping = true;
|
root.helping = true;
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.F2) {
|
if (Keys.chord(event) == "F2") {
|
||||||
root.rename-scope();
|
root.rename-scope();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- walking the grid ---------------------------------
|
// --- walking the grid ---------------------------------
|
||||||
//
|
//
|
||||||
|
// GESTURE: Move through the grid
|
||||||
|
// where: Library grid
|
||||||
|
// touch: Scroll, and tap a photograph
|
||||||
|
// pointer: Scroll, and click a photograph
|
||||||
|
// keys: `Left`, `Right`, `Up` and `Down` move one
|
||||||
|
// photograph; `PageUp` and `PageDown` a
|
||||||
|
// screenful; `Home` and `End` to the first and
|
||||||
|
// the last
|
||||||
|
// why: The cursor selects what it lands on, so
|
||||||
|
// walking and judging are one hand's work.
|
||||||
|
//
|
||||||
// The keys that make a cull possible without the mouse:
|
// The keys that make a cull possible without the mouse:
|
||||||
// arrows move the cursor, shift extends the selection from
|
// arrows move the cursor, shift extends the selection from
|
||||||
// the anchor, `Return` opens what the cursor is on. The
|
// the anchor, `Return` opens what the cursor is on. The
|
||||||
@@ -3252,28 +3299,28 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// The vertical steps are expressed in columns and rows,
|
// The vertical steps are expressed in columns and rows,
|
||||||
// which only the grid knows: how far "down" is depends on
|
// which only the grid knows: how far "down" is depends on
|
||||||
// how wide the window happens to be.
|
// how wide the window happens to be.
|
||||||
if (event.text == Key.LeftArrow) {
|
if (Keys.chord(event) == "Left" || Keys.chord(event) == "Shift+Left") {
|
||||||
root.move-cursor(-1, event.modifiers.shift);
|
root.move-cursor(-1, event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.RightArrow) {
|
if (Keys.chord(event) == "Right" || Keys.chord(event) == "Shift+Right") {
|
||||||
root.move-cursor(1, event.modifiers.shift);
|
root.move-cursor(1, event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.UpArrow) {
|
if (Keys.chord(event) == "Up" || Keys.chord(event) == "Shift+Up") {
|
||||||
root.move-cursor(-root.columns, event.modifiers.shift);
|
root.move-cursor(-root.columns, event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.DownArrow) {
|
if (Keys.chord(event) == "Down" || Keys.chord(event) == "Shift+Down") {
|
||||||
root.move-cursor(root.columns, event.modifiers.shift);
|
root.move-cursor(root.columns, event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.PageUp) {
|
if (Keys.chord(event) == "PageUp" || Keys.chord(event) == "Shift+PageUp") {
|
||||||
root.move-cursor(-root.columns * root.visible-rows,
|
root.move-cursor(-root.columns * root.visible-rows,
|
||||||
event.modifiers.shift);
|
event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.PageDown) {
|
if (Keys.chord(event) == "PageDown" || Keys.chord(event) == "Shift+PageDown") {
|
||||||
root.move-cursor(root.columns * root.visible-rows,
|
root.move-cursor(root.columns * root.visible-rows,
|
||||||
event.modifiers.shift);
|
event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
@@ -3281,15 +3328,15 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// A step longer than the library, clamped at the far end.
|
// A step longer than the library, clamped at the far end.
|
||||||
// `total` is what the grid was told the library holds, so
|
// `total` is what the grid was told the library holds, so
|
||||||
// this stays honest as it grows.
|
// this stays honest as it grows.
|
||||||
if (event.text == Key.Home) {
|
if (Keys.chord(event) == "Home" || Keys.chord(event) == "Shift+Home") {
|
||||||
root.move-cursor(-root.total, event.modifiers.shift);
|
root.move-cursor(-root.total, event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.End) {
|
if (Keys.chord(event) == "End" || Keys.chord(event) == "Shift+End") {
|
||||||
root.move-cursor(root.total, event.modifiers.shift);
|
root.move-cursor(root.total, event.modifiers.shift);
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
if (event.text == Key.Return) {
|
if (Keys.chord(event) == "Enter") {
|
||||||
root.open-cursor();
|
root.open-cursor();
|
||||||
return accept;
|
return accept;
|
||||||
}
|
}
|
||||||
@@ -3852,6 +3899,8 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: Tap it — a single tap, any length
|
// touch: Tap it — a single tap, any length
|
||||||
// pointer: Click it
|
// pointer: Click it
|
||||||
|
// keys: `Enter`, on the photograph the arrow
|
||||||
|
// keys have walked to
|
||||||
// why: A tap opens; a tap that *moved* does not.
|
// why: A tap opens; a tap that *moved* does not.
|
||||||
// Travel is what separates a deliberate tap
|
// Travel is what separates a deliberate tap
|
||||||
// from a hand brushing past, and it is the
|
// from a hand brushing past, and it is the
|
||||||
@@ -3973,7 +4022,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: Tap a star on the cell
|
// touch: Tap a star on the cell
|
||||||
// pointer: Hover the cell, then click a star
|
// pointer: Hover the cell, then click a star
|
||||||
// keys: 0 to 5 with the pointer over it, or
|
// keys: `0`–`5` with the pointer over it, or
|
||||||
// on the selection
|
// on the selection
|
||||||
// why: A star has to take the press without it
|
// why: A star has to take the press without it
|
||||||
// also reaching the cell, or every rating
|
// also reaching the cell, or every rating
|
||||||
@@ -4801,7 +4850,7 @@ export component LibraryGrid inherits Rectangle {
|
|||||||
// where: Library grid
|
// where: Library grid
|
||||||
// touch: While selecting, press "Select all"
|
// touch: While selecting, press "Select all"
|
||||||
// pointer: While selecting, press "Select all"
|
// pointer: While selecting, press "Select all"
|
||||||
// keys: Ctrl+A
|
// keys: `Ctrl+A`
|
||||||
// why: A scoped grid of two hundred frames is two hundred
|
// why: A scoped grid of two hundred frames is two hundred
|
||||||
// taps otherwise, and "all of them, except those
|
// taps otherwise, and "all of them, except those
|
||||||
// three" is a far more common shape than the taps it
|
// three" is a far more common shape than the taps it
|
||||||
|
|||||||
@@ -214,11 +214,10 @@ component MaskEntry inherits Rectangle {
|
|||||||
// where: Develop
|
// where: Develop
|
||||||
// touch: Tap the ring at the head of its row
|
// touch: Tap the ring at the head of its row
|
||||||
// pointer: Click the ring at the head of its row
|
// pointer: Click the ring at the head of its row
|
||||||
// keys: H, for the selected layer
|
// keys: `H`, for the selected layer
|
||||||
// why: Disabling a layer is the before-and-after a local
|
// why: Disabling a layer is the before-and-after a local
|
||||||
// edit constantly wants, so it is one press away rather
|
// edit constantly wants, so it is one press away rather
|
||||||
// than inside the row. It is an edit and does take a
|
// than inside the row. It is an edit and does take a
|
||||||
// keys: H, for the selected layer
|
|
||||||
// history step, unlike holding "Before" — the layer
|
// history step, unlike holding "Before" — the layer
|
||||||
// really is off until it is switched back on.
|
// really is off until it is switched back on.
|
||||||
// manual: local-adjustments
|
// manual: local-adjustments
|
||||||
|
|||||||
Reference in New Issue
Block a user