Merge: the collections sidebar on its own Slint global
This commit is contained in:
+35
-35
File diff suppressed because one or more lines are too long
+11
-11
@@ -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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2109`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2026`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2200`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2117`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2287`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2204`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2508`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2425`</sub>
|
||||
|
||||
### 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
|
||||
- **Keyboard** — Ctrl+Shift+Z
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2521`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2438`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2554`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2471`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2566`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2483`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2594`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2511`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2629`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2546`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2681`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2598`</sub>
|
||||
|
||||
### 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.
|
||||
|
||||
<sub>`ui/dr-ui/ui/app.slint:2805`</sub>
|
||||
<sub>`ui/dr-ui/ui/app.slint:2722`</sub>
|
||||
|
||||
### Put one control back to its default
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ use dr_catalog::collections::{self as coll, CollectionKind};
|
||||
use dr_catalog::Catalog;
|
||||
use dr_types::CollectionId;
|
||||
use rusqlite::OptionalExtension as _;
|
||||
use slint::Model as _;
|
||||
use slint::{ComponentHandle as _, Model as _};
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Collections};
|
||||
|
||||
use super::controller::{delete_warning, menu_detail, CollectionsController};
|
||||
use super::tree_sync::{direct_holdings, refresh_tree};
|
||||
@@ -50,13 +50,27 @@ pub(super) fn open_row_menu(
|
||||
|
||||
*ctl.menu_for.borrow_mut() = Some(id);
|
||||
ctl.menu_confirming.set(false);
|
||||
window.set_collection_menu_confirming(false);
|
||||
window.set_collection_menu_title(name.as_str().into());
|
||||
window.set_collection_menu_detail(menu_detail(holds, children).into());
|
||||
window.set_collection_menu_nested(parent.is_some());
|
||||
window.set_collection_menu_smart(kind == CollectionKind::Smart as i64);
|
||||
window.set_collection_menu_confirm_detail(delete_warning(&name, holds, children).into());
|
||||
window.set_collection_menu_delete_label("Delete".into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_confirming(false);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_title(name.as_str().into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_detail(menu_detail(holds, children).into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_nested(parent.is_some());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_smart(kind == CollectionKind::Smart as i64);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_confirm_detail(delete_warning(&name, holds, children).into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_delete_label("Delete".into());
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// Read off the row the sidebar already drew rather than recomputed: the
|
||||
@@ -67,9 +81,16 @@ pub(super) fn open_row_menu(
|
||||
.borrow()
|
||||
.iter()
|
||||
.position(|&c| c == id)
|
||||
.and_then(|row| window.get_collection_rows().row_data(row))
|
||||
.and_then(|row| {
|
||||
window
|
||||
.global::<Collections>()
|
||||
.get_collection_rows()
|
||||
.row_data(row)
|
||||
})
|
||||
.is_some_and(|r| r.pinned);
|
||||
window.set_collection_menu_pinned(pinned);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_pinned(pinned);
|
||||
}
|
||||
|
||||
/// Shut the row menu, both halves.
|
||||
@@ -80,8 +101,12 @@ pub(super) fn open_row_menu(
|
||||
pub(super) fn close_row_menu(window: &AppWindow, ctl: &Rc<CollectionsController>) {
|
||||
*ctl.menu_for.borrow_mut() = None;
|
||||
ctl.menu_confirming.set(false);
|
||||
window.set_collection_menu_title(slint::SharedString::new());
|
||||
window.set_collection_menu_confirming(false);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_title(slint::SharedString::new());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_menu_confirming(false);
|
||||
}
|
||||
|
||||
/// Close the rename field, whatever the outcome.
|
||||
@@ -92,7 +117,7 @@ pub(super) fn close_row_menu(window: &AppWindow, ctl: &Rc<CollectionsController>
|
||||
/// close, or closes one that Rust still believes is open.
|
||||
pub(super) fn close_rename(window: &AppWindow, ctl: &Rc<CollectionsController>) {
|
||||
*ctl.renaming.borrow_mut() = None;
|
||||
window.set_collection_renaming(0);
|
||||
window.global::<Collections>().set_collection_renaming(0);
|
||||
}
|
||||
|
||||
/// What a committed rename did.
|
||||
@@ -185,7 +210,9 @@ pub(super) fn create_child(
|
||||
|
||||
match coll::create(catalog.connection(), &name, parent, CollectionKind::Manual) {
|
||||
Ok(id) => {
|
||||
window.set_collection_error(slint::SharedString::new());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// A new child is useless if its parent is collapsed.
|
||||
if let Some(p) = parent {
|
||||
ctl.collapsed.borrow_mut().remove(&p);
|
||||
@@ -203,10 +230,14 @@ pub(super) fn create_child(
|
||||
// field on a row that already exists.
|
||||
refresh_tree(window, ctl, catalog);
|
||||
*ctl.renaming.borrow_mut() = Some(id);
|
||||
window.set_collection_renaming(id.0 as i32);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_renaming(id.0 as i32);
|
||||
log::info!("created collection {} ({name})", id.0);
|
||||
}
|
||||
Err(e) => window.set_collection_error(format!("creating collection: {e}").into()),
|
||||
Err(e) => window
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("creating collection: {e}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use dr_catalog::Catalog;
|
||||
use dr_types::ImageId;
|
||||
use slint::ComponentHandle;
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Collections};
|
||||
|
||||
use super::controller::CollectionsController;
|
||||
use super::tree_sync::refresh_tree;
|
||||
@@ -31,7 +31,9 @@ pub(super) fn start_trash(
|
||||
reload: &Rc<dyn Fn()>,
|
||||
) {
|
||||
let Some(conn) = session() else {
|
||||
window.set_collection_error("Open a library first.".into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -41,7 +43,9 @@ pub(super) fn start_trash(
|
||||
match crate::trash::plan_trash(cat, &conn.account.root, images) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
window.set_collection_error(format!("planning delete: {e}").into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("planning delete: {e}").into());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -97,7 +101,9 @@ pub(super) fn start_restore(
|
||||
reload: &Rc<dyn Fn()>,
|
||||
) {
|
||||
let Some(conn) = session() else {
|
||||
window.set_collection_error("Open a library first.".into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -107,7 +113,9 @@ pub(super) fn start_restore(
|
||||
match crate::trash::plan_restore(cat, images) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
window.set_collection_error(format!("planning restore: {e}").into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("planning restore: {e}").into());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -116,7 +124,7 @@ pub(super) fn start_restore(
|
||||
if moves.is_empty() {
|
||||
// Said out loud rather than passed over in silence: a button that does
|
||||
// nothing visible reads as broken, and the reason here is specific.
|
||||
window.set_collection_error(
|
||||
window.global::<Collections>().set_collection_error(
|
||||
"Nothing to restore — no record of where these came from.".into(),
|
||||
);
|
||||
return;
|
||||
@@ -227,7 +235,8 @@ pub(super) fn drain_trash(
|
||||
}
|
||||
w.set_library_status(status.into());
|
||||
if let Some(first) = failed.first() {
|
||||
w.set_collection_error(first.as_str().into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(first.as_str().into());
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
@@ -264,8 +273,8 @@ fn stop_trash(ctl: &Rc<CollectionsController>) {
|
||||
/// it.
|
||||
pub fn refresh_trash(window: &AppWindow, catalog: &Catalog) {
|
||||
let (n, bytes) = dr_catalog::trash::summary(catalog.connection()).unwrap_or((0, 0));
|
||||
window.set_trash_count(n as i32);
|
||||
window.set_trash_label(if n == 0 {
|
||||
window.global::<Collections>().set_trash_count(n as i32);
|
||||
window.global::<Collections>().set_trash_label(if n == 0 {
|
||||
slint::SharedString::new()
|
||||
} else {
|
||||
format!("{n} · {}", format_bytes(bytes)).into()
|
||||
|
||||
@@ -11,9 +11,9 @@ use std::rc::Rc;
|
||||
use dr_catalog::collections::{self as coll, CollectionKind};
|
||||
use dr_catalog::Catalog;
|
||||
use dr_types::{CollectionId, ImageId};
|
||||
use slint::Model as _;
|
||||
use slint::{ComponentHandle as _, Model as _};
|
||||
|
||||
use crate::{AppWindow, CollectionRow};
|
||||
use crate::{AppWindow, CollectionRow, Collections};
|
||||
|
||||
use super::controller::CollectionsController;
|
||||
|
||||
@@ -27,7 +27,9 @@ pub fn refresh_tree(window: &AppWindow, ctl: &Rc<CollectionsController>, catalog
|
||||
let rows = match coll::tree(catalog.connection()) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
window.set_collection_error(format!("reading collections: {e}").into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("reading collections: {e}").into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -95,7 +97,9 @@ pub fn refresh_tree(window: &AppWindow, ctl: &Rc<CollectionsController>, catalog
|
||||
*ctl.row_smart.borrow_mut() = smart;
|
||||
*ctl.row_has_children.borrow_mut() = has_kids;
|
||||
*ctl.row_nested.borrow_mut() = nested;
|
||||
window.set_collection_rows(slint::ModelRc::new(slint::VecModel::from(out)));
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_rows(slint::ModelRc::new(slint::VecModel::from(out)));
|
||||
|
||||
// The rows are what `sync_reorderable` reads, so it has to be told they
|
||||
// changed: a collection that gains a child, or one that is deleted out from
|
||||
@@ -251,7 +255,7 @@ pub fn sync_selection(window: &AppWindow, ctl: &Rc<CollectionsController>, ids:
|
||||
/// looking at, and a scope change costs no extra query.
|
||||
pub(super) fn sync_reorderable(window: &AppWindow) {
|
||||
let id = window.get_collection_selected();
|
||||
let rows = window.get_collection_rows();
|
||||
let rows = window.global::<Collections>().get_collection_rows();
|
||||
let manual = id > 0
|
||||
&& (0..rows.row_count())
|
||||
.filter_map(|i| rows.row_data(i))
|
||||
@@ -346,7 +350,9 @@ pub(super) fn refresh_membership(
|
||||
let rows = match coll::membership_of(catalog.connection(), &chosen) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
window.set_collection_error(format!("reading collections: {e}").into());
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("reading collections: {e}").into());
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
@@ -367,7 +373,9 @@ pub(super) fn refresh_membership(
|
||||
})
|
||||
.collect();
|
||||
|
||||
window.set_membership_rows(slint::ModelRc::new(slint::VecModel::from(out)));
|
||||
window
|
||||
.global::<Collections>()
|
||||
.set_membership_rows(slint::ModelRc::new(slint::VecModel::from(out)));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -16,7 +16,7 @@ use dr_types::{CollectionId, ImageId};
|
||||
use slint::{ComponentHandle, Model as _};
|
||||
|
||||
use crate::library;
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Collections};
|
||||
|
||||
use super::controller::{decide_drop, CollectionsController, Drop};
|
||||
use super::drag::{arm_hold, arm_spring, collapse_spring_opened, compose_drag_image};
|
||||
@@ -213,7 +213,9 @@ fn wire_selection(
|
||||
let current = match library::read_member_order(cat, scope) {
|
||||
Ok(current) => current,
|
||||
Err(e) => {
|
||||
w.set_collection_error(format!("reading the collection's order: {e}").into());
|
||||
w.global::<Collections>().set_collection_error(
|
||||
format!("reading the collection's order: {e}").into(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -221,7 +223,8 @@ fn wire_selection(
|
||||
|
||||
match coll::set_order(cat.connection(), scope, &wanted) {
|
||||
Ok(()) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{} photograph{} moved",
|
||||
@@ -240,7 +243,8 @@ fn wire_selection(
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
w.set_collection_error(format!("reordering: {e}").into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("reordering: {e}").into());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -339,7 +343,8 @@ fn wire_selection_filing(
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else {
|
||||
w.set_collection_error("Open a library first.".into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -361,7 +366,8 @@ fn wire_selection_filing(
|
||||
|
||||
match made {
|
||||
Ok(added) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status(
|
||||
format!(
|
||||
"{added} photograph{} in {name}",
|
||||
@@ -386,7 +392,8 @@ fn wire_selection_filing(
|
||||
}
|
||||
Err(e) => {
|
||||
drop(borrow);
|
||||
w.set_collection_error(format!("making a collection: {e}").into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("making a collection: {e}").into());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -449,7 +456,8 @@ fn wire_selection_filing(
|
||||
|
||||
match result {
|
||||
Ok(filed) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
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.
|
||||
@@ -474,7 +482,9 @@ fn wire_selection_filing(
|
||||
drop(borrow);
|
||||
reload();
|
||||
}
|
||||
Err(e) => w.set_collection_error(format!("filing: {e}").into()),
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("filing: {e}").into()),
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -528,11 +538,13 @@ fn wire_drag(
|
||||
// that cares to read it; the drop itself uses the remembered press, since
|
||||
// it is handed only the target.
|
||||
{
|
||||
window.on_collection_drag_payload(move |id| {
|
||||
let mut data = slint::DataTransfer::default();
|
||||
data.set_user_data(Rc::new(CollectionId(id as u64)));
|
||||
data
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_drag_payload(move |id| {
|
||||
let mut data = slint::DataTransfer::default();
|
||||
data.set_user_data(Rc::new(CollectionId(id as u64)));
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
@@ -608,24 +620,26 @@ fn wire_drag(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog_for_spring = catalog.clone();
|
||||
window.on_collection_drag_over(move |id, over| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_drag_over(move |id, over| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
|
||||
if !over {
|
||||
// Left this row. Cancel its pending expansion rather than
|
||||
// letting it fire over whatever the pointer moved on to.
|
||||
if *ctl.hover_id.borrow() == Some(id) {
|
||||
*ctl.hover_id.borrow_mut() = None;
|
||||
*ctl.spring_timer.borrow_mut() = None;
|
||||
if !over {
|
||||
// Left this row. Cancel its pending expansion rather than
|
||||
// letting it fire over whatever the pointer moved on to.
|
||||
if *ctl.hover_id.borrow() == Some(id) {
|
||||
*ctl.hover_id.borrow_mut() = None;
|
||||
*ctl.spring_timer.borrow_mut() = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
*ctl.hover_id.borrow_mut() = Some(id);
|
||||
let row = ctl.row_ids.borrow().iter().position(|&c| c == id);
|
||||
arm_spring(&w, &ctl, &catalog_for_spring, row);
|
||||
});
|
||||
*ctl.hover_id.borrow_mut() = Some(id);
|
||||
let row = ctl.row_ids.borrow().iter().position(|&c| c == id);
|
||||
arm_spring(&w, &ctl, &catalog_for_spring, row);
|
||||
});
|
||||
}
|
||||
|
||||
// A drop. Slint hit-tested the release and `can-drop` already refused a
|
||||
@@ -636,77 +650,85 @@ fn wire_drag(
|
||||
let catalog = catalog.clone();
|
||||
// No model refresh or reload here on purpose — see the note at the end
|
||||
// of this handler. `drag-finished` owns those.
|
||||
window.on_collection_dropped(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
let carried = ctl.dragging.borrow().clone();
|
||||
// Taken, not read: a remembered press must be spent by the drop it
|
||||
// belongs to, or a later empty drop — a file dragged in from
|
||||
// outside, say — would move a collection nobody touched.
|
||||
let pressed = ctl.dragging_collection.borrow_mut().take();
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_dropped(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
let carried = ctl.dragging.borrow().clone();
|
||||
// Taken, not read: a remembered press must be spent by the drop it
|
||||
// belongs to, or a later empty drop — a file dragged in from
|
||||
// outside, say — would move a collection nobody touched.
|
||||
let pressed = ctl.dragging_collection.borrow_mut().take();
|
||||
|
||||
let carried = match decide_drop(&carried, pressed, id) {
|
||||
Drop::Nothing => return,
|
||||
Drop::Reparent(source) => {
|
||||
let result = {
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
coll::set_parent(cat.connection(), source, Some(id))
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved collection".into());
|
||||
let carried = match decide_drop(&carried, pressed, id) {
|
||||
Drop::Nothing => return,
|
||||
Drop::Reparent(source) => {
|
||||
let result = {
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
coll::set_parent(cat.connection(), source, Some(id))
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved collection".into());
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
}
|
||||
}
|
||||
// The catalog refuses a cycle rather than letting the
|
||||
// tree walk spin. Saying so is better than a drop that
|
||||
// silently does nothing.
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("moving collection: {e}").into()),
|
||||
}
|
||||
// The catalog refuses a cycle rather than letting the
|
||||
// tree walk spin. Saying so is better than a drop that
|
||||
// silently does nothing.
|
||||
Err(e) => w.set_collection_error(format!("moving collection: {e}").into()),
|
||||
return;
|
||||
}
|
||||
return;
|
||||
Drop::FileImages(images) => images,
|
||||
};
|
||||
|
||||
// Scoped so the borrow is released before the spring cleanup below,
|
||||
// which needs the catalog itself.
|
||||
let result = {
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
coll::add_images(cat.connection(), id, &carried)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(added) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// "Added 3 of 12" is the honest report when nine were
|
||||
// already there; claiming 12 would teach the user to
|
||||
// distrust the count.
|
||||
let msg = if added == carried.len() {
|
||||
format!("Added {added} to collection")
|
||||
} else {
|
||||
format!(
|
||||
"Added {added} of {} — the rest were already there",
|
||||
carried.len()
|
||||
)
|
||||
};
|
||||
w.set_library_status(msg.into());
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("adding to collection: {e}").into()),
|
||||
}
|
||||
Drop::FileImages(images) => images,
|
||||
};
|
||||
|
||||
// Scoped so the borrow is released before the spring cleanup below,
|
||||
// which needs the catalog itself.
|
||||
let result = {
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
coll::add_images(cat.connection(), id, &carried)
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(added) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
// "Added 3 of 12" is the honest report when nine were
|
||||
// already there; claiming 12 would teach the user to
|
||||
// distrust the count.
|
||||
let msg = if added == carried.len() {
|
||||
format!("Added {added} to collection")
|
||||
} else {
|
||||
format!(
|
||||
"Added {added} of {} — the rest were already there",
|
||||
carried.len()
|
||||
)
|
||||
};
|
||||
w.set_library_status(msg.into());
|
||||
}
|
||||
Err(e) => w.set_collection_error(format!("adding to collection: {e}").into()),
|
||||
}
|
||||
|
||||
// Recorded, not acted on. Every visible consequence — rebuilding
|
||||
// the tree, refreshing the badges, rereading the grid — happens in
|
||||
// `drag-finished`, because all three replace models that Slint is
|
||||
// *currently walking* to deliver this very event. Tearing down a
|
||||
// live `DropArea` from inside its own `dropped` handler is the same
|
||||
// hazard `sync_rows` in lib.rs documents for the adjust panel.
|
||||
*ctl.dropped_on.borrow_mut() = Some(id);
|
||||
});
|
||||
// Recorded, not acted on. Every visible consequence — rebuilding
|
||||
// the tree, refreshing the badges, rereading the grid — happens in
|
||||
// `drag-finished`, because all three replace models that Slint is
|
||||
// *currently walking* to deliver this very event. Tearing down a
|
||||
// live `DropArea` from inside its own `dropped` handler is the same
|
||||
// hazard `sync_rows` in lib.rs documents for the adjust panel.
|
||||
*ctl.dropped_on.borrow_mut() = Some(id);
|
||||
});
|
||||
}
|
||||
|
||||
// The drag ended: dropped, or abandoned. This is where the consequences of
|
||||
@@ -784,7 +806,7 @@ fn wire_trash(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let session = session.clone();
|
||||
window.on_trash_dropped(move || {
|
||||
window.global::<Collections>().on_trash_dropped(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let carried = ctl.dragging.borrow().clone();
|
||||
if carried.is_empty() {
|
||||
@@ -795,7 +817,8 @@ fn wire_trash(
|
||||
// still being walked to deliver this event.
|
||||
*ctl.trash_requested.borrow_mut() = Some(carried);
|
||||
let _ = session;
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -808,7 +831,7 @@ fn wire_trash(
|
||||
let catalog = catalog.clone();
|
||||
let session = session.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_trash_restore(move || {
|
||||
window.global::<Collections>().on_trash_restore(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
@@ -887,7 +910,7 @@ fn wire_trash_from_grid(
|
||||
let catalog = catalog.clone();
|
||||
let session = session.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_trash_empty(move || {
|
||||
window.global::<Collections>().on_trash_empty(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
|
||||
let conn = match session() {
|
||||
@@ -904,7 +927,8 @@ fn wire_trash_from_grid(
|
||||
let listed = match dr_catalog::trash::list(cat.connection(), usize::MAX) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
w.set_collection_error(format!("reading trash: {e}").into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("reading trash: {e}").into());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ use dr_catalog::Catalog;
|
||||
use dr_types::{CollectionId, ImageId};
|
||||
use slint::{ComponentHandle, Model as _};
|
||||
|
||||
use crate::AppWindow;
|
||||
use crate::{AppWindow, Collections};
|
||||
|
||||
use super::controller::{
|
||||
decide_delete, decide_release, CollectionsController, DeleteStep, Release,
|
||||
@@ -58,7 +58,7 @@ pub(super) fn wire_tree_navigation(
|
||||
} else if id == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
let rows = w.get_collection_rows();
|
||||
let rows = w.global::<Collections>().get_collection_rows();
|
||||
(0..rows.row_count())
|
||||
.filter_map(|i| rows.row_data(i))
|
||||
.find(|r| r.id == id)
|
||||
@@ -66,7 +66,8 @@ pub(super) fn wire_tree_navigation(
|
||||
.unwrap_or_default()
|
||||
};
|
||||
w.set_collection_selected(id);
|
||||
w.set_collection_scope_label(label.into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_scope_label(label.into());
|
||||
sync_reorderable(&w);
|
||||
reload();
|
||||
});
|
||||
@@ -76,20 +77,22 @@ pub(super) fn wire_tree_navigation(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_toggle(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
{
|
||||
let mut collapsed = ctl.collapsed.borrow_mut();
|
||||
if !collapsed.remove(&id) {
|
||||
collapsed.insert(id);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_toggle(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
{
|
||||
let mut collapsed = ctl.collapsed.borrow_mut();
|
||||
if !collapsed.remove(&id) {
|
||||
collapsed.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
}
|
||||
});
|
||||
let borrow = catalog.borrow();
|
||||
if let Some(cat) = borrow.as_ref() {
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,11 +106,12 @@ pub(super) fn wire_create(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_new(move || {
|
||||
window.global::<Collections>().on_collection_new(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else {
|
||||
w.set_collection_error("Open a library first.".into());
|
||||
w.global::<Collections>()
|
||||
.set_collection_error("Open a library first.".into());
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -133,76 +137,88 @@ pub(super) fn wire_rename(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_rename_start(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// `F2` arrives with whatever the sidebar has selected, which may be
|
||||
// "All photographs" (0) or the trash (-1). Neither has a name to
|
||||
// change, so the key does nothing rather than opening a field on a
|
||||
// row that is not a collection.
|
||||
if id <= 0 {
|
||||
return;
|
||||
}
|
||||
let id = CollectionId(id as u64);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_rename_start(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// `F2` arrives with whatever the sidebar has selected, which may be
|
||||
// "All photographs" (0) or the trash (-1). Neither has a name to
|
||||
// change, so the key does nothing rather than opening a field on a
|
||||
// row that is not a collection.
|
||||
if id <= 0 {
|
||||
return;
|
||||
}
|
||||
let id = CollectionId(id as u64);
|
||||
|
||||
// A saved filter is renameable like any other — its *membership* is
|
||||
// computed, its name is not — so there is no kind check here.
|
||||
*ctl.renaming.borrow_mut() = Some(id);
|
||||
w.set_collection_renaming(id.0 as i32);
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
});
|
||||
// A saved filter is renameable like any other — its *membership* is
|
||||
// computed, its name is not — so there is no kind check here.
|
||||
*ctl.renaming.borrow_mut() = Some(id);
|
||||
w.global::<Collections>()
|
||||
.set_collection_renaming(id.0 as i32);
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_rename_commit(move |id, name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_rename_commit(move |id, name| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
|
||||
// The field reports a commit when it loses focus as well as on
|
||||
// Enter, so a second one can arrive for a rename already closed —
|
||||
// Enter commits, and the focus the field then gives up commits
|
||||
// again. Ignored rather than reapplied: the second would bump the
|
||||
// revision for no change and beat a real edit on another device.
|
||||
if *ctl.renaming.borrow() != Some(id) {
|
||||
return;
|
||||
}
|
||||
// The field reports a commit when it loses focus as well as on
|
||||
// Enter, so a second one can arrive for a rename already closed —
|
||||
// Enter commits, and the focus the field then gives up commits
|
||||
// again. Ignored rather than reapplied: the second would bump the
|
||||
// revision for no change and beat a real edit on another device.
|
||||
if *ctl.renaming.borrow() != Some(id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
match apply_rename(cat.connection(), id, name.as_str()) {
|
||||
Ok(Rename::Applied(name)) => {
|
||||
close_rename(&w, &ctl);
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
// The header names the collection being shown, so a rename
|
||||
// of the current scope has to reach it too.
|
||||
if *ctl.scope.borrow() == Some(id) {
|
||||
w.set_collection_scope_label(name.as_str().into());
|
||||
match apply_rename(cat.connection(), id, name.as_str()) {
|
||||
Ok(Rename::Applied(name)) => {
|
||||
close_rename(&w, &ctl);
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
// The header names the collection being shown, so a rename
|
||||
// of the current scope has to reach it too.
|
||||
if *ctl.scope.borrow() == Some(id) {
|
||||
w.global::<Collections>()
|
||||
.set_collection_scope_label(name.as_str().into());
|
||||
}
|
||||
log::info!("renamed collection {} to {name}", id.0);
|
||||
}
|
||||
Ok(Rename::Unchanged) => close_rename(&w, &ctl),
|
||||
Err(e) => {
|
||||
// The field stays open on what the user typed. Closing it
|
||||
// would drop their text and leave the old name showing,
|
||||
// with only a line of red to explain where it went.
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(format!("renaming: {e}").into());
|
||||
}
|
||||
log::info!("renamed collection {} to {name}", id.0);
|
||||
}
|
||||
Ok(Rename::Unchanged) => close_rename(&w, &ctl),
|
||||
Err(e) => {
|
||||
// The field stays open on what the user typed. Closing it
|
||||
// would drop their text and leave the old name showing,
|
||||
// with only a line of red to explain where it went.
|
||||
w.set_collection_error(format!("renaming: {e}").into());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_rename_cancel(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
close_rename(&w, &ctl);
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_rename_cancel(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
close_rename(&w, &ctl);
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +253,8 @@ pub(super) fn wire_remove(
|
||||
|
||||
match coll::remove_images(cat.connection(), scope, &chosen) {
|
||||
Ok(n) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
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.
|
||||
@@ -249,7 +266,9 @@ pub(super) fn wire_remove(
|
||||
sync_badges(&w, cat, &visible());
|
||||
reload();
|
||||
}
|
||||
Err(e) => w.set_collection_error(format!("removing: {e}").into()),
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("removing: {e}").into()),
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -276,44 +295,47 @@ pub(super) fn wire_row_drag(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_row_press(move |id, down| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_row_press(move |id, down| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
|
||||
if !down {
|
||||
// Let go. A drag that began has already been dealt with by the
|
||||
// drop; a hold that did not move is a request for the menu.
|
||||
*ctl.row_hold_timer.borrow_mut() = None;
|
||||
let held = ctl.lifted.borrow_mut().take();
|
||||
let dragged = ctl.drag_began.replace(false);
|
||||
w.set_collection_lifted(0);
|
||||
if let Release::OpenMenu(held) = decide_release(held, dragged) {
|
||||
w.invoke_collection_menu(held.0 as i32);
|
||||
if !down {
|
||||
// Let go. A drag that began has already been dealt with by the
|
||||
// drop; a hold that did not move is a request for the menu.
|
||||
*ctl.row_hold_timer.borrow_mut() = None;
|
||||
let held = ctl.lifted.borrow_mut().take();
|
||||
let dragged = ctl.drag_began.replace(false);
|
||||
w.global::<Collections>().set_collection_lifted(0);
|
||||
if let Release::OpenMenu(held) = decide_release(held, dragged) {
|
||||
w.global::<Collections>()
|
||||
.invoke_collection_menu(held.0 as i32);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// A drag of this row, if one follows, carries this collection.
|
||||
ctl.note_row_press(id);
|
||||
ctl.drag_began.set(false);
|
||||
// A drag of this row, if one follows, carries this collection.
|
||||
ctl.note_row_press(id);
|
||||
ctl.drag_began.set(false);
|
||||
|
||||
let timer = slint::Timer::default();
|
||||
let weak = w.as_weak();
|
||||
let ctl_cb = ctl.clone();
|
||||
timer.start(
|
||||
slint::TimerMode::SingleShot,
|
||||
std::time::Duration::from_millis(HOLD_DELAY_MS),
|
||||
move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Picked up, not yet acted on. The menu waits for the
|
||||
// release, because opening it here would put a sheet over
|
||||
// the tree the user may be about to drag this row across.
|
||||
*ctl_cb.lifted.borrow_mut() = Some(id);
|
||||
w.set_collection_lifted(id.0 as i32);
|
||||
},
|
||||
);
|
||||
*ctl.row_hold_timer.borrow_mut() = Some(timer);
|
||||
});
|
||||
let timer = slint::Timer::default();
|
||||
let weak = w.as_weak();
|
||||
let ctl_cb = ctl.clone();
|
||||
timer.start(
|
||||
slint::TimerMode::SingleShot,
|
||||
std::time::Duration::from_millis(HOLD_DELAY_MS),
|
||||
move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
// Picked up, not yet acted on. The menu waits for the
|
||||
// release, because opening it here would put a sheet over
|
||||
// the tree the user may be about to drag this row across.
|
||||
*ctl_cb.lifted.borrow_mut() = Some(id);
|
||||
w.global::<Collections>().set_collection_lifted(id.0 as i32);
|
||||
},
|
||||
);
|
||||
*ctl.row_hold_timer.borrow_mut() = Some(timer);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7 | FR-UI-4
|
||||
@@ -326,40 +348,44 @@ pub(super) fn wire_row_drag(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_drag_active(move |id, active| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_drag_active(move |id, active| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let id = CollectionId(id as u64);
|
||||
|
||||
if !active {
|
||||
w.set_collection_root_drop_allowed(false);
|
||||
w.set_collection_lifted(0);
|
||||
*ctl.lifted.borrow_mut() = None;
|
||||
return;
|
||||
}
|
||||
if !active {
|
||||
w.global::<Collections>()
|
||||
.set_collection_root_drop_allowed(false);
|
||||
w.global::<Collections>().set_collection_lifted(0);
|
||||
*ctl.lifted.borrow_mut() = None;
|
||||
return;
|
||||
}
|
||||
|
||||
// The hold may not have fired — a pointer drag needs no hold — so
|
||||
// the pick-up is recorded here as well as there, and the row lifts
|
||||
// either way.
|
||||
ctl.drag_began.set(true);
|
||||
ctl.note_row_press(id);
|
||||
*ctl.lifted.borrow_mut() = Some(id);
|
||||
w.set_collection_lifted(id.0 as i32);
|
||||
// The hold has been spent on a drag; nothing may fire behind it.
|
||||
*ctl.row_hold_timer.borrow_mut() = None;
|
||||
// The hold may not have fired — a pointer drag needs no hold — so
|
||||
// the pick-up is recorded here as well as there, and the row lifts
|
||||
// either way.
|
||||
ctl.drag_began.set(true);
|
||||
ctl.note_row_press(id);
|
||||
*ctl.lifted.borrow_mut() = Some(id);
|
||||
w.global::<Collections>().set_collection_lifted(id.0 as i32);
|
||||
// The hold has been spent on a drag; nothing may fire behind it.
|
||||
*ctl.row_hold_timer.borrow_mut() = None;
|
||||
|
||||
// Only a nested collection has a top level to be returned to, and
|
||||
// only a collection drag — never photographs — means anything
|
||||
// there at all.
|
||||
let carrying_images = !ctl.dragging.borrow().is_empty();
|
||||
let nested = ctl
|
||||
.row_ids
|
||||
.borrow()
|
||||
.iter()
|
||||
.position(|&c| c == id)
|
||||
.and_then(|row| ctl.row_nested.borrow().get(row).copied())
|
||||
.unwrap_or(false);
|
||||
w.set_collection_root_drop_allowed(nested && !carrying_images);
|
||||
});
|
||||
// Only a nested collection has a top level to be returned to, and
|
||||
// only a collection drag — never photographs — means anything
|
||||
// there at all.
|
||||
let carrying_images = !ctl.dragging.borrow().is_empty();
|
||||
let nested = ctl
|
||||
.row_ids
|
||||
.borrow()
|
||||
.iter()
|
||||
.position(|&c| c == id)
|
||||
.and_then(|row| ctl.row_nested.borrow().get(row).copied())
|
||||
.unwrap_or(false);
|
||||
w.global::<Collections>()
|
||||
.set_collection_root_drop_allowed(nested && !carrying_images);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
@@ -375,30 +401,35 @@ pub(super) fn wire_row_drag(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_dropped_on_root(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let source = ctl.dragging_collection.borrow_mut().take();
|
||||
let Some(source) = source else { return };
|
||||
// Photographs cannot land here. Guarded as well as refused in
|
||||
// `can-drop`, so a payload that somehow arrives does nothing
|
||||
// rather than reparenting whatever row was last pressed.
|
||||
if !ctl.dragging.borrow().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
let moved = coll::set_parent(cat.connection(), source, None);
|
||||
match moved {
|
||||
Ok(()) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved to the top level".into());
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
log::info!("promoted collection {} by drag", source.0);
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_dropped_on_root(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let source = ctl.dragging_collection.borrow_mut().take();
|
||||
let Some(source) = source else { return };
|
||||
// Photographs cannot land here. Guarded as well as refused in
|
||||
// `can-drop`, so a payload that somehow arrives does nothing
|
||||
// rather than reparenting whatever row was last pressed.
|
||||
if !ctl.dragging.borrow().is_empty() {
|
||||
return;
|
||||
}
|
||||
Err(e) => w.set_collection_error(format!("moving collection: {e}").into()),
|
||||
}
|
||||
});
|
||||
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
let moved = coll::set_parent(cat.connection(), source, None);
|
||||
match moved {
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved to the top level".into());
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
log::info!("promoted collection {} by drag", source.0);
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("moving collection: {e}").into()),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,12 +452,14 @@ pub(super) fn wire_row_menu(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_menu(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
open_row_menu(&w, &ctl, cat, CollectionId(id as u64));
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
open_row_menu(&w, &ctl, cat, CollectionId(id as u64));
|
||||
});
|
||||
}
|
||||
|
||||
// Rename, from the menu into the row's own inline field.
|
||||
@@ -440,15 +473,18 @@ pub(super) fn wire_row_menu(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_menu_rename(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
close_row_menu(&w, &ctl);
|
||||
*ctl.renaming.borrow_mut() = Some(id);
|
||||
w.set_collection_renaming(id.0 as i32);
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu_rename(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
close_row_menu(&w, &ctl);
|
||||
*ctl.renaming.borrow_mut() = Some(id);
|
||||
w.global::<Collections>()
|
||||
.set_collection_renaming(id.0 as i32);
|
||||
});
|
||||
}
|
||||
|
||||
// A new collection nested inside this one.
|
||||
@@ -460,16 +496,18 @@ pub(super) fn wire_row_menu(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_menu_new_child(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(parent) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
close_row_menu(&w, &ctl);
|
||||
create_child(&w, &ctl, cat, Some(parent));
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu_new_child(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(parent) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
close_row_menu(&w, &ctl);
|
||||
create_child(&w, &ctl, cat, Some(parent));
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
@@ -485,28 +523,33 @@ pub(super) fn wire_row_menu(
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
let catalog = catalog.clone();
|
||||
window.on_collection_menu_promote(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu_promote(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
match coll::set_parent(cat.connection(), id, None) {
|
||||
Ok(()) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved to the top level".into());
|
||||
close_row_menu(&w, &ctl);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
log::info!("promoted collection {} to the top level", id.0);
|
||||
match coll::set_parent(cat.connection(), id, None) {
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
w.set_library_status("Moved to the top level".into());
|
||||
close_row_menu(&w, &ctl);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
log::info!("promoted collection {} to the top level", id.0);
|
||||
}
|
||||
// Left open on failure, showing the collection it failed on:
|
||||
// closing would leave a red line referring to a row the user
|
||||
// can no longer tell was the one they aimed at.
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("moving collection: {e}").into()),
|
||||
}
|
||||
// Left open on failure, showing the collection it failed on:
|
||||
// closing would leave a red line referring to a row the user
|
||||
// can no longer tell was the one they aimed at.
|
||||
Err(e) => w.set_collection_error(format!("moving collection: {e}").into()),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
@@ -518,14 +561,16 @@ pub(super) fn wire_row_menu(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_menu_offline(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
close_row_menu(&w, &ctl);
|
||||
w.invoke_collection_offline_menu(id.0 as i32);
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu_offline(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
close_row_menu(&w, &ctl);
|
||||
w.invoke_collection_offline_menu(id.0 as i32);
|
||||
});
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7
|
||||
@@ -536,66 +581,75 @@ pub(super) fn wire_row_menu(
|
||||
let catalog = catalog.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
let visible = visible_ids.clone();
|
||||
window.on_collection_menu_delete(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu_delete(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let Some(id) = *ctl.menu_for.borrow() else {
|
||||
return;
|
||||
};
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
let (holds, children) = direct_holdings(cat.connection(), id).unwrap_or((0, 0));
|
||||
if decide_delete(holds, children, ctl.menu_confirming.get()) == DeleteStep::Confirm {
|
||||
// Asked once. The sheet swaps its other actions for the
|
||||
// warning, so the second press cannot be a mis-aimed first one.
|
||||
ctl.menu_confirming.set(true);
|
||||
w.set_collection_menu_confirming(true);
|
||||
w.set_collection_menu_delete_label("Delete anyway".into());
|
||||
return;
|
||||
}
|
||||
let (holds, children) = direct_holdings(cat.connection(), id).unwrap_or((0, 0));
|
||||
if decide_delete(holds, children, ctl.menu_confirming.get()) == DeleteStep::Confirm
|
||||
{
|
||||
// Asked once. The sheet swaps its other actions for the
|
||||
// warning, so the second press cannot be a mis-aimed first one.
|
||||
ctl.menu_confirming.set(true);
|
||||
w.global::<Collections>()
|
||||
.set_collection_menu_confirming(true);
|
||||
w.global::<Collections>()
|
||||
.set_collection_menu_delete_label("Delete anyway".into());
|
||||
return;
|
||||
}
|
||||
|
||||
// Hoisted out of the `match` rather than called in the scrutinee:
|
||||
// the borrow `cat` comes from lives as long as the match does, and
|
||||
// the arm below has to release it before rereading the grid.
|
||||
let deleted = coll::delete(cat.connection(), id);
|
||||
match deleted {
|
||||
Ok(()) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
close_row_menu(&w, &ctl);
|
||||
// Hoisted out of the `match` rather than called in the scrutinee:
|
||||
// the borrow `cat` comes from lives as long as the match does, and
|
||||
// the arm below has to release it before rereading the grid.
|
||||
let deleted = coll::delete(cat.connection(), id);
|
||||
match deleted {
|
||||
Ok(()) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
close_row_menu(&w, &ctl);
|
||||
|
||||
// The grid was showing what no longer exists.
|
||||
let was_scope = *ctl.scope.borrow() == Some(id);
|
||||
if was_scope {
|
||||
*ctl.scope.borrow_mut() = None;
|
||||
w.set_collection_selected(0);
|
||||
w.set_collection_scope_label(slint::SharedString::new());
|
||||
}
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
// Named as a membership change, as the removal button is:
|
||||
// a user who reads this as a delete of their photographs
|
||||
// will not trust the feature again.
|
||||
w.set_library_status(if holds > 0 {
|
||||
format!(
|
||||
// The grid was showing what no longer exists.
|
||||
let was_scope = *ctl.scope.borrow() == Some(id);
|
||||
if was_scope {
|
||||
*ctl.scope.borrow_mut() = None;
|
||||
w.set_collection_selected(0);
|
||||
w.global::<Collections>()
|
||||
.set_collection_scope_label(slint::SharedString::new());
|
||||
}
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
// Named as a membership change, as the removal button is:
|
||||
// a user who reads this as a delete of their photographs
|
||||
// will not trust the feature again.
|
||||
w.set_library_status(if holds > 0 {
|
||||
format!(
|
||||
"Deleted the collection; its {holds} photograph(s) stay in the library"
|
||||
)
|
||||
.into()
|
||||
} else {
|
||||
slint::SharedString::from("Deleted the collection")
|
||||
});
|
||||
log::info!("deleted collection {}", id.0);
|
||||
.into()
|
||||
} else {
|
||||
slint::SharedString::from("Deleted the collection")
|
||||
});
|
||||
log::info!("deleted collection {}", id.0);
|
||||
|
||||
// The badge on every visible cell just lost a collection,
|
||||
// and a scope that has gone needs the grid rereading.
|
||||
let cells = visible();
|
||||
sync_badges(&w, cat, &cells);
|
||||
drop(borrow);
|
||||
if was_scope {
|
||||
reload();
|
||||
// The badge on every visible cell just lost a collection,
|
||||
// and a scope that has gone needs the grid rereading.
|
||||
let cells = visible();
|
||||
sync_badges(&w, cat, &cells);
|
||||
drop(borrow);
|
||||
if was_scope {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("deleting: {e}").into()),
|
||||
}
|
||||
Err(e) => w.set_collection_error(format!("deleting: {e}").into()),
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Dismiss. A pending confirmation is backed out of one step rather than
|
||||
@@ -605,16 +659,20 @@ pub(super) fn wire_row_menu(
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let ctl = ctl.clone();
|
||||
window.on_collection_menu_dismiss(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if ctl.menu_confirming.get() {
|
||||
ctl.menu_confirming.set(false);
|
||||
w.set_collection_menu_confirming(false);
|
||||
w.set_collection_menu_delete_label("Delete".into());
|
||||
return;
|
||||
}
|
||||
close_row_menu(&w, &ctl);
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_collection_menu_dismiss(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if ctl.menu_confirming.get() {
|
||||
ctl.menu_confirming.set(false);
|
||||
w.global::<Collections>()
|
||||
.set_collection_menu_confirming(false);
|
||||
w.global::<Collections>()
|
||||
.set_collection_menu_delete_label("Delete".into());
|
||||
return;
|
||||
}
|
||||
close_row_menu(&w, &ctl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +693,7 @@ pub(super) fn wire_membership(
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
refresh_membership(&w, &ctl, cat);
|
||||
w.set_membership_open(true);
|
||||
w.global::<Collections>().set_membership_open(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -645,54 +703,62 @@ pub(super) fn wire_membership(
|
||||
let catalog = catalog.clone();
|
||||
let visible = visible_ids.clone();
|
||||
let reload = on_scope_changed.clone();
|
||||
window.on_membership_remove(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let target = CollectionId(id as u64);
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
return;
|
||||
}
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
// Hoisted for the same reason the delete above is: the arm
|
||||
// releases the catalog borrow before it rereads the grid.
|
||||
let removed = coll::remove_images(cat.connection(), target, &chosen);
|
||||
match removed {
|
||||
Ok(n) => {
|
||||
w.set_collection_error(slint::SharedString::new());
|
||||
// The same wording the scoped button uses, and for the same
|
||||
// reason: this is a membership change, not a delete.
|
||||
w.set_library_status(
|
||||
format!("Removed {n} from that collection; still in the library").into(),
|
||||
);
|
||||
// The sheet stays open. Taking photographs out of several
|
||||
// collections is one job, and a sheet that closed after
|
||||
// each would have to be reopened — with the selection
|
||||
// still live — to finish it.
|
||||
refresh_membership(&w, &ctl, cat);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
let cells = visible();
|
||||
sync_badges(&w, cat, &cells);
|
||||
// Only the collection on screen changes what the grid
|
||||
// holds. Removing from another one leaves the grid right,
|
||||
// and rereading it would scroll the user's place away.
|
||||
let showing = *ctl.scope.borrow() == Some(target);
|
||||
drop(borrow);
|
||||
if showing {
|
||||
reload();
|
||||
}
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_membership_remove(move |id| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
let target = CollectionId(id as u64);
|
||||
let chosen = ctl.selected();
|
||||
if chosen.is_empty() {
|
||||
return;
|
||||
}
|
||||
Err(e) => w.set_collection_error(format!("removing: {e}").into()),
|
||||
}
|
||||
});
|
||||
let borrow = catalog.borrow();
|
||||
let Some(cat) = borrow.as_ref() else { return };
|
||||
|
||||
// Hoisted for the same reason the delete above is: the arm
|
||||
// releases the catalog borrow before it rereads the grid.
|
||||
let removed = coll::remove_images(cat.connection(), target, &chosen);
|
||||
match removed {
|
||||
Ok(n) => {
|
||||
w.global::<Collections>()
|
||||
.set_collection_error(slint::SharedString::new());
|
||||
// The same wording the scoped button uses, and for the same
|
||||
// reason: this is a membership change, not a delete.
|
||||
w.set_library_status(
|
||||
format!("Removed {n} from that collection; still in the library")
|
||||
.into(),
|
||||
);
|
||||
// The sheet stays open. Taking photographs out of several
|
||||
// collections is one job, and a sheet that closed after
|
||||
// each would have to be reopened — with the selection
|
||||
// still live — to finish it.
|
||||
refresh_membership(&w, &ctl, cat);
|
||||
refresh_tree(&w, &ctl, cat);
|
||||
let cells = visible();
|
||||
sync_badges(&w, cat, &cells);
|
||||
// Only the collection on screen changes what the grid
|
||||
// holds. Removing from another one leaves the grid right,
|
||||
// and rereading it would scroll the user's place away.
|
||||
let showing = *ctl.scope.borrow() == Some(target);
|
||||
drop(borrow);
|
||||
if showing {
|
||||
reload();
|
||||
}
|
||||
}
|
||||
Err(e) => w
|
||||
.global::<Collections>()
|
||||
.set_collection_error(format!("removing: {e}").into()),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
window.on_membership_dismiss(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
w.set_membership_open(false);
|
||||
});
|
||||
window
|
||||
.global::<Collections>()
|
||||
.on_membership_dismiss(move || {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
w.global::<Collections>().set_membership_open(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+62
-145
@@ -10,7 +10,7 @@ import { IdentityScreen, IdentityPerson, IdentityFace } from "identity.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow, PersonChip } from "library.slint";
|
||||
import { GestureRow } from "gestures.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||
import { CollectionsPanel, CollectionRow, OfflinePrompt, CollectionMenu,
|
||||
import { CollectionsPanel, CollectionRow, Collections, OfflinePrompt, CollectionMenu,
|
||||
MembershipSheet, MembershipRow } from "collections.slint";
|
||||
import { HistogramPanel, HistogramView, Levels } from "histogram.slint";
|
||||
import { RecoveryPrompt } from "recovery.slint";
|
||||
@@ -31,6 +31,7 @@ export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
|
||||
// exports appears in the generated API. `session.slint` says why the develop
|
||||
// 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 { Collections }
|
||||
|
||||
/// Which top-level screen is on display. Exactly one at a time — replacing
|
||||
/// the `show-launch` / `show-library` / `show-identity` booleans, with
|
||||
@@ -539,75 +540,25 @@ export component AppWindow inherits Window {
|
||||
|
||||
// --- collections (FR-CAT-7) ---
|
||||
//
|
||||
// The sidebar's tree, flattened in Rust: Slint cannot instantiate a
|
||||
// component recursively, so depth arrives as an integer per row.
|
||||
in-out property <[CollectionRow]> collection-rows;
|
||||
// State that belongs to the sidebar itself — the tree, the row menu,
|
||||
// drag and drop between rows, the trash row — has moved to the
|
||||
// `Collections` global in `collections.slint`. What is left here is
|
||||
// shared with the library grid, whose Rust wiring still reaches it on
|
||||
// the window.
|
||||
/// Which collection scopes the grid. 0 is the whole library.
|
||||
in property <int> collection-selected: 0;
|
||||
in property <string> collection-scope-label: "";
|
||||
in property <string> collection-error: "";
|
||||
/// Whether the sidebar is shown at all. Closed by default in the compact
|
||||
/// layout, where 232px of the window is most of the photograph (FR-UI-1),
|
||||
/// and openable from the grid header either way — see `panel-visible` for
|
||||
/// how a default and a user's answer to it are kept apart.
|
||||
/// how a default and a user's answer to it are kept apart. Owned by
|
||||
/// `run()`'s panel-layout wiring rather than the sidebar's own global,
|
||||
/// because it seeds from the saved layout before the sidebar exists.
|
||||
in property <bool> collections-visible: true;
|
||||
|
||||
/// Which collection is being renamed in the sidebar, by id. 0 is none.
|
||||
in property <int> collection-renaming: 0;
|
||||
|
||||
callback collection-select(int);
|
||||
callback collection-toggle(int);
|
||||
callback collection-new();
|
||||
callback collection-menu(int);
|
||||
|
||||
// --- the row menu the gesture above opens (FR-CAT-7) -------------------
|
||||
//
|
||||
// Rust owns every one of these. What the menu offers depends on counts it
|
||||
// has to query — whether the collection is nested, what deleting would
|
||||
// take — and the confirmation step is a policy decision, not a local bit
|
||||
// the sheet can flip for itself.
|
||||
/// The collection the menu is about. Empty closes it.
|
||||
in property <string> collection-menu-title;
|
||||
/// "12 photographs · 2 collections inside".
|
||||
in property <string> collection-menu-detail;
|
||||
/// Whether it has a parent, and so somewhere to be promoted out to.
|
||||
in property <bool> collection-menu-nested: false;
|
||||
/// Whether it is a saved filter, which cannot hold a nested collection.
|
||||
in property <bool> collection-menu-smart: false;
|
||||
/// Whether a delete has been asked once and awaits confirmation.
|
||||
in property <bool> collection-menu-confirming: false;
|
||||
/// What the delete would take with it, and what survives it.
|
||||
in property <string> collection-menu-confirm-detail;
|
||||
in property <string> collection-menu-delete-label;
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Whether the collection the menu is about is kept on this device.
|
||||
in property <bool> collection-menu-pinned: false;
|
||||
callback collection-menu-rename();
|
||||
callback collection-menu-new-child();
|
||||
/// Move it back to the top level. The inverse of the drag that nests one,
|
||||
/// which until now had no gesture at all.
|
||||
callback collection-menu-promote();
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Hand the offline question over to the handler the tray on the row
|
||||
/// already uses, rather than a second route to the same prompt.
|
||||
callback collection-menu-offline();
|
||||
callback collection-menu-delete();
|
||||
callback collection-menu-dismiss();
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Ask about a collection's local copies: the tray on a row, or a long
|
||||
/// press anywhere on it.
|
||||
callback collection-offline-menu(int);
|
||||
/// A row is held or released — the long press is timed in Rust.
|
||||
callback collection-row-press(int, bool);
|
||||
// --- picking a collection up (FR-UI-3, FR-UI-4) ------------------------
|
||||
/// Which collection the hold has picked up, by id. 0 is none.
|
||||
in property <int> collection-lifted: 0;
|
||||
/// A row's drag crossed the threshold, or ended.
|
||||
callback collection-drag-active(int, bool);
|
||||
/// Whether "All photographs" would take the drag currently in flight.
|
||||
in property <bool> collection-root-drop-allowed: false;
|
||||
/// A collection was dropped on "All photographs": out to the top level.
|
||||
callback collection-dropped-on-root();
|
||||
/// TRACES: FR-CAT-7 | FR-UI-4
|
||||
/// File the grid's selection in a collection without dragging it there:
|
||||
/// the target's id, and whether to take the images out of the collection
|
||||
@@ -646,38 +597,6 @@ export component AppWindow inherits Window {
|
||||
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();
|
||||
/// TRACES: FR-UI-2 | FR-UI-4
|
||||
/// Two taps on a cell: in selection mode, the far end of a range.
|
||||
/// Renaming started on a row, by id — a double-click, or `F2`.
|
||||
callback collection-rename-start(int);
|
||||
/// A rename was committed: the collection's id and the new name.
|
||||
callback collection-rename-commit(int, string);
|
||||
/// Renaming was abandoned with Escape.
|
||||
callback collection-rename-cancel();
|
||||
/// Row index and collection id under the pointer. The id is what the
|
||||
/// drop highlight follows: a spring expansion rebuilds the row model
|
||||
/// mid-drag, and an index would then point at a different collection.
|
||||
/// A drag is dwelling over a collection, or has left it: id, and whether
|
||||
/// it is over. Drives the spring-loaded expansion, timed in Rust.
|
||||
callback collection-drag-over(int, bool);
|
||||
/// Images were dropped on a collection, by id. Slint hit-tests the release,
|
||||
/// so this is the collection actually under the pointer.
|
||||
callback collection-dropped(int);
|
||||
/// Payload for dragging a collection onto another to reparent it.
|
||||
pure callback collection-drag-payload(int) -> data-transfer;
|
||||
|
||||
// --- trash (FR-CAT-15) ---
|
||||
//
|
||||
// Not a collection: a drop here moves the file into a trash folder on the
|
||||
// server, where a collection drop only adds a reference.
|
||||
in property <int> trash-count: 0;
|
||||
in property <string> trash-label: "";
|
||||
/// A soft delete: move the dragged images to the trash folder.
|
||||
callback trash-dropped();
|
||||
/// A hard delete: permanently remove everything in the trash.
|
||||
callback trash-empty();
|
||||
/// Put the selected trashed images back where they came from.
|
||||
callback trash-restore();
|
||||
|
||||
// --- drag and drop ---
|
||||
//
|
||||
@@ -703,14 +622,12 @@ export component AppWindow inherits Window {
|
||||
// --- where the selection is filed (FR-CAT-7) ---------------------------
|
||||
//
|
||||
// `library-remove-from-collection` can only ever mean the collection the
|
||||
// grid is scoped to — it is the only one that button can name. This sheet
|
||||
// is the other half: which collections the selection is actually in, and
|
||||
// the way out of any of them without navigating there first.
|
||||
in property <bool> membership-open: false;
|
||||
in property <[MembershipRow]> membership-rows;
|
||||
// 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();
|
||||
callback membership-remove(int);
|
||||
callback membership-dismiss();
|
||||
|
||||
// The keyboard cursor: where the arrows are in the library, as an image
|
||||
// ordinal. Rust owns it — clamping it needs the library's length, and
|
||||
@@ -1287,12 +1204,12 @@ in property <bool> panel-visible: true;
|
||||
// menu's own confirmation is a step Back must be able to take
|
||||
// one at a time, which is why the dismiss handlers in Rust
|
||||
// back out of a pending delete rather than closing outright.
|
||||
if (root.collection-menu-title != "") {
|
||||
root.collection-menu-dismiss();
|
||||
if (Collections.collection-menu-title != "") {
|
||||
Collections.collection-menu-dismiss();
|
||||
return accept;
|
||||
}
|
||||
if (root.membership-open) {
|
||||
root.membership-dismiss();
|
||||
if (Collections.membership-open) {
|
||||
Collections.membership-dismiss();
|
||||
return accept;
|
||||
}
|
||||
if (root.offline-prompt-title != "") {
|
||||
@@ -1641,10 +1558,10 @@ in property <bool> panel-visible: true;
|
||||
CollectionsPanel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
rows: root.collection-rows;
|
||||
rows: Collections.collection-rows;
|
||||
selected-id: root.collection-selected;
|
||||
total-images: root.library-total;
|
||||
error: root.collection-error;
|
||||
error: Collections.collection-error;
|
||||
|
||||
// TRACES: FR-NC-6a
|
||||
// The two actions that were in the library header and
|
||||
@@ -1657,32 +1574,32 @@ in property <bool> panel-visible: true;
|
||||
change-library() => { root.library-change(); }
|
||||
|
||||
select(id) => { root.collection-select(id); }
|
||||
toggle(id) => { root.collection-toggle(id); }
|
||||
new-collection() => { root.collection-new(); }
|
||||
row-menu(id) => { root.collection-menu(id); }
|
||||
toggle(id) => { Collections.collection-toggle(id); }
|
||||
new-collection() => { Collections.collection-new(); }
|
||||
row-menu(id) => { Collections.collection-menu(id); }
|
||||
offline-menu(id) => { root.collection-offline-menu(id); }
|
||||
row-press(id, down) => { root.collection-row-press(id, down); }
|
||||
lifted-id: root.collection-lifted;
|
||||
drag-active(id, on) => { root.collection-drag-active(id, on); }
|
||||
root-drop-allowed: root.collection-root-drop-allowed;
|
||||
dropped-on-root() => { root.collection-dropped-on-root(); }
|
||||
row-press(id, down) => { Collections.collection-row-press(id, down); }
|
||||
lifted-id: Collections.collection-lifted;
|
||||
drag-active(id, on) => { Collections.collection-drag-active(id, on); }
|
||||
root-drop-allowed: Collections.collection-root-drop-allowed;
|
||||
dropped-on-root() => { Collections.collection-dropped-on-root(); }
|
||||
|
||||
renaming-id: root.collection-renaming;
|
||||
rename-start(id) => { root.collection-rename-start(id); }
|
||||
renaming-id: Collections.collection-renaming;
|
||||
rename-start(id) => { Collections.collection-rename-start(id); }
|
||||
rename-commit(id, name) => {
|
||||
root.collection-rename-commit(id, name);
|
||||
Collections.collection-rename-commit(id, name);
|
||||
}
|
||||
rename-cancel() => { root.collection-rename-cancel(); }
|
||||
trash-count: root.trash-count;
|
||||
trash-label: root.trash-label;
|
||||
rename-cancel() => { Collections.collection-rename-cancel(); }
|
||||
trash-count: Collections.trash-count;
|
||||
trash-label: Collections.trash-label;
|
||||
selected-count: root.library-selected-count;
|
||||
|
||||
dropped-on(id) => { root.collection-dropped(id); }
|
||||
collection-drag-payload(id) => { return root.collection-drag-payload(id); }
|
||||
dropped-on-trash() => { root.trash-dropped(); }
|
||||
empty-trash() => { root.trash-empty(); }
|
||||
restore-selected() => { root.trash-restore(); }
|
||||
drag-over(id, over) => { root.collection-drag-over(id, over); }
|
||||
dropped-on(id) => { Collections.collection-dropped(id); }
|
||||
collection-drag-payload(id) => { return Collections.collection-drag-payload(id); }
|
||||
dropped-on-trash() => { Collections.trash-dropped(); }
|
||||
empty-trash() => { Collections.trash-empty(); }
|
||||
restore-selected() => { Collections.trash-restore(); }
|
||||
drag-over(id, over) => { Collections.collection-drag-over(id, over); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1737,7 +1654,7 @@ in property <bool> panel-visible: true;
|
||||
window-label: root.library-window-label;
|
||||
offset: root.library-offset;
|
||||
selected-count: root.library-selected-count;
|
||||
scope-label: root.collection-scope-label;
|
||||
scope-label: Collections.collection-scope-label;
|
||||
settings-armed: Transfer.armed;
|
||||
paste-settings-to-selection => {
|
||||
root.paste-settings-to-selection();
|
||||
@@ -1804,7 +1721,7 @@ in property <bool> panel-visible: true;
|
||||
held-row <=> root.library-held-row;
|
||||
// The sidebar's rows, not a second model: the sheet files
|
||||
// into the same tree the sidebar draws.
|
||||
collections: root.collection-rows;
|
||||
collections: Collections.collection-rows;
|
||||
file-in-collection(id, moves) => {
|
||||
root.library-file-in-collection(id, moves);
|
||||
}
|
||||
@@ -1856,7 +1773,7 @@ in property <bool> panel-visible: true;
|
||||
// trash, neither of which has a name to change — Rust drops
|
||||
// those rather than the binding trying to reason about it.
|
||||
rename-scope() => {
|
||||
root.collection-rename-start(root.collection-selected);
|
||||
Collections.collection-rename-start(root.collection-selected);
|
||||
}
|
||||
filter-min-rating-changed(n) => {
|
||||
root.library-filter-min-rating-changed(n);
|
||||
@@ -3261,30 +3178,30 @@ in property <bool> panel-visible: true;
|
||||
MembershipSheet {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
open: root.membership-open;
|
||||
open: Collections.membership-open;
|
||||
selected-count: root.library-selected-count;
|
||||
rows: root.membership-rows;
|
||||
remove(id) => { root.membership-remove(id); }
|
||||
dismiss() => { root.membership-dismiss(); }
|
||||
rows: Collections.membership-rows;
|
||||
remove(id) => { Collections.membership-remove(id); }
|
||||
dismiss() => { Collections.membership-dismiss(); }
|
||||
}
|
||||
|
||||
CollectionMenu {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
title: root.collection-menu-title;
|
||||
detail: root.collection-menu-detail;
|
||||
nested: root.collection-menu-nested;
|
||||
smart: root.collection-menu-smart;
|
||||
confirming: root.collection-menu-confirming;
|
||||
confirm-detail: root.collection-menu-confirm-detail;
|
||||
delete-label: root.collection-menu-delete-label;
|
||||
pinned: root.collection-menu-pinned;
|
||||
rename() => { root.collection-menu-rename(); }
|
||||
new-child() => { root.collection-menu-new-child(); }
|
||||
promote() => { root.collection-menu-promote(); }
|
||||
offline() => { root.collection-menu-offline(); }
|
||||
delete() => { root.collection-menu-delete(); }
|
||||
dismiss() => { root.collection-menu-dismiss(); }
|
||||
title: Collections.collection-menu-title;
|
||||
detail: Collections.collection-menu-detail;
|
||||
nested: Collections.collection-menu-nested;
|
||||
smart: Collections.collection-menu-smart;
|
||||
confirming: Collections.collection-menu-confirming;
|
||||
confirm-detail: Collections.collection-menu-confirm-detail;
|
||||
delete-label: Collections.collection-menu-delete-label;
|
||||
pinned: Collections.collection-menu-pinned;
|
||||
rename() => { Collections.collection-menu-rename(); }
|
||||
new-child() => { Collections.collection-menu-new-child(); }
|
||||
promote() => { Collections.collection-menu-promote(); }
|
||||
offline() => { Collections.collection-menu-offline(); }
|
||||
delete() => { Collections.collection-menu-delete(); }
|
||||
dismiss() => { Collections.collection-menu-dismiss(); }
|
||||
}
|
||||
|
||||
// Last, and therefore over everything including the settings page and
|
||||
|
||||
@@ -1179,6 +1179,124 @@ export struct MembershipRow {
|
||||
detail: string,
|
||||
}
|
||||
|
||||
// TRACES: FR-CAT-7 | FR-CAT-15
|
||||
// The collections sidebar's own state: the tree, the row menu, renaming,
|
||||
// drag and drop between rows, the trash row, and the membership sheet this
|
||||
// file also declares.
|
||||
//
|
||||
// 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; the same reasoning applies here. What the sidebar
|
||||
// shares with the library grid (a photograph dragged out of a cell, the
|
||||
// selection a "file in collection" gesture acts on) stays on the window,
|
||||
// because Rust on the library side still reaches it there.
|
||||
export global Collections {
|
||||
// --- collections (FR-CAT-7) ---
|
||||
//
|
||||
// The sidebar's tree, flattened in Rust: Slint cannot instantiate a
|
||||
// component recursively, so depth arrives as an integer per row.
|
||||
in-out property <[CollectionRow]> collection-rows;
|
||||
in property <string> collection-scope-label: "";
|
||||
in property <string> collection-error: "";
|
||||
|
||||
/// Which collection is being renamed in the sidebar, by id. 0 is none.
|
||||
in property <int> collection-renaming: 0;
|
||||
|
||||
callback collection-toggle(int);
|
||||
callback collection-new();
|
||||
callback collection-menu(int);
|
||||
|
||||
// --- the row menu the gesture above opens (FR-CAT-7) -------------------
|
||||
//
|
||||
// Rust owns every one of these. What the menu offers depends on counts it
|
||||
// has to query — whether the collection is nested, what deleting would
|
||||
// take — and the confirmation step is a policy decision, not a local bit
|
||||
// the sheet can flip for itself.
|
||||
/// The collection the menu is about. Empty closes it.
|
||||
in property <string> collection-menu-title;
|
||||
/// "12 photographs · 2 collections inside".
|
||||
in property <string> collection-menu-detail;
|
||||
/// Whether it has a parent, and so somewhere to be promoted out to.
|
||||
in property <bool> collection-menu-nested: false;
|
||||
/// Whether it is a saved filter, which cannot hold a nested collection.
|
||||
in property <bool> collection-menu-smart: false;
|
||||
/// Whether a delete has been asked once and awaits confirmation.
|
||||
in property <bool> collection-menu-confirming: false;
|
||||
/// What the delete would take with it, and what survives it.
|
||||
in property <string> collection-menu-confirm-detail;
|
||||
in property <string> collection-menu-delete-label;
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Whether the collection the menu is about is kept on this device.
|
||||
in property <bool> collection-menu-pinned: false;
|
||||
callback collection-menu-rename();
|
||||
callback collection-menu-new-child();
|
||||
/// Move it back to the top level. The inverse of the drag that nests one,
|
||||
/// which until now had no gesture at all.
|
||||
callback collection-menu-promote();
|
||||
/// TRACES: FR-NC-6a
|
||||
/// Hand the offline question over to the handler the tray on the row
|
||||
/// already uses, rather than a second route to the same prompt.
|
||||
callback collection-menu-offline();
|
||||
callback collection-menu-delete();
|
||||
callback collection-menu-dismiss();
|
||||
/// A row is held or released — the long press is timed in Rust.
|
||||
callback collection-row-press(int, bool);
|
||||
// --- picking a collection up (FR-UI-3, FR-UI-4) ------------------------
|
||||
/// Which collection the hold has picked up, by id. 0 is none.
|
||||
in property <int> collection-lifted: 0;
|
||||
/// A row's drag crossed the threshold, or ended.
|
||||
callback collection-drag-active(int, bool);
|
||||
/// Whether "All photographs" would take the drag currently in flight.
|
||||
in property <bool> collection-root-drop-allowed: false;
|
||||
/// A collection was dropped on "All photographs": out to the top level.
|
||||
callback collection-dropped-on-root();
|
||||
/// TRACES: FR-UI-2 | FR-UI-4
|
||||
/// Renaming started on a row, by id — a double-click, or `F2`.
|
||||
callback collection-rename-start(int);
|
||||
/// A rename was committed: the collection's id and the new name.
|
||||
callback collection-rename-commit(int, string);
|
||||
/// Renaming was abandoned with Escape.
|
||||
callback collection-rename-cancel();
|
||||
/// Row index and collection id under the pointer. The id is what the
|
||||
/// drop highlight follows: a spring expansion rebuilds the row model
|
||||
/// mid-drag, and an index would then point at a different collection.
|
||||
/// A drag is dwelling over a collection, or has left it: id, and whether
|
||||
/// it is over. Drives the spring-loaded expansion, timed in Rust.
|
||||
callback collection-drag-over(int, bool);
|
||||
/// Images were dropped on a collection, by id. Slint hit-tests the release,
|
||||
/// so this is the collection actually under the pointer.
|
||||
callback collection-dropped(int);
|
||||
/// Payload for dragging a collection onto another to reparent it.
|
||||
pure callback collection-drag-payload(int) -> data-transfer;
|
||||
|
||||
// --- trash (FR-CAT-15) ---
|
||||
//
|
||||
// Not a collection: a drop here moves the file into a trash folder on the
|
||||
// server, where a collection drop only adds a reference.
|
||||
in property <int> trash-count: 0;
|
||||
in property <string> trash-label: "";
|
||||
/// A soft delete: move the dragged images to the trash folder.
|
||||
callback trash-dropped();
|
||||
/// A hard delete: permanently remove everything in the trash.
|
||||
callback trash-empty();
|
||||
/// Put the selected trashed images back where they came from.
|
||||
callback trash-restore();
|
||||
|
||||
// --- where the selection is filed (FR-CAT-7) ---------------------------
|
||||
//
|
||||
// The library's "Remove from collection" can only ever mean the
|
||||
// collection the grid is scoped to — it is the only one that button can
|
||||
// name. This sheet is the other half: which collections the selection is
|
||||
// actually in, and the way out of any of them without navigating there
|
||||
// first. Opened from the library grid via the window's own
|
||||
// `library-open-membership`, because only the library side knows the
|
||||
// selection has changed enough to be worth recomputing against.
|
||||
in property <bool> membership-open: false;
|
||||
in property <[MembershipRow]> membership-rows;
|
||||
callback membership-remove(int);
|
||||
callback membership-dismiss();
|
||||
}
|
||||
|
||||
// Where the selection is filed, and the way out of any of it.
|
||||
//
|
||||
// The library's "Remove from collection" button takes the selection out of the
|
||||
|
||||
Reference in New Issue
Block a user