fix(gui): keep the details pane under the toggle's control

Pin the split view's sidebar so the wide breakpoint no longer opens the
pane by itself, and show a "No item selected" state instead of closing
it when a navigation clears the entry. The pane follows the header
toggle alone, which also tracks an overlay closed by a click outside.
This commit is contained in:
nvrl committed 2026-09-23 03:58:43 +02:00
1 parent f6136ce344
commit 2dde69525e
4 files changed
+67 -28

No files matched your search

+2 -1
View File
@@ -1501,7 +1501,8 @@ fn install_shortcuts(ui: &Rc<Ui>, window: &adw::ApplicationWindow) {
{ {
clear_selection(&ui); clear_selection(&ui);
sync_bulk_bar(&ui); sync_bulk_bar(&ui);
hide_details(&ui); clear_details(&ui);
ui.details.details.toggle.set_active(false);
} }
_ => return glib::Propagation::Proceed, _ => return glib::Propagation::Proceed,
} }
+4 -1
View File
@@ -369,6 +369,9 @@ pub(crate) fn build_browser_page() -> (gtk4::Widget, BrowserWidgets) {
.sidebar_position(gtk4::PackType::End) .sidebar_position(gtk4::PackType::End)
.collapsed(true) .collapsed(true)
.show_sidebar(false) .show_sidebar(false)
// Unpinned, uncollapsing at the wide breakpoint would show the pane by
// itself; the header toggle alone decides.
.pin_sidebar(true)
.max_sidebar_width(300.0) .max_sidebar_width(300.0)
.content(&content) .content(&content)
.sidebar(&details_pane) .sidebar(&details_pane)
@@ -568,7 +571,7 @@ pub(crate) fn browser_status(ui: &Rc<Ui>, icon: &str, title: &str, description:
// status (loading, offline, error) turns them back off. // status (loading, offline, error) turns them back off.
ui.browser.empty_actions.set_visible(false); ui.browser.empty_actions.set_visible(false);
ui.browser.content.set_visible_child_name("status"); ui.browser.content.set_visible_child_name("status");
hide_details(ui); clear_details(ui);
} }
/// The selection model shared by the grid and the list. /// The selection model shared by the grid and the list.
+55 -26
View File
@@ -39,6 +39,8 @@ pub(crate) struct DetailsWidgets {
/// fills the pane; it is shown while this is on, so a click never moves /// fills the pane; it is shown while this is on, so a click never moves
/// the files under the pointer halfway through a double-click. /// the files under the pointer halfway through a double-click.
pub(crate) toggle: gtk4::ToggleButton, pub(crate) toggle: gtk4::ToggleButton,
/// "details" while an entry is shown, "empty" with nothing selected.
pub(crate) pages: gtk4::Stack,
} }
/// The details pane shown beside the file views: a big type icon over the entry's /// The details pane shown beside the file views: a big type icon over the entry's
@@ -171,8 +173,37 @@ pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) {
.hscrollbar_policy(gtk4::PolicyType::Never) .hscrollbar_policy(gtk4::PolicyType::Never)
.child(&inner) .child(&inner)
.build(); .build();
// With the pane open and nothing selected (a fresh folder, a cleared
// selection) it says so rather than closing: opening and closing it by
// itself would move the files under the pointer.
let empty_close = gtk4::Button::builder()
.icon_name("window-close-symbolic")
.tooltip_text("Close details")
.halign(gtk4::Align::End)
.margin_top(12)
.margin_end(12)
.build();
empty_close.add_css_class("flat");
empty_close.add_css_class("circular");
let toggle_off = toggle.clone();
empty_close.connect_clicked(move |_| toggle_off.set_active(false));
let empty_status = adw::StatusPage::builder()
.icon_name("sidebar-show-right-symbolic")
.title("No item selected")
.description("Select a file or folder to see its details.")
.vexpand(true)
.build();
empty_status.add_css_class("compact");
let empty = gtk4::Box::new(gtk4::Orientation::Vertical, 0);
empty.append(&empty_close);
empty.append(&empty_status);
let pages = gtk4::Stack::new();
pages.add_named(&scroll, Some("details"));
pages.add_named(&empty, Some("empty"));
pages.set_visible_child_name("empty");
let pane = adw::ToolbarView::new(); let pane = adw::ToolbarView::new();
pane.set_content(Some(&scroll)); pane.set_content(Some(&pages));
pane.add_css_class("background"); pane.add_css_class("background");
( (
@@ -196,6 +227,7 @@ pub(crate) fn build_details_pane() -> (gtk4::Widget, DetailsWidgets) {
trash_button, trash_button,
close_button, close_button,
toggle, toggle,
pages,
}, },
) )
} }
@@ -214,7 +246,7 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
let entries = selected_entries(&ui_sel); let entries = selected_entries(&ui_sel);
match entries.as_slice() { match entries.as_slice() {
[entry] => show_details(&ui_sel, entry), [entry] => show_details(&ui_sel, entry),
_ => hide_details(&ui_sel), _ => clear_details(&ui_sel),
} }
sync_bulk_bar(&ui_sel); sync_bulk_bar(&ui_sel);
}); });
@@ -227,8 +259,16 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
let ui_toggle = ui.clone(); let ui_toggle = ui.clone();
ui.details.details.toggle.connect_toggled(move |toggle| { ui.details.details.toggle.connect_toggled(move |toggle| {
let show = toggle.is_active() && ui_toggle.details.details_entry.borrow().is_some(); ui_toggle.browser.split.set_show_sidebar(toggle.is_active());
ui_toggle.browser.split.set_show_sidebar(show); });
// On a narrow window the pane overlays the files, and a click outside it
// closes it; the toggle follows so the next click on it opens it again.
let ui_split = ui.clone();
ui.browser.split.connect_show_sidebar_notify(move |split| {
let toggle = &ui_split.details.details.toggle;
if toggle.is_active() != split.shows_sidebar() {
toggle.set_active(split.shows_sidebar());
}
}); });
let ui_open = ui.clone(); let ui_open = ui.clone();
@@ -301,7 +341,7 @@ pub(crate) fn wire_details(ui: &Rc<Ui>) {
}); });
} }
/// Reveal the details pane and paint it from `entry`. /// Paint the details pane from `entry`. It shows while the header toggle is on.
pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) { pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) {
ui.details.details_suppress.set(true); ui.details.details_suppress.set(true);
let d = &ui.details.details; let d = &ui.details.details;
@@ -348,23 +388,11 @@ pub(crate) fn show_details(ui: &Rc<Ui>, entry: &DirEntry) {
if mounted { if mounted {
load_details_extras(ui, entry); load_details_extras(ui, entry);
} }
// Only a pane the user asked for is revealed. This runs from // Only the content changes. Whether the pane shows is the toggle's call:
// `selection_changed`, which fires on the *first* press of a double-click: // this runs from `selection_changed`, on the *first* press of a
// sliding the pane in there reflows the files, so the second press lands on // double-click, and sliding the pane in there would move the files so the
// a different spot (or item) and the folder never opens. With the pane // second press lands elsewhere and the folder never opens.
// already up, a new selection only repaints it. Deferred to idle all the d.pages.set_visible_child_name("details");
// same, since mutating the widget tree mid-gesture cancels GtkGridView's
// multi-press tracking; the guard lets a navigation that clears the entry
// win the race.
if !ui.details.details.toggle.is_active() {
return;
}
let ui = ui.clone();
glib::idle_add_local_once(move || {
if ui.details.details_entry.borrow().is_some() {
ui.browser.split.set_show_sidebar(true);
}
});
} }
/// Fill the Sharing and Versions rows, which need the network. The answers /// Fill the Sharing and Versions rows, which need the network. The answers
@@ -449,8 +477,6 @@ pub(crate) fn versions_summary(items: &[RevisionInfo]) -> String {
} }
} }
/// Hide the details pane and forget the entry it was showing, so a stale entry
/// can't be acted on after the listing moves on.
/// Show the pane for `entry`, turning the header toggle on. /// Show the pane for `entry`, turning the header toggle on.
pub(crate) fn open_details(ui: &Rc<Ui>, entry: &DirEntry) { pub(crate) fn open_details(ui: &Rc<Ui>, entry: &DirEntry) {
ui.details.details.toggle.set_active(true); ui.details.details.toggle.set_active(true);
@@ -463,8 +489,11 @@ pub(crate) fn toggle_details(ui: &Rc<Ui>) {
toggle.set_active(!toggle.is_active()); toggle.set_active(!toggle.is_active());
} }
pub(crate) fn hide_details(ui: &Rc<Ui>) { /// Forget the entry the pane was showing, so a stale entry can't be acted on
ui.browser.split.set_show_sidebar(false); /// after the listing moves on. An open pane stays open and says nothing is
/// selected.
pub(crate) fn clear_details(ui: &Rc<Ui>) {
ui.details.details.pages.set_visible_child_name("empty");
*ui.details.details_entry.borrow_mut() = None; *ui.details.details_entry.borrow_mut() = None;
} }
+6
View File
@@ -129,6 +129,12 @@ Schema: **31** (unchanged).
so the folder did not open. The pane now shows on request, from the header's Details toggle, 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 Alt+Enter or the item menu's Details, and then follows the selection. Closing it keeps the
selection. selection.
- **The details pane opens only when asked.** On a wide window it used to show at start, empty,
and close again whenever you entered a folder. It now follows the header toggle alone. With
nothing selected it says so instead of closing, so opening a folder no longer moves the files.
- **Context menus no longer have wide margins under some themes.** A menu opened on a file tile
was placed inside the tile, so a theme's rules for tile contents (Catppuccin's
`gridview > child box { margin: 12px }`) spaced out every section.
- **My Files behaves more like a file manager.** A click on empty space between the files clears - **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. 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 - **An upload parked behind a temp-file rename no longer stays parked forever.** A create for a