Give the selection one bar instead of two ends of the window
"What can I do with these twelve photographs?" was answered in two places. Six buttons in the header — Add to collection, Keywords, Presets, Paste to N, Export N, Remove from collection — and four more on the floating bar at the foot of the grid, beside the count that says what they would act on. The header half was the worse of the two. Those six appeared and disappeared from the middle of the row as photographs were picked, so Sync, Settings and everything beside them slid several hundred pixels sideways at the exact moment a hand was already travelling toward one. On a tablet the header scrolls sideways, so they were often not on screen at all. All six move to the bar. It now reads left to right as shaping the selection — Clear, Select all, Select to… — then acting on it, with a gap between the two thoughts. The header keeps only what belongs to the library, and changes only when the library does. Two consequences worth stating. The bar holds ten controls in the worst case, so it scrolls sideways like every other row in this view, for the reason set out on the header's Flickable: a layout given less width than its children need overruns rather than shrinking, and the buttons past the edge are simply gone. And the bar now stays up for a running export whatever the selection has since become, because Cancel export lived on a button that used to have its own `|| exporting` escape hatch — the grid's viewport inset follows the same condition so the last row of thumbnails is never trapped underneath. `settings-summary` went with them: threaded from the window into the grid and into HeaderActions, and never once drawn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-11
@@ -54,7 +54,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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/library.slint:1274`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1172`</sub>
|
||||
|
||||
### Add or remove one photograph
|
||||
|
||||
@@ -63,7 +63,7 @@ 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/library.slint:1283`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1181`</sub>
|
||||
|
||||
### Leave selecting
|
||||
|
||||
@@ -71,7 +71,7 @@ While selecting, a tap never opens. That is the whole point of the mode: one mea
|
||||
- **Pointer** — Press Done in the header
|
||||
- **Keyboard** — Escape
|
||||
|
||||
<sub>`ui/dr-ui/ui/library.slint:1291`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1189`</sub>
|
||||
|
||||
### Select a range
|
||||
|
||||
@@ -80,7 +80,7 @@ While selecting, a tap never opens. That is the whole point of the mode: one mea
|
||||
|
||||
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:1352`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1250`</sub>
|
||||
|
||||
### Find photographs with two people in them
|
||||
|
||||
@@ -89,7 +89,7 @@ This replaced a double tap, which had no visible state and could take forty phot
|
||||
|
||||
"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:2093`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1936`</sub>
|
||||
|
||||
### Resize the thumbnails
|
||||
|
||||
@@ -98,7 +98,7 @@ This replaced a double tap, which had no visible state and could take forty phot
|
||||
|
||||
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:2719`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:2562`</sub>
|
||||
|
||||
### File photographs in a collection
|
||||
|
||||
@@ -107,7 +107,7 @@ 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/library.slint:2878`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:2725`</sub>
|
||||
|
||||
### Open a photograph
|
||||
|
||||
@@ -116,7 +116,7 @@ The selection is what the drag carries, which is why selecting several is worth
|
||||
|
||||
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:3133`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:2980`</sub>
|
||||
|
||||
### Rate a photograph without opening it
|
||||
|
||||
@@ -126,7 +126,7 @@ A tap opens; a tap that *moved* does not. Travel is what separates a deliberate
|
||||
|
||||
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:3245`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3092`</sub>
|
||||
|
||||
### Drop the selection but keep selecting
|
||||
|
||||
@@ -135,7 +135,7 @@ A star has to take the press without it also reaching the cell, or every rating
|
||||
|
||||
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:3808`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3713`</sub>
|
||||
|
||||
### Select everything the grid is showing
|
||||
|
||||
@@ -144,4 +144,4 @@ Distinct from Done, which leaves the mode entirely. Clearing keeps it, so the ne
|
||||
|
||||
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:3825`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3730`</sub>
|
||||
|
||||
+18
-18
File diff suppressed because one or more lines are too long
@@ -1517,7 +1517,6 @@ in property <bool> panel-visible: true;
|
||||
selected-count: root.library-selected-count;
|
||||
scope-label: root.collection-scope-label;
|
||||
settings-armed: root.settings-armed;
|
||||
settings-summary: root.settings-summary;
|
||||
paste-settings-to-selection => {
|
||||
root.paste-settings-to-selection();
|
||||
}
|
||||
|
||||
+212
-194
@@ -827,18 +827,26 @@ export component FlagMark inherits Rectangle {
|
||||
// The header's action buttons, in one place so they can be drawn in two.
|
||||
//
|
||||
// A tablet in portrait is 768 logical pixels wide, and the header wants a
|
||||
// title, three status readouts and six buttons. Laid out in one row they all
|
||||
// shrink to their minimum and elide — "Change library" arrives as "Change
|
||||
// li…" — and the row still overflows its own width. Slint's HorizontalLayout
|
||||
// has no wrap and no overflow, so the row has to be told what to drop.
|
||||
// title, a status line and these buttons. Laid out in one row they all shrink
|
||||
// to their minimum and elide — "Change library" arrives as "Change li…" — and
|
||||
// the row still overflows its own width. Slint's HorizontalLayout has no wrap
|
||||
// and no overflow, so the row has to be told what to drop.
|
||||
//
|
||||
// Extracted rather than duplicated because the alternative is six buttons
|
||||
// Extracted rather than duplicated because the alternative is these buttons
|
||||
// written twice with their visibility rules and callbacks copied, and the
|
||||
// copy that gets forgotten is the one behind the disclosure nobody opens
|
||||
// while testing.
|
||||
//
|
||||
// **Nothing in here depends on the selection.** Everything a selection can be
|
||||
// done *to* lives on the bar at the foot of the grid, which is also where the
|
||||
// selection is reported. Six buttons used to appear and disappear from the
|
||||
// middle of this row as photographs were picked, shunting `Sync`, `Settings`
|
||||
// and everything beside them several hundred pixels sideways at the exact
|
||||
// moment a hand was already travelling toward one — and splitting "what can I
|
||||
// do with these?" across two bars at opposite ends of the window. A row that
|
||||
// only changes when the *library* changes can be aimed at from memory.
|
||||
component HeaderActions inherits HorizontalLayout {
|
||||
in property <string> scope-label;
|
||||
in property <int> selected-count: 0;
|
||||
/// TRACES: FR-UI-2 | FR-UI-4
|
||||
/// Whether taps are selecting rather than opening. The non-gesture half of
|
||||
/// touch multi-selection: the long press is the quick way in, and this is
|
||||
@@ -847,31 +855,11 @@ component HeaderActions inherits HorizontalLayout {
|
||||
in property <bool> scanning: false;
|
||||
in property <bool> syncing: false;
|
||||
in property <bool> scope-pinned: false;
|
||||
/// TRACES: FR-DEV-6
|
||||
/// Whether settings have been copied, and what pasting them would apply.
|
||||
in property <bool> settings-armed: false;
|
||||
in property <string> settings-summary;
|
||||
/// TRACES: FR-EXP-7
|
||||
/// Whether a batch is running, and where its files are going.
|
||||
in property <bool> exporting: false;
|
||||
in property <bool> export-to-server: false;
|
||||
/// Centres each button in a 44px header. Off in the disclosure row, which
|
||||
/// is sized to its content.
|
||||
in property <bool> centred: true;
|
||||
in property <length> row-height: 44px;
|
||||
|
||||
callback export-selection();
|
||||
callback cancel-export();
|
||||
callback paste-settings-to-selection();
|
||||
/// TRACES: FR-DEV-6
|
||||
/// Open the named-preset sheet over the selection.
|
||||
callback open-presets();
|
||||
callback remove-from-collection();
|
||||
/// Open the sheet that files the selection in a collection.
|
||||
callback add-to-collection();
|
||||
/// TRACES: FR-CAT-5
|
||||
/// Open the sheet that keywords the selection.
|
||||
callback add-keyword();
|
||||
callback toggle-select-mode();
|
||||
callback change-library();
|
||||
callback toggle-pin-scope();
|
||||
@@ -879,10 +867,10 @@ component HeaderActions inherits HorizontalLayout {
|
||||
/// TRACES: FR-CAT-10
|
||||
/// Whether this platform can reach a card at all (`dr_plat::imports_supported`).
|
||||
///
|
||||
/// Hidden rather than disabled, unlike the buttons above that come and go
|
||||
/// with the selection: those are unavailable *now* and will be available
|
||||
/// in a moment, where this one never will be on this device. A permanently
|
||||
/// disabled control teaches the reader that the row lies.
|
||||
/// Hidden rather than disabled, unlike the buttons that come and go with a
|
||||
/// scan: those are unavailable *now* and will be available in a moment,
|
||||
/// where this one never will be on this device. A permanently disabled
|
||||
/// control teaches the reader that the row lies.
|
||||
in property <bool> can-import: false;
|
||||
|
||||
callback rescan();
|
||||
@@ -896,8 +884,9 @@ component HeaderActions inherits HorizontalLayout {
|
||||
spacing: Theme.gap;
|
||||
|
||||
// TRACES: FR-UI-2 | FR-UI-4 | FR-CAT-7
|
||||
// Selection mode, first in the row because it is what the rest of the row
|
||||
// acts on: every button after this one appears once something is selected.
|
||||
// The way *into* selecting, and the only selection control left in this
|
||||
// row — everything a selection can be done *to* is on the bar at the foot
|
||||
// of the grid, beside the count that says what it would act on.
|
||||
//
|
||||
// It exists at all because the desktop's way in — ctrl-click, shift-click —
|
||||
// has no touch equivalent, and the drag that files a photograph in a
|
||||
@@ -914,96 +903,6 @@ component HeaderActions inherits HorizontalLayout {
|
||||
clicked => { root.toggle-select-mode(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7 | FR-UI-4
|
||||
// File the selection in a collection without dragging it there. The drag
|
||||
// is the faster gesture with a pointer and impossible with one finger on a
|
||||
// grid that scrolls, which is the whole reason this button exists.
|
||||
if root.selected-count > 0: Button {
|
||||
text: "Add to collection";
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => { root.add-to-collection(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-CAT-6
|
||||
// Keyword the selection. Beside "Add to collection" because they are the
|
||||
// same thought — these photographs are *of* something, and they belong
|
||||
// *with* something — and appearing under the same condition, because
|
||||
// neither means anything without a selection to act on.
|
||||
if root.selected-count > 0: Button {
|
||||
text: "Keywords";
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => { root.add-keyword(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// Batch-apply the copied settings. Shown only with both a selection and a
|
||||
// clipboard, because it is meaningless without either — and because a
|
||||
// permanently visible button that is usually disabled teaches the user to
|
||||
// stop reading this row.
|
||||
//
|
||||
// The count is in the label rather than in a confirmation: this writes to
|
||||
// every selected image, and "Paste to 40" said before the click is worth
|
||||
// more than a dialogue asking the same question after it.
|
||||
if root.selected-count > 0 && root.settings-armed: Button {
|
||||
text: "Paste to " + root.selected-count;
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => { root.paste-settings-to-selection(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// The saved settings, beside the copied ones.
|
||||
//
|
||||
// Gated on the selection alone, unlike the paste beside it: that button
|
||||
// needs a clipboard *this session*, where the preset list is whatever the
|
||||
// photographer saved last month. Requiring an armed clipboard here would
|
||||
// hide the saved presets behind an unrelated action — which is the shape
|
||||
// of bug that makes a feature only its author knows about (FR-UI-4).
|
||||
if root.selected-count > 0: Button {
|
||||
text: "Presets";
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => { root.open-presets(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-EXP-7 | NFR-ARCH-3
|
||||
// Export the selection, and stop the batch that is running.
|
||||
//
|
||||
// One button doing both, because they are the same thought a moment apart
|
||||
// and a separate cancel would have to appear from somewhere — shifting the
|
||||
// row under the pointer at the exact moment the user is reaching for it.
|
||||
//
|
||||
// It stays while a batch runs whatever the selection has since become: the
|
||||
// run is what the button now refers to, and a batch of three hundred that
|
||||
// could only be stopped by not changing the selection would be a trap.
|
||||
//
|
||||
// The count is in the label rather than behind a confirmation, exactly as
|
||||
// the paste above puts it there: "Export 40" read before the click is worth
|
||||
// more than a dialogue asking the same question after it.
|
||||
if root.selected-count > 0 || root.exporting: Button {
|
||||
text: root.exporting
|
||||
? "Cancel export"
|
||||
: (root.export-to-server
|
||||
? "Export " + root.selected-count + " to the library"
|
||||
: "Export " + root.selected-count);
|
||||
active: root.exporting;
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => {
|
||||
if (root.exporting) {
|
||||
root.cancel-export();
|
||||
} else {
|
||||
root.export-selection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removing from a collection is only meaningful while the grid is scoped
|
||||
// to one. Offering it unscoped would invite the reading "remove from the
|
||||
// library", which nothing here does.
|
||||
if root.scope-label != "" && root.selected-count > 0: Button {
|
||||
text: "Remove from collection";
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => { root.remove-from-collection(); }
|
||||
}
|
||||
|
||||
if !root.scanning: Button {
|
||||
text: "Change library";
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
@@ -1076,10 +975,9 @@ component HeaderActions inherits HorizontalLayout {
|
||||
clicked => { root.open-gestures(); }
|
||||
}
|
||||
|
||||
// Last in the row, and unconditional. The buttons before it come and go
|
||||
// with what the grid is showing; settings is always reachable, and a
|
||||
// control that moved as its neighbours appeared would be hunted for each
|
||||
// time.
|
||||
// Last in the row, and unconditional. Nothing before it moves with the
|
||||
// selection any more, so this is a fixed address rather than a control
|
||||
// that drifts as photographs are picked.
|
||||
Button {
|
||||
text: "Settings";
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
@@ -1497,7 +1395,6 @@ export component LibraryGrid inherits Rectangle {
|
||||
// itself belongs to the window — a copy is taken in the develop view and
|
||||
// pasted here — so the grid only reports what it has and asks.
|
||||
in property <bool> settings-armed: false;
|
||||
in property <string> settings-summary;
|
||||
callback paste-settings-to-selection();
|
||||
/// TRACES: FR-DEV-6
|
||||
callback open-presets();
|
||||
@@ -1875,39 +1772,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
// button that was already visible.
|
||||
if root.expanded: HeaderActions {
|
||||
scope-label: root.scope-label;
|
||||
selected-count: root.selected-count;
|
||||
scanning: root.scanning;
|
||||
syncing: root.syncing;
|
||||
scope-pinned: root.scope-pinned;
|
||||
can-import: root.can-import;
|
||||
settings-armed: root.settings-armed;
|
||||
settings-summary: root.settings-summary;
|
||||
exporting: root.exporting;
|
||||
export-to-server: root.export-to-server;
|
||||
export-selection => { root.export-selection(); }
|
||||
cancel-export => { root.cancel-export(); }
|
||||
paste-settings-to-selection => { root.paste-settings-to-selection(); }
|
||||
open-presets => { root.open-presets(); }
|
||||
remove-from-collection => { root.remove-from-collection(); }
|
||||
select-mode: root.select-mode;
|
||||
toggle-select-mode => { root.toggle-select-mode(); }
|
||||
add-to-collection => {
|
||||
// Reset every time it opens: see `filing-moves`.
|
||||
root.filing-moves = false;
|
||||
root.filing = true;
|
||||
// The disclosure has served its purpose and would
|
||||
// otherwise sit behind the sheet, so the row it belongs
|
||||
// to is still there when the sheet closes.
|
||||
root.actions-open = false;
|
||||
}
|
||||
add-keyword => {
|
||||
// Ask for the vocabulary before showing the sheet, so
|
||||
// it is answered against the selection as it stands now
|
||||
// rather than as it stood when the grid last loaded.
|
||||
root.keywords-opened();
|
||||
root.keywording = true;
|
||||
root.actions-open = false;
|
||||
}
|
||||
change-library => { root.change-library(); }
|
||||
toggle-pin-scope => { root.toggle-pin-scope(); }
|
||||
sync-now => { root.sync-now(); }
|
||||
@@ -1918,7 +1788,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
open-gestures => { root.helping = true; }
|
||||
}
|
||||
|
||||
// Compact: one button in place of six. Labelled rather than a
|
||||
// Compact: one button in place of several. Labelled rather than a
|
||||
// bare glyph, because "⋯" alone in a header of words reads as
|
||||
// a truncation of the label beside it — which is exactly what
|
||||
// this change exists to stop happening.
|
||||
@@ -1937,15 +1807,15 @@ export component LibraryGrid inherits Rectangle {
|
||||
// A row beneath the header rather than a popup over the grid: a popup
|
||||
// needs a dismiss rule, an anchor and a decision about what happens
|
||||
// when the window resizes under it, and all this needs to be is the
|
||||
// six buttons somewhere they fit. It closes itself when the window
|
||||
// widens — see `actions-open`.
|
||||
// library's buttons somewhere they fit. It closes itself when the
|
||||
// window widens — see `actions-open`.
|
||||
if !root.expanded && root.actions-open: Rectangle {
|
||||
height: 44px;
|
||||
background: Theme.surface;
|
||||
|
||||
// Scrolls, like the two rows above it. This is the row that exists
|
||||
// *because* the header did not fit, so it is the last place to
|
||||
// assume six buttons will.
|
||||
// assume its buttons will.
|
||||
Flickable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -1962,39 +1832,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
HeaderActions {
|
||||
centred: true;
|
||||
scope-label: root.scope-label;
|
||||
selected-count: root.selected-count;
|
||||
scanning: root.scanning;
|
||||
syncing: root.syncing;
|
||||
scope-pinned: root.scope-pinned;
|
||||
can-import: root.can-import;
|
||||
settings-armed: root.settings-armed;
|
||||
settings-summary: root.settings-summary;
|
||||
exporting: root.exporting;
|
||||
export-to-server: root.export-to-server;
|
||||
export-selection => { root.export-selection(); }
|
||||
cancel-export => { root.cancel-export(); }
|
||||
paste-settings-to-selection => { root.paste-settings-to-selection(); }
|
||||
open-presets => { root.open-presets(); }
|
||||
remove-from-collection => { root.remove-from-collection(); }
|
||||
select-mode: root.select-mode;
|
||||
toggle-select-mode => { root.toggle-select-mode(); }
|
||||
add-to-collection => {
|
||||
// Reset every time it opens: see `filing-moves`.
|
||||
root.filing-moves = false;
|
||||
root.filing = true;
|
||||
// The disclosure has served its purpose and would
|
||||
// otherwise sit behind the sheet, so the row it belongs
|
||||
// to is still there when the sheet closes.
|
||||
root.actions-open = false;
|
||||
}
|
||||
add-keyword => {
|
||||
// Ask for the vocabulary before showing the sheet, so
|
||||
// it is answered against the selection as it stands now
|
||||
// rather than as it stood when the grid last loaded.
|
||||
root.keywords-opened();
|
||||
root.keywording = true;
|
||||
root.actions-open = false;
|
||||
}
|
||||
change-library => { root.change-library(); }
|
||||
toggle-pin-scope => { root.toggle-pin-scope(); }
|
||||
sync-now => { root.sync-now(); }
|
||||
@@ -2819,8 +2662,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
// the change invisible: every cell stays exactly where it was
|
||||
// and there is simply further to scroll, so the last row can be
|
||||
// brought clear of the bar instead of being trapped under it.
|
||||
// Same condition as the bar itself, or a running export with
|
||||
// the selection since cleared would leave the last row of
|
||||
// thumbnails trapped under a bar the grid did not know was
|
||||
// there.
|
||||
property <length> selection-inset:
|
||||
root.selected-count > 0 ? 40px : 0px;
|
||||
(root.selected-count > 0 || root.exporting) ? 40px : 0px;
|
||||
|
||||
viewport-height: root.total-rows * (root.cell-size + Theme.gap)
|
||||
+ Theme.gap + self.selection-inset;
|
||||
@@ -3737,6 +3584,15 @@ export component LibraryGrid inherits Rectangle {
|
||||
// you could make and then not see. A selection you cannot see is one
|
||||
// you act on by accident.
|
||||
//
|
||||
// **Every selection verb, in one place.** For a while this bar held the
|
||||
// ones that *shape* a selection — clear it, extend it, select the lot —
|
||||
// while the ones that *act* on it stayed in the header, forty-odd pixels
|
||||
// from the top of a window whose bottom edge is where the count is. So
|
||||
// "what can I do with these twelve?" was answered in two places at
|
||||
// opposite ends of the screen, and the header half moved sideways every
|
||||
// time the answer changed. Both halves are here now: shaping first, then
|
||||
// acting, separated by a gap rather than by a journey.
|
||||
//
|
||||
// **Over the grid, not above it.** As a row in the vertical flow it
|
||||
// appeared the instant the first photograph was selected, and every
|
||||
// cell in the grid moved 40px — so the act of selecting shifted the
|
||||
@@ -3747,7 +3603,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
// selecting changes what is drawn and never where. The grid's viewport
|
||||
// grows by the same 40px while it is there (see `selection-inset`), so
|
||||
// the last row can still be scrolled clear of it.
|
||||
if root.selected-count > 0: Rectangle {
|
||||
//
|
||||
// Shown for a running export whatever the selection has since become. The
|
||||
// batch is what `Cancel export` refers to by then, and a run of three
|
||||
// hundred that could only be stopped by not touching the selection would
|
||||
// be a trap.
|
||||
if root.selected-count > 0 || root.exporting: Rectangle {
|
||||
y: parent.height - self.height;
|
||||
height: 40px;
|
||||
background: Theme.surface;
|
||||
@@ -3755,11 +3616,38 @@ export component LibraryGrid inherits Rectangle {
|
||||
// Swallows presses that land on the bar rather than letting them
|
||||
// through to the cell beneath. A bar floating over the grid is a bar a
|
||||
// thumb can reach for and miss into.
|
||||
//
|
||||
// Before the Flickable below, so the flick that scrolls this row is
|
||||
// hit-tested first and this only catches what misses it.
|
||||
TouchArea { }
|
||||
|
||||
HorizontalLayout {
|
||||
// Reads as a bar rather than as a strip of grid that happens to be
|
||||
// grey: the cells behind it run right up to this edge.
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
// Scrolls sideways, for the reason the header and the chip row both
|
||||
// give at length: **a layout cannot be narrower than its children's
|
||||
// minimums**, so a row of ten buttons handed 768 logical pixels does
|
||||
// not shrink, it overruns — and the buttons past the edge are simply
|
||||
// gone. That was survivable while this bar held four controls and is
|
||||
// not now that it holds every verb.
|
||||
//
|
||||
// `min-width`, not `preferred-width`, exactly as the header row uses:
|
||||
// the count beside the buttons elides, so the row genuinely has a
|
||||
// smaller footprint than its natural one and a window with room for
|
||||
// the shrunk row should get it shrunk rather than scrolled.
|
||||
Flickable {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
viewport-height: self.height;
|
||||
viewport-width: max(self.width, selection-row.min-width);
|
||||
|
||||
selection-row := HorizontalLayout {
|
||||
width: parent.viewport-width;
|
||||
height: parent.viewport-height;
|
||||
padding-left: Theme.gap;
|
||||
padding-right: Theme.gap;
|
||||
spacing: Theme.gap-sm;
|
||||
@@ -3776,6 +3664,12 @@ export component LibraryGrid inherits Rectangle {
|
||||
font-weight: 600;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
// Stretch is what actually lets a `Text` shrink under
|
||||
// pressure in Slint, not `elide` alone — see the note on the
|
||||
// header row's Flickable. Without it this readout keeps its
|
||||
// full natural width and is what pushes the buttons beside
|
||||
// it past the right-hand edge.
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
if root.ranging: Rectangle { width: Theme.gap; }
|
||||
@@ -3791,13 +3685,24 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
// State rather than a label: it is what the buttons beside it
|
||||
// act on.
|
||||
//
|
||||
// A running export with nothing selected says so rather than
|
||||
// reporting "0 photographs selected", which would be true and
|
||||
// useless — the bar is on screen at that moment for the batch,
|
||||
// not for the selection.
|
||||
if !root.ranging: Value {
|
||||
text: root.selected-count + (root.selected-count == 1
|
||||
? " photograph selected" : " photographs selected");
|
||||
text: root.selected-count == 0
|
||||
? "Exporting…"
|
||||
: root.selected-count + (root.selected-count == 1
|
||||
? " photograph selected" : " photographs selected");
|
||||
modified: true;
|
||||
font-weight: 600;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
// See the `ranging` readout above: stretch, not `elide`, is
|
||||
// what lets this collapse instead of shoving the buttons off
|
||||
// the end of the bar.
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
if !root.ranging: Rectangle { width: Theme.gap; }
|
||||
@@ -3812,7 +3717,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// why: Distinct from Done, which leaves the mode
|
||||
// entirely. Clearing keeps it, so the next
|
||||
// selection can start straight away.
|
||||
if !root.ranging: Button {
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Clear";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.clear-selection(); }
|
||||
@@ -3830,7 +3735,7 @@ export component LibraryGrid inherits Rectangle {
|
||||
// taps otherwise, and "all of them, except those
|
||||
// three" is a far more common shape than the taps it
|
||||
// took to say it.
|
||||
if !root.ranging: Button {
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Select all";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.select-all(); }
|
||||
@@ -3840,12 +3745,33 @@ export component LibraryGrid inherits Rectangle {
|
||||
// the double-tap it replaces was not good enough. Deliberately
|
||||
// an unfinished sentence: the ellipsis is the promise that a
|
||||
// second tap is coming.
|
||||
if !root.ranging: Button {
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Select to…";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.ranging = true; }
|
||||
}
|
||||
|
||||
// --- and here the bar turns from shaping to acting -------------
|
||||
//
|
||||
// A gap rather than a rule: the two halves are the same subject
|
||||
// and the eye only needs to be told they are different sentences.
|
||||
if !root.ranging: Rectangle { width: Theme.gap-lg; }
|
||||
|
||||
// TRACES: FR-CAT-7 | FR-UI-4
|
||||
// File the selection in a collection without dragging it there.
|
||||
// The drag is the faster gesture with a pointer and impossible
|
||||
// with one finger on a grid that scrolls, which is the whole
|
||||
// reason this button exists.
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Add to collection";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
// Reset every time it opens: see `filing-moves`.
|
||||
root.filing-moves = false;
|
||||
root.filing = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Filing a selection into a collection that does not exist yet
|
||||
// took four steps: make a collection, find it, select the
|
||||
// photographs again, add them. One press instead, which is how
|
||||
@@ -3854,12 +3780,104 @@ export component LibraryGrid inherits Rectangle {
|
||||
// "New collection holding 12 photographs" at the top, so a
|
||||
// button repeating it is width the strip does not have on a
|
||||
// tablet in portrait.
|
||||
if !root.ranging: Button {
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "New collection";
|
||||
primary: true;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.naming = true; }
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-5 | FR-CAT-6
|
||||
// Keyword the selection. Beside the two filing buttons because
|
||||
// they are the same thought — these photographs are *of*
|
||||
// something, and they belong *with* something.
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Keywords";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
// Ask for the vocabulary before showing the sheet, so it
|
||||
// is answered against the selection as it stands now
|
||||
// rather than as it stood when the grid last loaded.
|
||||
root.keywords-opened();
|
||||
root.keywording = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// The saved settings, beside the copied ones.
|
||||
//
|
||||
// Gated on the selection alone, unlike the paste after it: that
|
||||
// button needs a clipboard *this session*, where the preset list
|
||||
// is whatever the photographer saved last month. Requiring an
|
||||
// armed clipboard here would hide the saved presets behind an
|
||||
// unrelated action — which is the shape of bug that makes a
|
||||
// feature only its author knows about (FR-UI-4).
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Presets";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.open-presets(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-DEV-6
|
||||
// Batch-apply the copied settings. Shown only with both a
|
||||
// selection and a clipboard, because it is meaningless without
|
||||
// either — and because a permanently visible button that is
|
||||
// usually disabled teaches the user to stop reading this bar.
|
||||
//
|
||||
// The count is in the label rather than in a confirmation: this
|
||||
// writes to every selected image, and "Paste to 40" said before
|
||||
// the click is worth more than a dialogue asking the same
|
||||
// question after it.
|
||||
if !root.ranging && root.selected-count > 0 && root.settings-armed: Button {
|
||||
text: "Paste to " + root.selected-count;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.paste-settings-to-selection(); }
|
||||
}
|
||||
|
||||
// Removing from a collection is only meaningful while the grid is
|
||||
// scoped to one. Offering it unscoped would invite the reading
|
||||
// "remove from the library", which nothing here does.
|
||||
if !root.ranging && root.selected-count > 0 && root.scope-label != "": Button {
|
||||
text: "Remove from collection";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.remove-from-collection(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-EXP-7 | NFR-ARCH-3
|
||||
// Export the selection, and stop the batch that is running.
|
||||
//
|
||||
// One button doing both, because they are the same thought a
|
||||
// moment apart and a separate cancel would have to appear from
|
||||
// somewhere — shifting the row under the pointer at the exact
|
||||
// moment the user is reaching for it.
|
||||
//
|
||||
// Last on the bar because it is the last thing done to a
|
||||
// selection, and because it is the one control here that outlives
|
||||
// the selection: the bar itself stays up for a running batch (see
|
||||
// the note on the bar) so this is always reachable while there is
|
||||
// something to cancel.
|
||||
//
|
||||
// The count is in the label rather than behind a confirmation,
|
||||
// exactly as the paste above puts it there: "Export 40" read
|
||||
// before the click is worth more than a dialogue asking the same
|
||||
// question after it.
|
||||
if !root.ranging: Button {
|
||||
text: root.exporting
|
||||
? "Cancel export"
|
||||
: (root.export-to-server
|
||||
? "Export " + root.selected-count + " to the library"
|
||||
: "Export " + root.selected-count);
|
||||
active: root.exporting;
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => {
|
||||
if (root.exporting) {
|
||||
root.cancel-export();
|
||||
} else {
|
||||
root.export-selection();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user