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:
+113
-1
@@ -9,7 +9,8 @@ import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip } from "library.slint";
|
||||
import { GestureRow } from "gestures.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow, OfflinePrompt } from "collections.slint";
|
||||
import { CollectionsPanel, CollectionRow, OfflinePrompt, CollectionMenu,
|
||||
MembershipSheet, MembershipRow } from "collections.slint";
|
||||
import { HistogramPanel, HistogramView } from "histogram.slint";
|
||||
import { RecoveryPrompt } from "recovery.slint";
|
||||
import { PresetSheet, ScopeChips, ScopeKind } from "presets.slint";
|
||||
@@ -619,12 +620,55 @@ export component AppWindow inherits Window {
|
||||
callback collection-toggle(int);
|
||||
callback collection-new();
|
||||
callback collection-menu(int);
|
||||
|
||||
// --- the row menu the gesture above opens (FR-CAT-7) -------------------
|
||||
//
|
||||
// Rust owns every one of these. What the menu offers depends on counts it
|
||||
// has to query — whether the collection is nested, what deleting would
|
||||
// take — and the confirmation step is a policy decision, not a local bit
|
||||
// the sheet can flip for itself.
|
||||
/// The collection the menu is about. Empty closes it.
|
||||
in property <string> collection-menu-title;
|
||||
/// "12 photographs · 2 collections inside".
|
||||
in property <string> collection-menu-detail;
|
||||
/// Whether it has a parent, and so somewhere to be promoted out to.
|
||||
in property <bool> collection-menu-nested: false;
|
||||
/// Whether it is a saved filter, which cannot hold a nested collection.
|
||||
in property <bool> collection-menu-smart: false;
|
||||
/// Whether a delete has been asked once and awaits confirmation.
|
||||
in property <bool> collection-menu-confirming: false;
|
||||
/// What the delete would take with it, and what survives it.
|
||||
in property <string> collection-menu-confirm-detail;
|
||||
in property <string> collection-menu-delete-label;
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Whether the collection the menu is about is kept on this device.
|
||||
in property <bool> collection-menu-pinned: false;
|
||||
callback collection-menu-rename();
|
||||
callback collection-menu-new-child();
|
||||
/// Move it back to the top level. The inverse of the drag that nests one,
|
||||
/// which until now had no gesture at all.
|
||||
callback collection-menu-promote();
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Hand the offline question over to the handler the tray on the row
|
||||
/// already uses, rather than a second route to the same prompt.
|
||||
callback collection-menu-offline();
|
||||
callback collection-menu-delete();
|
||||
callback collection-menu-dismiss();
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Ask about a collection's local copies: the tray on a row, or a long
|
||||
/// press anywhere on it.
|
||||
callback collection-offline-menu(int);
|
||||
/// A row is held or released — the long press is timed in Rust.
|
||||
callback collection-row-press(int, bool);
|
||||
// --- picking a collection up (FR-UI-3, FR-UI-4) ------------------------
|
||||
/// Which collection the hold has picked up, by id. 0 is none.
|
||||
in property <int> collection-lifted: 0;
|
||||
/// A row's drag crossed the threshold, or ended.
|
||||
callback collection-drag-active(int, bool);
|
||||
/// Whether "All photographs" would take the drag currently in flight.
|
||||
in property <bool> collection-root-drop-allowed: false;
|
||||
/// A collection was dropped on "All photographs": out to the top level.
|
||||
callback collection-dropped-on-root();
|
||||
/// TRACES: FR-CAT-7 | FR-UI-4
|
||||
/// File the grid's selection in a collection without dragging it there:
|
||||
/// the target's id, and whether to take the images out of the collection
|
||||
@@ -717,6 +761,18 @@ export component AppWindow inherits Window {
|
||||
callback library-cell-pressed(int, bool, bool);
|
||||
callback library-remove-from-collection();
|
||||
|
||||
// --- where the selection is filed (FR-CAT-7) ---------------------------
|
||||
//
|
||||
// `library-remove-from-collection` can only ever mean the collection the
|
||||
// grid is scoped to — it is the only one that button can name. This sheet
|
||||
// is the other half: which collections the selection is actually in, and
|
||||
// the way out of any of them without navigating there first.
|
||||
in property <bool> membership-open: false;
|
||||
in property <[MembershipRow]> membership-rows;
|
||||
callback library-open-membership();
|
||||
callback membership-remove(int);
|
||||
callback membership-dismiss();
|
||||
|
||||
// The keyboard cursor: where the arrows are in the library, as an image
|
||||
// ordinal. Rust owns it — clamping it needs the library's length, and
|
||||
// moving it may have to swap the loaded window underneath.
|
||||
@@ -1298,6 +1354,19 @@ in property <bool> panel-visible: true;
|
||||
root.recovery-dismiss();
|
||||
return accept;
|
||||
}
|
||||
// The row menu and the membership sheet are drawn over the
|
||||
// offline prompt, so Back has to reach them first — and the
|
||||
// menu's own confirmation is a step Back must be able to take
|
||||
// one at a time, which is why the dismiss handlers in Rust
|
||||
// back out of a pending delete rather than closing outright.
|
||||
if (root.collection-menu-title != "") {
|
||||
root.collection-menu-dismiss();
|
||||
return accept;
|
||||
}
|
||||
if (root.membership-open) {
|
||||
root.membership-dismiss();
|
||||
return accept;
|
||||
}
|
||||
if (root.offline-prompt-title != "") {
|
||||
root.offline-prompt-dismiss();
|
||||
return accept;
|
||||
@@ -1610,6 +1679,10 @@ in property <bool> panel-visible: true;
|
||||
row-menu(id) => { root.collection-menu(id); }
|
||||
offline-menu(id) => { root.collection-offline-menu(id); }
|
||||
row-press(id, down) => { root.collection-row-press(id, down); }
|
||||
lifted-id: root.collection-lifted;
|
||||
drag-active(id, on) => { root.collection-drag-active(id, on); }
|
||||
root-drop-allowed: root.collection-root-drop-allowed;
|
||||
dropped-on-root() => { root.collection-dropped-on-root(); }
|
||||
|
||||
renaming-id: root.collection-renaming;
|
||||
rename-start(id) => { root.collection-rename-start(id); }
|
||||
@@ -1766,6 +1839,7 @@ in property <bool> panel-visible: true;
|
||||
remove-from-collection() => {
|
||||
root.library-remove-from-collection();
|
||||
}
|
||||
open-membership() => { root.library-open-membership(); }
|
||||
|
||||
filter-people: root.library-filter-people;
|
||||
filter-people-all: root.library-filter-people-all;
|
||||
@@ -3194,6 +3268,44 @@ in property <bool> panel-visible: true;
|
||||
dismiss() => { root.offline-prompt-dismiss(); }
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7 | FR-UI-4
|
||||
// The row menu and the membership sheet, above the offline prompt for
|
||||
// the same reason it sits above the shell: both are opened from a
|
||||
// 232px sidebar that disappears entirely in the compact layout, so
|
||||
// neither can be drawn inside the thing that raises it.
|
||||
//
|
||||
// The two never coexist — one is about a collection, the other about a
|
||||
// selection — but they are laid out in the order Back unwinds them,
|
||||
// which is the order they are checked in above.
|
||||
MembershipSheet {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
open: root.membership-open;
|
||||
selected-count: root.library-selected-count;
|
||||
rows: root.membership-rows;
|
||||
remove(id) => { root.membership-remove(id); }
|
||||
dismiss() => { root.membership-dismiss(); }
|
||||
}
|
||||
|
||||
CollectionMenu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
title: root.collection-menu-title;
|
||||
detail: root.collection-menu-detail;
|
||||
nested: root.collection-menu-nested;
|
||||
smart: root.collection-menu-smart;
|
||||
confirming: root.collection-menu-confirming;
|
||||
confirm-detail: root.collection-menu-confirm-detail;
|
||||
delete-label: root.collection-menu-delete-label;
|
||||
pinned: root.collection-menu-pinned;
|
||||
rename() => { root.collection-menu-rename(); }
|
||||
new-child() => { root.collection-menu-new-child(); }
|
||||
promote() => { root.collection-menu-promote(); }
|
||||
offline() => { root.collection-menu-offline(); }
|
||||
delete() => { root.collection-menu-delete(); }
|
||||
dismiss() => { root.collection-menu-dismiss(); }
|
||||
}
|
||||
|
||||
// Last, and therefore over everything including the settings page and
|
||||
// the offline prompt. Not a preference about layering: this is asked
|
||||
// before the grid exists, and nothing else in the window is about a
|
||||
|
||||
Reference in New Issue
Block a user