Merge: one selection bar, and a header that decides what it gives up
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-15
@@ -23,7 +23,7 @@ Grouping over-merges on siblings, on parents and children, and on the same perso
|
||||
- **Touch** — Tick to confirm it, cross to reject it
|
||||
- **Pointer** — Tick to confirm it, cross to reject it
|
||||
|
||||
A face is either the system's guess or the user's judgement, and the two are never conflated. A rejection is remembered, so the face is not suggested for that person again.
|
||||
A face is either the system's guess or the user's judgement, and the two are never conflated. A rejection is remembered, so the face is not suggested for that person again. The gesture note above is the whole label: a tick and a cross are only "confirm" and "reject" to someone who can see the suggestion they sit beside, and `IconButton`'s fallback would announce them as "check" and "cross" — two icon names that say nothing about which person is being ruled on.
|
||||
|
||||
<sub>`ui/dr-ui/ui/identity.slint:150`</sub>
|
||||
|
||||
@@ -34,7 +34,7 @@ A face is either the system's guess or the user's judgement, and the two are nev
|
||||
|
||||
This is the point of having identified anybody. Without it the screen is a filing cabinet with no drawer handles.
|
||||
|
||||
<sub>`ui/dr-ui/ui/identity.slint:545`</sub>
|
||||
<sub>`ui/dr-ui/ui/identity.slint:552`</sub>
|
||||
|
||||
### Change how faces are grouped
|
||||
|
||||
@@ -43,7 +43,7 @@ This is the point of having identified anybody. Without it the screen is a filin
|
||||
|
||||
The right match confidence is a property of your library, not of the model. "What would this do?" answers for this library without writing anything; names, confirmations and the groups you have set aside are kept whatever the dials say.
|
||||
|
||||
<sub>`ui/dr-ui/ui/identity.slint:582`</sub>
|
||||
<sub>`ui/dr-ui/ui/identity.slint:589`</sub>
|
||||
|
||||
## Library grid
|
||||
|
||||
@@ -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:1184`</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:1193`</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:1201`</sub>
|
||||
|
||||
### Pick a photograph up to drag it
|
||||
|
||||
@@ -80,7 +80,7 @@ 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/library.slint:1321`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1231`</sub>
|
||||
|
||||
### Select a range
|
||||
|
||||
@@ -89,7 +89,7 @@ A finger on a photograph might be starting a scroll, and for the first half-seco
|
||||
|
||||
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:1386`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1296`</sub>
|
||||
|
||||
### Find photographs with two people in them
|
||||
|
||||
@@ -98,7 +98,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:2141`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:1978`</sub>
|
||||
|
||||
### Resize the thumbnails
|
||||
|
||||
@@ -107,7 +107,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:2783`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:2620`</sub>
|
||||
|
||||
### File photographs in a collection
|
||||
|
||||
@@ -116,7 +116,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:2942`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:2783`</sub>
|
||||
|
||||
### Open a photograph
|
||||
|
||||
@@ -125,7 +125,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:3210`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3051`</sub>
|
||||
|
||||
### Rate a photograph without opening it
|
||||
|
||||
@@ -135,7 +135,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:3330`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3171`</sub>
|
||||
|
||||
### Drop the selection but keep selecting
|
||||
|
||||
@@ -144,7 +144,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:3938`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3879`</sub>
|
||||
|
||||
### Select everything the grid is showing
|
||||
|
||||
@@ -153,4 +153,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:3955`</sub>
|
||||
<sub>`ui/dr-ui/ui/library.slint:3896`</sub>
|
||||
|
||||
+19
-19
File diff suppressed because one or more lines are too long
+11
-5
@@ -1468,6 +1468,16 @@ in property <bool> panel-visible: true;
|
||||
total-images: root.library-total;
|
||||
error: root.collection-error;
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// The two actions that were in the library header and
|
||||
// referred to the tree rather than to the library.
|
||||
// They read the same window state the header did — one
|
||||
// source per fact — and call the same handlers.
|
||||
scanning: root.library-scanning;
|
||||
scope-pinned: root.library-scope-pinned;
|
||||
toggle-pin-scope() => { root.library-toggle-pin-scope(); }
|
||||
change-library() => { root.library-change(); }
|
||||
|
||||
select(id) => { root.collection-select(id); }
|
||||
toggle(id) => { root.collection-toggle(id); }
|
||||
new-collection() => { root.collection-new(); }
|
||||
@@ -1499,7 +1509,7 @@ in property <bool> panel-visible: true;
|
||||
// FR-UI-1: the header collapses its actions behind a
|
||||
// disclosure below the breakpoint. A tablet in portrait is
|
||||
// 768 logical pixels, which fits the title and the status
|
||||
// readouts but not six buttons beside them.
|
||||
// line but not the library's buttons beside them.
|
||||
expanded: root.expanded;
|
||||
cells: root.library-cells;
|
||||
total: root.library-total;
|
||||
@@ -1536,10 +1546,8 @@ in property <bool> panel-visible: true;
|
||||
collections-visible: root.collections-visible;
|
||||
toggle-collections() => { root.toggle-collections(); }
|
||||
|
||||
scope-pinned: root.library-scope-pinned;
|
||||
pin-done: root.library-pin-done;
|
||||
pin-total: root.library-pin-total;
|
||||
toggle-pin-scope() => { root.library-toggle-pin-scope(); }
|
||||
root-label: root.library-root-label;
|
||||
timeline: root.library-timeline;
|
||||
timeline-label: root.library-timeline-label;
|
||||
@@ -1548,7 +1556,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();
|
||||
}
|
||||
@@ -1598,7 +1605,6 @@ in property <bool> panel-visible: true;
|
||||
viewport-cells-changed(n) => { root.library-viewport-cells(n); }
|
||||
cell-clicked(i) => { root.library-cell-clicked(i); }
|
||||
rescan() => { root.library-rescan(); }
|
||||
change-library() => { root.library-change(); }
|
||||
can-import: root.import-supported;
|
||||
open-import() => { root.import-open(); }
|
||||
open-settings() => { root.settings-open(); }
|
||||
|
||||
@@ -349,6 +349,32 @@ export component CollectionsPanel inherits Rectangle {
|
||||
in property <int> selected-id: 0;
|
||||
in property <int> total-images: 0;
|
||||
in property <string> error: "";
|
||||
/// Whether a scan is running, which is when neither of the two actions at
|
||||
/// the foot of this panel can be honoured: pinning walks a collection the
|
||||
/// scan is still writing, and changing library pulls the catalog out from
|
||||
/// under it.
|
||||
in property <bool> scanning: false;
|
||||
|
||||
// --- what to do with the collection this panel has scoped --------------
|
||||
//
|
||||
// TRACES: FR-NC-6a
|
||||
// These two used to be buttons in the library header, where they were the
|
||||
// only things in a row of library-wide actions that referred to *one
|
||||
// collection* — and where "Keep offline" could only ever mean the scoped
|
||||
// one while sitting nowhere near the tree that says which that is.
|
||||
//
|
||||
// Here they are under the tree, in the panel whose selection decides what
|
||||
// they act on, and in the same place and shape the trash already puts its
|
||||
// own two. The header keeps only what belongs to the whole library.
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Whether the scoped collection is being kept on this device.
|
||||
in property <bool> scope-pinned: false;
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Ask about the scoped collection's local copies — the same question the
|
||||
/// tray on a row and a held row open.
|
||||
callback toggle-pin-scope();
|
||||
/// Open a different library altogether.
|
||||
callback change-library();
|
||||
|
||||
callback select(int);
|
||||
callback toggle(int);
|
||||
@@ -567,6 +593,29 @@ export component CollectionsPanel inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// Keep the scoped collection on this device.
|
||||
//
|
||||
// Only while one is scoped: "pin the whole library" is a different and
|
||||
// much more expensive request, and a button that meant either
|
||||
// depending on invisible state would be a trap. Ids at or below zero
|
||||
// are "All photographs" and the trash, neither of which is a
|
||||
// collection to pin.
|
||||
//
|
||||
// No tick on the kept label: `active` already inverts the button,
|
||||
// which says the same thing without a symbol inside a string.
|
||||
//
|
||||
// It opens the same question the tray on a row and a held row open,
|
||||
// rather than pinning outright — see the note on the callback in
|
||||
// `library_ui::wire`. This is the version with a word on it, for the
|
||||
// collection the grid is actually showing.
|
||||
if root.selected-id > 0: Button {
|
||||
text: root.scope-pinned ? "Kept offline" : "Keep offline";
|
||||
active: root.scope-pinned;
|
||||
enabled: !root.scanning;
|
||||
clicked => { root.toggle-pin-scope(); }
|
||||
}
|
||||
|
||||
// --- trash --------------------------------------------------------
|
||||
//
|
||||
// TRACES: FR-CAT-15
|
||||
@@ -669,6 +718,22 @@ export component CollectionsPanel inherits Rectangle {
|
||||
font-size: Theme.text-sm;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
// The foot of the panel, under everything the current library holds,
|
||||
// because it is the one control here that replaces all of it.
|
||||
//
|
||||
// It was in the library header, wedged between Sync and Rescan — two
|
||||
// buttons that act on the library you already have. Reading "Change
|
||||
// library" as one of that group is a way to lose a scan by aiming
|
||||
// badly. Down here it is the last thing in the panel and the only
|
||||
// thing below the tree it would replace.
|
||||
Rectangle { height: 1px; background: Theme.rule; }
|
||||
|
||||
Button {
|
||||
text: "Change library";
|
||||
enabled: !root.scanning;
|
||||
clicked => { root.change-library(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Right edge, separating the panel from the grid.
|
||||
|
||||
+354
-310
@@ -829,18 +829,30 @@ 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.
|
||||
//
|
||||
// Nor does anything in here refer to *one collection*. "Keep offline" and
|
||||
// "Change library" are in the collections panel, under the tree whose
|
||||
// selection decides what the first of them acts on and which the second
|
||||
// replaces wholesale.
|
||||
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
|
||||
@@ -848,43 +860,20 @@ component HeaderActions inherits HorizontalLayout {
|
||||
in property <bool> select-mode: false;
|
||||
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();
|
||||
callback sync-now();
|
||||
/// 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();
|
||||
@@ -898,8 +887,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
|
||||
@@ -916,121 +906,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;
|
||||
clicked => { root.change-library(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// Keep this collection offline. Only offered when the grid is scoped to
|
||||
// one: "pin the whole library" is a different and much more expensive
|
||||
// request, and a button that meant either depending on invisible state
|
||||
// would be a trap.
|
||||
if root.scope-label != "": Button {
|
||||
// No tick on the kept label: `active` already inverts the button,
|
||||
// which says the same thing without a symbol inside a string.
|
||||
//
|
||||
// It opens the same question the sidebar's tray and a held row open,
|
||||
// rather than pinning outright — see the note on the callback in
|
||||
// `library_ui::wire`.
|
||||
text: root.scope-pinned ? "Kept offline" : "Keep offline";
|
||||
active: root.scope-pinned;
|
||||
enabled: !root.scanning;
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
clicked => { root.toggle-pin-scope(); }
|
||||
}
|
||||
|
||||
Button {
|
||||
// Shares the finished index so a second device inherits it rather
|
||||
// than repeating hours of range fetches.
|
||||
@@ -1078,10 +953,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;
|
||||
@@ -1182,6 +1056,42 @@ export component LibraryGrid inherits Rectangle {
|
||||
in property <bool> syncing: false;
|
||||
property <bool> sweeping: root.sweep-total > 0 && root.sweep-done < root.sweep-total;
|
||||
|
||||
// --- the header's one status line -------------------------------------
|
||||
//
|
||||
// Four readouts used to sit in the header beside the title: the image
|
||||
// count, the selection count, the scan's status and where in the library
|
||||
// the visible window sits. Each carried `horizontal-stretch: 1` — which is
|
||||
// what actually lets a `Text` shrink in Slint — so between them they took
|
||||
// about a third of a 768px header and left the buttons to scroll off the
|
||||
// end of it.
|
||||
//
|
||||
// The selection count is gone from here entirely: it belongs beside the
|
||||
// buttons that act on it, which is the bar at the foot of the grid.
|
||||
//
|
||||
// The other three are one subject — what this grid is showing and what is
|
||||
// being done to it — so they are one sentence with separators, and one
|
||||
// stretch between them. Two of them even said the same words while a sweep
|
||||
// ran: "indexing 300 / 12 480" appeared as the status *and* as the window
|
||||
// position.
|
||||
//
|
||||
// Assembled in two steps rather than one so the separators can be dropped
|
||||
// along with the parts they follow. A library with nothing scanned and
|
||||
// nothing loaded should read as empty, not as " · · ".
|
||||
property <string> status-phrase:
|
||||
root.scanning ? root.scan-status
|
||||
: (root.sweeping
|
||||
? "indexing " + root.sweep-done + " / " + root.sweep-total
|
||||
: root.scan-status);
|
||||
property <string> status-line:
|
||||
(root.total > 0 ? root.total + " images" : "")
|
||||
+ (root.status-phrase != ""
|
||||
? (root.total > 0 ? " · " : "") + root.status-phrase
|
||||
: "")
|
||||
+ (root.window-label != ""
|
||||
? ((root.total > 0 || root.status-phrase != "") ? " · " : "")
|
||||
+ root.window-label
|
||||
: "");
|
||||
|
||||
callback cell-clicked(int);
|
||||
/// A star was clicked on a cell: row, and the rating 0..5.
|
||||
callback cell-rated(int, int);
|
||||
@@ -1222,8 +1132,6 @@ export component LibraryGrid inherits Rectangle {
|
||||
callback toggle-collections();
|
||||
|
||||
callback rescan();
|
||||
/// Back to the launch screen, to change library or account.
|
||||
callback change-library();
|
||||
/// Open the settings page.
|
||||
/// TRACES: FR-CAT-10
|
||||
/// Whether this platform can import at all. Forwarded to the header, which
|
||||
@@ -1500,14 +1408,17 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
// --- pinning (FR-NC-6a) -----------------------------------------------
|
||||
//
|
||||
// Whether the collection the grid is scoped to is kept offline, and how
|
||||
// far the download has got. Progress is shown because pinning a trip is
|
||||
// gigabytes of transfer — a button that appeared to do nothing for twenty
|
||||
// minutes would read as broken.
|
||||
in property <bool> scope-pinned: false;
|
||||
// How far the download has got. Progress is shown because pinning a trip
|
||||
// is gigabytes of transfer — a button that appeared to do nothing for
|
||||
// twenty minutes would read as broken.
|
||||
//
|
||||
// Only the progress. Asking for the pin, and saying whether the scoped
|
||||
// collection already has one, belong to the collections panel — the tree
|
||||
// is what decides which collection is meant. This bar reports on the
|
||||
// transfer wherever it was started, including from a row this grid is not
|
||||
// scoped to, which is why it is not gated on the scope.
|
||||
in property <int> pin-done: 0;
|
||||
in property <int> pin-total: 0;
|
||||
callback toggle-pin-scope();
|
||||
/// Narrow to images whose RAW is stored locally — the ones openable now.
|
||||
in property <bool> local-only: false;
|
||||
in property <int> local-count: 0;
|
||||
@@ -1547,7 +1458,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();
|
||||
@@ -1861,105 +1771,61 @@ export component LibraryGrid inherits Rectangle {
|
||||
text: root.scope-label != "" ? root.scope-label
|
||||
: (root.root-label != "" ? root.root-label : "Library");
|
||||
overflow: elide;
|
||||
// A floor, for the same reason the status line beside it
|
||||
// has one — and it is named here because the two of them
|
||||
// compete. An eliding `Text` has a minimum of nothing, so
|
||||
// once the buttons had claimed their own minimums the
|
||||
// title was the cheapest thing in the row to give away and
|
||||
// it went to a bare "…": the window's own name, gone,
|
||||
// while the byte counts beside it stayed. Whatever else
|
||||
// this row drops, it may not stop saying what is being
|
||||
// looked at.
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
// `elide` + `horizontal-stretch: 1`, like `window-label` below:
|
||||
// stretch is what actually lets a `Text` shrink under pressure
|
||||
// in Slint, not `elide` alone (see the comment on the header
|
||||
// row's Flickable) — without it these three readouts kept
|
||||
// their full natural width and were what pushed the sidebar
|
||||
// toggle and the "More" button past the right edge on a
|
||||
// narrow window, reachable only by knowing to flick-scroll.
|
||||
// Everything the grid has to say about itself, in one line:
|
||||
// how many images, what is running over them, and where in the
|
||||
// library the visible window sits. See `status-line` for why
|
||||
// the four readouts that used to be here are one.
|
||||
//
|
||||
// `elide` + `horizontal-stretch: 1`: stretch is what actually
|
||||
// lets a `Text` shrink under pressure in Slint, not `elide`
|
||||
// alone (see the comment on the header row's Flickable) —
|
||||
// without it this readout keeps its full natural width and is
|
||||
// what pushes the sidebar toggle and the "More" button past
|
||||
// the right edge on a narrow window, reachable only by knowing
|
||||
// to flick-scroll.
|
||||
Caption {
|
||||
text: root.total > 0 ? root.total + " images" : "";
|
||||
text: root.status-line;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
// The selection count, and how to act on it. Both appear only
|
||||
// when something is selected — an empty selection has nothing
|
||||
// to say and the buttons would be permanently greyed chrome.
|
||||
// A live selection is state, not a label — it is the thing the
|
||||
// buttons beside it act on — so it keeps `active` rather than
|
||||
// dropping to ink with the counts around it.
|
||||
Value {
|
||||
text: root.selected-count > 0
|
||||
? root.selected-count + " selected" : "";
|
||||
modified: true;
|
||||
compact: true;
|
||||
font-weight: 600;
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
Label {
|
||||
// The scan's own status while it runs; once it is done,
|
||||
// thumbnail progress takes the line over — that is the
|
||||
// work the user is actually waiting on by then.
|
||||
// No per-window preview count: it counted an arbitrary
|
||||
// batch, so "48 / 120" described the window's size rather
|
||||
// than anything the user cares about. Whole-library
|
||||
// indexing is the number worth showing, and the bar below
|
||||
// already says that the window itself is still filling.
|
||||
text: root.scanning ? root.scan-status
|
||||
: (root.sweeping
|
||||
? "indexing " + root.sweep-done + " / " + root.sweep-total
|
||||
: root.scan-status);
|
||||
overflow: elide;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
|
||||
// Where in the library the visible window sits. A scrubbable
|
||||
// grid is disorienting without it.
|
||||
Caption {
|
||||
text: root.sweeping
|
||||
? "indexing " + root.sweep-done + " / " + root.sweep-total
|
||||
: root.window-label;
|
||||
horizontal-alignment: right;
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
// Deliberately without a `min-width`, unlike the title.
|
||||
//
|
||||
// This row cannot always hold everything: after the
|
||||
// sidebar takes its 232, a 1100pt window leaves the header
|
||||
// about 868, and the buttons alone want most of that
|
||||
// before a single character is drawn. Something has to
|
||||
// give, and the order matters — a floor here bought a
|
||||
// readable status by pushing `Settings` off the right
|
||||
// edge, reachable only by knowing to flick-scroll, which
|
||||
// is the exact fault this row's Flickable comment warns
|
||||
// about.
|
||||
//
|
||||
// So this is what gives. It is the most redundant thing in
|
||||
// the header — the sidebar states the library's count and
|
||||
// the filter chips state it again — and it grows back the
|
||||
// moment there is room.
|
||||
}
|
||||
|
||||
// Expanded: the actions sit in the header as one row. There
|
||||
// is room, and a disclosure would be a click in front of a
|
||||
// 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(); }
|
||||
rescan => { root.rescan(); }
|
||||
open-import => { root.open-import(); }
|
||||
@@ -1968,10 +1834,10 @@ export component LibraryGrid inherits Rectangle {
|
||||
open-gestures => { root.helping = true; }
|
||||
}
|
||||
|
||||
// Compact: one button in place of six. 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.
|
||||
// Compact: one button in place of the row. 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.
|
||||
if !root.expanded: Button {
|
||||
text: root.actions-open ? "Close" : "More";
|
||||
active: root.actions-open;
|
||||
@@ -1987,15 +1853,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%;
|
||||
@@ -2011,42 +1877,11 @@ 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(); }
|
||||
rescan => { root.rescan(); }
|
||||
open-import => { root.open-import(); }
|
||||
@@ -2885,8 +2720,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;
|
||||
@@ -3662,8 +3501,14 @@ export component LibraryGrid inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Points at the button below rather than at the sidebar's
|
||||
// `+`. On a tablet the sidebar is instantiated but not
|
||||
// drawn, so the old text sent the user to look for a
|
||||
// control that was not on the screen — and this sheet is
|
||||
// now the one place that can make a collection without
|
||||
// one.
|
||||
if root.collections.length == 0: Text {
|
||||
text: "No collections yet. Make one with + in the sidebar.";
|
||||
text: "No collections yet — make the first one below.";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
wrap: word-wrap;
|
||||
@@ -3673,9 +3518,37 @@ export component LibraryGrid inherits Rectangle {
|
||||
|
||||
Rectangle { height: 1px; background: Theme.rule; }
|
||||
|
||||
Button {
|
||||
text: "Cancel";
|
||||
clicked => { root.filing = false; }
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
Button {
|
||||
text: "Cancel";
|
||||
horizontal-stretch: 1;
|
||||
clicked => { root.filing = false; }
|
||||
}
|
||||
|
||||
// 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 a selection is usually meant.
|
||||
//
|
||||
// Here rather than on the bar because it is the answer to
|
||||
// a question this sheet has just asked and failed to
|
||||
// answer: the list above is every collection there is, and
|
||||
// this is what you press when none of them is the one.
|
||||
//
|
||||
// The naming sheet replaces this one rather than stacking
|
||||
// over it — two scrims deep is a place where dismissing
|
||||
// once looks like it did nothing.
|
||||
Button {
|
||||
text: "New collection…";
|
||||
primary: true;
|
||||
horizontal-stretch: 1;
|
||||
clicked => {
|
||||
root.filing = false;
|
||||
root.naming = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3869,6 +3742,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
|
||||
@@ -3879,7 +3761,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;
|
||||
@@ -3887,15 +3774,50 @@ 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;
|
||||
alignment: start;
|
||||
// No `alignment: start`, which this row used to carry.
|
||||
//
|
||||
// Slint only distributes space to `horizontal-stretch` children
|
||||
// under the default `stretch` alignment; under `start` every child
|
||||
// gets its preferred width and stretch is silently ignored. With
|
||||
// four controls that was invisible. With every selection verb on
|
||||
// the bar it is the difference between a count that gives way and
|
||||
// a row that can only scroll, so the count keeps its stretch and
|
||||
// this keeps the default.
|
||||
|
||||
// While a range is armed the strip stops reporting and starts
|
||||
// instructing. The count is still true, but it is not what the
|
||||
@@ -3908,6 +3830,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; }
|
||||
@@ -3923,13 +3851,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; }
|
||||
@@ -3944,7 +3883,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(); }
|
||||
@@ -3962,7 +3901,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(); }
|
||||
@@ -3972,26 +3911,131 @@ 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; }
|
||||
}
|
||||
|
||||
// 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
|
||||
// a selection is usually meant.
|
||||
// No longer "…from selection": the sheet it opens says
|
||||
// "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 {
|
||||
text: "New collection";
|
||||
primary: 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.
|
||||
//
|
||||
// The only filing button on the bar. "New collection" used to sit
|
||||
// beside it, which put the same decision — where do these go? — in
|
||||
// front of the user twice, before they had seen the list that
|
||||
// answers it. The sheet asks once and offers "New collection…"
|
||||
// underneath its list, which is where you look after failing to
|
||||
// find the one you wanted.
|
||||
if !root.ranging && root.selected-count > 0: Button {
|
||||
text: "Add to collection";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.naming = true; }
|
||||
clicked => {
|
||||
// Reset every time it opens: see `filing-moves`.
|
||||
root.filing-moves = false;
|
||||
root.filing = 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