Let a collection be picked up, rearranged, and emptied after the fact
Collections could be made and filled and never reorganised. Nesting had a drag; un-nesting had nothing, in either direction — "All photographs" refused every drop, which is right for a photograph and wrong for a collection, which has a top level to be returned to. So a collection put inside another was in there permanently. Right-click deleted an *empty* collection outright and refused otherwise, which is wrong in both directions at once: destructive with no confirmation, and no way at all to delete a collection that held anything without emptying it by hand, child by child. And a photograph could only leave the collection the grid was scoped to, since that is the only one a button in the header can name — the cell's badge says a photograph is in three collections and never which three. Three ways in, one vocabulary: **Hold a row.** The tree is inside a Flickable, which claims any drag beginning inside it, so with a finger a drag on a row is a scroll until something says otherwise. The hold is that something. It lifts the row — drawn before anything moves, so the gesture says it has been understood — and then what the user does decides which of two things they meant: move, and it is a rearrangement; let go, and it is the row menu. The same fork the grid already uses to tell hold-to-select from drag-to-file. `decide_release` is that fork, and it is tested, because getting it wrong one way puts a sheet over every tidied tree and the other way makes the menu unreachable by touch. **The row menu.** Rename, new collection inside, move to top level, keep offline, delete. Deleting asks once when there is anything to lose and says what survives: the photographs stay in the library, and nested collections move up rather than going with it — which is what the catalog does, and what a user would never assume. An empty collection goes on the first press, because a dialogue about losing nothing is how people learn to dismiss dialogues. **"Collections…" on a selection.** Every collection the selection is filed in, each with a count — "3 of 40", so nobody takes forty photographs out of a collection thirty-seven were never in — and a way out of any of them without navigating there first. The long press used to open the offline question by itself. That question is one item in this menu now: there is one hold per row, and while it was spent on a single action nothing else the tree can do had a touch route at all. Nothing is lost — the tray on the row keeps its tap, and the question gains a full-width control in place of a 30px icon in a row shorter than the touch minimum. The row-press handler moves to `collections_ui` with the rest of what a collection row does; it lived in `library_ui` only because it opened that prompt.
This commit is contained in:
+447
-17
@@ -72,6 +72,13 @@ component TreeRow inherits Rectangle {
|
||||
/// Whether this row is being renamed, in which case its name is drawn as an
|
||||
/// editable field rather than as text.
|
||||
in property <bool> renaming: false;
|
||||
/// TRACES: FR-UI-3 | FR-UI-4
|
||||
/// Picked up by a hold, and now the thing a movement would carry.
|
||||
///
|
||||
/// Drawn before any movement happens, which is the whole point: the hold
|
||||
/// has committed the next drag to *rearranging the tree* rather than
|
||||
/// scrolling it, and nothing else on screen would say so.
|
||||
in property <bool> lifted: false;
|
||||
|
||||
callback clicked();
|
||||
callback toggle();
|
||||
@@ -105,15 +112,20 @@ component TreeRow inherits Rectangle {
|
||||
// row never lights up as though it would take the images.
|
||||
background: drop.has-drag
|
||||
? Theme.selected
|
||||
: (root.selected ? Theme.surface-raised
|
||||
: ((root.lifted || root.selected) ? Theme.surface-raised
|
||||
: (touch.has-hover ? Theme.hover : transparent));
|
||||
border-radius: Theme.radius-sm;
|
||||
|
||||
// A drop target is outlined as well as filled: on a dark ground a fill
|
||||
// change alone is easy to miss mid-drag, when the user is watching the
|
||||
// thumbnail under the cursor rather than the row.
|
||||
border-width: drop.has-drag ? 1px : 0px;
|
||||
border-color: Theme.selected-ring;
|
||||
//
|
||||
// A *lifted* row is outlined in the active colour instead, and the
|
||||
// distinction matters: one ring says "let go here", the other says "this
|
||||
// is what you are holding". Drawn the moment the hold fires, so the user
|
||||
// knows the drag is armed before they move rather than after.
|
||||
border-width: (drop.has-drag || root.lifted) ? 1px : 0px;
|
||||
border-color: root.lifted ? Theme.active : Theme.selected-ring;
|
||||
|
||||
// Behind the content, so the row's own TouchArea still gets ordinary
|
||||
// clicks. A DropArea only takes part in a drag; it does not block presses.
|
||||
@@ -135,9 +147,9 @@ component TreeRow inherits Rectangle {
|
||||
// source's allowed set, treating anything outside it as `none`.
|
||||
//
|
||||
// So a hard-coded `copy` accepted images and *silently refused
|
||||
// every collection*: nesting by drag could not work at all, and it
|
||||
// failed with nothing to see, because an action clamped to `none`
|
||||
// is indistinguishable from a target that simply said no.
|
||||
// every collection*: nesting by drag could not work, and it failed
|
||||
// with no error anywhere, because a clamped-to-none action is
|
||||
// indistinguishable from a target that simply said no.
|
||||
return ev.proposed-action;
|
||||
}
|
||||
|
||||
@@ -320,6 +332,47 @@ component TreeRow inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// GESTURE: Pick a collection up to rearrange the tree
|
||||
// where: Collections sidebar
|
||||
// touch: Press and hold it until it lifts, then drag it
|
||||
// pointer: Drag it, or hold it until it lifts and then drag
|
||||
// why: The tree is inside a Flickable, which claims any drag
|
||||
// beginning inside it — so with a finger a drag on a row is a
|
||||
// scroll until something says otherwise. The hold is that
|
||||
// something, and it is what every mobile list already uses to
|
||||
// pick a row up. The row lifts the moment it fires, so the
|
||||
// gesture says it has been understood before anything moves.
|
||||
//
|
||||
// GESTURE: Act on a collection — rename, nest, un-nest, delete
|
||||
// where: Collections sidebar
|
||||
// touch: Press and hold the collection, then let go without moving
|
||||
// pointer: Right-click it
|
||||
// why: The hold arms a drag and opens this menu, and which one you
|
||||
// get is decided by whether you moved — the same fork the grid
|
||||
// uses. One menu for everything done to a row, because there is
|
||||
// one hold per row: while the hold opened the offline question
|
||||
// by itself, nothing else the tree can do had a touch route.
|
||||
//
|
||||
// GESTURE: Take a collection back out of the one it is nested in
|
||||
// where: Collections sidebar
|
||||
// touch: Hold it, then drag it onto "All photographs" — or let go and
|
||||
// choose "Move to top level"
|
||||
// pointer: Drag it onto "All photographs", or right-click it and choose
|
||||
// "Move to top level"
|
||||
// why: Nesting is a drag of one row onto another, and its inverse
|
||||
// had no gesture at all: "All photographs" refused every drop,
|
||||
// which is right for a photograph — it is already in the
|
||||
// library — and wrong for a collection, which has a top level
|
||||
// to be returned to. Without it a collection dragged into
|
||||
// another was in there permanently.
|
||||
//
|
||||
// GESTURE: Rename a collection
|
||||
// where: Collections sidebar
|
||||
// touch: Hold the collection, then "Rename"
|
||||
// pointer: Double-click its name, or right-click it and choose "Rename"
|
||||
// why: Double-click is what a file manager and a Lightroom panel use
|
||||
// for the same thing, so it needs no discovering — but nothing
|
||||
// on screen says so, which is what the menu item is for.
|
||||
touch := TouchArea {
|
||||
// Disabled while the field is up, so a click landing on the row rather
|
||||
// than inside the input does not re-select the collection out from
|
||||
@@ -400,6 +453,29 @@ export component CollectionsPanel inherits Rectangle {
|
||||
/// A drag is dwelling over a collection, or has left it. Drives the
|
||||
/// spring-loaded expansion, which is timed in Rust.
|
||||
callback drag-over(int, bool);
|
||||
|
||||
// --- picking a collection up (FR-UI-3, FR-UI-4) ------------------------
|
||||
//
|
||||
// A press and hold arms the drag and lifts the row; moving then rearranges
|
||||
// the tree, and letting go without moving opens the row menu instead.
|
||||
//
|
||||
// The hold is what makes this reliable with a finger. This tree is inside
|
||||
// a Flickable, and a Flickable claims any drag beginning inside it — so a
|
||||
// touch drag on a row is a scroll until something says otherwise. The hold
|
||||
// is that something, and it is the gesture every mobile list already uses
|
||||
// to pick a row up, so it needs no discovering.
|
||||
/// Which collection is picked up, by id. 0 is none.
|
||||
in property <int> lifted-id: 0;
|
||||
/// A row's drag crossed the threshold, or ended. What tells Rust the hold
|
||||
/// became a rearrangement rather than a menu.
|
||||
callback drag-active(int, bool);
|
||||
/// Whether a drop on "All photographs" would do anything — true only while
|
||||
/// a *nested* collection is in flight. Rust decides, because only it knows
|
||||
/// what the drag is carrying and whether that thing has a parent.
|
||||
in property <bool> root-drop-allowed: false;
|
||||
/// A collection was dropped on "All photographs": take it out to the top
|
||||
/// level. The inverse of dropping one row onto another.
|
||||
callback dropped-on-root();
|
||||
callback new-collection();
|
||||
// Right-click on a row: rename, delete, new child.
|
||||
callback row-menu(int);
|
||||
@@ -500,9 +576,43 @@ export component CollectionsPanel inherits Rectangle {
|
||||
// everything" a thing you have to find.
|
||||
Rectangle {
|
||||
height: Theme.row-height;
|
||||
background: root.selected-id == 0 ? Theme.surface-raised
|
||||
: (all-touch.has-hover ? Theme.hover : transparent);
|
||||
background: all-drop.has-drag
|
||||
? Theme.selected
|
||||
: (root.selected-id == 0 ? Theme.surface-raised
|
||||
: (all-touch.has-hover ? Theme.hover : transparent));
|
||||
border-radius: Theme.radius-sm;
|
||||
border-width: all-drop.has-drag ? 1px : 0px;
|
||||
border-color: Theme.selected-ring;
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
// Dropping a *collection* here takes it out to the top level —
|
||||
// the inverse of dropping one row onto another, which nests it.
|
||||
//
|
||||
// Photographs still cannot be dropped here, and the refusal is the
|
||||
// same one this row has always made: an image is already in the
|
||||
// library, so there is nothing for the drop to do. `can-drop`
|
||||
// separates the two, so the cursor says which of them this is
|
||||
// before the release rather than after.
|
||||
all-drop := DropArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
can-drop(ev) => {
|
||||
if (!root.root-drop-allowed) {
|
||||
return DragAction.none;
|
||||
}
|
||||
// Echoed rather than named, as everywhere else here: only
|
||||
// a collection can reach this branch, and its DragArea
|
||||
// allows `move` — but saying so twice is how the two drift
|
||||
// apart.
|
||||
return ev.proposed-action;
|
||||
}
|
||||
|
||||
dropped(ev) => {
|
||||
root.dropped-on-root();
|
||||
return ev.proposed-action;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: Theme.gap-sm;
|
||||
@@ -526,9 +636,6 @@ export component CollectionsPanel inherits Rectangle {
|
||||
}
|
||||
all-touch := TouchArea {
|
||||
clicked => { root.select(0); }
|
||||
// Dropping onto "All photographs" would mean nothing — an
|
||||
// image is already in the library — so this reports no hover
|
||||
// during a drag and stays inert.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,11 +664,17 @@ export component CollectionsPanel inherits Rectangle {
|
||||
allow-move: true;
|
||||
data: root.collection-drag-payload(row.id);
|
||||
|
||||
// The hold armed this drag and lifted the row; this is where
|
||||
// Rust learns the movement actually began, so the release that
|
||||
// ends it opens no menu.
|
||||
changed dragging => { root.drag-active(row.id, self.dragging); }
|
||||
|
||||
TreeRow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
entry: row;
|
||||
selected: row.id == root.selected-id;
|
||||
lifted: row.id == root.lifted-id;
|
||||
// A saved filter's membership is computed, so a drop cannot
|
||||
// land there. Refused in `can-drop`, so the cursor says no
|
||||
// before the release rather than after.
|
||||
@@ -653,15 +766,15 @@ export component CollectionsPanel inherits Rectangle {
|
||||
trash-drop := DropArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// Echoed from the source, like the row above: the grid's
|
||||
// cells allow `copy` alone, so the `move` this used to insist
|
||||
// on was clamped to `none` and the trash refused every
|
||||
// photograph dragged onto it.
|
||||
// Echoed from the source, like every other target here: the
|
||||
// grid's cells allow `copy` alone, so the `move` this used to
|
||||
// return was clamped to `none` and the trash refused every
|
||||
// photograph dragged to it.
|
||||
//
|
||||
// What this row does is destructive whatever the negotiated
|
||||
// action is called — `dropped-on-trash` moves the file — so
|
||||
// the name was never what made it so, and naming it cost the
|
||||
// feature.
|
||||
// the name of the action was never what made it so, and
|
||||
// insisting on `move` only cost the feature.
|
||||
can-drop(ev) => { return ev.proposed-action; }
|
||||
dropped(ev) => {
|
||||
root.dropped-on-trash();
|
||||
@@ -878,3 +991,320 @@ export component OfflinePrompt inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The actions on one collection row: rename, nest a new one inside, take it
|
||||
// back out to the top level, delete it.
|
||||
//
|
||||
// This replaced a right-click that *deleted an empty collection outright* and
|
||||
// declined with an error message otherwise. Two things were wrong with that,
|
||||
// and they pull in opposite directions: the safe half was undiscoverable — no
|
||||
// affordance says a row can be deleted, and the one gesture that could was
|
||||
// destructive with no confirmation — while the unsafe half was unreachable, so
|
||||
// a collection that held anything could not be deleted at all without first
|
||||
// emptying it by hand, child by child.
|
||||
//
|
||||
// Drawn over the whole window rather than as a popup anchored to the row, for
|
||||
// the reason `OfflinePrompt` above gives: the sidebar is 232px wide and closes
|
||||
// entirely in the compact layout, so a menu living inside it would be
|
||||
// unreachable on the device the long-press exists for.
|
||||
export component CollectionMenu inherits Rectangle {
|
||||
/// The collection being acted on. Empty closes the menu — one source for
|
||||
/// "is this open", rather than a bool that can disagree with the name
|
||||
/// beside it.
|
||||
in property <string> title;
|
||||
/// What is in there — "12 photographs · 2 collections inside". Composed in
|
||||
/// Rust, which has the counts; this file does no arithmetic on them.
|
||||
in property <string> detail;
|
||||
/// Whether this collection has a parent, and so has somewhere to be moved
|
||||
/// out to. Hidden rather than disabled at the top level: "move to top
|
||||
/// level" on something already there is not a refusal worth explaining.
|
||||
in property <bool> nested: false;
|
||||
/// A saved filter, whose membership is its selector. It can still be
|
||||
/// renamed, moved and deleted; what it cannot do is take images, so
|
||||
/// nesting a new collection inside one would build a container whose
|
||||
/// contents can never reach it.
|
||||
in property <bool> smart: false;
|
||||
/// Whether the delete has been asked once and is waiting to be confirmed.
|
||||
/// Owned by Rust, which decides from the counts whether a confirmation is
|
||||
/// needed at all — an empty collection is deleted on the first press.
|
||||
in property <bool> confirming: false;
|
||||
/// What deleting would actually do, spelled out. The photographs survive,
|
||||
/// and a user who reads "Delete" as "delete my pictures" will not use this
|
||||
/// feature again.
|
||||
in property <string> confirm-detail;
|
||||
/// The destructive label, with what it takes with it.
|
||||
in property <string> delete-label: "Delete";
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Whether this collection is being kept on the device, for the label on
|
||||
/// the offline item.
|
||||
in property <bool> pinned: false;
|
||||
|
||||
callback rename();
|
||||
callback new-child();
|
||||
/// Reparent to the top level — the un-nesting that had no gesture at all.
|
||||
/// Nesting has one (drag a row onto another); its inverse had none, so a
|
||||
/// collection put inside another was there permanently.
|
||||
callback promote();
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Open the offline question about this collection — the same one the tray
|
||||
/// on the row opens.
|
||||
callback offline();
|
||||
callback delete();
|
||||
callback dismiss();
|
||||
|
||||
visible: root.title != "";
|
||||
background: #000000CC;
|
||||
|
||||
// Swallows everything that misses the card, and dismisses. Declared before
|
||||
// the card so the card's own controls sit above it.
|
||||
TouchArea {
|
||||
clicked => { root.dismiss(); }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: min(420px, parent.width - 2 * Theme.gap-lg);
|
||||
height: card.preferred-height;
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
background: Theme.surface;
|
||||
border-radius: Theme.radius;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
// A press on the card must not reach the scrim behind it and close the
|
||||
// thing being read.
|
||||
TouchArea { }
|
||||
|
||||
card := VerticalLayout {
|
||||
padding: Theme.gap-lg;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Text {
|
||||
text: "COLLECTION";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.title;
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text-lg;
|
||||
font-weight: 600;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.detail;
|
||||
color: Theme.ink-dim;
|
||||
font-size: Theme.text;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
Rectangle { height: 1px; background: Theme.rule; }
|
||||
|
||||
// The three non-destructive actions, hidden while a delete is
|
||||
// waiting to be confirmed: a confirmation that leaves the other
|
||||
// choices live is one mis-aimed press away from doing something
|
||||
// else entirely, and the user is at that moment reading rather
|
||||
// than aiming.
|
||||
if !root.confirming: Button {
|
||||
text: "Rename";
|
||||
clicked => { root.rename(); }
|
||||
}
|
||||
|
||||
if !root.confirming && !root.smart: Button {
|
||||
text: "New collection inside";
|
||||
clicked => { root.new-child(); }
|
||||
}
|
||||
|
||||
if !root.confirming && root.nested: Button {
|
||||
text: "Move to top level";
|
||||
clicked => { root.promote(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-NC-6a | FR-UI-3
|
||||
// The offline question, which the long press used to open on its
|
||||
// own. It is here now because the press had to be given to this
|
||||
// menu — there is one hold per row and the tree's other actions
|
||||
// had no touch route at all without it.
|
||||
//
|
||||
// Nothing is lost by the move and something is gained: the tray on
|
||||
// the row keeps its direct tap for the fast way in, and this is a
|
||||
// full-width control rather than a 30px icon in a row shorter than
|
||||
// the touch minimum, so the question now has a target that meets
|
||||
// FR-UI-3 as well as a gesture that finds it.
|
||||
if !root.confirming: Button {
|
||||
text: root.pinned ? "Kept offline" : "Keep offline";
|
||||
active: root.pinned;
|
||||
clicked => { root.offline(); }
|
||||
}
|
||||
|
||||
if !root.confirming: Rectangle { height: 1px; background: Theme.rule; }
|
||||
|
||||
// What the delete takes with it, in the words of the thing it
|
||||
// takes. Only while confirming: shown always, it would be a
|
||||
// warning about an action nobody has asked for yet, which is how a
|
||||
// dialogue teaches people to stop reading it.
|
||||
if root.confirming: Text {
|
||||
text: root.confirm-detail;
|
||||
color: Theme.warn-ink;
|
||||
font-size: Theme.text;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
Button {
|
||||
text: root.delete-label;
|
||||
clicked => { root.delete(); }
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Cancel";
|
||||
clicked => { root.dismiss(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One collection the current selection is filed in.
|
||||
export struct MembershipRow {
|
||||
id: int,
|
||||
name: string,
|
||||
// How many of the selected photographs are in here, and how many were
|
||||
// selected. Both, because "3" alone is ambiguous and "3 of 40" is not.
|
||||
holding: int,
|
||||
// Already formatted — "3 of 40", or just the count when one photograph is
|
||||
// selected and the fraction would be noise.
|
||||
detail: string,
|
||||
}
|
||||
|
||||
// Where the selection is filed, and the way out of any of it.
|
||||
//
|
||||
// The library's "Remove from collection" button takes the selection out of the
|
||||
// collection the *grid is scoped to*, which is the only one it can name. That
|
||||
// leaves no way to take a photograph out of a collection without first
|
||||
// navigating to that collection — and no way at all to find out which
|
||||
// collections a photograph is in, since the grid badges a cell with a count
|
||||
// and nothing else.
|
||||
export component MembershipSheet inherits Rectangle {
|
||||
/// Whether the sheet is up. A bool rather than a non-empty string as the
|
||||
/// menu above uses, because this sheet is legitimately empty: a selection
|
||||
/// filed nowhere is a real state, and it is worth saying so.
|
||||
in property <bool> open: false;
|
||||
/// How many photographs the answers are about.
|
||||
in property <int> selected-count: 0;
|
||||
/// The collections holding at least one of them, by name.
|
||||
in property <[MembershipRow]> rows;
|
||||
|
||||
/// Take the selection out of this collection, by id.
|
||||
callback remove(int);
|
||||
callback dismiss();
|
||||
|
||||
visible: root.open;
|
||||
background: #000000CC;
|
||||
|
||||
TouchArea {
|
||||
clicked => { root.dismiss(); }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: min(420px, parent.width - 2 * Theme.gap-lg);
|
||||
height: min(card.preferred-height, parent.height - 2 * Theme.gap-lg);
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
background: Theme.surface;
|
||||
border-radius: Theme.radius;
|
||||
border-width: 1px;
|
||||
border-color: Theme.rule;
|
||||
|
||||
TouchArea { }
|
||||
|
||||
card := VerticalLayout {
|
||||
padding: Theme.gap-lg;
|
||||
spacing: Theme.gap;
|
||||
|
||||
Text {
|
||||
text: "FILED IN";
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.selected-count == 1 ? "1 photograph"
|
||||
: root.selected-count + " photographs";
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text-lg;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// The empty case says what it means. A blank list under a heading
|
||||
// reads as a sheet that failed to load.
|
||||
if root.rows.length == 0: Text {
|
||||
text: "Not in any collection. Drag onto a collection in the sidebar to file it.";
|
||||
color: Theme.ink-dim;
|
||||
font-size: Theme.text;
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
if root.rows.length > 0: Rectangle { height: 1px; background: Theme.rule; }
|
||||
|
||||
if root.rows.length > 0: Flickable {
|
||||
// A floor so one row is not squeezed out of existence, and a
|
||||
// ceiling so a photograph filed in thirty collections does not
|
||||
// push the Close button off the bottom of the window. The card
|
||||
// is capped in height above; this is what keeps the control
|
||||
// below the list inside that cap.
|
||||
min-height: min(root.rows.length * (Theme.touch-target + 2px), 100px);
|
||||
max-height: 260px;
|
||||
viewport-height: root.rows.length * (Theme.touch-target + 2px);
|
||||
|
||||
for entry[i] in root.rows: HorizontalLayout {
|
||||
y: i * (Theme.touch-target + 2px);
|
||||
width: parent.width;
|
||||
height: Theme.touch-target;
|
||||
spacing: Theme.gap-sm;
|
||||
|
||||
VerticalLayout {
|
||||
horizontal-stretch: 1;
|
||||
alignment: center;
|
||||
|
||||
Text {
|
||||
text: entry.name;
|
||||
color: Theme.ink;
|
||||
font-size: Theme.text;
|
||||
overflow: elide;
|
||||
}
|
||||
// Absent, not blank, with one photograph selected:
|
||||
// "1 of 1" is arithmetic nobody asked for, and an
|
||||
// empty line still takes its height.
|
||||
if entry.detail != "": Text {
|
||||
text: entry.detail;
|
||||
color: Theme.ink-faint;
|
||||
font-size: Theme.text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// "Remove", not "Delete": this drops a membership row and
|
||||
// the photographs stay in the library and in every other
|
||||
// collection. The two words are one keystroke apart in the
|
||||
// source and a catastrophe apart to the user.
|
||||
Button {
|
||||
text: "Remove";
|
||||
y: (parent.height - self.height) / 2;
|
||||
clicked => { root.remove(entry.id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 1px; background: Theme.rule; }
|
||||
|
||||
Button {
|
||||
text: "Close";
|
||||
clicked => { root.dismiss(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user