The scan/opening/status/error lines, offline mode and its retry, pinning a collection offline, the sync and thumbnail-sweep state, and the callbacks that route the grid to a rescan, a sync, another library, a panorama merge or an export — the last of what AppWindow still carried under the library- prefix — move onto the `Library` global started earlier on this branch. Rust reaches them through window.global::<Library>() rather than window.set_/get_/on_/invoke_ on the root. library-visible is the one name that stays: it is computed from active-page and active-view, the shell's own routing state, which a global cannot read. AppWindow now declares no other library- property or callback.
772 lines
32 KiB
Rust
772 lines
32 KiB
Rust
//! Sidebar-facing wiring: tree navigation, creating and renaming
|
|
//! collections, removing images from the one on screen, the row's own
|
|
//! hold-drag, its context menu, and the membership sheet.
|
|
//!
|
|
//! Split from [`super::wiring_grid`] because that half is about the grid
|
|
//! and this half is about the tree; [`super::wire`] calls into both.
|
|
|
|
use std::cell::RefCell;
|
|
use std::rc::Rc;
|
|
|
|
use dr_catalog::collections::{self as coll};
|
|
use dr_catalog::Catalog;
|
|
use dr_types::{CollectionId, ImageId};
|
|
use slint::{ComponentHandle, Model as _};
|
|
|
|
use crate::{AppWindow, Collections, Library};
|
|
|
|
use super::controller::{
|
|
decide_delete, decide_release, CollectionsController, DeleteStep, Release,
|
|
};
|
|
use super::drag::HOLD_DELAY_MS;
|
|
use super::rename_menu::{
|
|
apply_rename, close_rename, close_row_menu, create_child, open_row_menu, Rename,
|
|
};
|
|
use super::tree_sync::{
|
|
direct_holdings, refresh_membership, refresh_tree, sync_badges, sync_reorderable,
|
|
};
|
|
|
|
/// Tree navigation.
|
|
pub(super) fn wire_tree_navigation(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
on_scope_changed: &Rc<dyn Fn()>,
|
|
) {
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let reload = on_scope_changed.clone();
|
|
window.on_collection_select(move |id| {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
|
|
// -1 is the trash. It is not a collection, so it clears `scope`
|
|
// rather than setting it — see `viewing_trash`.
|
|
ctl.viewing_trash.set(id == -1);
|
|
*ctl.scope.borrow_mut() = if id <= 0 {
|
|
None
|
|
} else {
|
|
Some(CollectionId(id as u64))
|
|
};
|
|
|
|
// A selection the user cannot see is one they will act on by
|
|
// accident, and the new scope shows different images.
|
|
ctl.clear_selection();
|
|
|
|
let label = if id == -1 {
|
|
"Trash".to_string()
|
|
} else if id == 0 {
|
|
String::new()
|
|
} else {
|
|
let rows = w.global::<Collections>().get_collection_rows();
|
|
(0..rows.row_count())
|
|
.filter_map(|i| rows.row_data(i))
|
|
.find(|r| r.id == id)
|
|
.map(|r| r.name.to_string())
|
|
.unwrap_or_default()
|
|
};
|
|
w.set_collection_selected(id);
|
|
w.global::<Collections>()
|
|
.set_collection_scope_label(label.into());
|
|
sync_reorderable(&w);
|
|
reload();
|
|
});
|
|
}
|
|
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Create.
|
|
pub(super) fn wire_create(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
) {
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
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.global::<Collections>()
|
|
.set_collection_error("Open a library first.".into());
|
|
return;
|
|
};
|
|
|
|
// Created inside whatever is selected, which is how a hierarchy
|
|
// gets built without leaving the header: select the parent, press
|
|
// +. The row menu aims the same act at a row instead.
|
|
create_child(&w, &ctl, cat, *ctl.scope.borrow());
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Rename.
|
|
///
|
|
/// Inline in the row, opened by a double-click or `F2`. The gesture is worth
|
|
/// the field rather than a dialog: renaming is how a hierarchy gets tidied,
|
|
/// and it is done in runs of several — a modal per collection would make
|
|
/// that a chore.
|
|
pub(super) fn wire_rename(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
) {
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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.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
|
|
.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;
|
|
}
|
|
|
|
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.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());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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());
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Remove from the collection being shown.
|
|
pub(super) fn wire_remove(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
visible_ids: &Rc<dyn Fn() -> Vec<ImageId>>,
|
|
on_scope_changed: &Rc<dyn Fn()>,
|
|
) {
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
let visible = visible_ids.clone();
|
|
let reload = on_scope_changed.clone();
|
|
window
|
|
.global::<Library>()
|
|
.on_library_remove_from_collection(move || {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
let Some(scope) = *ctl.scope.borrow() else {
|
|
// Unscoped, there is no collection to remove from. The button
|
|
// is hidden in that state; this guards the callback anyway.
|
|
return;
|
|
};
|
|
let chosen = ctl.selected();
|
|
if chosen.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let borrow = catalog.borrow();
|
|
let Some(cat) = borrow.as_ref() else { return };
|
|
|
|
match coll::remove_images(cat.connection(), scope, &chosen) {
|
|
Ok(n) => {
|
|
w.global::<Collections>()
|
|
.set_collection_error(slint::SharedString::new());
|
|
// Named explicitly as a membership change: the images are
|
|
// still in the library, and a user who reads this as a
|
|
// delete will not trust the feature again.
|
|
w.global::<Library>().set_library_status(
|
|
format!("Removed {n} from this collection; still in the library")
|
|
.into(),
|
|
);
|
|
ctl.clear_selection();
|
|
refresh_tree(&w, &ctl, cat);
|
|
sync_badges(&w, cat, &visible());
|
|
reload();
|
|
}
|
|
Err(e) => w
|
|
.global::<Collections>()
|
|
.set_collection_error(format!("removing: {e}").into()),
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// The tree row's own hold-drag: lift a row, drag it onto another to
|
|
/// reparent, or onto the root to un-nest it.
|
|
pub(super) fn wire_row_drag(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
) {
|
|
// TRACES: FR-CAT-7 | FR-UI-2 | FR-UI-3 | FR-UI-4
|
|
// The hold on a sidebar row: it arms the drag that rearranges the tree,
|
|
// and it opens the row menu. Which one the user gets is decided on release
|
|
// by whether they moved.
|
|
//
|
|
// Both from one gesture because there is only one to spend. A finger has
|
|
// no right button, and this tree lives in a Flickable that claims any drag
|
|
// beginning inside it — so without a hold, a touch drag on a row is a
|
|
// scroll, and arranging the tree is a pointer-only feature. The row lifts
|
|
// the moment the timer fires, which is what tells the user the next
|
|
// movement will carry the collection rather than scroll past it.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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.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;
|
|
}
|
|
|
|
// 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.global::<Collections>().set_collection_lifted(id.0 as i32);
|
|
},
|
|
);
|
|
*ctl.row_hold_timer.borrow_mut() = Some(timer);
|
|
});
|
|
}
|
|
|
|
// TRACES: FR-CAT-7 | FR-UI-4
|
|
// A row's drag crossed the threshold, or ended.
|
|
//
|
|
// This is what turns a hold into a rearrangement: once it has fired, the
|
|
// release that follows opens no menu. It also decides whether
|
|
// "All photographs" will accept the drop, which only makes sense for a
|
|
// collection that has a parent to be taken out of.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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.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.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.global::<Collections>()
|
|
.set_collection_root_drop_allowed(nested && !carrying_images);
|
|
});
|
|
}
|
|
|
|
// TRACES: FR-CAT-7
|
|
// Dropped on "All photographs": out to the top level.
|
|
//
|
|
// The gestural inverse of dropping one row onto another. `can-drop` has
|
|
// already refused everything but a nested collection, so reaching here
|
|
// means the drop is one — but the payload is taken rather than read, for
|
|
// the reason the row's own drop gives: a remembered press must be spent by
|
|
// the drop it belongs to, or a later empty drop moves a collection nobody
|
|
// touched.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
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;
|
|
}
|
|
|
|
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.global::<Library>()
|
|
.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()),
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// The row menu.
|
|
///
|
|
/// Right-click, or a long press on touch. This gesture used to *delete an
|
|
/// empty collection outright* and refuse with an error message otherwise,
|
|
/// which was wrong in both directions at once: the destructive half fired
|
|
/// with no confirmation and nothing on screen said it would, and the
|
|
/// refusing half meant a collection holding anything could not be deleted
|
|
/// at all — the user emptied it by hand and then did the same thing.
|
|
pub(super) fn wire_row_menu(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
on_scope_changed: &Rc<dyn Fn()>,
|
|
visible_ids: &Rc<dyn Fn() -> Vec<ImageId>>,
|
|
) {
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
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.
|
|
//
|
|
// The menu closes first and the field opens second, and the order is
|
|
// load-bearing for the same reason it is in `collection-new`: the field is
|
|
// focused by an `init` on a row, and a row torn down by a model rebuild
|
|
// takes the focus with it. Here the tear-down is the sheet's, and a field
|
|
// opened behind a sheet that is still up is a field the user cannot see
|
|
// they are typing into.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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.
|
|
//
|
|
// The `+` in the header already creates inside whatever the grid is scoped
|
|
// to, which builds a hierarchy only if you first go and look at the parent.
|
|
// This is the same act aimed at a row, so a tree can be built from the tree.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
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
|
|
// Un-nesting: back out to the top level.
|
|
//
|
|
// Nesting has had a gesture since collections landed — drag a row onto
|
|
// another row — and its inverse had none, in either direction: "All
|
|
// photographs" ignores drops on purpose (an image is already in the
|
|
// library), and no menu existed to ask. So a collection dragged into
|
|
// another was in there permanently, and the only way out was to delete it
|
|
// and build it again.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
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.global::<Collections>()
|
|
.set_collection_error(slint::SharedString::new());
|
|
w.global::<Library>()
|
|
.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()),
|
|
}
|
|
});
|
|
}
|
|
|
|
// TRACES: FR-NC-6a
|
|
// The offline question, handed to the handler the tray on the row already
|
|
// uses. Routed through the window's own callback rather than reaching into
|
|
// `library_ui`'s prompt directly: that module owns the transfer state the
|
|
// prompt reads, and a second entry point into it is a second place for the
|
|
// two to disagree about what is downloading.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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
|
|
// Delete, in one press or two — see [`decide_delete`].
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
let reload = on_scope_changed.clone();
|
|
let visible = visible_ids.clone();
|
|
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.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.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.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.global::<Library>().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);
|
|
|
|
// 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()),
|
|
}
|
|
});
|
|
}
|
|
|
|
// Dismiss. A pending confirmation is backed out of one step rather than
|
|
// closing the whole menu: Escape from "are you sure" means "no", and
|
|
// taking the sheet away with it would leave the user unsure whether the
|
|
// key had cancelled the delete or performed it.
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Where the selection is filed.
|
|
pub(super) fn wire_membership(
|
|
window: &AppWindow,
|
|
ctl: &Rc<CollectionsController>,
|
|
catalog: &Rc<RefCell<Option<Catalog>>>,
|
|
visible_ids: &Rc<dyn Fn() -> Vec<ImageId>>,
|
|
on_scope_changed: &Rc<dyn Fn()>,
|
|
) {
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
window
|
|
.global::<Library>()
|
|
.on_library_open_membership(move || {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
let borrow = catalog.borrow();
|
|
let Some(cat) = borrow.as_ref() else { return };
|
|
refresh_membership(&w, &ctl, cat);
|
|
w.global::<Collections>().set_membership_open(true);
|
|
});
|
|
}
|
|
|
|
{
|
|
let weak = window.as_weak();
|
|
let ctl = ctl.clone();
|
|
let catalog = catalog.clone();
|
|
let visible = visible_ids.clone();
|
|
let reload = on_scope_changed.clone();
|
|
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;
|
|
}
|
|
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.global::<Library>().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
|
|
.global::<Collections>()
|
|
.on_membership_dismiss(move || {
|
|
let Some(w) = weak.upgrade() else { return };
|
|
w.global::<Collections>().set_membership_open(false);
|
|
});
|
|
}
|
|
}
|