mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
A popover is sized and placed again when its parent's layout manager allocates it. Context menus were parented to the outermost list view, and the keyboard menu to the ScrolledWindow; neither has a layout manager, so a menu kept the size it was first given and often opened too short, with a scrollbar. The host is now the first widget from the list view up that has a layout manager, which still keeps menus out of list items and the theme rules that style their insides.
162 lines
5.8 KiB
Rust
162 lines
5.8 KiB
Rust
use crate::*;
|
|
|
|
/// A `gio::Menu` whose items run closures, for menus whose contents depend on
|
|
/// what they were opened for (a row, a selection, a photo). Each item gets its
|
|
/// own action in a private `menu` group, installed on whatever hosts the
|
|
/// popover, so the result is a real [`gtk4::PopoverMenu`]: arrow keys, Enter,
|
|
/// Escape and mnemonics work, and it reads as a menu to assistive technology.
|
|
pub(crate) struct ActionMenu {
|
|
menu: gio::Menu,
|
|
section: gio::Menu,
|
|
group: gio::SimpleActionGroup,
|
|
count: usize,
|
|
}
|
|
|
|
impl ActionMenu {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
menu: gio::Menu::new(),
|
|
section: gio::Menu::new(),
|
|
group: gio::SimpleActionGroup::new(),
|
|
count: 0,
|
|
}
|
|
}
|
|
|
|
fn next_action(&mut self) -> String {
|
|
self.count += 1;
|
|
format!("a{}", self.count)
|
|
}
|
|
|
|
/// A plain item.
|
|
pub(crate) fn item(&mut self, label: &str, run: impl Fn() + 'static) -> &mut Self {
|
|
let name = self.next_action();
|
|
let action = gio::SimpleAction::new(&name, None);
|
|
action.connect_activate(move |_, _| run());
|
|
self.group.add_action(&action);
|
|
self.section
|
|
.append(Some(label), Some(&format!("menu.{name}")));
|
|
self
|
|
}
|
|
|
|
/// A check item showing `active`; `run` gets the state the user asked for.
|
|
pub(crate) fn toggle(
|
|
&mut self,
|
|
label: &str,
|
|
active: bool,
|
|
run: impl Fn(bool) + 'static,
|
|
) -> &mut Self {
|
|
let name = self.next_action();
|
|
let action = gio::SimpleAction::new_stateful(&name, None, &active.to_variant());
|
|
action.connect_activate(move |action, _| {
|
|
let next = !action
|
|
.state()
|
|
.and_then(|s| s.get::<bool>())
|
|
.unwrap_or(false);
|
|
action.set_state(&next.to_variant());
|
|
run(next);
|
|
});
|
|
self.group.add_action(&action);
|
|
self.section
|
|
.append(Some(label), Some(&format!("menu.{name}")));
|
|
self
|
|
}
|
|
|
|
/// Close the current section; the next item starts a new one below a
|
|
/// separator. An empty section is skipped, so callers can end a group of
|
|
/// conditional items unconditionally.
|
|
pub(crate) fn section(&mut self) -> &mut Self {
|
|
self.flush(None);
|
|
self
|
|
}
|
|
|
|
/// Close the current section under a heading.
|
|
pub(crate) fn labelled_section(&mut self, heading: &str) -> &mut Self {
|
|
self.flush(Some(heading));
|
|
self
|
|
}
|
|
|
|
fn flush(&mut self, heading: Option<&str>) {
|
|
if self.section.n_items() > 0 {
|
|
self.menu.append_section(heading, &self.section);
|
|
self.section = gio::Menu::new();
|
|
}
|
|
}
|
|
|
|
fn finish(mut self) -> (gio::Menu, gio::SimpleActionGroup) {
|
|
self.flush(None);
|
|
(self.menu, self.group)
|
|
}
|
|
|
|
/// Pop the menu up over `anchor` at `(x, y)` in its coordinates. The
|
|
/// popover removes itself once dismissed; it waits for an idle turn, so the
|
|
/// item that closed it still finds its action.
|
|
pub(crate) fn popup_at(self, anchor: &impl IsA<gtk4::Widget>, x: f64, y: f64) {
|
|
let (menu, group) = self.finish();
|
|
let anchor = anchor.as_ref();
|
|
let host = popover_host(anchor);
|
|
let (x, y) = anchor
|
|
.compute_point(&host, >k4::graphene::Point::new(x as f32, y as f32))
|
|
.map(|p| (p.x() as f64, p.y() as f64))
|
|
.unwrap_or((x, y));
|
|
let popover = gtk4::PopoverMenu::from_model(Some(&menu));
|
|
popover.set_has_arrow(false);
|
|
popover.set_position(gtk4::PositionType::Bottom);
|
|
popover.set_halign(gtk4::Align::Start);
|
|
popover.set_pointing_to(Some(>k4::gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
|
|
popover.insert_action_group("menu", Some(&group));
|
|
popover.set_parent(&host);
|
|
popover.connect_closed(|p| {
|
|
let p = p.clone();
|
|
glib::idle_add_local_once(move || p.unparent());
|
|
});
|
|
popover.popup();
|
|
}
|
|
|
|
/// A flat ⋮ button that opens this menu.
|
|
pub(crate) fn button(self) -> gtk4::MenuButton {
|
|
let (menu, group) = self.finish();
|
|
let button = gtk4::MenuButton::builder()
|
|
.icon_name("view-more-symbolic")
|
|
// Translators: tooltip of a button that opens a menu of further actions.
|
|
.tooltip_text(gettext("More"))
|
|
.valign(gtk4::Align::Center)
|
|
.menu_model(&menu)
|
|
.build();
|
|
button.insert_action_group("menu", Some(&group));
|
|
button.add_css_class("flat");
|
|
button
|
|
}
|
|
}
|
|
|
|
/// Where a menu opened over `anchor` should live in the widget tree.
|
|
///
|
|
/// Not inside a list item: CSS descendant selectors reach through a popover to
|
|
/// its parent, and themes style the insides of list items (`gridview > child
|
|
/// box { margin: 12px }` in Catppuccin); a menu parented to a tile picks those
|
|
/// rules up and puts a wide margin around every section. So the search starts
|
|
/// at the outermost grid or list view around `anchor`.
|
|
///
|
|
/// Not on a widget without a layout manager either: a popover is sized and
|
|
/// placed again when its parent's layout manager allocates it. The list views
|
|
/// and `ScrolledWindow` have none, so a menu parented there kept the size it was
|
|
/// first given, often too small, and opened scrolling. The host is the first
|
|
/// widget from there up that has one.
|
|
fn popover_host(anchor: >k4::Widget) -> gtk4::Widget {
|
|
let mut start = anchor.clone();
|
|
let mut widget = anchor.parent();
|
|
while let Some(w) = widget {
|
|
widget = w.parent();
|
|
if w.is::<gtk4::GridView>() || w.is::<gtk4::ListView>() || w.is::<gtk4::ColumnView>() {
|
|
start = w;
|
|
}
|
|
}
|
|
let mut host = Some(start.clone());
|
|
while let Some(w) = host {
|
|
if w.layout_manager().is_some() {
|
|
return w;
|
|
}
|
|
host = w.parent();
|
|
}
|
|
start
|
|
}
|