Move the library grid's cells and selection onto their own Slint global
AppWindow carried the grid's loaded window of cells, the keyboard cursor, drag and drop, the held-row long-press state, columns and cell size, the scroll and viewport bookkeeping, the photo roll's pick and centre-request, and the local-only/reorder/collection-filing gestures that act on a selection, as properties and callbacks on the root component. That state now lives in the `Library` global declared in library.slint, next to the structs (LibraryCell, TimelineBar, KeywordRow, PersonChip) it and the grid's other components already share; Rust reaches it through window.global::<Library>() instead of window.set_/get_/on_/invoke_ on the root, the same change collections.slint's `Collections` global made for the sidebar. library-visible stays on AppWindow: it is computed from active-page and active-view, the shell's own routing state, which a global cannot read. Everything else still prefixed library- — the timeline, the filter bar, ratings and flags, keywording, and the routes and status lines — stays on the window for now and moves in the commits that follow.
This commit is contained in:
+64
-64
File diff suppressed because one or more lines are too long
+26
-26
@@ -25,7 +25,7 @@ Sampling a neutral is the first move of the tonal pass — every colour judgemen
|
|||||||
|
|
||||||
Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as magnifying the picture rather than sliding it about. Double-tap is the way to an exact 1:1; this is the way to everything in between.
|
Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as magnifying the picture rather than sliding it about. Double-tap is the way to an exact 1:1; this is the way to everything in between.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2026`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:1919`</sub>
|
||||||
|
|
||||||
### Move a magnified photograph about
|
### Move a magnified photograph about
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ Anchored on the fingers' midpoint, and on the pointer, so the gesture reads as m
|
|||||||
|
|
||||||
Only once there is something outside the viewport to reach, which is why the cursor becomes a hand exactly then. The view is clamped to the frame: panning past the edge would show undefined area beside the photograph, and that reads as a rendering fault rather than as the end of the picture.
|
Only once there is something outside the viewport to reach, which is why the cursor becomes a hand exactly then. The view is clamped to the frame: panning past the edge would show undefined area beside the photograph, and that reads as a rendering fault rather than as the end of the picture.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2117`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2010`</sub>
|
||||||
|
|
||||||
### Paint a mask by hand
|
### Paint a mask by hand
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ Only once there is something outside the viewport to reach, which is why the cur
|
|||||||
|
|
||||||
A model's mask stops inside a shoulder and leaks into the hair, and no single edge control fixes two errors that go opposite ways. The whole stroke is one step in the history, so taking a mark back costs one press however long it took to make.
|
A model's mask stops inside a shoulder and leaks into the hair, and no single edge control fixes two errors that go opposite ways. The whole stroke is one step in the history, so taking a mark back costs one press however long it took to make.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2204`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2097`</sub>
|
||||||
|
|
||||||
### Take back the last change
|
### Take back the last change
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ A model's mask stops inside a shoulder and leaks into the hair, and no single ed
|
|||||||
|
|
||||||
A whole drag is one step, so undo takes back a decision rather than a frame of a gesture. The list is there because arriving six steps back costs what arriving from one does.
|
A whole drag is one step, so undo takes back a decision rather than a frame of a gesture. The list is there because arriving six steps back costs what arriving from one does.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2425`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2318`</sub>
|
||||||
|
|
||||||
### Do it again after taking it back
|
### Do it again after taking it back
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ A whole drag is one step, so undo takes back a decision rather than a frame of a
|
|||||||
- **Pointer** — Click it, or press Redo in the History header
|
- **Pointer** — Click it, or press Redo in the History header
|
||||||
- **Keyboard** — Ctrl+Shift+Z
|
- **Keyboard** — Ctrl+Shift+Z
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2438`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2331`</sub>
|
||||||
|
|
||||||
### Copy the settings from this photograph
|
### Copy the settings from this photograph
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ A whole drag is one step, so undo takes back a decision rather than a frame of a
|
|||||||
|
|
||||||
The panel is the copy that has to work: a tablet has no modifier key to hold and no menu bar to hang the action from. The shortcut is an accelerator for a control that is on screen either way.
|
The panel is the copy that has to work: a tablet has no modifier key to hold and no menu bar to hang the action from. The shortcut is an accelerator for a control that is on screen either way.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2471`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2364`</sub>
|
||||||
|
|
||||||
### Paste the settings onto this photograph
|
### Paste the settings onto this photograph
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ The panel is the copy that has to work: a tablet has no modifier key to hold and
|
|||||||
|
|
||||||
The button names what would be pasted — "3 adjustments", and whether the crop is coming with it — which the shortcut cannot say. Both paste the same scope.
|
The button names what would be pasted — "3 adjustments", and whether the crop is coming with it — which the shortcut cannot say. Both paste the same scope.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2483`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2376`</sub>
|
||||||
|
|
||||||
### Change which group of adjustments is on screen
|
### Change which group of adjustments is on screen
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ The button names what would be pasted — "3 adjustments", and whether the crop
|
|||||||
|
|
||||||
The groups are whatever the operation set declares itself to be about, so there are as many as the pipeline has and no key can be assigned to one of them by name. Stepping is the binding that survives a node being added.
|
The groups are whatever the operation set declares itself to be about, so there are as many as the pipeline has and no key can be assigned to one of them by name. Stepping is the binding that survives a node being added.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2511`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2404`</sub>
|
||||||
|
|
||||||
### Look at the photograph at 1:1
|
### Look at the photograph at 1:1
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ The groups are whatever the operation set declares itself to be about, so there
|
|||||||
|
|
||||||
Noise reduction and capture sharpening are judgements about single pixels, and a fitted view averages several of the file's into each one on screen — so the frame looks softer than it is and the correction goes too far. The point and the magnification survive opening the next photograph, which is what makes checking the same eye across forty portraits forty keystrokes rather than forty pans.
|
Noise reduction and capture sharpening are judgements about single pixels, and a fitted view averages several of the file's into each one on screen — so the frame looks softer than it is and the correction goes too far. The point and the magnification survive opening the next photograph, which is what makes checking the same eye across forty portraits forty keystrokes rather than forty pans.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2546`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2439`</sub>
|
||||||
|
|
||||||
### Move to the next or previous photograph
|
### Move to the next or previous photograph
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ Noise reduction and capture sharpening are judgements about single pixels, and a
|
|||||||
|
|
||||||
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing.
|
The edit on screen is saved on the way out, so stepping through a folder is as much a departure as going back to the grid and loses nothing.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2598`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2491`</sub>
|
||||||
|
|
||||||
### See the photograph before you edited it
|
### See the photograph before you edited it
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ The edit on screen is saved on the way out, so stepping through a folder is as m
|
|||||||
|
|
||||||
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
|
Held rather than toggled, and no split screen: a split halves the working image on the tablet the column was sized for, and the comparison photographers describe making is a flick back and forth. It takes no history step, so checking whether a frame is overcooked costs nothing to undo afterwards.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/app.slint:2722`</sub>
|
<sub>`ui/dr-ui/ui/app.slint:2615`</sub>
|
||||||
|
|
||||||
### Put one control back to its default
|
### Put one control back to its default
|
||||||
|
|
||||||
@@ -264,7 +264,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.
|
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:1302`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1425`</sub>
|
||||||
|
|
||||||
### Add or remove one photograph
|
### Add or remove one photograph
|
||||||
|
|
||||||
@@ -273,7 +273,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.
|
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:1311`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1434`</sub>
|
||||||
|
|
||||||
### Leave selecting
|
### Leave selecting
|
||||||
|
|
||||||
@@ -281,7 +281,7 @@ While selecting, a tap never opens. That is the whole point of the mode: one mea
|
|||||||
- **Pointer** — Press Done in the header
|
- **Pointer** — Press Done in the header
|
||||||
- **Keyboard** — Escape
|
- **Keyboard** — Escape
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:1319`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1442`</sub>
|
||||||
|
|
||||||
### Pick a photograph up to drag it
|
### Pick a photograph up to drag it
|
||||||
|
|
||||||
@@ -290,7 +290,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.
|
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:1349`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1472`</sub>
|
||||||
|
|
||||||
### Select a range
|
### Select a range
|
||||||
|
|
||||||
@@ -299,7 +299,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.
|
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:1414`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:1537`</sub>
|
||||||
|
|
||||||
### Take the blinks out of a burst
|
### Take the blinks out of a burst
|
||||||
|
|
||||||
@@ -308,7 +308,7 @@ This replaced a double tap, which had no visible state and could take forty phot
|
|||||||
|
|
||||||
Face indexing reads each face's eyes. The chip drops frames where the chosen people are caught blinking, and leaves sunglasses and eyes it could not read alone.
|
Face indexing reads each face's eyes. The chip drops frames where the chosen people are caught blinking, and leaves sunglasses and eyes it could not read alone.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:2106`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:2229`</sub>
|
||||||
|
|
||||||
### Find photographs with two people in them
|
### Find photographs with two people in them
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ Face indexing reads each face's eyes. The chip drops frames where the chosen peo
|
|||||||
|
|
||||||
"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.
|
"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:2135`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:2258`</sub>
|
||||||
|
|
||||||
### Resize the thumbnails
|
### Resize the thumbnails
|
||||||
|
|
||||||
@@ -326,7 +326,7 @@ Face indexing reads each face's eyes. The chip drops frames where the chosen peo
|
|||||||
|
|
||||||
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.
|
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:2795`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:2918`</sub>
|
||||||
|
|
||||||
### File photographs in a collection
|
### File photographs in a collection
|
||||||
|
|
||||||
@@ -335,7 +335,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.
|
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:2992`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3115`</sub>
|
||||||
|
|
||||||
### Open a photograph
|
### Open a photograph
|
||||||
|
|
||||||
@@ -344,7 +344,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.
|
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:3260`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3383`</sub>
|
||||||
|
|
||||||
### Rate a photograph without opening it
|
### Rate a photograph without opening it
|
||||||
|
|
||||||
@@ -354,7 +354,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.
|
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:3380`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3503`</sub>
|
||||||
|
|
||||||
### Choose the frame a folded burst shows
|
### Choose the frame a folded burst shows
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ A star has to take the press without it also reaching the cell, or every rating
|
|||||||
|
|
||||||
A folded burst draws its earliest frame, which is a fact about the clock and not a judgement about the photograph — nothing in this application ranks a frame (FR-CULL-5). But the point of a burst is that one of the twelve is better than the other eleven, and the photographer is the only one who knows which. So the choice is offered on the frames themselves, while they are open and side by side, which is the one moment the alternatives are on screen to be compared.
|
A folded burst draws its earliest frame, which is a fact about the clock and not a judgement about the photograph — nothing in this application ranks a frame (FR-CULL-5). But the point of a burst is that one of the twelve is better than the other eleven, and the photographer is the only one who knows which. So the choice is offered on the frames themselves, while they are open and side by side, which is the one moment the alternatives are on screen to be compared.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:3511`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:3634`</sub>
|
||||||
|
|
||||||
### Drop the selection but keep selecting
|
### Drop the selection but keep selecting
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ A folded burst draws its earliest frame, which is a fact about the clock and not
|
|||||||
|
|
||||||
Distinct from Done, which leaves the mode entirely. Clearing keeps it, so the next selection can start straight away.
|
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:4177`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4300`</sub>
|
||||||
|
|
||||||
### Select everything the grid is showing
|
### Select everything the grid is showing
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ 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.
|
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:4194`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4317`</sub>
|
||||||
|
|
||||||
### Take photographs out of a collection
|
### Take photographs out of a collection
|
||||||
|
|
||||||
@@ -390,4 +390,4 @@ A scoped grid of two hundred frames is two hundred taps otherwise, and "all of t
|
|||||||
|
|
||||||
The badge on a cell says a photograph is filed in three collections and never which. This is the sheet that names them, and the only way out of one the grid is not currently scoped to.
|
The badge on a cell says a photograph is filed in three collections and never which. This is the sheet that names them, and the only way out of one the grid is not currently scoped to.
|
||||||
|
|
||||||
<sub>`ui/dr-ui/ui/library.slint:4318`</sub>
|
<sub>`ui/dr-ui/ui/library.slint:4441`</sub>
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ use dr_catalog::bursts::{self, Rules, Signature};
|
|||||||
use dr_catalog::Catalog;
|
use dr_catalog::Catalog;
|
||||||
use dr_thumbs::{ThumbSize, ThumbStore};
|
use dr_thumbs::{ThumbSize, ThumbStore};
|
||||||
use dr_types::ImageId;
|
use dr_types::ImageId;
|
||||||
use slint::Model as _;
|
use slint::{ComponentHandle as _, Model as _};
|
||||||
|
|
||||||
use crate::AppWindow;
|
use crate::{AppWindow, Library};
|
||||||
|
|
||||||
/// How many signatures are written per transaction.
|
/// How many signatures are written per transaction.
|
||||||
///
|
///
|
||||||
@@ -364,7 +364,7 @@ pub fn sync_badges(window: &AppWindow, catalog: &Catalog, ids: &[ImageId]) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let model = window.get_library_cells();
|
let model = window.global::<Library>().get_library_cells();
|
||||||
for (row, id) in ids.iter().enumerate() {
|
for (row, id) in ids.iter().enumerate() {
|
||||||
let (count, expanded, representative) = match found.get(id) {
|
let (count, expanded, representative) = match found.get(id) {
|
||||||
Some(m) => (m.size as i32, m.expanded, m.representative),
|
Some(m) => (m.size as i32, m.expanded, m.representative),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use dr_catalog::Catalog;
|
|||||||
use dr_types::CollectionId;
|
use dr_types::CollectionId;
|
||||||
use slint::ComponentHandle;
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
use crate::AppWindow;
|
use crate::{AppWindow, Library};
|
||||||
|
|
||||||
use super::controller::CollectionsController;
|
use super::controller::CollectionsController;
|
||||||
use super::tree_sync::refresh_tree;
|
use super::tree_sync::refresh_tree;
|
||||||
@@ -189,11 +189,11 @@ pub(super) fn arm_hold(window: &AppWindow, ctl: &Rc<CollectionsController>, row:
|
|||||||
// This half happens whether or not selection mode was already on —
|
// This half happens whether or not selection mode was already on —
|
||||||
// it is the half a *drag* needs, and a drag out of a selection of
|
// it is the half a *drag* needs, and a drag out of a selection of
|
||||||
// forty starts in a mode that is already on.
|
// forty starts in a mode that is already on.
|
||||||
w.set_library_held_row(row);
|
w.global::<Library>().set_library_held_row(row);
|
||||||
|
|
||||||
if !ctl_cb.select_mode.get() {
|
if !ctl_cb.select_mode.get() {
|
||||||
ctl_cb.select_mode.set(true);
|
ctl_cb.select_mode.set(true);
|
||||||
w.set_library_select_mode(true);
|
w.global::<Library>().set_library_select_mode(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The release that follows this hold must not also open the image:
|
// The release that follows this hold must not also open the image:
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ use std::collections::BTreeSet;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use dr_types::ImageId;
|
use dr_types::ImageId;
|
||||||
|
use slint::ComponentHandle as _;
|
||||||
|
|
||||||
use crate::AppWindow;
|
use crate::{AppWindow, Library};
|
||||||
|
|
||||||
use super::controller::CollectionsController;
|
use super::controller::CollectionsController;
|
||||||
use super::tree_sync::sync_selection;
|
use super::tree_sync::sync_selection;
|
||||||
@@ -292,7 +293,7 @@ pub fn cancel_press(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[
|
|||||||
// has to put it back down, or the ring stays open around a cell nobody is
|
// has to put it back down, or the ring stays open around a cell nobody is
|
||||||
// touching and the grid stays frozen with it.
|
// touching and the grid stays frozen with it.
|
||||||
ctl.pending_toggle.set(None);
|
ctl.pending_toggle.set(None);
|
||||||
window.set_library_held_row(-1);
|
window.global::<Library>().set_library_held_row(-1);
|
||||||
|
|
||||||
let Some(undo) = ctl.press_undo.borrow_mut().take() else {
|
let Some(undo) = ctl.press_undo.borrow_mut().take() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
|
|||||||
use dr_types::{CollectionId, ImageId};
|
use dr_types::{CollectionId, ImageId};
|
||||||
use slint::{ComponentHandle as _, Model as _};
|
use slint::{ComponentHandle as _, Model as _};
|
||||||
|
|
||||||
use crate::{AppWindow, CollectionRow, Collections};
|
use crate::{AppWindow, CollectionRow, Collections, Library};
|
||||||
|
|
||||||
use super::controller::CollectionsController;
|
use super::controller::CollectionsController;
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ fn offline_state(
|
|||||||
/// Separate from [`sync_selection`] because the two differ: the selection
|
/// Separate from [`sync_selection`] because the two differ: the selection
|
||||||
/// persists after the drag, the lift lasts only while it is in flight.
|
/// persists after the drag, the lift lasts only while it is in flight.
|
||||||
pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) {
|
pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) {
|
||||||
let model = window.get_library_cells();
|
let model = window.global::<Library>().get_library_cells();
|
||||||
for (row, id) in ids.iter().enumerate() {
|
for (row, id) in ids.iter().enumerate() {
|
||||||
let want = lifted.contains(id);
|
let want = lifted.contains(id);
|
||||||
if let Some(mut cell) = model.row_data(row) {
|
if let Some(mut cell) = model.row_data(row) {
|
||||||
@@ -213,7 +213,7 @@ pub fn sync_lifted(window: &AppWindow, lifted: &[ImageId], ids: &[ImageId]) {
|
|||||||
/// this is what keeps the two in step after a scrub, a drop, or a click.
|
/// this is what keeps the two in step after a scrub, a drop, or a click.
|
||||||
pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[ImageId]) {
|
pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids: &[ImageId]) {
|
||||||
let selection = ctl.selection.borrow();
|
let selection = ctl.selection.borrow();
|
||||||
let model = window.get_library_cells();
|
let model = window.global::<Library>().get_library_cells();
|
||||||
|
|
||||||
// The anchor is deliberately **not** pushed to the grid.
|
// The anchor is deliberately **not** pushed to the grid.
|
||||||
//
|
//
|
||||||
@@ -238,7 +238,9 @@ pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.set_library_selected_count(selection.len() as i32);
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_selected_count(selection.len() as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TRACES: FR-CAT-7
|
/// TRACES: FR-CAT-7
|
||||||
@@ -261,7 +263,7 @@ pub(super) fn sync_reorderable(window: &AppWindow) {
|
|||||||
.filter_map(|i| rows.row_data(i))
|
.filter_map(|i| rows.row_data(i))
|
||||||
.find(|r| r.id == id)
|
.find(|r| r.id == id)
|
||||||
.is_some_and(|r| !r.smart && !r.has_children);
|
.is_some_and(|r| !r.smart && !r.has_children);
|
||||||
window.set_library_reorderable(manual);
|
window.global::<Library>().set_library_reorderable(manual);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Refresh the per-cell "in this many collections" badges.
|
/// Refresh the per-cell "in this many collections" badges.
|
||||||
@@ -300,7 +302,7 @@ pub fn sync_badges(window: &AppWindow, catalog: &Catalog, ids: &[ImageId]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let model = window.get_library_cells();
|
let model = window.global::<Library>().get_library_cells();
|
||||||
for (row, id) in ids.iter().enumerate() {
|
for (row, id) in ids.iter().enumerate() {
|
||||||
let want = counts.get(&(id.0 as i64)).copied().unwrap_or(0);
|
let want = counts.get(&(id.0 as i64)).copied().unwrap_or(0);
|
||||||
if let Some(mut cell) = model.row_data(row) {
|
if let Some(mut cell) = model.row_data(row) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use dr_types::{CollectionId, ImageId};
|
|||||||
use slint::{ComponentHandle, Model as _};
|
use slint::{ComponentHandle, Model as _};
|
||||||
|
|
||||||
use crate::library;
|
use crate::library;
|
||||||
use crate::{AppWindow, Collections};
|
use crate::{AppWindow, Collections, Library};
|
||||||
|
|
||||||
use super::controller::{decide_drop, CollectionsController, Drop};
|
use super::controller::{decide_drop, CollectionsController, Drop};
|
||||||
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
|
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
|
||||||
@@ -99,35 +99,37 @@ fn wire_selection(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
window.on_library_cell_pressed(move |row, ctrl_held, shift_held| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let ids = visible();
|
.on_library_cell_pressed(move |row, ctrl_held, shift_held| {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
let ids = visible();
|
||||||
|
|
||||||
// Consulted by the click that follows: a modified press is building
|
// Consulted by the click that follows: a modified press is building
|
||||||
// a selection and must not also navigate to develop.
|
// a selection and must not also navigate to develop.
|
||||||
ctl.modified_press.set(ctrl_held || shift_held);
|
ctl.modified_press.set(ctrl_held || shift_held);
|
||||||
|
|
||||||
// Where the window starts, so the press is recorded as the ordinal
|
// Where the window starts, so the press is recorded as the ordinal
|
||||||
// it is rather than as a row that stops meaning this photograph on
|
// it is rather than as a row that stops meaning this photograph on
|
||||||
// the next scroll.
|
// the next scroll.
|
||||||
let offset = w.get_library_offset().max(0) as usize;
|
let offset = w.get_library_offset().max(0) as usize;
|
||||||
select_row(&w, &ctl, &ids, offset, row as usize, ctrl_held, shift_held);
|
select_row(&w, &ctl, &ids, offset, row as usize, ctrl_held, shift_held);
|
||||||
|
|
||||||
// TRACES: FR-UI-2 | FR-UI-4 | FR-CAT-7
|
// TRACES: FR-UI-2 | FR-UI-4 | FR-CAT-7
|
||||||
// And start counting, in case this press is a hold. The press has
|
// And start counting, in case this press is a hold. The press has
|
||||||
// already selected this one cell; what the hold adds is the *mode*,
|
// already selected this one cell; what the hold adds is the *mode*,
|
||||||
// so the taps that follow go on selecting instead of opening the
|
// so the taps that follow go on selecting instead of opening the
|
||||||
// next photograph the user touches — and the *pick-up*, which is
|
// next photograph the user touches — and the *pick-up*, which is
|
||||||
// what makes the drag reliable.
|
// what makes the drag reliable.
|
||||||
//
|
//
|
||||||
// Armed even when the mode is already on, which it did not used to
|
// Armed even when the mode is already on, which it did not used to
|
||||||
// be: there was nothing left for the hold to switch on, so it was
|
// be: there was nothing left for the hold to switch on, so it was
|
||||||
// skipped. But the mode being on is exactly the state a
|
// skipped. But the mode being on is exactly the state a
|
||||||
// multi-image drag starts from, and skipping the hold left that
|
// multi-image drag starts from, and skipping the hold left that
|
||||||
// drag with no pick-up and no cue — the one gesture that most
|
// drag with no pick-up and no cue — the one gesture that most
|
||||||
// needed both. See `arm_hold`.
|
// needed both. See `arm_hold`.
|
||||||
arm_hold(&w, &ctl, row);
|
arm_hold(&w, &ctl, row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-UI-2 | FR-UI-4
|
// TRACES: FR-UI-2 | FR-UI-4
|
||||||
@@ -135,9 +137,11 @@ fn wire_selection(
|
|||||||
// travelled. Either way the hold is off.
|
// travelled. Either way the hold is off.
|
||||||
{
|
{
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_cell_press_ended(move || {
|
window
|
||||||
*ctl.hold_timer.borrow_mut() = None;
|
.global::<Library>()
|
||||||
});
|
.on_library_cell_press_ended(move || {
|
||||||
|
*ctl.hold_timer.borrow_mut() = None;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-CAT-5
|
// TRACES: FR-CAT-5
|
||||||
@@ -152,11 +156,13 @@ fn wire_selection(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
window.on_library_clear_selection(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
ctl.clear_selection();
|
.on_library_clear_selection(move || {
|
||||||
sync_selection(&w, &ctl, &visible());
|
let Some(w) = weak.upgrade() else { return };
|
||||||
});
|
ctl.clear_selection();
|
||||||
|
sync_selection(&w, &ctl, &visible());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-CAT-7
|
// TRACES: FR-CAT-7
|
||||||
@@ -179,75 +185,77 @@ fn wire_selection(
|
|||||||
let catalog = catalog.clone();
|
let catalog = catalog.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
let reload = on_scope_changed.clone();
|
let reload = on_scope_changed.clone();
|
||||||
window.on_library_reorder_to(move |row, after| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
// Both guards belong here rather than only in `library.slint`: the
|
.on_library_reorder_to(move |row, after| {
|
||||||
// scope can change between the drag starting and the drop landing.
|
let Some(w) = weak.upgrade() else { return };
|
||||||
if !w.get_library_reorderable() {
|
// Both guards belong here rather than only in `library.slint`: the
|
||||||
return;
|
// scope can change between the drag starting and the drop landing.
|
||||||
}
|
if !w.global::<Library>().get_library_reorderable() {
|
||||||
let Some(scope) = *ctl.scope.borrow() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let moving = ctl.selected();
|
|
||||||
if moving.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ids = visible();
|
|
||||||
let Some(&target) = usize::try_from(row).ok().and_then(|r| ids.get(r)) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dropped on one of its own. There is no gap between a run and
|
|
||||||
// itself to land in, and rewriting the whole membership to say so
|
|
||||||
// would be a revision bump for no change.
|
|
||||||
if moving.contains(&target) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let borrow = catalog.borrow();
|
|
||||||
let Some(cat) = borrow.as_ref() else { return };
|
|
||||||
|
|
||||||
let current = match library::read_member_order(cat, scope) {
|
|
||||||
Ok(current) => current,
|
|
||||||
Err(e) => {
|
|
||||||
w.global::<Collections>().set_collection_error(
|
|
||||||
format!("reading the collection's order: {e}").into(),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
let Some(scope) = *ctl.scope.borrow() else {
|
||||||
let wanted = library::reordered(¤t, &moving, target, after);
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
match coll::set_order(cat.connection(), scope, &wanted) {
|
let moving = ctl.selected();
|
||||||
Ok(()) => {
|
if moving.is_empty() {
|
||||||
w.global::<Collections>()
|
return;
|
||||||
.set_collection_error(slint::SharedString::new());
|
}
|
||||||
w.set_library_status(
|
|
||||||
format!(
|
|
||||||
"{} photograph{} moved",
|
|
||||||
moving.len(),
|
|
||||||
if moving.len() == 1 { "" } else { "s" }
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
drop(borrow);
|
|
||||||
|
|
||||||
// The selection survives. It is what was just moved, and
|
let ids = visible();
|
||||||
// dropping it would make a second nudge — which is how a
|
let Some(&target) = usize::try_from(row).ok().and_then(|r| ids.get(r)) else {
|
||||||
// drag-to-reorder is usually corrected — start over.
|
return;
|
||||||
reload();
|
};
|
||||||
sync_selection(&w, &ctl, &visible());
|
|
||||||
|
// Dropped on one of its own. There is no gap between a run and
|
||||||
|
// itself to land in, and rewriting the whole membership to say so
|
||||||
|
// would be a revision bump for no change.
|
||||||
|
if moving.contains(&target) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
drop(borrow);
|
let borrow = catalog.borrow();
|
||||||
w.global::<Collections>()
|
let Some(cat) = borrow.as_ref() else { return };
|
||||||
.set_collection_error(format!("reordering: {e}").into());
|
|
||||||
|
let current = match library::read_member_order(cat, scope) {
|
||||||
|
Ok(current) => current,
|
||||||
|
Err(e) => {
|
||||||
|
w.global::<Collections>().set_collection_error(
|
||||||
|
format!("reading the collection's order: {e}").into(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let wanted = library::reordered(¤t, &moving, target, after);
|
||||||
|
|
||||||
|
match coll::set_order(cat.connection(), scope, &wanted) {
|
||||||
|
Ok(()) => {
|
||||||
|
w.global::<Collections>()
|
||||||
|
.set_collection_error(slint::SharedString::new());
|
||||||
|
w.set_library_status(
|
||||||
|
format!(
|
||||||
|
"{} photograph{} moved",
|
||||||
|
moving.len(),
|
||||||
|
if moving.len() == 1 { "" } else { "s" }
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
drop(borrow);
|
||||||
|
|
||||||
|
// The selection survives. It is what was just moved, and
|
||||||
|
// dropping it would make a second nudge — which is how a
|
||||||
|
// drag-to-reorder is usually corrected — start over.
|
||||||
|
reload();
|
||||||
|
sync_selection(&w, &ctl, &visible());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
drop(borrow);
|
||||||
|
w.global::<Collections>()
|
||||||
|
.set_collection_error(format!("reordering: {e}").into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-CAT-5 | FR-UI-4
|
// TRACES: FR-CAT-5 | FR-UI-4
|
||||||
@@ -267,10 +275,10 @@ fn wire_selection(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
window.on_library_select_all(move || {
|
window.global::<Library>().on_library_select_all(move || {
|
||||||
let Some(w) = weak.upgrade() else { return };
|
let Some(w) = weak.upgrade() else { return };
|
||||||
let ids = visible();
|
let ids = visible();
|
||||||
let total = w.get_library_total().max(0) as usize;
|
let total = w.global::<Library>().get_library_total().max(0) as usize;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -334,69 +342,71 @@ fn wire_selection_filing(
|
|||||||
let catalog = catalog.clone();
|
let catalog = catalog.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
let reload = on_scope_changed.clone();
|
let reload = on_scope_changed.clone();
|
||||||
window.on_library_collection_from_selection(move |name| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let images = ctl.selected();
|
.on_library_collection_from_selection(move |name| {
|
||||||
if images.is_empty() {
|
let Some(w) = weak.upgrade() else { return };
|
||||||
return;
|
let images = ctl.selected();
|
||||||
}
|
if images.is_empty() {
|
||||||
|
return;
|
||||||
let borrow = catalog.borrow();
|
|
||||||
let Some(cat) = borrow.as_ref() else {
|
|
||||||
w.global::<Collections>()
|
|
||||||
.set_collection_error("Open a library first.".into());
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// The sheet refuses an empty name in two places, so this is
|
|
||||||
// belt-and-braces rather than a path the UI can reach — but a
|
|
||||||
// collection with no name at all is unfindable in the tree, and
|
|
||||||
// falling back to the placeholder is recoverable where an empty
|
|
||||||
// row is not.
|
|
||||||
let name = match name.trim() {
|
|
||||||
"" => unique_name(cat.connection(), None),
|
|
||||||
typed => typed.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// The id is not carried out. It used to be, to open the rename
|
|
||||||
// field on the row it names; now the name is already right, and
|
|
||||||
// `refresh_tree` redraws the tree from the catalog either way.
|
|
||||||
let made = coll::create(cat.connection(), &name, None, CollectionKind::Manual)
|
|
||||||
.and_then(|id| coll::add_images(cat.connection(), id, &images));
|
|
||||||
|
|
||||||
match made {
|
|
||||||
Ok(added) => {
|
|
||||||
w.global::<Collections>()
|
|
||||||
.set_collection_error(slint::SharedString::new());
|
|
||||||
w.set_library_status(
|
|
||||||
format!(
|
|
||||||
"{added} photograph{} in {name}",
|
|
||||||
if added == 1 { "" } else { "s" }
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
refresh_tree(&w, &ctl, cat);
|
|
||||||
drop(borrow);
|
|
||||||
|
|
||||||
// The selection has been filed; holding on to it invites
|
|
||||||
// the next press acting on photographs the user considers
|
|
||||||
// dealt with.
|
|
||||||
ctl.clear_selection();
|
|
||||||
sync_selection(&w, &ctl, &visible());
|
|
||||||
|
|
||||||
// No rename opened here. The name was chosen before the
|
|
||||||
// collection existed, so there is nothing left to correct —
|
|
||||||
// and opening the sidebar's field is what stranded the
|
|
||||||
// keyboard on a tablet.
|
|
||||||
reload();
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
drop(borrow);
|
let borrow = catalog.borrow();
|
||||||
|
let Some(cat) = borrow.as_ref() else {
|
||||||
w.global::<Collections>()
|
w.global::<Collections>()
|
||||||
.set_collection_error(format!("making a collection: {e}").into());
|
.set_collection_error("Open a library first.".into());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The sheet refuses an empty name in two places, so this is
|
||||||
|
// belt-and-braces rather than a path the UI can reach — but a
|
||||||
|
// collection with no name at all is unfindable in the tree, and
|
||||||
|
// falling back to the placeholder is recoverable where an empty
|
||||||
|
// row is not.
|
||||||
|
let name = match name.trim() {
|
||||||
|
"" => unique_name(cat.connection(), None),
|
||||||
|
typed => typed.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// The id is not carried out. It used to be, to open the rename
|
||||||
|
// field on the row it names; now the name is already right, and
|
||||||
|
// `refresh_tree` redraws the tree from the catalog either way.
|
||||||
|
let made = coll::create(cat.connection(), &name, None, CollectionKind::Manual)
|
||||||
|
.and_then(|id| coll::add_images(cat.connection(), id, &images));
|
||||||
|
|
||||||
|
match made {
|
||||||
|
Ok(added) => {
|
||||||
|
w.global::<Collections>()
|
||||||
|
.set_collection_error(slint::SharedString::new());
|
||||||
|
w.set_library_status(
|
||||||
|
format!(
|
||||||
|
"{added} photograph{} in {name}",
|
||||||
|
if added == 1 { "" } else { "s" }
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
refresh_tree(&w, &ctl, cat);
|
||||||
|
drop(borrow);
|
||||||
|
|
||||||
|
// The selection has been filed; holding on to it invites
|
||||||
|
// the next press acting on photographs the user considers
|
||||||
|
// dealt with.
|
||||||
|
ctl.clear_selection();
|
||||||
|
sync_selection(&w, &ctl, &visible());
|
||||||
|
|
||||||
|
// No rename opened here. The name was chosen before the
|
||||||
|
// collection existed, so there is nothing left to correct —
|
||||||
|
// and opening the sidebar's field is what stranded the
|
||||||
|
// keyboard on a tablet.
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
drop(borrow);
|
||||||
|
w.global::<Collections>()
|
||||||
|
.set_collection_error(format!("making a collection: {e}").into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-UI-2 | FR-UI-4
|
// TRACES: FR-UI-2 | FR-UI-4
|
||||||
@@ -407,23 +417,25 @@ fn wire_selection_filing(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
window.on_library_toggle_select_mode(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let on = !ctl.select_mode.get();
|
.on_library_toggle_select_mode(move || {
|
||||||
ctl.select_mode.set(on);
|
let Some(w) = weak.upgrade() else { return };
|
||||||
w.set_library_select_mode(on);
|
let on = !ctl.select_mode.get();
|
||||||
*ctl.hold_timer.borrow_mut() = None;
|
ctl.select_mode.set(on);
|
||||||
|
w.global::<Library>().set_library_select_mode(on);
|
||||||
|
*ctl.hold_timer.borrow_mut() = None;
|
||||||
|
|
||||||
// Leaving the mode drops the selection. "Done" reads as finishing
|
// Leaving the mode drops the selection. "Done" reads as finishing
|
||||||
// with these photographs, and a selection that outlived the mode
|
// with these photographs, and a selection that outlived the mode
|
||||||
// would still be acted on by the buttons in the header — which is
|
// would still be acted on by the buttons in the header — which is
|
||||||
// how forty images get exported by a user who thought they had put
|
// how forty images get exported by a user who thought they had put
|
||||||
// them down.
|
// them down.
|
||||||
if !on {
|
if !on {
|
||||||
ctl.clear_selection();
|
ctl.clear_selection();
|
||||||
sync_selection(&w, &ctl, &visible());
|
sync_selection(&w, &ctl, &visible());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-CAT-7 | FR-UI-4
|
// TRACES: FR-CAT-7 | FR-UI-4
|
||||||
@@ -434,59 +446,61 @@ fn wire_selection_filing(
|
|||||||
let catalog = catalog.clone();
|
let catalog = catalog.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
let reload = on_scope_changed.clone();
|
let reload = on_scope_changed.clone();
|
||||||
window.on_library_file_in_collection(move |id, moves| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let target = CollectionId(id as u64);
|
.on_library_file_in_collection(move |id, moves| {
|
||||||
let chosen = ctl.selected();
|
let Some(w) = weak.upgrade() else { return };
|
||||||
if chosen.is_empty() {
|
let target = CollectionId(id as u64);
|
||||||
return;
|
let chosen = ctl.selected();
|
||||||
}
|
if chosen.is_empty() {
|
||||||
|
return;
|
||||||
let borrow = catalog.borrow();
|
|
||||||
let Some(cat) = borrow.as_ref() else { return };
|
|
||||||
|
|
||||||
// Moving is only a move when there is somewhere to move *from*.
|
|
||||||
// Unscoped, the sheet does not offer it; this guards the callback
|
|
||||||
// anyway, and degrades to filing rather than refusing.
|
|
||||||
let from = if moves { *ctl.scope.borrow() } else { None };
|
|
||||||
let result = match from {
|
|
||||||
Some(from) => coll::move_images(cat.connection(), from, target, &chosen),
|
|
||||||
None => coll::add_images(cat.connection(), target, &chosen),
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(filed) => {
|
|
||||||
w.global::<Collections>()
|
|
||||||
.set_collection_error(slint::SharedString::new());
|
|
||||||
// The honest count, as the drop reports it: claiming all
|
|
||||||
// twelve when nine were already there teaches the user to
|
|
||||||
// distrust every number the app shows them.
|
|
||||||
let msg = if from.is_some() {
|
|
||||||
format!("Moved {filed} of {}", chosen.len())
|
|
||||||
} else if filed == chosen.len() {
|
|
||||||
format!("Added {filed} to collection")
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"Added {filed} of {} — the rest were already there",
|
|
||||||
chosen.len()
|
|
||||||
)
|
|
||||||
};
|
|
||||||
w.set_library_status(msg.into());
|
|
||||||
|
|
||||||
refresh_tree(&w, &ctl, cat);
|
|
||||||
sync_badges(&w, cat, &visible());
|
|
||||||
// A move changes what the collection on screen holds, so
|
|
||||||
// the grid has to be reread; filing elsewhere does not, but
|
|
||||||
// the badge count on every cell just changed and the reread
|
|
||||||
// is one query.
|
|
||||||
drop(borrow);
|
|
||||||
reload();
|
|
||||||
}
|
}
|
||||||
Err(e) => w
|
|
||||||
.global::<Collections>()
|
let borrow = catalog.borrow();
|
||||||
.set_collection_error(format!("filing: {e}").into()),
|
let Some(cat) = borrow.as_ref() else { return };
|
||||||
}
|
|
||||||
});
|
// Moving is only a move when there is somewhere to move *from*.
|
||||||
|
// Unscoped, the sheet does not offer it; this guards the callback
|
||||||
|
// anyway, and degrades to filing rather than refusing.
|
||||||
|
let from = if moves { *ctl.scope.borrow() } else { None };
|
||||||
|
let result = match from {
|
||||||
|
Some(from) => coll::move_images(cat.connection(), from, target, &chosen),
|
||||||
|
None => coll::add_images(cat.connection(), target, &chosen),
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(filed) => {
|
||||||
|
w.global::<Collections>()
|
||||||
|
.set_collection_error(slint::SharedString::new());
|
||||||
|
// The honest count, as the drop reports it: claiming all
|
||||||
|
// twelve when nine were already there teaches the user to
|
||||||
|
// distrust every number the app shows them.
|
||||||
|
let msg = if from.is_some() {
|
||||||
|
format!("Moved {filed} of {}", chosen.len())
|
||||||
|
} else if filed == chosen.len() {
|
||||||
|
format!("Added {filed} to collection")
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Added {filed} of {} — the rest were already there",
|
||||||
|
chosen.len()
|
||||||
|
)
|
||||||
|
};
|
||||||
|
w.set_library_status(msg.into());
|
||||||
|
|
||||||
|
refresh_tree(&w, &ctl, cat);
|
||||||
|
sync_badges(&w, cat, &visible());
|
||||||
|
// A move changes what the collection on screen holds, so
|
||||||
|
// the grid has to be reread; filing elsewhere does not, but
|
||||||
|
// the badge count on every cell just changed and the reread
|
||||||
|
// is one query.
|
||||||
|
drop(borrow);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
Err(e) => w
|
||||||
|
.global::<Collections>()
|
||||||
|
.set_collection_error(format!("filing: {e}").into()),
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,7 +540,7 @@ fn wire_drag(
|
|||||||
// nothing outside the application can interpret it as a paste.
|
// nothing outside the application can interpret it as a paste.
|
||||||
{
|
{
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_drag_payload(move || {
|
window.global::<Library>().on_library_drag_payload(move || {
|
||||||
let carried = ctl.dragging.borrow().clone();
|
let carried = ctl.dragging.borrow().clone();
|
||||||
let mut data = slint::DataTransfer::default();
|
let mut data = slint::DataTransfer::default();
|
||||||
data.set_user_data(Rc::new(carried));
|
data.set_user_data(Rc::new(carried));
|
||||||
@@ -551,67 +565,70 @@ fn wire_drag(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
window.on_library_drag_started(move |row| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let ids = visible();
|
.on_library_drag_started(move |row| {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
let ids = visible();
|
||||||
|
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
// A drag is not a hold, and the timer must not outlive the press
|
// A drag is not a hold, and the timer must not outlive the press
|
||||||
// that armed it. Grabbing a cell and moving inside 450 ms left the
|
// that armed it. Grabbing a cell and moving inside 450 ms left the
|
||||||
// hold armed underneath the drag, so it fired mid-gesture and put
|
// hold armed underneath the drag, so it fired mid-gesture and put
|
||||||
// the grid into selection mode the user had not asked for — the
|
// the grid into selection mode the user had not asked for — the
|
||||||
// drag finished into a mode that changed what every later tap
|
// drag finished into a mode that changed what every later tap
|
||||||
// meant. The pinch already cancels for exactly this reason.
|
// meant. The pinch already cancels for exactly this reason.
|
||||||
*ctl.hold_timer.borrow_mut() = None;
|
*ctl.hold_timer.borrow_mut() = None;
|
||||||
|
|
||||||
// TRACES: FR-CAT-7
|
// TRACES: FR-CAT-7
|
||||||
// And this press was not a tap, so it never gets to take anything
|
// And this press was not a tap, so it never gets to take anything
|
||||||
// out of the selection — see [`Press::Deferred`]. Dropped here as
|
// out of the selection — see [`Press::Deferred`]. Dropped here as
|
||||||
// well as on the next press because the drag reads the selection
|
// well as on the next press because the drag reads the selection
|
||||||
// one line below, and a removal still pending would be a
|
// one line below, and a removal still pending would be a
|
||||||
// photograph the user can see is selected and the drop would not
|
// photograph the user can see is selected and the drop would not
|
||||||
// carry.
|
// carry.
|
||||||
ctl.pending_toggle.set(None);
|
ctl.pending_toggle.set(None);
|
||||||
|
|
||||||
// Dragging an *unselected* cell carries only that one, and makes it
|
// Dragging an *unselected* cell carries only that one, and makes it
|
||||||
// the selection — otherwise the images that travel are not the ones
|
// the selection — otherwise the images that travel are not the ones
|
||||||
// the user grabbed. Dragging a selected cell carries the whole
|
// the user grabbed. Dragging a selected cell carries the whole
|
||||||
// selection, which is the multi-image gesture.
|
// selection, which is the multi-image gesture.
|
||||||
let carried: Vec<ImageId> = {
|
let carried: Vec<ImageId> = {
|
||||||
let mut selection = ctl.selection.borrow_mut();
|
let mut selection = ctl.selection.borrow_mut();
|
||||||
match ids.get(row as usize) {
|
match ids.get(row as usize) {
|
||||||
Some(id) if !selection.contains(id) => {
|
Some(id) if !selection.contains(id) => {
|
||||||
selection.clear();
|
selection.clear();
|
||||||
selection.insert(*id);
|
selection.insert(*id);
|
||||||
vec![*id]
|
vec![*id]
|
||||||
|
}
|
||||||
|
_ => selection.iter().copied().collect(),
|
||||||
}
|
}
|
||||||
_ => selection.iter().copied().collect(),
|
};
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// The bitmap under the cursor, built from the thumbnails already in
|
// The bitmap under the cursor, built from the thumbnails already in
|
||||||
// the model — the drag carries what the user can see, and a cell
|
// the model — the drag carries what the user can see, and a cell
|
||||||
// whose preview has not landed yet contributes nothing rather than
|
// whose preview has not landed yet contributes nothing rather than
|
||||||
// a placeholder.
|
// a placeholder.
|
||||||
let thumbs: Vec<slint::Image> = {
|
let thumbs: Vec<slint::Image> = {
|
||||||
let model = w.get_library_cells();
|
let model = w.global::<Library>().get_library_cells();
|
||||||
carried
|
carried
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|id| ids.iter().position(|v| v == id))
|
.filter_map(|id| ids.iter().position(|v| v == id))
|
||||||
.filter_map(|row| model.row_data(row))
|
.filter_map(|row| model.row_data(row))
|
||||||
.filter(|c| c.has_thumb)
|
.filter(|c| c.has_thumb)
|
||||||
.map(|c| c.thumbnail)
|
.map(|c| c.thumbnail)
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
w.set_library_drag_image(compose_drag_image(&thumbs));
|
w.global::<Library>()
|
||||||
|
.set_library_drag_image(compose_drag_image(&thumbs));
|
||||||
|
|
||||||
*ctl.dragging.borrow_mut() = carried.clone();
|
*ctl.dragging.borrow_mut() = carried.clone();
|
||||||
sync_selection(&w, &ctl, &ids);
|
sync_selection(&w, &ctl, &ids);
|
||||||
// The lift-out: these cells fade and shrink in place, so the grid
|
// The lift-out: these cells fade and shrink in place, so the grid
|
||||||
// shows where the photographs came from while the cursor shows them
|
// shows where the photographs came from while the cursor shows them
|
||||||
// in full colour.
|
// in full colour.
|
||||||
sync_lifted(&w, &carried, &ids);
|
sync_lifted(&w, &carried, &ids);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// A drag dwelling over a collapsed collection springs it open, so a nested
|
// A drag dwelling over a collapsed collection springs it open, so a nested
|
||||||
@@ -740,52 +757,55 @@ fn wire_drag(
|
|||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
let reload = on_scope_changed.clone();
|
let reload = on_scope_changed.clone();
|
||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
window.on_library_drag_finished(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
|
.on_library_drag_finished(move || {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
|
||||||
let landed = ctl.dropped_on.borrow_mut().take();
|
let landed = ctl.dropped_on.borrow_mut().take();
|
||||||
let to_trash = ctl.trash_requested.borrow_mut().take();
|
let to_trash = ctl.trash_requested.borrow_mut().take();
|
||||||
ctl.dragging.borrow_mut().clear();
|
ctl.dragging.borrow_mut().clear();
|
||||||
// The grid clears this itself on the cancel that starts a drag;
|
// The grid clears this itself on the cancel that starts a drag;
|
||||||
// this is for the endings that reach no cell — a drop, or a drag
|
// this is for the endings that reach no cell — a drop, or a drag
|
||||||
// abandoned over nothing.
|
// abandoned over nothing.
|
||||||
w.set_library_held_row(-1);
|
w.global::<Library>().set_library_held_row(-1);
|
||||||
*ctl.hover_id.borrow_mut() = None;
|
*ctl.hover_id.borrow_mut() = None;
|
||||||
*ctl.spring_timer.borrow_mut() = None;
|
*ctl.spring_timer.borrow_mut() = None;
|
||||||
|
|
||||||
// A soft delete, deferred out of the drop handler so the models it
|
// A soft delete, deferred out of the drop handler so the models it
|
||||||
// replaces are no longer being walked.
|
// replaces are no longer being walked.
|
||||||
if let Some(images) = to_trash {
|
if let Some(images) = to_trash {
|
||||||
start_trash(&w, &ctl, &catalog, &session, &images, &reload);
|
start_trash(&w, &ctl, &catalog, &session, &images, &reload);
|
||||||
}
|
|
||||||
|
|
||||||
// The cells settle back into the grid, and the cursor bitmap is
|
|
||||||
// released — it holds a copy of every thumbnail it composited.
|
|
||||||
sync_lifted(&w, &[], &visible());
|
|
||||||
w.set_library_drag_image(slint::Image::default());
|
|
||||||
|
|
||||||
if landed.is_some() {
|
|
||||||
let borrow = catalog.borrow();
|
|
||||||
if let Some(cat) = borrow.as_ref() {
|
|
||||||
// Counts changed on the target and every ancestor, and the
|
|
||||||
// dropped images now carry one more collection badge.
|
|
||||||
refresh_tree(&w, &ctl, cat);
|
|
||||||
sync_badges(&w, cat, &visible());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Put back whatever the spring opened on the way. The collection
|
// The cells settle back into the grid, and the cursor bitmap is
|
||||||
// that received the images — and its ancestors — stay open, since
|
// released — it holds a copy of every thumbnail it composited.
|
||||||
// that is where the user is now working; on an abandoned drag
|
sync_lifted(&w, &[], &visible());
|
||||||
// `landed` is `None` and everything closes.
|
w.global::<Library>()
|
||||||
collapse_spring_opened(&w, &ctl, &catalog, landed);
|
.set_library_drag_image(slint::Image::default());
|
||||||
|
|
||||||
// A drop into the collection currently being shown changes what that
|
if landed.is_some() {
|
||||||
// collection holds, so the grid has to be reread.
|
let borrow = catalog.borrow();
|
||||||
if landed.is_some() && landed == *ctl.scope.borrow() {
|
if let Some(cat) = borrow.as_ref() {
|
||||||
reload();
|
// Counts changed on the target and every ancestor, and the
|
||||||
}
|
// dropped images now carry one more collection badge.
|
||||||
});
|
refresh_tree(&w, &ctl, cat);
|
||||||
|
sync_badges(&w, cat, &visible());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put back whatever the spring opened on the way. The collection
|
||||||
|
// that received the images — and its ancestors — stay open, since
|
||||||
|
// that is where the user is now working; on an abandoned drag
|
||||||
|
// `landed` is `None` and everything closes.
|
||||||
|
collapse_spring_opened(&w, &ctl, &catalog, landed);
|
||||||
|
|
||||||
|
// A drop into the collection currently being shown changes what that
|
||||||
|
// collection holds, so the grid has to be reread.
|
||||||
|
if landed.is_some() && landed == *ctl.scope.borrow() {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
|
|||||||
use dr_types::{CollectionId, ImageId};
|
use dr_types::{CollectionId, ImageId};
|
||||||
use slint::{ComponentHandle, Model as _};
|
use slint::{ComponentHandle, Model as _};
|
||||||
|
|
||||||
use crate::{AppWindow, Collections};
|
use crate::{AppWindow, Collections, Library};
|
||||||
|
|
||||||
use super::controller::{
|
use super::controller::{
|
||||||
decide_delete, decide_release, CollectionsController, DeleteStep, Release,
|
decide_delete, decide_release, CollectionsController, DeleteStep, Release,
|
||||||
@@ -236,41 +236,44 @@ pub(super) fn wire_remove(
|
|||||||
let catalog = catalog.clone();
|
let catalog = catalog.clone();
|
||||||
let visible = visible_ids.clone();
|
let visible = visible_ids.clone();
|
||||||
let reload = on_scope_changed.clone();
|
let reload = on_scope_changed.clone();
|
||||||
window.on_library_remove_from_collection(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let Some(scope) = *ctl.scope.borrow() else {
|
.on_library_remove_from_collection(move || {
|
||||||
// Unscoped, there is no collection to remove from. The button
|
let Some(w) = weak.upgrade() else { return };
|
||||||
// is hidden in that state; this guards the callback anyway.
|
let Some(scope) = *ctl.scope.borrow() else {
|
||||||
return;
|
// Unscoped, there is no collection to remove from. The button
|
||||||
};
|
// is hidden in that state; this guards the callback anyway.
|
||||||
let chosen = ctl.selected();
|
return;
|
||||||
if chosen.is_empty() {
|
};
|
||||||
return;
|
let chosen = ctl.selected();
|
||||||
}
|
if chosen.is_empty() {
|
||||||
|
return;
|
||||||
let borrow = catalog.borrow();
|
|
||||||
let Some(cat) = borrow.as_ref() else { return };
|
|
||||||
|
|
||||||
match coll::remove_images(cat.connection(), scope, &chosen) {
|
|
||||||
Ok(n) => {
|
|
||||||
w.global::<Collections>()
|
|
||||||
.set_collection_error(slint::SharedString::new());
|
|
||||||
// Named explicitly as a membership change: the images are
|
|
||||||
// still in the library, and a user who reads this as a
|
|
||||||
// delete will not trust the feature again.
|
|
||||||
w.set_library_status(
|
|
||||||
format!("Removed {n} from this collection; still in the library").into(),
|
|
||||||
);
|
|
||||||
ctl.clear_selection();
|
|
||||||
refresh_tree(&w, &ctl, cat);
|
|
||||||
sync_badges(&w, cat, &visible());
|
|
||||||
reload();
|
|
||||||
}
|
}
|
||||||
Err(e) => w
|
|
||||||
.global::<Collections>()
|
let borrow = catalog.borrow();
|
||||||
.set_collection_error(format!("removing: {e}").into()),
|
let Some(cat) = borrow.as_ref() else { return };
|
||||||
}
|
|
||||||
});
|
match coll::remove_images(cat.connection(), scope, &chosen) {
|
||||||
|
Ok(n) => {
|
||||||
|
w.global::<Collections>()
|
||||||
|
.set_collection_error(slint::SharedString::new());
|
||||||
|
// Named explicitly as a membership change: the images are
|
||||||
|
// still in the library, and a user who reads this as a
|
||||||
|
// delete will not trust the feature again.
|
||||||
|
w.set_library_status(
|
||||||
|
format!("Removed {n} from this collection; still in the library")
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
ctl.clear_selection();
|
||||||
|
refresh_tree(&w, &ctl, cat);
|
||||||
|
sync_badges(&w, cat, &visible());
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
Err(e) => w
|
||||||
|
.global::<Collections>()
|
||||||
|
.set_collection_error(format!("removing: {e}").into()),
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,13 +691,15 @@ pub(super) fn wire_membership(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let catalog = catalog.clone();
|
let catalog = catalog.clone();
|
||||||
window.on_library_open_membership(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let borrow = catalog.borrow();
|
.on_library_open_membership(move || {
|
||||||
let Some(cat) = borrow.as_ref() else { return };
|
let Some(w) = weak.upgrade() else { return };
|
||||||
refresh_membership(&w, &ctl, cat);
|
let borrow = catalog.borrow();
|
||||||
w.global::<Collections>().set_membership_open(true);
|
let Some(cat) = borrow.as_ref() else { return };
|
||||||
});
|
refresh_membership(&w, &ctl, cat);
|
||||||
|
w.global::<Collections>().set_membership_open(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-1
@@ -72,6 +72,7 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dr_decode::{Metadata, PreviewSize};
|
use dr_decode::{Metadata, PreviewSize};
|
||||||
|
use slint::ComponentHandle as _;
|
||||||
|
|
||||||
pub use develop::DevelopSession;
|
pub use develop::DevelopSession;
|
||||||
|
|
||||||
@@ -3436,7 +3437,7 @@ fn back_one_step(w: &AppWindow) -> bool {
|
|||||||
// Files named on the command line have no grid behind them — the same
|
// Files named on the command line have no grid behind them — the same
|
||||||
// condition the status strip uses to decide whether to offer the way
|
// condition the status strip uses to decide whether to offer the way
|
||||||
// back at all.
|
// back at all.
|
||||||
has_grid: w.get_library_total() > 0,
|
has_grid: w.global::<Library>().get_library_total() > 0,
|
||||||
scoped: w.get_collection_selected() != 0,
|
scoped: w.get_collection_selected() != 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::rc::Rc;
|
|||||||
use slint::ComponentHandle;
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
use crate::library;
|
use crate::library;
|
||||||
use crate::{AppWindow, PersonChip, View};
|
use crate::{AppWindow, Library, PersonChip, View};
|
||||||
|
|
||||||
use super::controller::LibraryController;
|
use super::controller::LibraryController;
|
||||||
use super::offline::{
|
use super::offline::{
|
||||||
@@ -384,14 +384,16 @@ pub(super) fn wire_filter_scope_and_offline(
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_toggle_local_only(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let on = !ctl.local_only();
|
.on_library_toggle_local_only(move || {
|
||||||
ctl.set_local_only(on);
|
let Some(w) = weak.upgrade() else { return };
|
||||||
ctl.filter.borrow_mut().local_only = on;
|
let on = !ctl.local_only();
|
||||||
w.set_library_local_only(on);
|
ctl.set_local_only(on);
|
||||||
refilter(&w, &ctl);
|
ctl.filter.borrow_mut().local_only = on;
|
||||||
});
|
w.global::<Library>().set_library_local_only(on);
|
||||||
|
refilter(&w, &ctl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-NC-6a
|
// TRACES: FR-NC-6a
|
||||||
|
|||||||
+246
-226
@@ -12,7 +12,7 @@ use std::rc::Rc;
|
|||||||
use slint::{ComponentHandle, Model as _};
|
use slint::{ComponentHandle, Model as _};
|
||||||
|
|
||||||
use crate::AppWindow;
|
use crate::AppWindow;
|
||||||
use crate::{GestureRow, View};
|
use crate::{GestureRow, Library, View};
|
||||||
|
|
||||||
use super::controller::{
|
use super::controller::{
|
||||||
LibraryController, OpenImage, MAX_CELL_SIZE, MIN_CELL_SIZE, MIN_WINDOW, SCREENFULS,
|
LibraryController, OpenImage, MAX_CELL_SIZE, MIN_CELL_SIZE, MIN_WINDOW, SCREENFULS,
|
||||||
@@ -49,7 +49,7 @@ fn move_cursor(
|
|||||||
delta: i32,
|
delta: i32,
|
||||||
extend: bool,
|
extend: bool,
|
||||||
) {
|
) {
|
||||||
let total = window.get_library_total().max(0) as usize;
|
let total = window.global::<Library>().get_library_total().max(0) as usize;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -93,11 +93,11 @@ fn place_cursor(
|
|||||||
// quarter in, exactly as a scroll does it, so continuing in the same
|
// quarter in, exactly as a scroll does it, so continuing in the same
|
||||||
// direction has loaded cells to move into rather than another reload on
|
// direction has loaded cells to move into rather than another reload on
|
||||||
// the very next press.
|
// the very next press.
|
||||||
let loaded = window.get_library_cells().row_count();
|
let loaded = window.global::<Library>().get_library_cells().row_count();
|
||||||
let offset = *ctl.offset.borrow();
|
let offset = *ctl.offset.borrow();
|
||||||
if at < offset || at >= offset + loaded {
|
if at < offset || at >= offset + loaded {
|
||||||
let size = *ctl.window.borrow();
|
let size = *ctl.window.borrow();
|
||||||
let total = window.get_library_total().max(0) as usize;
|
let total = window.global::<Library>().get_library_total().max(0) as usize;
|
||||||
*ctl.offset.borrow_mut() = window_start(at, size, total);
|
*ctl.offset.borrow_mut() = window_start(at, size, total);
|
||||||
load_window(window, ctl);
|
load_window(window, ctl);
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ fn place_cursor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
crate::collections_ui::select_row(window, coll, &ids, offset, row, false, extend);
|
crate::collections_ui::select_row(window, coll, &ids, offset, row, false, extend);
|
||||||
window.set_library_cursor(at as i32);
|
window.global::<Library>().set_library_cursor(at as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Say where in the library the photograph now open sits.
|
/// Say where in the library the photograph now open sits.
|
||||||
@@ -136,7 +136,7 @@ fn place_cursor(
|
|||||||
fn report_position(window: &AppWindow, ctl: &Rc<LibraryController>, row: usize) {
|
fn report_position(window: &AppWindow, ctl: &Rc<LibraryController>, row: usize) {
|
||||||
let offset = *ctl.offset.borrow();
|
let offset = *ctl.offset.borrow();
|
||||||
window.set_index((offset + row) as i32);
|
window.set_index((offset + row) as i32);
|
||||||
window.set_total(window.get_library_total());
|
window.set_total(window.global::<Library>().get_library_total());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect the grid's callbacks.
|
/// Connect the grid's callbacks.
|
||||||
@@ -175,7 +175,9 @@ pub fn wire<F>(
|
|||||||
// claims the gesture before the delay it would forward after. Seeding it
|
// claims the gesture before the delay it would forward after. Seeding it
|
||||||
// here means the stars are on screen before the first touch rather than
|
// here means the stars are on screen before the first touch rather than
|
||||||
// after it, which is the whole point of showing them.
|
// after it, which is the whole point of showing them.
|
||||||
window.set_library_touched(cfg!(target_os = "android"));
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_touched(cfg!(target_os = "android"));
|
||||||
|
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
// The gesture reference. Pushed once, here, rather than on demand: the
|
// The gesture reference. Pushed once, here, rather than on demand: the
|
||||||
@@ -209,42 +211,44 @@ pub fn wire<F>(
|
|||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let coll_for_click = coll_ctl.clone();
|
let coll_for_click = coll_ctl.clone();
|
||||||
let on_open_image = on_open_image.clone();
|
let on_open_image = on_open_image.clone();
|
||||||
window.on_library_cell_clicked(move |i| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
|
.on_library_cell_clicked(move |i| {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
|
||||||
// The press stayed put, so it was a tap and not a drag: whatever it
|
// The press stayed put, so it was a tap and not a drag: whatever it
|
||||||
// held back can be applied now. See `collections_ui::Press`.
|
// held back can be applied now. See `collections_ui::Press`.
|
||||||
crate::collections_ui::commit_press(&w, &coll_for_click, &ctl.visible_ids());
|
crate::collections_ui::commit_press(&w, &coll_for_click, &ctl.visible_ids());
|
||||||
|
|
||||||
// A ctrl- or shift-click is a selection gesture. Opening the image
|
// A ctrl- or shift-click is a selection gesture. Opening the image
|
||||||
// too would throw the user out of the grid mid-selection.
|
// too would throw the user out of the grid mid-selection.
|
||||||
if coll_for_click.press_was_modified() {
|
if coll_for_click.press_was_modified() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = ctl.paths.borrow().get(i as usize).cloned();
|
let path = ctl.paths.borrow().get(i as usize).cloned();
|
||||||
if let Some(path) = path {
|
if let Some(path) = path {
|
||||||
// Leave the grid for the develop view. The status bar's
|
// Leave the grid for the develop view. The status bar's
|
||||||
// "‹ Library" button comes back here.
|
// "‹ Library" button comes back here.
|
||||||
w.set_active_view(View::Develop);
|
w.set_active_view(View::Develop);
|
||||||
// Which cell the develop view is now showing, so the photo
|
// Which cell the develop view is now showing, so the photo
|
||||||
// roll opens marking it rather than marking nothing.
|
// roll opens marking it rather than marking nothing.
|
||||||
w.set_library_roll_current(i);
|
w.global::<Library>().set_library_roll_current(i);
|
||||||
// A develop session begins here, so the roll centres on this
|
// A develop session begins here, so the roll centres on this
|
||||||
// photograph the first time it settles rather than merely
|
// photograph the first time it settles rather than merely
|
||||||
// scrolling it into view at one edge. Raised here and not in
|
// scrolling it into view at one edge. Raised here and not in
|
||||||
// `on_library_roll_pick`, which is a step *within* a session —
|
// `on_library_roll_pick`, which is a step *within* a session —
|
||||||
// see `PhotoRoll::centre-request`.
|
// see `PhotoRoll::centre-request`.
|
||||||
w.set_library_roll_centre(true);
|
w.global::<Library>().set_library_roll_centre(true);
|
||||||
on_open_image(path);
|
on_open_image(path);
|
||||||
report_position(&w, &ctl, i as usize);
|
report_position(&w, &ctl, i as usize);
|
||||||
// TRACES: FR-UI-8
|
// TRACES: FR-UI-8
|
||||||
// Written now rather than on a settle: which view you are in is
|
// Written now rather than on a settle: which view you are in is
|
||||||
// the field a debounce is most likely to lose, and quitting
|
// the field a debounce is most likely to lose, and quitting
|
||||||
// straight from develop is exactly the case worth getting right.
|
// straight from develop is exactly the case worth getting right.
|
||||||
write_place(&w, &ctl);
|
write_place(&w, &ctl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
@@ -259,12 +263,12 @@ pub fn wire<F>(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let on_open_image = on_open_image.clone();
|
let on_open_image = on_open_image.clone();
|
||||||
window.on_library_roll_pick(move |i| {
|
window.global::<Library>().on_library_roll_pick(move |i| {
|
||||||
let Some(w) = weak.upgrade() else { return };
|
let Some(w) = weak.upgrade() else { return };
|
||||||
let Some(path) = ctl.paths.borrow().get(i as usize).cloned() else {
|
let Some(path) = ctl.paths.borrow().get(i as usize).cloned() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
w.set_library_roll_current(i);
|
w.global::<Library>().set_library_roll_current(i);
|
||||||
on_open_image(path);
|
on_open_image(path);
|
||||||
report_position(&w, &ctl, i as usize);
|
report_position(&w, &ctl, i as usize);
|
||||||
// TRACES: FR-UI-8
|
// TRACES: FR-UI-8
|
||||||
@@ -307,10 +311,12 @@ fn wire_grid_cursor_and_zoom(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let coll = coll_ctl.clone();
|
let coll = coll_ctl.clone();
|
||||||
window.on_library_move_cursor(move |delta, extend| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
move_cursor(&w, &ctl, &coll, delta, extend);
|
.on_library_move_cursor(move |delta, extend| {
|
||||||
});
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
move_cursor(&w, &ctl, &coll, delta, extend);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -318,7 +324,7 @@ fn wire_grid_cursor_and_zoom(
|
|||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let coll = coll_ctl.clone();
|
let coll = coll_ctl.clone();
|
||||||
let on_open = on_open_image.clone();
|
let on_open = on_open_image.clone();
|
||||||
window.on_library_open_cursor(move || {
|
window.global::<Library>().on_library_open_cursor(move || {
|
||||||
let Some(w) = weak.upgrade() else { return };
|
let Some(w) = weak.upgrade() else { return };
|
||||||
// The cursor is a library ordinal and `paths` is the loaded
|
// The cursor is a library ordinal and `paths` is the loaded
|
||||||
// window, so the row is the difference. A cursor outside the
|
// window, so the row is the difference. A cursor outside the
|
||||||
@@ -333,8 +339,9 @@ fn wire_grid_cursor_and_zoom(
|
|||||||
w.set_active_view(View::Develop);
|
w.set_active_view(View::Develop);
|
||||||
// As on a click: the roll marks what is open, and centres on it
|
// As on a click: the roll marks what is open, and centres on it
|
||||||
// because this too begins a session.
|
// because this too begins a session.
|
||||||
w.set_library_roll_current(row.unwrap_or(0) as i32);
|
w.global::<Library>()
|
||||||
w.set_library_roll_centre(true);
|
.set_library_roll_current(row.unwrap_or(0) as i32);
|
||||||
|
w.global::<Library>().set_library_roll_centre(true);
|
||||||
on_open(path);
|
on_open(path);
|
||||||
report_position(&w, &ctl, row.unwrap_or(0));
|
report_position(&w, &ctl, row.unwrap_or(0));
|
||||||
write_place(&w, &ctl);
|
write_place(&w, &ctl);
|
||||||
@@ -350,33 +357,35 @@ fn wire_grid_cursor_and_zoom(
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_zoom_cells(move |delta| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
|
.on_library_zoom_cells(move |delta| {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
|
||||||
let current = w.get_library_cell_size();
|
let current = w.global::<Library>().get_library_cell_size();
|
||||||
let next = if delta > 0 {
|
let next = if delta > 0 {
|
||||||
current * 1.25
|
current * 1.25
|
||||||
} else {
|
} else {
|
||||||
current / 1.25
|
current / 1.25
|
||||||
}
|
}
|
||||||
.clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
|
.clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
|
||||||
|
|
||||||
if (next - current).abs() < 0.5 {
|
if (next - current).abs() < 0.5 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
w.set_library_cell_size(next);
|
w.global::<Library>().set_library_cell_size(next);
|
||||||
|
|
||||||
// No need to forget anything on a class change: the class is part
|
// No need to forget anything on a class change: the class is part
|
||||||
// of the request key, so cells that now want the large resolution
|
// of the request key, so cells that now want the large resolution
|
||||||
// simply miss and ask for it, while the 256px ones they already
|
// simply miss and ask for it, while the 256px ones they already
|
||||||
// hold stay served.
|
// hold stay served.
|
||||||
//
|
//
|
||||||
// Deferred, like every other geometry change. This one was still
|
// Deferred, like every other geometry change. This one was still
|
||||||
// reloading inline — a full catalog re-read and model rebuild per
|
// reloading inline — a full catalog re-read and model rebuild per
|
||||||
// step, which is what a wheel spun through six steps paid six
|
// step, which is what a wheel spun through six steps paid six
|
||||||
// times over.
|
// times over.
|
||||||
schedule_reload(&w, &ctl);
|
schedule_reload(&w, &ctl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
@@ -391,20 +400,22 @@ fn wire_grid_cursor_and_zoom(
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_pinch_cells(move |ratio| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
if !(ratio.is_finite() && ratio > 0.0) {
|
.on_library_pinch_cells(move |ratio| {
|
||||||
return;
|
let Some(w) = weak.upgrade() else { return };
|
||||||
}
|
if !(ratio.is_finite() && ratio > 0.0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let current = w.get_library_cell_size();
|
let current = w.global::<Library>().get_library_cell_size();
|
||||||
let next = (current * ratio).clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
|
let next = (current * ratio).clamp(MIN_CELL_SIZE, MAX_CELL_SIZE);
|
||||||
if (next - current).abs() < 0.5 {
|
if (next - current).abs() < 0.5 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
w.set_library_cell_size(next);
|
w.global::<Library>().set_library_cell_size(next);
|
||||||
schedule_reload(&w, &ctl);
|
schedule_reload(&w, &ctl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACES: FR-UI-4
|
// TRACES: FR-UI-4
|
||||||
@@ -418,10 +429,12 @@ fn wire_grid_cursor_and_zoom(
|
|||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
let coll = coll_ctl.clone();
|
let coll = coll_ctl.clone();
|
||||||
window.on_library_pinch_started(move || {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
crate::collections_ui::cancel_press(&w, &coll, &ctl.visible_ids());
|
.on_library_pinch_started(move || {
|
||||||
});
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
crate::collections_ui::cancel_press(&w, &coll, &ctl.visible_ids());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,25 +475,27 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_columns_changed(move || {
|
window
|
||||||
if let Some(w) = weak.upgrade() {
|
.global::<Library>()
|
||||||
if !w.get_library_visible() {
|
.on_library_columns_changed(move || {
|
||||||
return;
|
if let Some(w) = weak.upgrade() {
|
||||||
}
|
if !w.get_library_visible() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Deferred, and re-anchored when it lands — see
|
// Deferred, and re-anchored when it lands — see
|
||||||
// [`schedule_reload`]. A column change moves every cell in the
|
// [`schedule_reload`]. A column change moves every cell in the
|
||||||
// grid, because a cell is drawn at its absolute place in the
|
// grid, because a cell is drawn at its absolute place in the
|
||||||
// library and the row that resolves to is `index / columns`.
|
// library and the row that resolves to is `index / columns`.
|
||||||
// The viewport does not move with them, so without the
|
// The viewport does not move with them, so without the
|
||||||
// re-anchor the view is left pointing at rows the loaded window
|
// re-anchor the view is left pointing at rows the loaded window
|
||||||
// no longer covers and the grid draws nothing at all — the
|
// no longer covers and the grid draws nothing at all — the
|
||||||
// "gallery randomly goes blank until I scroll" report, whose
|
// "gallery randomly goes blank until I scroll" report, whose
|
||||||
// triggers are a resize, the sidebar opening, a zoom step, or
|
// triggers are a resize, the sidebar opening, a zoom step, or
|
||||||
// turning the tablet over.
|
// turning the tablet over.
|
||||||
schedule_reload(&w, &ctl);
|
schedule_reload(&w, &ctl);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// The viewport changed size, so the window it can usefully hold changed
|
// The viewport changed size, so the window it can usefully hold changed
|
||||||
@@ -489,23 +504,25 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_viewport_cells(move |on_screen| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
if !w.get_library_visible() {
|
.on_library_viewport_cells(move |on_screen| {
|
||||||
return;
|
let Some(w) = weak.upgrade() else { return };
|
||||||
}
|
if !w.get_library_visible() {
|
||||||
let on_screen = (on_screen.max(0) as usize).max(1);
|
return;
|
||||||
let window_size = (on_screen * SCREENFULS).max(MIN_WINDOW);
|
}
|
||||||
if window_size == *ctl.window.borrow() && on_screen == ctl.viewport_cells.get() {
|
let on_screen = (on_screen.max(0) as usize).max(1);
|
||||||
return;
|
let window_size = (on_screen * SCREENFULS).max(MIN_WINDOW);
|
||||||
}
|
if window_size == *ctl.window.borrow() && on_screen == ctl.viewport_cells.get() {
|
||||||
ctl.viewport_cells.set(on_screen);
|
return;
|
||||||
*ctl.window.borrow_mut() = window_size;
|
}
|
||||||
// Coalesced with the column change that almost always accompanies
|
ctl.viewport_cells.set(on_screen);
|
||||||
// it: resizing the cells alters both, and reloading once per report
|
*ctl.window.borrow_mut() = window_size;
|
||||||
// meant two full rebuilds per zoom step.
|
// Coalesced with the column change that almost always accompanies
|
||||||
schedule_reload(&w, &ctl);
|
// it: resizing the cells alters both, and reloading once per report
|
||||||
});
|
// meant two full rebuilds per zoom step.
|
||||||
|
schedule_reload(&w, &ctl);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scrolling moves the loaded window through the library.
|
// Scrolling moves the loaded window through the library.
|
||||||
@@ -515,118 +532,121 @@ fn wire_grid_sync_and_load(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
{
|
{
|
||||||
let weak = window.as_weak();
|
let weak = window.as_weak();
|
||||||
let ctl = ctl.clone();
|
let ctl = ctl.clone();
|
||||||
window.on_library_scrolled(move |first_visible| {
|
window
|
||||||
let Some(w) = weak.upgrade() else { return };
|
.global::<Library>()
|
||||||
let first_visible = first_visible.max(0) as usize;
|
.on_library_scrolled(move |first_visible| {
|
||||||
|
let Some(w) = weak.upgrade() else { return };
|
||||||
|
let first_visible = first_visible.max(0) as usize;
|
||||||
|
|
||||||
// **A report from a grid that is not on screen is not a scroll.**
|
// **A report from a grid that is not on screen is not a scroll.**
|
||||||
//
|
//
|
||||||
// The grid is gated on an `if`, so leaving it tears the whole
|
// The grid is gated on an `if`, so leaving it tears the whole
|
||||||
// subtree down — and a Flickable being destroyed passes its viewport
|
// subtree down — and a Flickable being destroyed passes its viewport
|
||||||
// through zero on the way out, which arrives here indistinguishable
|
// through zero on the way out, which arrives here indistinguishable
|
||||||
// from the user having flung the grid to the top. Everything below
|
// from the user having flung the grid to the top. Everything below
|
||||||
// then ran on the way *into* develop: the loaded window was reset to
|
// then ran on the way *into* develop: the loaded window was reset to
|
||||||
// offset zero, the model was rebuilt against the first rows of the
|
// offset zero, the model was rebuilt against the first rows of the
|
||||||
// library, and a thumbnail batch was issued for photographs nobody
|
// library, and a thumbnail batch was issued for photographs nobody
|
||||||
// had asked to see. Those rebuilds landed while the grid was still
|
// had asked to see. Those rebuilds landed while the grid was still
|
||||||
// being taken apart, which is what flashed the library over the
|
// being taken apart, which is what flashed the library over the
|
||||||
// develop view for the first few frames after a click — and on a
|
// develop view for the first few frames after a click — and on a
|
||||||
// remote library it also spent a burst of requests on the top of the
|
// remote library it also spent a burst of requests on the top of the
|
||||||
// catalog every single time an image was opened.
|
// catalog every single time an image was opened.
|
||||||
//
|
//
|
||||||
// **`library-visible`, not `show-library`, and the difference is the
|
// **`library-visible`, not `show-library`, and the difference is the
|
||||||
// whole bug.** `show-library` says "the library rather than develop"
|
// whole bug.** `show-library` says "the library rather than develop"
|
||||||
// and stays true while Settings, Import, People or the launch screen
|
// and stays true while Settings, Import, People or the launch screen
|
||||||
// replaces the window — all four of which take the grid down just as
|
// replaces the window — all four of which take the grid down just as
|
||||||
// opening an image does. So the teardown's scroll-to-zero passed this
|
// opening an image does. So the teardown's scroll-to-zero passed this
|
||||||
// guard, `resume_at` was set to 0, and coming back from any of those
|
// guard, `resume_at` was set to 0, and coming back from any of those
|
||||||
// four screens landed at the top of the library however deep in it the
|
// four screens landed at the top of the library however deep in it the
|
||||||
// user had been. The property is computed once in `app.slint` beside
|
// user had been. The property is computed once in `app.slint` beside
|
||||||
// the `if` it is spelled from, so the two cannot drift apart again.
|
// the `if` it is spelled from, so the two cannot drift apart again.
|
||||||
//
|
//
|
||||||
// The guard used to cover only `resume_at`, for a narrower version
|
// The guard used to cover only `resume_at`, for a narrower version
|
||||||
// of the same reason. It belongs over the whole handler.
|
// of the same reason. It belongs over the whole handler.
|
||||||
if !w.get_library_visible() {
|
if !w.get_library_visible() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember where the view is, so leaving for the develop view and
|
// Remember where the view is, so leaving for the develop view and
|
||||||
// coming back returns here. Latched on every event rather than read
|
// coming back returns here. Latched on every event rather than read
|
||||||
// at departure: by the time the grid is hidden its scroll position
|
// at departure: by the time the grid is hidden its scroll position
|
||||||
// is only in the Flickable, which is about to be destroyed.
|
// is only in the Flickable, which is about to be destroyed.
|
||||||
ctl.resume_at.set(first_visible);
|
ctl.resume_at.set(first_visible);
|
||||||
|
|
||||||
// TRACES: FR-UI-8
|
// TRACES: FR-UI-8
|
||||||
// And where the *next launch* will look for it. Debounced: a flick
|
// And where the *next launch* will look for it. Debounced: a flick
|
||||||
// reports several of these per screenful.
|
// reports several of these per screenful.
|
||||||
note_place(&w, &ctl);
|
note_place(&w, &ctl);
|
||||||
|
|
||||||
// The same position where a rebuilt grid will look for it.
|
// The same position where a rebuilt grid will look for it.
|
||||||
//
|
//
|
||||||
// `scroll-to` is read by `seek()`, which runs on a `scroll-token`
|
// `scroll-to` is read by `seek()`, which runs on a `scroll-token`
|
||||||
// change and on `init` — so writing it here without bumping the
|
// change and on `init` — so writing it here without bumping the
|
||||||
// token cannot move the grid that is on screen, and *is* what the
|
// token cannot move the grid that is on screen, and *is* what the
|
||||||
// next one reads when it is built. That is every route back to the
|
// next one reads when it is built. That is every route back to the
|
||||||
// grid at once: Settings, Import, People and the launch screen all
|
// grid at once: Settings, Import, People and the launch screen all
|
||||||
// tear the subtree down and rebuild it, and none of them goes
|
// tear the subtree down and rebuild it, and none of them goes
|
||||||
// through `on_back_to_library` to have the position replayed by
|
// through `on_back_to_library` to have the position replayed by
|
||||||
// hand. Without this they each rebuilt against whatever `scroll-to`
|
// hand. Without this they each rebuilt against whatever `scroll-to`
|
||||||
// was last *set* to — a stale scrub, or zero — and landed there.
|
// was last *set* to — a stale scrub, or zero — and landed there.
|
||||||
w.set_library_scroll_to(first_visible as i32);
|
w.global::<Library>()
|
||||||
|
.set_library_scroll_to(first_visible as i32);
|
||||||
|
|
||||||
// Move the timeline marker with the view. Scrolling the grid is a
|
// Move the timeline marker with the view. Scrolling the grid is a
|
||||||
// way of moving through time just as scrubbing is, and a marker
|
// way of moving through time just as scrubbing is, and a marker
|
||||||
// that only ever moved on a scrub sat still while the photographs
|
// that only ever moved on a scrub sat still while the photographs
|
||||||
// beside it advanced by months — the axis said "when you are" and
|
// beside it advanced by months — the axis said "when you are" and
|
||||||
// was wrong the moment the user touched the wheel.
|
// was wrong the moment the user touched the wheel.
|
||||||
//
|
//
|
||||||
// This runs before the reload guard below, which fires only a few
|
// This runs before the reload guard below, which fires only a few
|
||||||
// times per screenful; the marker has to follow every event or it
|
// times per screenful; the marker has to follow every event or it
|
||||||
// would advance in visible jerks.
|
// would advance in visible jerks.
|
||||||
//
|
//
|
||||||
// Only the marker is moved, not the whole histogram: rebuilding
|
// Only the marker is moved, not the whole histogram: rebuilding
|
||||||
// the bars means a `GROUP BY strftime` aggregate over the library,
|
// the bars means a `GROUP BY strftime` aggregate over the library,
|
||||||
// far too much for every event of a flick. The bars do not change
|
// far too much for every event of a flick. The bars do not change
|
||||||
// as the grid scrolls anyway — only where the marker sits on them.
|
// as the grid scrolls anyway — only where the marker sits on them.
|
||||||
//
|
//
|
||||||
// Re-running the scrub would be wrong for a second reason: it sets
|
// Re-running the scrub would be wrong for a second reason: it sets
|
||||||
// `scroll-to`, which would drive the grid from its own scroll.
|
// `scroll-to`, which would drive the grid from its own scroll.
|
||||||
{
|
{
|
||||||
let borrow = ctl.catalog.borrow();
|
let borrow = ctl.catalog.borrow();
|
||||||
if let Some(catalog) = borrow.as_ref() {
|
if let Some(catalog) = borrow.as_ref() {
|
||||||
if let Some(when) = capture_time_at(&ctl, catalog, first_visible) {
|
if let Some(when) = capture_time_at(&ctl, catalog, first_visible) {
|
||||||
*ctl.current_bucket.borrow_mut() = Some(when);
|
*ctl.current_bucket.borrow_mut() = Some(when);
|
||||||
|
|
||||||
let zoom = *ctl.timeline_zoom.borrow();
|
let zoom = *ctl.timeline_zoom.borrow();
|
||||||
let centre = *ctl.timeline_centre.borrow();
|
let centre = *ctl.timeline_centre.borrow();
|
||||||
if let Some(full) = catalog_span(catalog, &ctl) {
|
if let Some(full) = catalog_span(catalog, &ctl) {
|
||||||
let (from, to) = zoomed_span(full, zoom, centre);
|
let (from, to) = zoomed_span(full, zoom, centre);
|
||||||
w.set_library_current_bucket(when as i32);
|
w.set_library_current_bucket(when as i32);
|
||||||
w.set_library_current_fraction(
|
w.set_library_current_fraction(
|
||||||
((when - from) as f64 / (to - from).max(1) as f64).clamp(0.0, 1.0)
|
((when - from) as f64 / (to - from).max(1) as f64)
|
||||||
as f32,
|
.clamp(0.0, 1.0) as f32,
|
||||||
);
|
);
|
||||||
w.set_library_timeline_anchored(true);
|
w.set_library_timeline_anchored(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Centre the window on the view, so scrolling either way has
|
// Centre the window on the view, so scrolling either way has
|
||||||
// loaded rows ahead of it rather than only below.
|
// loaded rows ahead of it rather than only below.
|
||||||
let Some(offset) = window_move(
|
let Some(offset) = window_move(
|
||||||
first_visible,
|
first_visible,
|
||||||
*ctl.offset.borrow(),
|
*ctl.offset.borrow(),
|
||||||
*ctl.window.borrow(),
|
*ctl.window.borrow(),
|
||||||
ctl.viewport_cells.get(),
|
ctl.viewport_cells.get(),
|
||||||
w.get_library_total().max(0) as usize,
|
w.global::<Library>().get_library_total().max(0) as usize,
|
||||||
) else {
|
) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
*ctl.offset.borrow_mut() = offset;
|
*ctl.offset.borrow_mut() = offset;
|
||||||
load_window(&w, &ctl);
|
load_window(&w, &ctl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
|
|||||||
use slint::ComponentHandle;
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
use crate::library;
|
use crate::library;
|
||||||
use crate::AppWindow;
|
use crate::{AppWindow, Library};
|
||||||
|
|
||||||
use super::controller::{stop, LibraryController};
|
use super::controller::{stop, LibraryController};
|
||||||
|
|
||||||
@@ -499,7 +499,9 @@ fn refresh_local_count(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
let Some(catalog) = borrow.as_ref() else {
|
let Some(catalog) = borrow.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
window.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TRACES: FR-NC-6a
|
/// TRACES: FR-NC-6a
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use dr_types::FormatFilter;
|
|||||||
use slint::ComponentHandle;
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
use crate::library::{self, ScanMessage};
|
use crate::library::{self, ScanMessage};
|
||||||
use crate::{AppWindow, View};
|
use crate::{AppWindow, Library, View};
|
||||||
|
|
||||||
use super::controller::{stop, LibraryController};
|
use super::controller::{stop, LibraryController};
|
||||||
use super::offline::refresh_offline;
|
use super::offline::refresh_offline;
|
||||||
@@ -677,11 +677,12 @@ pub(super) fn schedule_reload(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
// change to `columns` moves every one of them and a viewport left
|
// change to `columns` moves every one of them and a viewport left
|
||||||
// where it was would be pointing at rows the window no longer covers.
|
// where it was would be pointing at rows the window no longer covers.
|
||||||
let window_size = *ctl_cb.window.borrow();
|
let window_size = *ctl_cb.window.borrow();
|
||||||
let total = w.get_library_total().max(0) as usize;
|
let total = w.global::<Library>().get_library_total().max(0) as usize;
|
||||||
*ctl_cb.offset.borrow_mut() = window_start(anchor, window_size, total);
|
*ctl_cb.offset.borrow_mut() = window_start(anchor, window_size, total);
|
||||||
load_window(&w, &ctl_cb);
|
load_window(&w, &ctl_cb);
|
||||||
w.set_library_scroll_to(anchor as i32);
|
w.global::<Library>().set_library_scroll_to(anchor as i32);
|
||||||
w.set_library_scroll_token(w.get_library_scroll_token() + 1);
|
w.global::<Library>()
|
||||||
|
.set_library_scroll_token(w.global::<Library>().get_library_scroll_token() + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replacing the slot drops the previous timer, which is what makes this
|
// Replacing the slot drops the previous timer, which is what makes this
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use dr_catalog::Catalog;
|
|||||||
use slint::{ComponentHandle, Model as _};
|
use slint::{ComponentHandle, Model as _};
|
||||||
|
|
||||||
use crate::library;
|
use crate::library;
|
||||||
use crate::{AppWindow, KeywordRow};
|
use crate::{AppWindow, KeywordRow, Library};
|
||||||
|
|
||||||
use super::controller::{stop, LibraryController};
|
use super::controller::{stop, LibraryController};
|
||||||
use super::window::load_window;
|
use super::window::load_window;
|
||||||
@@ -40,7 +40,7 @@ pub fn sync_ratings(window: &AppWindow, catalog: &Catalog, ids: &[dr_types::Imag
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let model = window.get_library_cells();
|
let model = window.global::<Library>().get_library_cells();
|
||||||
for (row, id) in ids.iter().enumerate() {
|
for (row, id) in ids.iter().enumerate() {
|
||||||
// Absent means unrated, which is a real state rather than missing data.
|
// Absent means unrated, which is a real state rather than missing data.
|
||||||
let j = found.get(id).copied().unwrap_or_default();
|
let j = found.get(id).copied().unwrap_or_default();
|
||||||
@@ -71,7 +71,9 @@ pub(super) fn refresh_rating_counts(window: &AppWindow, catalog: &Catalog) {
|
|||||||
// Shown for the same reason the star counts are: a filter that silently
|
// Shown for the same reason the star counts are: a filter that silently
|
||||||
// empties the grid reads as broken, and this one will legitimately be zero
|
// empties the grid reads as broken, and this one will legitimately be zero
|
||||||
// on a library nothing has been downloaded from yet.
|
// on a library nothing has been downloaded from yet.
|
||||||
window.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_local_count(library::local_original_count(catalog).unwrap_or(0) as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- keywords (FR-CAT-5, FR-CAT-6) ---------------------------------------
|
// --- keywords (FR-CAT-5, FR-CAT-6) ---------------------------------------
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use dr_catalog::Catalog;
|
|||||||
use slint::ComponentHandle;
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
use crate::library;
|
use crate::library;
|
||||||
use crate::{AppWindow, TimelineBar, View};
|
use crate::{AppWindow, Library, TimelineBar, View};
|
||||||
|
|
||||||
use super::controller::LibraryController;
|
use super::controller::LibraryController;
|
||||||
use super::filter_bar::{push_people_chips, refilter};
|
use super::filter_bar::{push_people_chips, refilter};
|
||||||
@@ -484,8 +484,12 @@ fn scrub_to(window: &AppWindow, ctl: &Rc<LibraryController>, when: i64) {
|
|||||||
// Move the viewport as well as the window. Cells are drawn at their
|
// Move the viewport as well as the window. Cells are drawn at their
|
||||||
// absolute place in the library, so loading rows around image 15,000 while
|
// absolute place in the library, so loading rows around image 15,000 while
|
||||||
// the viewport sits at row 0 shows an empty grid until the user scrolls.
|
// the viewport sits at row 0 shows an empty grid until the user scrolls.
|
||||||
window.set_library_scroll_to(position as i32);
|
window
|
||||||
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
|
.global::<Library>()
|
||||||
|
.set_library_scroll_to(position as i32);
|
||||||
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_scroll_token(window.global::<Library>().get_library_scroll_token() + 1);
|
||||||
load_window(window, ctl);
|
load_window(window, ctl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,7 +510,7 @@ fn scrub_to(window: &AppWindow, ctl: &Rc<LibraryController>, when: i64) {
|
|||||||
/// delete — after removing one frame from a wall of twenty thousand, the next
|
/// delete — after removing one frame from a wall of twenty thousand, the next
|
||||||
/// one you want is the one that just moved into its place.
|
/// one you want is the one that just moved into its place.
|
||||||
pub fn restore_position(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
pub fn restore_position(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
||||||
let total = window.get_library_total().max(0) as usize;
|
let total = window.global::<Library>().get_library_total().max(0) as usize;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -522,8 +526,12 @@ pub fn restore_position(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
// `resume_at` is what the grid last reported as its first visible image,
|
// `resume_at` is what the grid last reported as its first visible image,
|
||||||
// which is the thing the photographer is actually looking at.
|
// which is the thing the photographer is actually looking at.
|
||||||
let anchor = ctl.resume_at.get().min(total - 1);
|
let anchor = ctl.resume_at.get().min(total - 1);
|
||||||
window.set_library_scroll_to(anchor as i32);
|
window
|
||||||
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
|
.global::<Library>()
|
||||||
|
.set_library_scroll_to(anchor as i32);
|
||||||
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_scroll_token(window.global::<Library>().get_library_scroll_token() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format a bucket start for the histogram's hover label.
|
/// Format a bucket start for the histogram's hover label.
|
||||||
@@ -765,7 +773,7 @@ pub(crate) fn apply_place(
|
|||||||
|
|
||||||
// The filter and the scope have both moved the window to the top; this is
|
// The filter and the scope have both moved the window to the top; this is
|
||||||
// what puts it back, and it has to run after them for that reason.
|
// what puts it back, and it has to run after them for that reason.
|
||||||
let total = window.get_library_total().max(0) as usize;
|
let total = window.global::<Library>().get_library_total().max(0) as usize;
|
||||||
let resolved = resolved.filter(|(at, _)| *at < total);
|
let resolved = resolved.filter(|(at, _)| *at < total);
|
||||||
match resolved {
|
match resolved {
|
||||||
Some((at, _)) => {
|
Some((at, _)) => {
|
||||||
@@ -786,16 +794,18 @@ pub(crate) fn apply_place(
|
|||||||
if let Some((at, Found::Photograph)) = resolved {
|
if let Some((at, Found::Photograph)) = resolved {
|
||||||
if let Some(open) = ctl.open_image.borrow().clone() {
|
if let Some(open) = ctl.open_image.borrow().clone() {
|
||||||
window.set_active_view(View::Develop);
|
window.set_active_view(View::Develop);
|
||||||
window.set_library_roll_centre(true);
|
window.global::<Library>().set_library_roll_centre(true);
|
||||||
let offset = *ctl.offset.borrow();
|
let offset = *ctl.offset.borrow();
|
||||||
if let Some(row) = at.checked_sub(offset) {
|
if let Some(row) = at.checked_sub(offset) {
|
||||||
window.set_library_roll_current(row as i32);
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_roll_current(row as i32);
|
||||||
}
|
}
|
||||||
open(place.path.clone());
|
open(place.path.clone());
|
||||||
// After the open, which resets the readout to "1 of 1" on its
|
// After the open, which resets the readout to "1 of 1" on its
|
||||||
// way in — the same ordering `report_position` documents.
|
// way in — the same ordering `report_position` documents.
|
||||||
window.set_index(at as i32);
|
window.set_index(at as i32);
|
||||||
window.set_total(window.get_library_total());
|
window.set_total(window.global::<Library>().get_library_total());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -884,7 +894,9 @@ fn push_filter(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
Some(dr_types::FlagState::Reject) => 2,
|
Some(dr_types::FlagState::Reject) => 2,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
});
|
});
|
||||||
window.set_library_local_only(filter.local_only);
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_local_only(filter.local_only);
|
||||||
ctl.local_only.set(filter.local_only);
|
ctl.local_only.set(filter.local_only);
|
||||||
window.set_library_filter_eyes_open(filter.eyes_open);
|
window.set_library_filter_eyes_open(filter.eyes_open);
|
||||||
push_people_chips(window, ctl);
|
push_people_chips(window, ctl);
|
||||||
@@ -925,7 +937,7 @@ pub(super) fn resume_position(
|
|||||||
coll: &Rc<crate::collections_ui::CollectionsController>,
|
coll: &Rc<crate::collections_ui::CollectionsController>,
|
||||||
focus: Option<usize>,
|
focus: Option<usize>,
|
||||||
) {
|
) {
|
||||||
let total = window.get_library_total().max(0) as usize;
|
let total = window.global::<Library>().get_library_total().max(0) as usize;
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
// Files named on the command line: there is no grid behind them, and
|
// Files named on the command line: there is no grid behind them, and
|
||||||
// an ordinal read off the readout would name a row of nothing.
|
// an ordinal read off the readout would name a row of nothing.
|
||||||
@@ -955,12 +967,16 @@ pub(super) fn resume_position(
|
|||||||
// Before the grid is shown, not after: the markup gates it on an `if`, and
|
// Before the grid is shown, not after: the markup gates it on an `if`, and
|
||||||
// the rebuilt Flickable reads `scroll-to` in its `init`. Setting these
|
// the rebuilt Flickable reads `scroll-to` in its `init`. Setting these
|
||||||
// afterwards would leave that init to run against the previous position.
|
// afterwards would leave that init to run against the previous position.
|
||||||
window.set_library_scroll_to(anchor as i32);
|
window
|
||||||
window.set_library_scroll_token(window.get_library_scroll_token() + 1);
|
.global::<Library>()
|
||||||
|
.set_library_scroll_to(anchor as i32);
|
||||||
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_scroll_token(window.global::<Library>().get_library_scroll_token() + 1);
|
||||||
|
|
||||||
if let Some(at) = focus {
|
if let Some(at) = focus {
|
||||||
coll.set_cursor(Some(at));
|
coll.set_cursor(Some(at));
|
||||||
window.set_library_cursor(at as i32);
|
window.global::<Library>().set_library_cursor(at as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The view has moved, so the capture-time marker has to move with it.
|
// The view has moved, so the capture-time marker has to move with it.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use std::sync::mpsc::Receiver;
|
|||||||
use slint::{ComponentHandle, Model as _};
|
use slint::{ComponentHandle, Model as _};
|
||||||
|
|
||||||
use crate::library::{self, ThumbnailMessage};
|
use crate::library::{self, ThumbnailMessage};
|
||||||
use crate::{AppWindow, LibraryCell};
|
use crate::{AppWindow, Library, LibraryCell};
|
||||||
|
|
||||||
use super::controller::{stop, LibraryController, LibraryFacts};
|
use super::controller::{stop, LibraryController, LibraryFacts};
|
||||||
use super::filter_bar::period_headings;
|
use super::filter_bar::period_headings;
|
||||||
@@ -245,8 +245,8 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
// Read before it is overwritten: the property still holds what the library
|
// Read before it is overwritten: the property still holds what the library
|
||||||
// was last time this ran, and a library that has become shorter is the
|
// was last time this ran, and a library that has become shorter is the
|
||||||
// signal that something was deleted out from under the view.
|
// signal that something was deleted out from under the view.
|
||||||
let was = window.get_library_total().max(0) as usize;
|
let was = window.global::<Library>().get_library_total().max(0) as usize;
|
||||||
window.set_library_total(total as i32);
|
window.global::<Library>().set_library_total(total as i32);
|
||||||
let shrank = total < was;
|
let shrank = total < was;
|
||||||
|
|
||||||
// Clamp so a scrub to the very end still fills the window rather than
|
// Clamp so a scrub to the very end still fills the window rather than
|
||||||
@@ -278,7 +278,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
//
|
//
|
||||||
// The trash is never reorderable. It is a view of what was deleted, ordered
|
// The trash is never reorderable. It is a view of what was deleted, ordered
|
||||||
// by when, and it is not a collection at all.
|
// by when, and it is not a collection at all.
|
||||||
window.set_library_reorderable(
|
window.global::<Library>().set_library_reorderable(
|
||||||
!trash
|
!trash
|
||||||
&& scope.is_some_and(|c| {
|
&& scope.is_some_and(|c| {
|
||||||
dr_catalog::collections::orders_manually(catalog.connection(), c).unwrap_or(false)
|
dr_catalog::collections::orders_manually(catalog.connection(), c).unwrap_or(false)
|
||||||
@@ -321,7 +321,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
// Month headings. The grid is ordered by capture time, so without these a
|
// Month headings. The grid is ordered by capture time, so without these a
|
||||||
// wall of thumbnails gives no sense of *when* you are looking — the
|
// wall of thumbnails gives no sense of *when* you are looking — the
|
||||||
// sidebar says it, but only if you consult it.
|
// sidebar says it, but only if you consult it.
|
||||||
let columns = window.get_library_columns().max(1) as usize;
|
let columns = window.global::<Library>().get_library_columns().max(1) as usize;
|
||||||
let headings = period_headings(
|
let headings = period_headings(
|
||||||
cells.iter().map(|c| {
|
cells.iter().map(|c| {
|
||||||
c.captured_at.map(|t| {
|
c.captured_at.map(|t| {
|
||||||
@@ -335,7 +335,7 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
|
|
||||||
// What the outgoing model is still holding — see [`hold_thumbnails`].
|
// What the outgoing model is still holding — see [`hold_thumbnails`].
|
||||||
let held = {
|
let held = {
|
||||||
let previous = window.get_library_cells();
|
let previous = window.global::<Library>().get_library_cells();
|
||||||
let ids = ctl.image_ids.borrow();
|
let ids = ctl.image_ids.borrow();
|
||||||
let classes = ctl.thumb_class.borrow();
|
let classes = ctl.thumb_class.borrow();
|
||||||
hold_thumbnails(&previous, &ids, &classes)
|
hold_thumbnails(&previous, &ids, &classes)
|
||||||
@@ -391,7 +391,9 @@ pub(super) fn load_window(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
// those fetches would have filled are absent from `requested` above, so the
|
// those fetches would have filled are absent from `requested` above, so the
|
||||||
// batch started below asks for them again.
|
// batch started below asks for them again.
|
||||||
ctl.generation.set(ctl.generation.get().wrapping_add(1));
|
ctl.generation.set(ctl.generation.get().wrapping_add(1));
|
||||||
window.set_library_cells(slint::ModelRc::new(slint::VecModel::from(rows)));
|
window
|
||||||
|
.global::<Library>()
|
||||||
|
.set_library_cells(slint::ModelRc::new(slint::VecModel::from(rows)));
|
||||||
|
|
||||||
// The model is fresh, so the "in this many collections" badges are all zero
|
// The model is fresh, so the "in this many collections" badges are all zero
|
||||||
// until refilled. One query for the whole window, not one per cell.
|
// until refilled. One query for the whole window, not one per cell.
|
||||||
@@ -481,7 +483,7 @@ fn request_thumbnails(window: &AppWindow, ctl: &Rc<LibraryController>) {
|
|||||||
// The drawn cell size decides which class to ask for. Chosen once for the
|
// The drawn cell size decides which class to ask for. Chosen once for the
|
||||||
// batch rather than per row, and carried through to the drain so a cell it
|
// batch rather than per row, and carried through to the drain so a cell it
|
||||||
// fills can record what it is now showing.
|
// fills can record what it is now showing.
|
||||||
let cell_pixels = window.get_library_cell_size().max(1.0) as u32;
|
let cell_pixels = window.global::<Library>().get_library_cell_size().max(1.0) as u32;
|
||||||
let class = dr_thumbs::ThumbSize::for_cell(cell_pixels);
|
let class = dr_thumbs::ThumbSize::for_cell(cell_pixels);
|
||||||
|
|
||||||
let mut wanted: Vec<library::ThumbnailRequest> = {
|
let mut wanted: Vec<library::ThumbnailRequest> = {
|
||||||
@@ -602,7 +604,9 @@ pub fn refresh_thumbnail(
|
|||||||
|
|
||||||
// What the grid is drawing at, so the cell can be corrected on screen
|
// What the grid is drawing at, so the cell can be corrected on screen
|
||||||
// rather than only on disk.
|
// rather than only on disk.
|
||||||
let drawn = dr_thumbs::ThumbSize::for_cell(window.get_library_cell_size().max(1.0) as u32);
|
let drawn = dr_thumbs::ThumbSize::for_cell(
|
||||||
|
window.global::<Library>().get_library_cell_size().max(1.0) as u32,
|
||||||
|
);
|
||||||
|
|
||||||
for class in [dr_thumbs::ThumbSize::Grid, dr_thumbs::ThumbSize::Large] {
|
for class in [dr_thumbs::ThumbSize::Grid, dr_thumbs::ThumbSize::Large] {
|
||||||
let (w, h, rgba) = match render(class.edge()) {
|
let (w, h, rgba) = match render(class.edge()) {
|
||||||
@@ -634,7 +638,7 @@ pub fn refresh_thumbnail(
|
|||||||
// Straight into the model, so the edit is on the cell the moment
|
// Straight into the model, so the edit is on the cell the moment
|
||||||
// the grid comes back rather than after a scroll evicts and
|
// the grid comes back rather than after a scroll evicts and
|
||||||
// refetches it.
|
// refetches it.
|
||||||
let model = window.get_library_cells();
|
let model = window.global::<Library>().get_library_cells();
|
||||||
if let Some(mut cell) = model.row_data(row) {
|
if let Some(mut cell) = model.row_data(row) {
|
||||||
cell.thumbnail = to_slint_image(w, h, &rgba);
|
cell.thumbnail = to_slint_image(w, h, &rgba);
|
||||||
cell.has_thumb = true;
|
cell.has_thumb = true;
|
||||||
@@ -707,7 +711,7 @@ fn drain_thumbnails(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let model = w.get_library_cells();
|
let model = w.global::<Library>().get_library_cells();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let msg = match rx.try_recv() {
|
let msg = match rx.try_recv() {
|
||||||
|
|||||||
+51
-158
@@ -7,7 +7,7 @@ import { CropOverlay } from "crop.slint";
|
|||||||
import { HistoryPanel, HistoryRow, Steps } from "history.slint";
|
import { HistoryPanel, HistoryRow, Steps } from "history.slint";
|
||||||
import { LaunchScreen } from "launch.slint";
|
import { LaunchScreen } from "launch.slint";
|
||||||
import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
|
import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
|
||||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip } from "library.slint";
|
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip, Library } from "library.slint";
|
||||||
import { GestureRow } from "gestures.slint";
|
import { GestureRow } from "gestures.slint";
|
||||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||||
import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, CollectionMenu,
|
import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, CollectionMenu,
|
||||||
@@ -32,6 +32,7 @@ export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
|
|||||||
// panels take their wiring this way and why there is one global per family.
|
// panels take their wiring this way and why there is one global per family.
|
||||||
export { Develop, Capture, Levels, Peaking, Adjustments, Framing, Transfer, Masking, Repair, Steps }
|
export { Develop, Capture, Levels, Peaking, Adjustments, Framing, Transfer, Masking, Repair, Steps }
|
||||||
export { Collections }
|
export { Collections }
|
||||||
|
export { Library }
|
||||||
|
|
||||||
/// Which top-level screen is on display. Exactly one at a time — replacing
|
/// Which top-level screen is on display. Exactly one at a time — replacing
|
||||||
/// the `show-launch` / `show-library` / `show-identity` booleans, with
|
/// the `show-launch` / `show-library` / `show-identity` booleans, with
|
||||||
@@ -288,8 +289,6 @@ export component AppWindow inherits Window {
|
|||||||
/// the two can drift apart again.
|
/// the two can drift apart again.
|
||||||
out property <bool> library-visible:
|
out property <bool> library-visible:
|
||||||
root.active-page == Page.none && root.active-view == View.library;
|
root.active-page == Page.none && root.active-view == View.library;
|
||||||
in-out property <[LibraryCell]> library-cells;
|
|
||||||
in property <int> library-total: 0;
|
|
||||||
// --- background activity (FR-CAT-1, FR-NC-6c) ---
|
// --- background activity (FR-CAT-1, FR-NC-6c) ---
|
||||||
//
|
//
|
||||||
// Every background job in one register, filled by `activity.rs`. The
|
// Every background job in one register, filled by `activity.rs`. The
|
||||||
@@ -338,28 +337,9 @@ export component AppWindow inherits Window {
|
|||||||
in property <bool> library-offline: false;
|
in property <bool> library-offline: false;
|
||||||
in property <string> library-offline-reason: "";
|
in property <string> library-offline-reason: "";
|
||||||
in property <string> library-offline-since: "";
|
in property <string> library-offline-since: "";
|
||||||
// Narrows the grid to images whose RAW is stored locally — the ones that
|
|
||||||
// can actually be opened while offline. Off by default: the catalog is the
|
|
||||||
// library, and hiding most of it the moment a connection drops would read
|
|
||||||
// as data loss rather than as a filter.
|
|
||||||
in-out property <bool> library-local-only: false;
|
|
||||||
in property <int> library-local-count: 0;
|
|
||||||
callback library-toggle-local-only();
|
|
||||||
/// Whether the grid is narrowed to the timeline's visible span.
|
/// Whether the grid is narrowed to the timeline's visible span.
|
||||||
in-out property <bool> library-range-active: false;
|
in-out property <bool> library-range-active: false;
|
||||||
callback library-toggle-date-range();
|
callback library-toggle-date-range();
|
||||||
/// TRACES: FR-CAT-5
|
|
||||||
callback library-clear-selection();
|
|
||||||
callback library-select-all();
|
|
||||||
/// TRACES: FR-CAT-7
|
|
||||||
/// Whether the grid is showing something with a manual order to change —
|
|
||||||
/// a single manual collection, not a set and not a saved filter.
|
|
||||||
in property <bool> library-reorderable: false;
|
|
||||||
/// Move the selection beside the photograph at this row of the loaded
|
|
||||||
/// window: after it where the drop landed on the cell's trailing half,
|
|
||||||
/// before it otherwise.
|
|
||||||
callback library-reorder-to(int, bool);
|
|
||||||
callback library-collection-from-selection(string);
|
|
||||||
/// TRACES: FR-CAT-6
|
/// TRACES: FR-CAT-6
|
||||||
in property <string> library-range-from;
|
in property <string> library-range-from;
|
||||||
in property <string> library-range-to;
|
in property <string> library-range-to;
|
||||||
@@ -425,7 +405,6 @@ export component AppWindow inherits Window {
|
|||||||
callback library-timeline-pinch(float);
|
callback library-timeline-pinch(float);
|
||||||
callback library-timeline-pan(float);
|
callback library-timeline-pan(float);
|
||||||
callback library-timeline-zoom(int);
|
callback library-timeline-zoom(int);
|
||||||
in-out property <int> library-columns: 1;
|
|
||||||
in property <bool> library-syncing: false;
|
in property <bool> library-syncing: false;
|
||||||
/// TRACES: FR-CAT-3 | FR-NC-3
|
/// TRACES: FR-CAT-3 | FR-NC-3
|
||||||
/// The whole-library thumbnail pass, launched from the settings page.
|
/// The whole-library thumbnail pass, launched from the settings page.
|
||||||
@@ -496,42 +475,10 @@ export component AppWindow inherits Window {
|
|||||||
callback identity-check-coverage();
|
callback identity-check-coverage();
|
||||||
callback identity-delete-all();
|
callback identity-delete-all();
|
||||||
callback library-thumbnail-all();
|
callback library-thumbnail-all();
|
||||||
in-out property <length> library-cell-size: 180px;
|
|
||||||
/// Whether this session is a touch one, which decides if the rating strip
|
|
||||||
/// waits to be hovered or stands open. Seeded from the platform by Rust
|
|
||||||
/// and latched by the grid on the first finger it sees.
|
|
||||||
in-out property <bool> library-touched: false;
|
|
||||||
callback library-zoom-cells(int);
|
|
||||||
/// A pinch step on the grid: the ratio since the last update.
|
|
||||||
callback library-pinch-cells(float);
|
|
||||||
/// A pinch on the grid has begun, so the press that opened it was not one.
|
|
||||||
callback library-pinch-started();
|
|
||||||
in property <int> library-scroll-to: 0;
|
|
||||||
in property <int> library-scroll-token: 0;
|
|
||||||
/// Which row of the loaded window is the photograph currently open in
|
|
||||||
/// develop, so the roll can mark it. `-1` when it is not in the window.
|
|
||||||
in property <int> library-roll-current: -1;
|
|
||||||
/// Whether the photo roll is out. Owned here rather than by the roll, so
|
/// Whether the photo roll is out. Owned here rather than by the roll, so
|
||||||
/// it survives the develop view being torn down and rebuilt.
|
/// it survives the develop view being torn down and rebuilt.
|
||||||
in-out property <bool> roll-open: false;
|
in-out property <bool> roll-open: false;
|
||||||
/// Centre the roll on the open photograph the next time the strip settles.
|
|
||||||
///
|
|
||||||
/// Raised by Rust when a develop session begins — a photograph opened from
|
|
||||||
/// the grid — and cleared by the strip itself once honoured. Owned here for
|
|
||||||
/// the same reason `roll-open` is: the develop view is rebuilt whenever an
|
|
||||||
/// overlay screen covers it, and a request raised before that must survive
|
|
||||||
/// it. See `PhotoRoll::centre-request`.
|
|
||||||
in-out property <bool> library-roll-centre: false;
|
|
||||||
/// A photograph was chosen from the roll: the row within the loaded
|
|
||||||
/// window, which is what a cell click reports too.
|
|
||||||
callback library-roll-pick(int);
|
|
||||||
callback library-sync-now();
|
callback library-sync-now();
|
||||||
callback library-columns-changed();
|
|
||||||
callback library-scrolled(int);
|
|
||||||
/// How many cells the grid's viewport shows at once. Rust sizes the
|
|
||||||
/// loaded window from it — see `library_ui::window_move`.
|
|
||||||
callback library-viewport-cells(int);
|
|
||||||
callback library-cell-clicked(int);
|
|
||||||
callback library-rescan();
|
callback library-rescan();
|
||||||
/// Grid → launch screen, to pick a different folder or account.
|
/// Grid → launch screen, to pick a different folder or account.
|
||||||
callback library-change();
|
callback library-change();
|
||||||
@@ -559,11 +506,6 @@ export component AppWindow inherits Window {
|
|||||||
/// Ask about a collection's local copies: the tray on a row, or a long
|
/// Ask about a collection's local copies: the tray on a row, or a long
|
||||||
/// press anywhere on it.
|
/// press anywhere on it.
|
||||||
callback collection-offline-menu(int);
|
callback collection-offline-menu(int);
|
||||||
/// 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
|
|
||||||
/// currently being shown.
|
|
||||||
callback library-file-in-collection(int, bool);
|
|
||||||
/// TRACES: FR-CAT-5 | FR-CAT-6
|
/// TRACES: FR-CAT-5 | FR-CAT-6
|
||||||
/// Keywording the grid's selection. The catalog has been searchable by
|
/// Keywording the grid's selection. The catalog has been searchable by
|
||||||
/// keyword since it existed and there was nowhere to type one; this is it.
|
/// keyword since it existed and there was nowhere to type one; this is it.
|
||||||
@@ -583,58 +525,9 @@ export component AppWindow inherits Window {
|
|||||||
/// Take a keyword off the selection. Never deletes the keyword itself —
|
/// Take a keyword off the selection. Never deletes the keyword itself —
|
||||||
/// it stays in the vocabulary and on every other photograph that carries it.
|
/// it stays in the vocabulary and on every other photograph that carries it.
|
||||||
callback library-unassign-keyword(string);
|
callback library-unassign-keyword(string);
|
||||||
/// TRACES: FR-UI-2
|
|
||||||
/// Whether a tap in the grid selects rather than opens, and the button
|
|
||||||
/// that turns it on. The long press does the same thing without it.
|
|
||||||
in property <bool> library-select-mode: false;
|
|
||||||
callback library-toggle-select-mode();
|
|
||||||
/// TRACES: FR-CAT-7 | FR-UI-4
|
|
||||||
/// The row a press has held long enough to pick up, or `-1`.
|
|
||||||
///
|
|
||||||
/// Set by the same hold that turns on selection mode and cleared by the
|
|
||||||
/// grid when the press ends — `in-out` because both ends write it. See
|
|
||||||
/// `held-row` in `library.slint` for what it draws and what it stops.
|
|
||||||
in-out property <int> library-held-row: -1;
|
|
||||||
/// A press on a cell ended, so the long-press timer can be cancelled.
|
|
||||||
callback library-cell-press-ended();
|
|
||||||
|
|
||||||
// --- drag and drop ---
|
|
||||||
//
|
|
||||||
// Slint's own `DragArea`/`DropArea` carry this: pointer capture, the
|
|
||||||
// click-versus-drag threshold, arbitration against the grid's Flickable,
|
|
||||||
// and the image drawn under the cursor. So there is no badge to position
|
|
||||||
// and no hover state to mirror here — only the payload, which Rust builds
|
|
||||||
// from the selection, and the count the header shows.
|
|
||||||
/// The drag payload, built by Rust from the current selection.
|
|
||||||
pure callback library-drag-payload() -> data-transfer;
|
|
||||||
/// The bitmap that travels under the cursor: one thumbnail, or a fanned
|
|
||||||
/// stack where several images are being dragged. Composited in Rust.
|
|
||||||
in property <image> library-drag-image;
|
|
||||||
/// A drag began on a cell, so an unselected one can be promoted before the
|
|
||||||
/// payload is read.
|
|
||||||
callback library-drag-started(int);
|
|
||||||
callback library-drag-finished();
|
|
||||||
in property <int> library-selected-count: 0;
|
|
||||||
|
|
||||||
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. Opens the
|
|
||||||
// membership sheet, whose state (`Collections.membership-open` and
|
|
||||||
// `.membership-rows`) is on the sidebar's own global — only the library
|
|
||||||
// side knows the selection has changed enough to be worth recomputing
|
|
||||||
// the sheet against.
|
|
||||||
callback library-open-membership();
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
in property <int> library-cursor: -1;
|
|
||||||
callback library-move-cursor(int, bool);
|
|
||||||
callback library-open-cursor();
|
|
||||||
|
|
||||||
// --- ratings and flags (FR-CAT-5, FR-CULL-4) ---
|
// --- ratings and flags (FR-CAT-5, FR-CULL-4) ---
|
||||||
//
|
//
|
||||||
@@ -1560,7 +1453,7 @@ in property <bool> panel-visible: true;
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
rows: Collections.collection-rows;
|
rows: Collections.collection-rows;
|
||||||
selected-id: root.collection-selected;
|
selected-id: root.collection-selected;
|
||||||
total-images: root.library-total;
|
total-images: Library.library-total;
|
||||||
error: Collections.collection-error;
|
error: Collections.collection-error;
|
||||||
|
|
||||||
// TRACES: FR-NC-6a
|
// TRACES: FR-NC-6a
|
||||||
@@ -1592,7 +1485,7 @@ in property <bool> panel-visible: true;
|
|||||||
rename-cancel() => { Collections.collection-rename-cancel(); }
|
rename-cancel() => { Collections.collection-rename-cancel(); }
|
||||||
trash-count: Collections.trash-count;
|
trash-count: Collections.trash-count;
|
||||||
trash-label: Collections.trash-label;
|
trash-label: Collections.trash-label;
|
||||||
selected-count: root.library-selected-count;
|
selected-count: Library.library-selected-count;
|
||||||
|
|
||||||
dropped-on(id) => { Collections.collection-dropped(id); }
|
dropped-on(id) => { Collections.collection-dropped(id); }
|
||||||
collection-drag-payload(id) => { return Collections.collection-drag-payload(id); }
|
collection-drag-payload(id) => { return Collections.collection-drag-payload(id); }
|
||||||
@@ -1610,8 +1503,8 @@ in property <bool> panel-visible: true;
|
|||||||
// 768 logical pixels, which fits the title and the status
|
// 768 logical pixels, which fits the title and the status
|
||||||
// line but not the library's buttons beside them.
|
// line but not the library's buttons beside them.
|
||||||
expanded: root.expanded;
|
expanded: root.expanded;
|
||||||
cells: root.library-cells;
|
cells: Library.library-cells;
|
||||||
total: root.library-total;
|
total: Library.library-total;
|
||||||
scanning: root.library-scanning;
|
scanning: root.library-scanning;
|
||||||
opening: root.library-opening;
|
opening: root.library-opening;
|
||||||
scan-status: root.library-status;
|
scan-status: root.library-status;
|
||||||
@@ -1621,17 +1514,17 @@ in property <bool> panel-visible: true;
|
|||||||
offline-reason: root.library-offline-reason;
|
offline-reason: root.library-offline-reason;
|
||||||
offline-since: root.library-offline-since;
|
offline-since: root.library-offline-since;
|
||||||
retry-connection() => { root.library-retry-connection(); }
|
retry-connection() => { root.library-retry-connection(); }
|
||||||
local-only: root.library-local-only;
|
local-only: Library.library-local-only;
|
||||||
local-count: root.library-local-count;
|
local-count: Library.library-local-count;
|
||||||
toggle-local-only() => { root.library-toggle-local-only(); }
|
toggle-local-only() => { Library.library-toggle-local-only(); }
|
||||||
range-active: root.library-range-active;
|
range-active: root.library-range-active;
|
||||||
toggle-date-range() => { root.library-toggle-date-range(); }
|
toggle-date-range() => { root.library-toggle-date-range(); }
|
||||||
clear-selection() => { root.library-clear-selection(); }
|
clear-selection() => { Library.library-clear-selection(); }
|
||||||
select-all() => { root.library-select-all(); }
|
select-all() => { Library.library-select-all(); }
|
||||||
reorderable: root.library-reorderable;
|
reorderable: Library.library-reorderable;
|
||||||
reorder-to(row, after) => { root.library-reorder-to(row, after); }
|
reorder-to(row, after) => { Library.library-reorder-to(row, after); }
|
||||||
collection-from-selection(name) => {
|
collection-from-selection(name) => {
|
||||||
root.library-collection-from-selection(name);
|
Library.library-collection-from-selection(name);
|
||||||
}
|
}
|
||||||
range-from: root.library-range-from;
|
range-from: root.library-range-from;
|
||||||
range-to: root.library-range-to;
|
range-to: root.library-range-to;
|
||||||
@@ -1653,7 +1546,7 @@ in property <bool> panel-visible: true;
|
|||||||
timeline-label: root.library-timeline-label;
|
timeline-label: root.library-timeline-label;
|
||||||
window-label: root.library-window-label;
|
window-label: root.library-window-label;
|
||||||
offset: root.library-offset;
|
offset: root.library-offset;
|
||||||
selected-count: root.library-selected-count;
|
selected-count: Library.library-selected-count;
|
||||||
scope-label: Collections.collection-scope-label;
|
scope-label: Collections.collection-scope-label;
|
||||||
settings-armed: Transfer.armed;
|
settings-armed: Transfer.armed;
|
||||||
paste-settings-to-selection => {
|
paste-settings-to-selection => {
|
||||||
@@ -1664,7 +1557,7 @@ in property <bool> panel-visible: true;
|
|||||||
// sheet says it would act on and what the apply handler
|
// sheet says it would act on and what the apply handler
|
||||||
// reads back to decide it means the batch.
|
// reads back to decide it means the batch.
|
||||||
open-presets => {
|
open-presets => {
|
||||||
root.preset-apply-count = root.library-selected-count;
|
root.preset-apply-count = Library.library-selected-count;
|
||||||
root.preset-can-save = false;
|
root.preset-can-save = false;
|
||||||
root.presets-open = true;
|
root.presets-open = true;
|
||||||
}
|
}
|
||||||
@@ -1689,23 +1582,23 @@ in property <bool> panel-visible: true;
|
|||||||
timeline-pan(d) => { root.library-timeline-pan(d); }
|
timeline-pan(d) => { root.library-timeline-pan(d); }
|
||||||
timeline-zoom(d) => { root.library-timeline-zoom(d); }
|
timeline-zoom(d) => { root.library-timeline-zoom(d); }
|
||||||
syncing: root.library-syncing;
|
syncing: root.library-syncing;
|
||||||
requested-cell-size: root.library-cell-size;
|
requested-cell-size: Library.library-cell-size;
|
||||||
touched <=> root.library-touched;
|
touched <=> Library.library-touched;
|
||||||
zoom-cells(d) => { root.library-zoom-cells(d); }
|
zoom-cells(d) => { Library.library-zoom-cells(d); }
|
||||||
pinch-cells(r) => { root.library-pinch-cells(r); }
|
pinch-cells(r) => { Library.library-pinch-cells(r); }
|
||||||
pinch-started() => { root.library-pinch-started(); }
|
pinch-started() => { Library.library-pinch-started(); }
|
||||||
scroll-to: root.library-scroll-to;
|
scroll-to: Library.library-scroll-to;
|
||||||
scroll-token: root.library-scroll-token;
|
scroll-token: Library.library-scroll-token;
|
||||||
sync-now() => { root.library-sync-now(); }
|
sync-now() => { root.library-sync-now(); }
|
||||||
columns-changed(n) => {
|
columns-changed(n) => {
|
||||||
root.library-columns = n;
|
Library.library-columns = n;
|
||||||
// Which cell begins a row just changed, and month headings sit on
|
// Which cell begins a row just changed, and month headings sit on
|
||||||
// row-leading cells — so they must be recomputed, not just moved.
|
// row-leading cells — so they must be recomputed, not just moved.
|
||||||
root.library-columns-changed();
|
Library.library-columns-changed();
|
||||||
}
|
}
|
||||||
scrolled(i) => { root.library-scrolled(i); }
|
scrolled(i) => { Library.library-scrolled(i); }
|
||||||
viewport-cells-changed(n) => { root.library-viewport-cells(n); }
|
viewport-cells-changed(n) => { Library.library-viewport-cells(n); }
|
||||||
cell-clicked(i) => { root.library-cell-clicked(i); }
|
cell-clicked(i) => { Library.library-cell-clicked(i); }
|
||||||
rescan() => { root.library-rescan(); }
|
rescan() => { root.library-rescan(); }
|
||||||
can-import: root.import-supported;
|
can-import: root.import-supported;
|
||||||
open-import() => { root.import-open(); }
|
open-import() => { root.import-open(); }
|
||||||
@@ -1713,35 +1606,35 @@ in property <bool> panel-visible: true;
|
|||||||
open-people() => { root.identity-open(); }
|
open-people() => { root.identity-open(); }
|
||||||
|
|
||||||
cell-pressed(i, ctrl, shift) => {
|
cell-pressed(i, ctrl, shift) => {
|
||||||
root.library-cell-pressed(i, ctrl, shift);
|
Library.library-cell-pressed(i, ctrl, shift);
|
||||||
}
|
}
|
||||||
cell-press-ended() => { root.library-cell-press-ended(); }
|
cell-press-ended() => { Library.library-cell-press-ended(); }
|
||||||
select-mode: root.library-select-mode;
|
select-mode: Library.library-select-mode;
|
||||||
toggle-select-mode() => { root.library-toggle-select-mode(); }
|
toggle-select-mode() => { Library.library-toggle-select-mode(); }
|
||||||
held-row <=> root.library-held-row;
|
held-row <=> Library.library-held-row;
|
||||||
// The sidebar's rows, not a second model: the sheet files
|
// The sidebar's rows, not a second model: the sheet files
|
||||||
// into the same tree the sidebar draws.
|
// into the same tree the sidebar draws.
|
||||||
collections: Collections.collection-rows;
|
collections: Collections.collection-rows;
|
||||||
file-in-collection(id, moves) => {
|
file-in-collection(id, moves) => {
|
||||||
root.library-file-in-collection(id, moves);
|
Library.library-file-in-collection(id, moves);
|
||||||
}
|
}
|
||||||
keywords: root.library-keywords;
|
keywords: root.library-keywords;
|
||||||
keywords-opened() => { root.library-keywords-opened(); }
|
keywords-opened() => { root.library-keywords-opened(); }
|
||||||
assign-keyword(word) => { root.library-assign-keyword(word); }
|
assign-keyword(word) => { root.library-assign-keyword(word); }
|
||||||
unassign-keyword(word) => { root.library-unassign-keyword(word); }
|
unassign-keyword(word) => { root.library-unassign-keyword(word); }
|
||||||
cursor: root.library-cursor;
|
cursor: Library.library-cursor;
|
||||||
move-cursor(delta, extend) => {
|
move-cursor(delta, extend) => {
|
||||||
root.library-move-cursor(delta, extend);
|
Library.library-move-cursor(delta, extend);
|
||||||
}
|
}
|
||||||
open-cursor() => { root.library-open-cursor(); }
|
open-cursor() => { Library.library-open-cursor(); }
|
||||||
drag-image: root.library-drag-image;
|
drag-image: Library.library-drag-image;
|
||||||
drag-payload() => { return root.library-drag-payload(); }
|
drag-payload() => { return Library.library-drag-payload(); }
|
||||||
drag-started(i) => { root.library-drag-started(i); }
|
drag-started(i) => { Library.library-drag-started(i); }
|
||||||
drag-finished() => { root.library-drag-finished(); }
|
drag-finished() => { Library.library-drag-finished(); }
|
||||||
remove-from-collection() => {
|
remove-from-collection() => {
|
||||||
root.library-remove-from-collection();
|
Library.library-remove-from-collection();
|
||||||
}
|
}
|
||||||
open-membership() => { root.library-open-membership(); }
|
open-membership() => { Library.library-open-membership(); }
|
||||||
|
|
||||||
filter-people: root.library-filter-people;
|
filter-people: root.library-filter-people;
|
||||||
filter-people-all: root.library-filter-people-all;
|
filter-people-all: root.library-filter-people-all;
|
||||||
@@ -1805,7 +1698,7 @@ in property <bool> panel-visible: true;
|
|||||||
position: root.total > 0 ? root.index + 1 + " / " + root.total : "";
|
position: root.total > 0 ? root.index + 1 + " / " + root.total : "";
|
||||||
// Only where a library was opened; command-line files have no
|
// Only where a library was opened; command-line files have no
|
||||||
// grid to return to.
|
// grid to return to.
|
||||||
can-return-to-library: root.library-total > 0;
|
can-return-to-library: Library.library-total > 0;
|
||||||
panel-visible: root.panel-visible;
|
panel-visible: root.panel-visible;
|
||||||
export-label: root.export-label;
|
export-label: root.export-label;
|
||||||
export-busy: root.export-busy;
|
export-busy: root.export-busy;
|
||||||
@@ -2657,7 +2550,7 @@ in property <bool> panel-visible: true;
|
|||||||
// instead — the controls ride on top of the roll's
|
// instead — the controls ride on top of the roll's
|
||||||
// band, at whatever height it currently stands.
|
// band, at whatever height it currently stands.
|
||||||
property <length> roll-clear:
|
property <length> roll-clear:
|
||||||
root.library-total > 0
|
Library.library-total > 0
|
||||||
? (root.roll-open
|
? (root.roll-open
|
||||||
? Theme.roll-strip + Theme.roll-reach
|
? Theme.roll-strip + Theme.roll-reach
|
||||||
: Theme.roll-reach)
|
: Theme.roll-reach)
|
||||||
@@ -2770,14 +2663,14 @@ in property <bool> panel-visible: true;
|
|||||||
//
|
//
|
||||||
// Last in the canvas, so it draws over the photograph
|
// Last in the canvas, so it draws over the photograph
|
||||||
// rather than under it.
|
// rather than under it.
|
||||||
if root.library-total > 0: PhotoRoll {
|
if Library.library-total > 0: PhotoRoll {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cells: root.library-cells;
|
cells: Library.library-cells;
|
||||||
current: root.library-roll-current;
|
current: Library.library-roll-current;
|
||||||
open <=> root.roll-open;
|
open <=> root.roll-open;
|
||||||
centre-request <=> root.library-roll-centre;
|
centre-request <=> Library.library-roll-centre;
|
||||||
pick(i) => { root.library-roll-pick(i); }
|
pick(i) => { Library.library-roll-pick(i); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report size changes so the render target can be resized to
|
// Report size changes so the render target can be resized to
|
||||||
@@ -3179,7 +3072,7 @@ in property <bool> panel-visible: true;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
open: Collections.membership-open;
|
open: Collections.membership-open;
|
||||||
selected-count: root.library-selected-count;
|
selected-count: Library.library-selected-count;
|
||||||
rows: Collections.membership-rows;
|
rows: Collections.membership-rows;
|
||||||
remove(id) => { Collections.membership-remove(id); }
|
remove(id) => { Collections.membership-remove(id); }
|
||||||
dismiss() => { Collections.membership-dismiss(); }
|
dismiss() => { Collections.membership-dismiss(); }
|
||||||
|
|||||||
@@ -479,6 +479,129 @@ export struct LibraryCell {
|
|||||||
burst-representative: bool,
|
burst-representative: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TRACES: FR-CAT-3 | FR-CAT-4 | FR-CAT-5 | FR-CAT-6 | FR-CAT-7 | FR-CULL-4 | FR-CULL-5
|
||||||
|
// The library grid's own state: the loaded window of cells, the selection,
|
||||||
|
// the timeline, the filter bar, ratings and flags, keywording, drag and drop,
|
||||||
|
// and the routes into and out of the screen.
|
||||||
|
//
|
||||||
|
// A global rather than properties and callbacks on the window root — see
|
||||||
|
// `session.slint` for why the develop panels stopped taking their wiring
|
||||||
|
// through the window root, and `collections.slint` for the sidebar's own
|
||||||
|
// global; the same reasoning applies here. What is shared with the sidebar
|
||||||
|
// (the collection the grid is scoped to, whether the sidebar is shown) stays
|
||||||
|
// on the window, because `run()`'s panel-layout wiring and the sidebar's own
|
||||||
|
// Rust still reach it there.
|
||||||
|
export global Library {
|
||||||
|
in-out property <[LibraryCell]> library-cells;
|
||||||
|
in property <int> library-total: 0;
|
||||||
|
// Narrows the grid to images whose RAW is stored locally — the ones that
|
||||||
|
// can actually be opened while offline. Off by default: the catalog is the
|
||||||
|
// library, and hiding most of it the moment a connection drops would read
|
||||||
|
// as data loss rather than as a filter.
|
||||||
|
in-out property <bool> library-local-only: false;
|
||||||
|
in property <int> library-local-count: 0;
|
||||||
|
callback library-toggle-local-only();
|
||||||
|
/// TRACES: FR-CAT-5
|
||||||
|
callback library-clear-selection();
|
||||||
|
callback library-select-all();
|
||||||
|
/// TRACES: FR-CAT-7
|
||||||
|
/// Whether the grid is showing something with a manual order to change —
|
||||||
|
/// a single manual collection, not a set and not a saved filter.
|
||||||
|
in property <bool> library-reorderable: false;
|
||||||
|
/// Move the selection beside the photograph at this row of the loaded
|
||||||
|
/// window: after it where the drop landed on the cell's trailing half,
|
||||||
|
/// before it otherwise.
|
||||||
|
callback library-reorder-to(int, bool);
|
||||||
|
callback library-collection-from-selection(string);
|
||||||
|
in-out property <int> library-columns: 1;
|
||||||
|
in-out property <length> library-cell-size: 180px;
|
||||||
|
/// Whether this session is a touch one, which decides if the rating strip
|
||||||
|
/// waits to be hovered or stands open. Seeded from the platform by Rust
|
||||||
|
/// and latched by the grid on the first finger it sees.
|
||||||
|
in-out property <bool> library-touched: false;
|
||||||
|
callback library-zoom-cells(int);
|
||||||
|
/// A pinch step on the grid: the ratio since the last update.
|
||||||
|
callback library-pinch-cells(float);
|
||||||
|
/// A pinch on the grid has begun, so the press that opened it was not one.
|
||||||
|
callback library-pinch-started();
|
||||||
|
in property <int> library-scroll-to: 0;
|
||||||
|
in property <int> library-scroll-token: 0;
|
||||||
|
/// Which row of the loaded window is the photograph currently open in
|
||||||
|
/// develop, so the roll can mark it. `-1` when it is not in the window.
|
||||||
|
in property <int> library-roll-current: -1;
|
||||||
|
/// Centre the roll on the open photograph the next time the strip settles.
|
||||||
|
///
|
||||||
|
/// Raised by Rust when a develop session begins — a photograph opened from
|
||||||
|
/// the grid — and cleared by the strip itself once honoured. Owned here for
|
||||||
|
/// the same reason `roll-open` is: the develop view is rebuilt whenever an
|
||||||
|
/// overlay screen covers it, and a request raised before that must survive
|
||||||
|
/// it. See `PhotoRoll::centre-request`.
|
||||||
|
in-out property <bool> library-roll-centre: false;
|
||||||
|
/// A photograph was chosen from the roll: the row within the loaded
|
||||||
|
/// window, which is what a cell click reports too.
|
||||||
|
callback library-roll-pick(int);
|
||||||
|
callback library-columns-changed();
|
||||||
|
callback library-scrolled(int);
|
||||||
|
/// How many cells the grid's viewport shows at once. Rust sizes the
|
||||||
|
/// loaded window from it — see `library_ui::window_move`.
|
||||||
|
callback library-viewport-cells(int);
|
||||||
|
callback library-cell-clicked(int);
|
||||||
|
/// 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
|
||||||
|
/// currently being shown.
|
||||||
|
callback library-file-in-collection(int, bool);
|
||||||
|
/// TRACES: FR-UI-2
|
||||||
|
/// Whether a tap in the grid selects rather than opens, and the button
|
||||||
|
/// that turns it on. The long press does the same thing without it.
|
||||||
|
in property <bool> library-select-mode: false;
|
||||||
|
callback library-toggle-select-mode();
|
||||||
|
/// TRACES: FR-CAT-7 | FR-UI-4
|
||||||
|
/// The row a press has held long enough to pick up, or `-1`.
|
||||||
|
///
|
||||||
|
/// Set by the same hold that turns on selection mode and cleared by the
|
||||||
|
/// grid when the press ends — `in-out` because both ends write it. See
|
||||||
|
/// `held-row` in `library.slint` for what it draws and what it stops.
|
||||||
|
in-out property <int> library-held-row: -1;
|
||||||
|
/// A press on a cell ended, so the long-press timer can be cancelled.
|
||||||
|
callback library-cell-press-ended();
|
||||||
|
// --- drag and drop ---
|
||||||
|
//
|
||||||
|
// Slint's own `DragArea`/`DropArea` carry this: pointer capture, the
|
||||||
|
// click-versus-drag threshold, arbitration against the grid's Flickable,
|
||||||
|
// and the image drawn under the cursor. So there is no badge to position
|
||||||
|
// and no hover state to mirror here — only the payload, which Rust builds
|
||||||
|
// from the selection, and the count the header shows.
|
||||||
|
/// The drag payload, built by Rust from the current selection.
|
||||||
|
pure callback library-drag-payload() -> data-transfer;
|
||||||
|
/// The bitmap that travels under the cursor: one thumbnail, or a fanned
|
||||||
|
/// stack where several images are being dragged. Composited in Rust.
|
||||||
|
in property <image> library-drag-image;
|
||||||
|
/// A drag began on a cell, so an unselected one can be promoted before the
|
||||||
|
/// payload is read.
|
||||||
|
callback library-drag-started(int);
|
||||||
|
callback library-drag-finished();
|
||||||
|
in property <int> library-selected-count: 0;
|
||||||
|
|
||||||
|
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. Opens the
|
||||||
|
// membership sheet, whose state (`Collections.membership-open` and
|
||||||
|
// `.membership-rows`) is on the sidebar's own global — only the library
|
||||||
|
// side knows the selection has changed enough to be worth recomputing
|
||||||
|
// the sheet against.
|
||||||
|
callback library-open-membership();
|
||||||
|
// 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.
|
||||||
|
in property <int> library-cursor: -1;
|
||||||
|
callback library-move-cursor(int, bool);
|
||||||
|
callback library-open-cursor();
|
||||||
|
}
|
||||||
|
|
||||||
// The photo roll: the grid's loaded window along the foot of the develop view.
|
// The photo roll: the grid's loaded window along the foot of the develop view.
|
||||||
//
|
//
|
||||||
// # Why this exists
|
// # Why this exists
|
||||||
|
|||||||
Reference in New Issue
Block a user