Walk the grid with the arrow keys, and open with Return
Build and test / Desktop (Linux) (push) Successful in 17m36s
Build and test / Layer separation (push) Successful in 34s
Traceability / Requirement traces (push) Failing after 25s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 9m17s
Build and test / Desktop (Linux) (push) Successful in 17m36s
Build and test / Layer separation (push) Successful in 34s
Traceability / Requirement traces (push) Failing after 25s
🐳 Android image / Build and push (push) Successful in 3s
Build and test / android-image (push) Successful in 3s
Build and test / Android (aarch64) (push) Failing after 9m17s
A cull is thousands of decisions. The grid already bound 0-5, P, X and U so the judgement itself needs no mouse, and then made the photographer reach for one to move to the next frame — which is most of the gesture, most of the time. Arrows now move a cursor, shift+arrow extends the selection, Home/End and PageUp/PageDown cover ground, and Return opens what the cursor is on. The cursor is a library ordinal, not a row of the loaded window. The window is a few screenfuls around wherever the user is looking, so a cursor held as a row would stop at its edge or keep counting into cells belonging to other photographs; walking out of the window reloads it around the new position, exactly as scrolling does. This is the argument the selection already made by keying on ids rather than rows. The anchor had that fault for real: it was a window row, so a shift-click after a scroll extended from whatever image had since drifted into it. It is an ordinal now, and apply_press takes the window's offset to map between the two. A range longer than the loaded window truncates to what is loaded, since selection is by id and an image the catalog has not been asked for has none. The pointer and the keyboard share select_row so the two cannot drift apart. The one deliberate difference: a plain arrow collapses the selection onto the cursor, where a plain click on an already-selected cell leaves it alone — that exception exists so a multi-image drag can start from one of its members, and there is no drag behind a keystroke. The grid scrolls to the cursor only when the cursor leaves the viewport, and then by as little as will do it. Reusing the scrub's seek() would put the cursor's row at the top on every press, which makes a row unreadable as you walk along it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -672,6 +672,29 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
/// How many images are selected, for the header's count.
|
||||
in property <int> selected-count: 0;
|
||||
|
||||
// --- the keyboard cursor ------------------------------------------------
|
||||
//
|
||||
// Where the keyboard is in the library, as an **image ordinal** — not a
|
||||
// row of the loaded window, which names a different photograph after every
|
||||
// scroll. Rust owns it, because clamping it needs the library's length and
|
||||
// moving it may have to swap the window underneath.
|
||||
//
|
||||
// -1 before the user has taken hold of it, so a fresh grid draws no cursor
|
||||
// and the first arrow press picks up where the view already is rather than
|
||||
// teleporting to image zero.
|
||||
in property <int> cursor: -1;
|
||||
/// Move the cursor by a number of images; the flag extends the selection
|
||||
/// from the anchor instead of replacing it.
|
||||
///
|
||||
/// A signed step and nothing else. Home and End are this with a step
|
||||
/// longer than the library, which Rust clamps — so this file needs to know
|
||||
/// neither how many images there are nor where the loaded window starts.
|
||||
callback move-cursor(int, bool);
|
||||
/// Open the image under the cursor. `Return`, and the reason the arrows
|
||||
/// are worth having: a cull is walk, judge, open, back, without the hand
|
||||
/// ever leaving the keyboard.
|
||||
callback open-cursor();
|
||||
/// Which collection scopes the grid, for the header. Empty means all.
|
||||
in property <string> scope-label: "";
|
||||
/// Take the selection out of the collection currently being shown. Only
|
||||
@@ -1165,6 +1188,65 @@ export component LibraryGrid inherits Rectangle {
|
||||
root.rename-scope();
|
||||
return accept;
|
||||
}
|
||||
|
||||
// --- walking the grid ---------------------------------
|
||||
//
|
||||
// The keys that make a cull possible without the mouse:
|
||||
// arrows move the cursor, shift extends the selection from
|
||||
// the anchor, `Return` opens what the cursor is on. The
|
||||
// judgement keys above act on the selection, so walking
|
||||
// with the arrows and rating as you go is one hand's work.
|
||||
//
|
||||
// Every one of these is `accept`ed. The `Flickable` scrolls
|
||||
// on arrow keys of its own accord, and letting it would
|
||||
// move the view out from under a cursor that had not
|
||||
// moved — the grid is scrolled *to* the cursor instead,
|
||||
// and only when the cursor leaves the viewport.
|
||||
//
|
||||
// The vertical steps are expressed in columns and rows,
|
||||
// which only the grid knows: how far "down" is depends on
|
||||
// how wide the window happens to be.
|
||||
if (event.text == Key.LeftArrow) {
|
||||
root.move-cursor(-1, event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.RightArrow) {
|
||||
root.move-cursor(1, event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.UpArrow) {
|
||||
root.move-cursor(-root.columns, event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.DownArrow) {
|
||||
root.move-cursor(root.columns, event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.PageUp) {
|
||||
root.move-cursor(-root.columns * root.visible-rows,
|
||||
event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.PageDown) {
|
||||
root.move-cursor(root.columns * root.visible-rows,
|
||||
event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
// A step longer than the library, clamped at the far end.
|
||||
// `total` is what the grid was told the library holds, so
|
||||
// this stays honest as it grows.
|
||||
if (event.text == Key.Home) {
|
||||
root.move-cursor(-root.total, event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.End) {
|
||||
root.move-cursor(root.total, event.modifiers.shift);
|
||||
return accept;
|
||||
}
|
||||
if (event.text == Key.Return) {
|
||||
root.open-cursor();
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
@@ -1235,6 +1317,34 @@ export component LibraryGrid inherits Rectangle {
|
||||
property <int> token: root.scroll-token;
|
||||
changed token => { self.seek(); }
|
||||
|
||||
// Keep the keyboard cursor in view, moving as little as will
|
||||
// do it.
|
||||
//
|
||||
// Deliberately *not* `seek()`: that puts the requested row at
|
||||
// the top, which is right for a scrub — the user asked to go
|
||||
// to a date and expects to arrive there — and wrong for an
|
||||
// arrow key, where the grid jumping a row upward on every
|
||||
// press makes the row impossible to read. So a cursor already
|
||||
// on screen moves nothing at all, and one that has just left
|
||||
// brings in exactly its own row.
|
||||
property <length> pitch: root.cell-size + Theme.gap;
|
||||
property <int> cursor-row: floor(root.cursor / max(1, root.columns));
|
||||
changed cursor-row => { self.reveal(); }
|
||||
|
||||
function reveal() {
|
||||
if (root.cursor < 0) {
|
||||
return;
|
||||
}
|
||||
let top = Theme.gap + self.cursor-row * self.pitch;
|
||||
let shown = -self.viewport-y;
|
||||
let bottom = max(0px, self.viewport-height - self.height);
|
||||
if (top < shown) {
|
||||
self.viewport-y = -min(bottom, top);
|
||||
} else if (top + self.pitch > shown + self.height) {
|
||||
self.viewport-y = -min(bottom, top + self.pitch - self.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Also on creation, which is what returning from the develop
|
||||
// view needs. `show-library` gates an `if`, so the grid is built
|
||||
// anew and `token` is *initialised* to the already-bumped value
|
||||
|
||||
Reference in New Issue
Block a user