Merge: the collections sidebar on its own Slint global
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user