diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index 5d15b33..2625d12 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -435,8 +435,7 @@ fn build_window(app: &adw::Application) { details: browser_widgets.details, details_entry: RefCell::new(None), details_suppress: Cell::new(false), - grid_selection: browser_widgets.grid_selection.clone(), - list_selection: browser_widgets.list_selection.clone(), + selection: browser_widgets.selection.clone(), }, trash: TrashState { model: trash_widgets.model.clone(), diff --git a/crates/pdfs-gui/src/app/pages/browser.rs b/crates/pdfs-gui/src/app/pages/browser.rs index 7757f52..8054552 100644 --- a/crates/pdfs-gui/src/app/pages/browser.rs +++ b/crates/pdfs-gui/src/app/pages/browser.rs @@ -53,8 +53,7 @@ pub(crate) struct BrowserState { /// requested repeatedly, so comparing their text alone cannot reject an /// older response that finishes after a manual refresh. pub(crate) load_generation: Cell, - /// The grid/list view stack, read to find out which of the two selections is - /// the one the user is actually working in. + /// The grid/list view stack, read to find out which view is on screen. pub(crate) views: gtk4::Stack, /// The bulk-action bar, revealed once more than one entry is selected. pub(crate) bulk: gtk4::Revealer, @@ -138,11 +137,10 @@ pub(crate) struct BrowserWidgets { /// Wraps the views + the details pane; the pane slides in on selection. pub(crate) split: adw::OverlaySplitView, pub(crate) details: DetailsWidgets, - /// The two selection models, so a selection change can drive the details pane - /// and so an action can re-read the entries the user has highlighted. - pub(crate) grid_selection: gtk4::MultiSelection, - pub(crate) list_selection: gtk4::MultiSelection, - /// The grid/list stack, so the page can tell which selection is live. + /// The selection shared by both views, so a selection change can drive the + /// details pane and an action can re-read the entries the user highlighted. + pub(crate) selection: gtk4::MultiSelection, + /// The grid/list stack, so the page can tell which view is on screen. pub(crate) views: gtk4::Stack, pub(crate) bulk: gtk4::Revealer, pub(crate) bulk_label: gtk4::Label, @@ -310,11 +308,14 @@ pub(crate) fn build_browser_page() -> (gtk4::Widget, BrowserWidgets) { // Icon grid. Multi-select: acting on a batch is the common case for trashing // and for taking a folder's worth of files offline, and doing it one // confirmation dialog at a time is not a workflow. - let grid_selection = gtk4::MultiSelection::new(Some(model.clone())); + // One selection for both views: switching layout keeps what is selected. + // Rubberband drags a selection box from empty space, as a file manager does. + let selection = gtk4::MultiSelection::new(Some(model.clone())); let grid = gtk4::GridView::builder() - .model(&grid_selection) + .model(&selection) .min_columns(2) .max_columns(10) + .enable_rubberband(true) .build(); grid.add_css_class("file-grid"); let grid_scroll = gtk4::ScrolledWindow::builder() @@ -322,9 +323,11 @@ pub(crate) fn build_browser_page() -> (gtk4::Widget, BrowserWidgets) { .child(&grid) .build(); - // Column list, with its own selection over the same model. - let list_selection = gtk4::MultiSelection::new(Some(model.clone())); - let column_view = gtk4::ColumnView::builder().model(&list_selection).build(); + // Column list, over the same selection. + let column_view = gtk4::ColumnView::builder() + .model(&selection) + .enable_rubberband(true) + .build(); column_view.add_css_class("data-table"); let column_scroll = gtk4::ScrolledWindow::builder() .vexpand(true) @@ -519,8 +522,7 @@ pub(crate) fn build_browser_page() -> (gtk4::Widget, BrowserWidgets) { refresh, split, details, - grid_selection, - list_selection, + selection, views: view_stack, bulk, bulk_label, @@ -550,15 +552,9 @@ pub(crate) fn browser_status(ui: &Rc, icon: &str, title: &str, description: hide_details(ui); } -/// The selection model of whichever view is on screen. The grid and the list -/// each own one over the same model, so "what is selected" depends on which the -/// user is looking at. +/// The selection model shared by the grid and the list. pub(crate) fn active_selection(ui: &Rc) -> gtk4::MultiSelection { - if ui.browser.views.visible_child_name().as_deref() == Some("list") { - ui.details.list_selection.clone() - } else { - ui.details.grid_selection.clone() - } + ui.details.selection.clone() } /// Every entry highlighted in the view on screen, in model order. @@ -575,10 +571,9 @@ pub(crate) fn selected_entries(ui: &Rc) -> Vec { .collect() } -/// Drop the selection in both views, which also retracts the bulk bar. +/// Drop the selection, which also retracts the bulk bar. pub(crate) fn clear_selection(ui: &Rc) { - ui.details.grid_selection.unselect_all(); - ui.details.list_selection.unselect_all(); + ui.details.selection.unselect_all(); } /// Reflect the current selection in the bulk bar: how many are selected, and diff --git a/crates/pdfs-gui/src/app/widgets/details.rs b/crates/pdfs-gui/src/app/widgets/details.rs index 84684fe..61d4f59 100644 --- a/crates/pdfs-gui/src/app/widgets/details.rs +++ b/crates/pdfs-gui/src/app/widgets/details.rs @@ -9,10 +9,9 @@ pub(crate) struct DetailsState { /// Set while the details pane is being populated, so setting the offline /// switch programmatically doesn't fire a pin/unpin round-trip. pub(crate) details_suppress: Cell, - /// The grid's and list's selection models. Both wrap the one browser model, so - /// a selection in either drives the details pane. - pub(crate) grid_selection: gtk4::MultiSelection, - pub(crate) list_selection: gtk4::MultiSelection, + /// The one selection model behind both the grid and the list, so switching + /// views keeps what is selected; it drives the details pane. + pub(crate) selection: gtk4::MultiSelection, } /// The widgets in the browser's details pane that a selection repaints. @@ -152,23 +151,21 @@ pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) { /// Connect the details pane: mirror the grid's and list's selection into it, and /// wire its buttons back onto the entry it's showing. pub(crate) fn wire_details(ui: &Rc) { - // Both views share one model but have their own selection, so watch both. - for selection in [ - ui.details.grid_selection.clone(), - ui.details.list_selection.clone(), - ] { + { let ui_sel = ui.clone(); - selection.connect_selection_changed(move |_, _, _| { - // The details pane describes *one* entry. A batch is the bulk bar's - // business, so the pane steps aside rather than picking a member of - // the selection to speak for the rest. - let entries = selected_entries(&ui_sel); - match entries.as_slice() { - [entry] => show_details(&ui_sel, entry), - _ => hide_details(&ui_sel), - } - sync_bulk_bar(&ui_sel); - }); + ui.details + .selection + .connect_selection_changed(move |_, _, _| { + // The details pane describes *one* entry. A batch is the bulk bar's + // business, so the pane steps aside rather than picking a member of + // the selection to speak for the rest. + let entries = selected_entries(&ui_sel); + match entries.as_slice() { + [entry] => show_details(&ui_sel, entry), + _ => hide_details(&ui_sel), + } + sync_bulk_bar(&ui_sel); + }); } let ui_close = ui.clone(); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7c494f6..fde787f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -72,6 +72,8 @@ Schema: **31** (unchanged). Copy Link · Rename…, Move To…, Versions… · Move to Trash. Right-clicking empty space offers New Folder, the uploads, Select All, Refresh and Open in File Manager. Open With… asks which application to use. +- **One selection in My Files.** The grid and the list share their selection, so switching views + keeps what is selected, and dragging across empty space draws a selection box in both. - **US spelling throughout.** The app, the CLI and the docs say "Favorite" and "color", matching the `pdfs favorite` command.