diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs index d295406..8259a17 100644 --- a/crates/pdfs-gui/src/app/main.rs +++ b/crates/pdfs-gui/src/app/main.rs @@ -1342,6 +1342,11 @@ fn install_shortcuts(ui: &Rc, window: &adw::ApplicationWindow) { gtk4::gdk::Key::Up if alt && on_browser => { ui.browser.actions.activate_action("up", None) } + // A focused text field takes Backspace first, so this only fires + // with the focus on the files, as in a file manager. + gtk4::gdk::Key::BackSpace if on_browser => { + ui.browser.actions.activate_action("back", None) + } gtk4::gdk::Key::u | gtk4::gdk::Key::U if ctrl && on_browser => prompt_upload(&ui), gtk4::gdk::Key::Menu if on_browser => popup_keyboard_context_menu(&ui), gtk4::gdk::Key::Return | gtk4::gdk::Key::KP_Enter if alt && on_browser => { diff --git a/crates/pdfs-gui/src/app/pages/browser.rs b/crates/pdfs-gui/src/app/pages/browser.rs index 97d3a75..d1fd118 100644 --- a/crates/pdfs-gui/src/app/pages/browser.rs +++ b/crates/pdfs-gui/src/app/pages/browser.rs @@ -815,6 +815,8 @@ pub(crate) fn browser_views(ui: &Rc) { pub(crate) fn wire_browser(ui: &Rc, grid: >k4::GridView, column_view: >k4::ColumnView) { attach_background_menu(ui, grid); attach_background_menu(ui, column_view); + attach_background_deselect(ui, grid); + attach_background_deselect(ui, column_view); // Resize only realised grid cells. Rebuilding the whole model for every // slider step would repeatedly tear down selection state while the pointer // is still moving. @@ -1102,6 +1104,48 @@ pub(crate) fn attach_background_menu(ui: &Rc, view: &impl IsA) view.add_controller(gesture); } +/// Clicking the empty space of a view drops the selection, as in a file +/// manager. GTK's list views leave it alone, so a stray highlight otherwise +/// sticks around until another item is clicked. +pub(crate) fn attach_background_deselect(ui: &Rc, view: &impl IsA) { + let gesture = gtk4::GestureClick::new(); + gesture.set_button(gtk4::gdk::BUTTON_PRIMARY); + // Capture sees the press before an item's own gesture claims it; this only + // looks, so the item still gets its click. + gesture.set_propagation_phase(gtk4::PropagationPhase::Capture); + let ui = ui.clone(); + let target = view.clone().upcast::(); + gesture.connect_pressed(move |gesture, _, x, y| { + let modifiers = gesture.current_event_state(); + if modifiers + .intersects(gtk4::gdk::ModifierType::CONTROL_MASK | gtk4::gdk::ModifierType::SHIFT_MASK) + { + return; + } + if !on_item(&target, x, y) { + clear_selection(&ui); + sync_bulk_bar(&ui); + } + }); + view.add_controller(gesture); +} + +/// Whether `(x, y)` in `view` lands on an item: a grid tile ("child"), a list +/// row ("row"), or a column header ("header"), which has clicks of its own. +fn on_item(view: >k4::Widget, x: f64, y: f64) -> bool { + let mut widget = view.pick(x, y, gtk4::PickFlags::DEFAULT); + while let Some(w) = widget { + if &w == view { + return false; + } + if matches!(w.css_name().as_str(), "child" | "row" | "header") { + return true; + } + widget = w.parent(); + } + false +} + /// The Menu key or Shift+F10: the menu for the selection, or for the folder /// when nothing is selected, opened at the focused item. pub(crate) fn popup_keyboard_context_menu(ui: &Rc) { diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e98d77c..d599bd7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -121,6 +121,8 @@ Schema: **31** (unchanged). so the folder did not open. The pane now shows on request, from the header's Details toggle, Alt+Enter or the item menu's Details, and then follows the selection. Closing it keeps the selection. +- **My Files behaves more like a file manager.** A click on empty space between the files clears + the selection; Ctrl or Shift held keeps it. Backspace goes back while the focus is on the files. - **An upload parked behind a temp-file rename no longer stays parked forever.** A create for a name that looks like a writer's temporary file waits for the rename to the final name. If that rename never came — the writer crashed, the file was deleted, or the name was never temporary —