Grab-fria menyer överallt, popover-stack, radmeny i fönsterlistan
All checks were successful
check / check (push) Successful in 1m34s

- ALLA popovers är nu grab-fria (autohide av) med gemensam
  auto-stängning: pekar-leave + delay, watchdog om pekaren aldrig når
  popovern, och skroll-stegning räknas som aktivitet. Grabbande menyer
  krockade med tooltips ('grabbing popup with non-top most parent') —
  det var därför högerklicksmenyn ibland inte öppnades.
- Popover-stack ersätter en-åt-gången: en popover vars ankare sitter i
  en öppen popover nästlas i stället för att stänga föräldern, och
  förälderns auto-stängning pausas medan ett barn är överst.
- Högerklick på en rad i fönsterlistan öppnar den rutans egen meny
  (nästlad); högerklick på gruppikonen gäller som förut alla fönster
  i gruppen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 00:58:32 +02:00
parent 266e25a9ff
commit 570ff085bb
3 changed files with 113 additions and 53 deletions

View File

@@ -45,8 +45,8 @@ pub struct Ctx {
/// (att riva en öppen popovers förälder låser GTK:s pekar-grab)
pub open_popovers: Cell<u32>,
pub refresh_pending: Cell<bool>,
/// den enda popover som får vara öppen åt gången
pub current_popover: RefCell<Option<gtk::Popover>>,
/// öppna popovers, nästlade i ordning (sist = överst)
pub popover_stack: RefCell<Vec<gtk::Popover>>,
/// öppen fönsterlista (för skroll-stegning från gruppikonen)
pub group_list: RefCell<Option<menu::GroupList>>,
}

View File

@@ -157,7 +157,7 @@ fn activate(app: &gtk::Application, cfg_path: PathBuf) {
hotspot_windows: RefCell::new(Vec::new()),
open_popovers: Cell::new(0),
refresh_pending: Cell::new(false),
current_popover: RefCell::new(None),
popover_stack: RefCell::new(Vec::new()),
group_list: RefCell::new(None),
});

View File

@@ -159,27 +159,38 @@ fn toggle_pin(ctx: &Rc<Ctx>, connector: &str) {
crate::dock::refresh_items(ctx);
}
/// Registrera en popover: stänger ev. redan öppen (bara en åt gången —
/// två grabbing-popups samtidigt låser GTK:s pekar-grab på Wayland),
/// bokför öppen-räknaren, kör uppskjuten ikon-refresh när den stängs
/// och städar bort sig själv.
/// Registrera en popover i stacken. Alla våra popovers är GRAB-FRIA
/// (autohide av) — grabbande popups låser GTK:s pekar-grab på Wayland
/// och krockar med tooltips. En popover vars ankare sitter i en redan
/// öppen popover nästlas (t.ex. radmeny i fönsterlistan); annars stängs
/// allt gammalt först. Uppskjuten ikon-refresh körs när allt stängts.
fn register_popover(ctx: &Rc<Ctx>, popover: &impl IsA<gtk::Popover>) {
if let Some(old) = ctx.current_popover.borrow_mut().take() {
old.popdown();
}
let p = popover.as_ref().clone();
*ctx.current_popover.borrow_mut() = Some(p.clone());
p.set_autohide(false);
// nästlad om ankaret sitter inne i någon redan öppen popover
let nested = p
.parent()
.map(|anchor| {
ctx.popover_stack
.borrow()
.iter()
.any(|open| anchor.is_ancestor(open))
})
.unwrap_or(false);
if !nested {
for old in ctx.popover_stack.borrow_mut().drain(..) {
old.popdown();
}
}
ctx.popover_stack.borrow_mut().push(p.clone());
ctx.open_popovers.set(ctx.open_popovers.get() + 1);
let ctx = ctx.clone();
p.connect_closed(move |p| {
ctx.open_popovers
.set(ctx.open_popovers.get().saturating_sub(1));
{
let mut cur = ctx.current_popover.borrow_mut();
if cur.as_ref() == Some(p) {
*cur = None;
}
}
ctx.popover_stack.borrow_mut().retain(|x| x != p);
let p = p.clone();
let ctx2 = ctx.clone();
glib::idle_add_local_once(move || {
@@ -191,6 +202,62 @@ fn register_popover(ctx: &Rc<Ctx>, popover: &impl IsA<gtk::Popover>) {
});
}
/// Är popovern överst i stacken? (bara då får den auto-stängas — annars
/// stänger förälderns leave-timer den medan en nästlad meny är öppen)
fn is_topmost(ctx: &Ctx, p: &gtk::Popover) -> bool {
ctx.popover_stack.borrow().last() == Some(p)
}
/// Gemensam auto-stängning: pekaren lämnar → stäng efter delay;
/// watchdog om pekaren aldrig når popovern.
fn setup_autoclose(
ctx: &Rc<Ctx>,
popover: &impl IsA<gtk::Popover>,
leave_delay_ms: u64,
keep_open: Option<Rc<dyn Fn() -> bool>>,
) {
let p = popover.as_ref().clone();
let mc = gtk::EventControllerMotion::new();
{
let p2 = p.clone();
let ctx2 = ctx.clone();
mc.connect_leave(move |ctl| {
let p3 = p2.clone();
let ctl2 = ctl.clone();
let ctx3 = ctx2.clone();
glib::timeout_add_local_once(
std::time::Duration::from_millis(leave_delay_ms),
move || {
if !ctl2.contains_pointer() && is_topmost(&ctx3, &p3) {
p3.popdown();
}
},
);
});
}
p.add_controller(mc.clone());
// watchdog: aldrig besökt + överst + inget händer → stäng
{
let p2 = p.clone();
let ctx2 = ctx.clone();
let visited = std::cell::Cell::new(false);
glib::timeout_add_local(std::time::Duration::from_millis(1800), move || {
if mc.contains_pointer() {
visited.set(true);
}
if visited.get() || !is_topmost(&ctx2, &p2) {
return glib::ControlFlow::Break; // besökt/inte överst: leave-logiken tar över
}
// aktivitet utan pekare (t.ex. skroll-stegning från ikonen)
if keep_open.as_ref().map(|f| f()).unwrap_or(false) {
return glib::ControlFlow::Continue;
}
p2.popdown();
glib::ControlFlow::Break
});
}
}
/// Popover-riktning bort från dockans kant (så listan inte öppnas rakt
/// under muspekaren och råkar hovra första raden direkt).
fn popover_position(ctx: &Rc<Ctx>, anchor: &gtk::Button) -> gtk::PositionType {
@@ -281,6 +348,7 @@ pub fn popup_item_menu(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
popover.set_position(popover_position(ctx, anchor));
popover.set_parent(anchor);
register_popover(ctx, &popover);
setup_autoclose(ctx, &popover, 700, None);
popover.popup();
}
@@ -447,6 +515,26 @@ pub fn popup_group_list(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
});
row.add_controller(mc);
}
// högerklick på raden: den rutans egen meny (nästlad popover)
{
let rc = gtk::GestureClick::new();
rc.set_button(3);
let ctx3 = ctx.clone();
let w3 = w.clone();
let meta3 = item.meta.clone();
let pinned3 = item.pinned;
let row3 = row.clone();
rc.connect_released(move |gesture, _, _, _| {
gesture.set_state(gtk::EventSequenceState::Claimed);
let single = Rc::new(Item {
meta: meta3.clone(),
windows: vec![w3.clone()],
pinned: pinned3,
});
popup_item_menu(&ctx3, &single, &row3);
});
row.add_controller(rc);
}
}
// skroll över själva listan stegar också
@@ -489,43 +577,14 @@ pub fn popup_group_list(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
});
}
// stäng när pekaren lämnar listan (snabbare i hover-läge)
let leave_delay = if hover_mode { 400 } else { 700 };
let mc = gtk::EventControllerMotion::new();
{
let pop3 = popover.clone();
mc.connect_leave(move |ctl| {
let pop4 = pop3.clone();
let ctl2 = ctl.clone();
glib::timeout_add_local_once(
std::time::Duration::from_millis(leave_delay),
move || {
if !ctl2.contains_pointer() {
pop4.popdown();
}
},
);
});
}
popover.add_controller(mc.clone());
// watchdog: om pekaren aldrig når listan OCH ingen skroll-stegning
// sker, stäng efter en stund (upprepas; aktivitet = markerad rad)
{
let pop3 = popover.clone();
let selected2 = selected.clone();
let last_seen = std::cell::Cell::new(selected.get());
glib::timeout_add_local(std::time::Duration::from_millis(1500), move || {
if mc.contains_pointer() || selected2.get() != last_seen.get() {
last_seen.set(selected2.get());
return glib::ControlFlow::Continue;
}
pop3.popdown();
glib::ControlFlow::Break
});
}
register_popover(ctx, &popover);
// skroll-stegning från ikonen räknas som aktivitet — markerad rad
// håller listan öppen även om pekaren aldrig går in i den
let keep = {
let selected = selected.clone();
Rc::new(move || selected.get().is_some()) as Rc<dyn Fn() -> bool>
};
setup_autoclose(ctx, &popover, if hover_mode { 400 } else { 700 }, Some(keep));
popover.popup();
}
@@ -571,6 +630,7 @@ pub fn popup_panel_menu(ctx: &Rc<Ctx>, panel: &gtk::Box, x: f64, y: f64) {
popover.set_parent(panel);
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
register_popover(ctx, &popover);
setup_autoclose(ctx, &popover, 700, None);
popover.popup();
}