Files
waydock/src/menu.rs
claude 1c6ed7815b
All checks were successful
check / check (push) Successful in 1m34s
Skroll-stegning i fönsterlistan + klick/launch-debugloggar
Med en grupps lista öppen stegar skroll (på gruppikonen eller över
listan) markeringen mellan raderna — markerad rad får accentbakgrund
och triggar samma förhandsvisning som hover (peek/fokus enligt
list_hover). Funkar i både hover- och klickläge; utan öppen lista
beter sig skroll som förut (cykla fönster). Watchdogen räknar
stegning som aktivitet så listan inte stängs mitt i.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 00:53:16 +02:00

582 lines
20 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Kontextmenyer: höger­klick på app-ikon (konfigurerbar via
// config.context_menu), grupp-popup med fönsterlistan, samt panelmenyn
// på tom yta (Inställningar…, Ladda om, Avsluta).
use std::rc::Rc;
use gtk4 as gtk;
use gtk4::gdk;
use gtk4::gio;
use gtk4::glib;
use gtk4::prelude::*;
use log::warn;
use crate::config::{self, GroupPopup, ListHover, MenuItem};
use crate::dock::{Ctx, Item};
use crate::icons;
use crate::settings;
use crate::toplevel::ToplevelInfo;
use crate::wayfire_ipc;
/// Handle till en öppen fönsterlista: gruppikonen skroll-stegar via denna.
pub struct GroupList {
pub key: String,
pub connector: String,
pub step: Rc<dyn Fn(f64)>,
}
/// Målet för den senast öppnade ikon-menyn (actions läser härifrån).
pub struct MenuTarget {
pub key: String,
pub windows: Vec<ToplevelInfo>,
pub pinned: bool,
pub connector: String,
}
fn with_service(ctx: &Ctx, f: impl Fn(&crate::toplevel::ToplevelService, &ToplevelInfo)) {
let target = ctx.menu_target.borrow();
let service = ctx.service.borrow();
if let (Some(t), Some(s)) = (target.as_ref(), service.as_ref()) {
for w in &t.windows {
f(s, w);
}
}
}
/// Registrera action-gruppen "dock" på ett dockfönster.
pub fn setup_dock_actions(ctx: &Rc<Ctx>, win: &gtk::ApplicationWindow) {
let group = gio::SimpleActionGroup::new();
let minimize = gio::SimpleAction::new("minimize", None);
{
let ctx = ctx.clone();
minimize.connect_activate(move |_, _| {
let all_min = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| t.windows.iter().all(|w| w.minimized))
.unwrap_or(false);
with_service(&ctx, |s, w| s.set_minimized(w, !all_min));
});
}
group.add_action(&minimize);
let maximize = gio::SimpleAction::new("maximize", None);
{
let ctx = ctx.clone();
maximize.connect_activate(move |_, _| {
let all_max = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| t.windows.iter().all(|w| w.maximized))
.unwrap_or(false);
with_service(&ctx, |s, w| s.set_maximized(w, !all_max));
});
}
group.add_action(&maximize);
let close = gio::SimpleAction::new("close", None);
{
let ctx = ctx.clone();
close.connect_activate(move |_, _| with_service(&ctx, |s, w| s.close(w)));
}
group.add_action(&close);
let pin = gio::SimpleAction::new("pin", None);
{
let ctx = ctx.clone();
pin.connect_activate(move |_, _| {
let connector = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| t.connector.clone())
.unwrap_or_default();
toggle_pin(&ctx, &connector);
});
}
group.add_action(&pin);
let move_to = gio::SimpleAction::new("move-to-output", Some(glib::VariantTy::STRING));
{
let ctx = ctx.clone();
move_to.connect_activate(move |_, param| {
let Some(output) = param.and_then(|v| v.get::<String>()) else { return };
let target = ctx.menu_target.borrow();
let Some(t) = target.as_ref() else { return };
for w in &t.windows {
match wayfire_ipc::find_view_id(&w.app_id, &w.title) {
Some(id) => wayfire_ipc::move_view_to_output(id, &output),
None => warn!("hittade ingen wayfire-vy för {}", w.app_id),
}
}
});
}
group.add_action(&move_to);
win.insert_action_group("dock", Some(&group));
}
fn toggle_pin(ctx: &Rc<Ctx>, connector: &str) {
let Some((key, pinned)) = ctx
.menu_target
.borrow()
.as_ref()
.map(|t| (t.key.clone(), t.pinned))
else {
return;
};
{
let mut cfg = ctx.cfg.borrow_mut();
// skriv i skärmens pin-override om den har en, annars i de globala
let has_own = cfg
.outputs
.get(connector)
.map(|o| o["pins"].is_array())
.unwrap_or(false);
let mutate = |pins: &mut Vec<String>| {
if pinned {
pins.retain(|p| p.to_lowercase() != key);
} else if !pins.iter().any(|p| p.to_lowercase() == key) {
pins.push(key.clone());
}
};
if has_own {
let over = cfg.outputs.get_mut(connector).expect("has_own kollades nyss");
let mut pins: Vec<String> =
serde_json::from_value(over["pins"].clone()).unwrap_or_default();
mutate(&mut pins);
over["pins"] = serde_json::to_value(pins).expect("strängar serialiserbara");
} else {
mutate(&mut cfg.pins);
}
if let Err(e) = config::save(&ctx.cfg_path, &cfg) {
warn!("kunde inte spara config: {e}");
}
}
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.
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());
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;
}
}
let p = p.clone();
let ctx2 = ctx.clone();
glib::idle_add_local_once(move || {
p.unparent();
if ctx2.open_popovers.get() == 0 && ctx2.refresh_pending.get() {
crate::dock::refresh_items(&ctx2);
}
});
});
}
/// 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 {
let connector = connector_of(ctx, anchor);
match ctx.cfg.borrow().for_output(&connector).position {
crate::config::Position::Bottom => gtk::PositionType::Top,
crate::config::Position::Top => gtk::PositionType::Bottom,
crate::config::Position::Left => gtk::PositionType::Right,
crate::config::Position::Right => gtk::PositionType::Left,
}
}
fn connector_of(ctx: &Rc<Ctx>, widget: &gtk::Button) -> String {
ctx.docks
.borrow()
.iter()
.find(|d| {
widget
.ancestor(gtk::ApplicationWindow::static_type())
.map(|w| w == *d.window.upcast_ref::<gtk::Widget>())
.unwrap_or(false)
})
.map(|d| d.connector.clone())
.unwrap_or_default()
}
/// Högerklicksmenyn på en app-ikon.
pub fn popup_item_menu(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
let cfg = ctx.cfg.borrow();
let has_windows = !item.windows.is_empty();
let all_min = has_windows && item.windows.iter().all(|w| w.minimized);
let all_max = has_windows && item.windows.iter().all(|w| w.maximized);
let menu = gio::Menu::new();
for entry in &cfg.context_menu {
match entry {
MenuItem::Minimize if has_windows => {
menu.append(
Some(if all_min { "Återställ" } else { "Minimera" }),
Some("dock.minimize"),
);
}
MenuItem::Maximize if has_windows => {
menu.append(
Some(if all_max { "Avmaximera" } else { "Maximera" }),
Some("dock.maximize"),
);
}
MenuItem::MoveToOutput if has_windows && wayfire_ipc::available() => {
let sub = gio::Menu::new();
let docks = ctx.docks.borrow();
for dock in docks.iter() {
let mi = gio::MenuItem::new(Some(&dock.connector), None);
mi.set_action_and_target_value(
Some("dock.move-to-output"),
Some(&dock.connector.to_variant()),
);
sub.append_item(&mi);
}
menu.append_submenu(Some("Flytta till skärm"), &sub);
}
MenuItem::Pin => {
menu.append(
Some(if item.pinned { "Släpp pin" } else { "Pinna" }),
Some("dock.pin"),
);
}
MenuItem::Close if has_windows => {
menu.append(Some("Stäng"), Some("dock.close"));
}
_ => {}
}
}
drop(cfg);
// hitta skärmens connector via dockan som äger knappen
let connector = connector_of(ctx, anchor);
*ctx.menu_target.borrow_mut() = Some(MenuTarget {
key: item.meta.key.clone(),
windows: item.windows.clone(),
pinned: item.pinned,
connector,
});
let popover = gtk::PopoverMenu::from_model(Some(&menu));
popover.add_css_class("waydock-menu");
popover.set_position(popover_position(ctx, anchor));
popover.set_parent(anchor);
register_popover(ctx, &popover);
popover.popup();
}
/// Tona ner alla andra fönster än det hovrade (peek). Returnerar id:n
/// som dimmats så de kan återställas.
fn peek_apply(hovered: &crate::toplevel::ToplevelInfo) -> Vec<i64> {
let Some(views) = wayfire_ipc::list_views() else {
return Vec::new();
};
let hovered_id = views
.iter()
.find(|v| {
v["app-id"].as_str() == Some(hovered.app_id.as_str())
&& v["title"].as_str() == Some(hovered.title.as_str())
})
.and_then(|v| v["id"].as_i64());
let mut dimmed = Vec::new();
for v in &views {
if v["role"].as_str() != Some("toplevel")
|| !v["mapped"].as_bool().unwrap_or(false)
|| v["minimized"].as_bool().unwrap_or(false)
{
continue;
}
let Some(id) = v["id"].as_i64() else { continue };
if Some(id) != hovered_id {
wayfire_ipc::set_view_alpha(id, 0.15);
dimmed.push(id);
}
}
dimmed
}
fn peek_restore(dimmed: &[i64]) {
for id in dimmed {
wayfire_ipc::set_view_alpha(*id, 1.0);
}
}
/// Vänsterklick (eller hover) på grupp med flera fönster: lista fönstren.
pub fn popup_group_list(ctx: &Rc<Ctx>, item: &Rc<Item>, anchor: &gtk::Button) {
let popover = gtk::Popover::new();
popover.add_css_class("waydock-menu");
popover.set_position(popover_position(ctx, anchor));
let list = gtk::Box::new(gtk::Orientation::Vertical, 2);
let connector = connector_of(ctx, anchor);
let (list_hover, hover_mode) = {
let b = ctx.cfg.borrow().for_output(&connector).behavior;
(b.list_hover.clone(), b.group_popup == GroupPopup::Hover)
};
// fönsterlistan grabbar ALDRIG: grabben gör att första klicket
// utanför bara stänger listan (man kan inte öppna nästa grupp
// direkt) och grabbing-popups i följd låser GTK på Wayland.
// Stängs i stället på pointer-leave + watchdog.
popover.set_autohide(false);
// hover-effekterna armeras först efter en stund så att raden som
// råkar hamna under pekaren vid öppning inte triggar direkt
let armed = Rc::new(std::cell::Cell::new(false));
{
let armed = armed.clone();
glib::timeout_add_local_once(std::time::Duration::from_millis(250), move || {
armed.set(true);
});
}
let peek_possible = list_hover == ListHover::Peek && wayfire_ipc::available();
// fönster som tonats ner av peek — återställs vid radbyte/stängning
let dimmed: Rc<std::cell::RefCell<Vec<i64>>> = Rc::new(std::cell::RefCell::new(Vec::new()));
// bygg raderna först, koppla handlers efteråt (select behöver alla)
let windows: Rc<Vec<crate::toplevel::ToplevelInfo>> = Rc::new(item.windows.clone());
let mut rows_vec = Vec::new();
for w in windows.iter() {
let row = gtk::Button::new();
row.add_css_class("flat");
let title = if w.title.is_empty() { item.meta.display_name.clone() } else { w.title.clone() };
let label = gtk::Label::new(Some(&title));
label.set_ellipsize(gtk4::pango::EllipsizeMode::End);
label.set_max_width_chars(40);
label.set_halign(gtk::Align::Start);
row.set_child(Some(&label));
list.append(&row);
rows_vec.push(row);
}
let rows: Rc<Vec<gtk::Button>> = Rc::new(rows_vec);
// markera rad idx: visuell markering + förhandsvisning (peek/fokus)
let selected = Rc::new(std::cell::Cell::new(None::<usize>));
let select: Rc<dyn Fn(usize)> = Rc::new({
let rows = rows.clone();
let windows = windows.clone();
let dimmed = dimmed.clone();
let selected = selected.clone();
let list_hover = list_hover.clone();
let ctx2 = ctx.clone();
move |idx: usize| {
for (i, r) in rows.iter().enumerate() {
if i == idx {
r.add_css_class("selected");
} else {
r.remove_css_class("selected");
}
}
selected.set(Some(idx));
let w = &windows[idx];
match list_hover {
ListHover::Peek if peek_possible => {
peek_restore(&dimmed.borrow());
*dimmed.borrow_mut() = peek_apply(w);
}
ListHover::Focus => {
if let Some(s) = ctx2.service.borrow().as_ref() {
s.activate(w);
}
}
_ => {}
}
}
});
// skroll-stegning (från gruppikonen eller över listan)
let step: Rc<dyn Fn(f64)> = Rc::new({
let select = select.clone();
let selected = selected.clone();
let n = windows.len();
move |dy: f64| {
if n == 0 {
return;
}
let next = match selected.get() {
None => {
if dy > 0.0 { 0 } else { n - 1 }
}
Some(i) => {
if dy > 0.0 { (i + 1) % n } else { (i + n - 1) % n }
}
};
select(next);
}
});
// per rad: klick aktiverar; hover markerar (efter armering)
for (i, (row, w)) in rows.iter().zip(windows.iter()).enumerate() {
let ctx2 = ctx.clone();
let w2 = w.clone();
let pop2 = popover.clone();
let dimmed2 = dimmed.clone();
row.connect_clicked(move |_| {
peek_restore(&dimmed2.borrow());
dimmed2.borrow_mut().clear();
if let Some(s) = ctx2.service.borrow().as_ref() {
s.activate(&w2);
}
pop2.popdown();
});
if list_hover != ListHover::None {
let mc = gtk::EventControllerMotion::new();
let select2 = select.clone();
let armed2 = armed.clone();
mc.connect_enter(move |_, _, _| {
if armed2.get() {
select2(i);
}
});
row.add_controller(mc);
}
}
// skroll över själva listan stegar också
{
let sc = gtk::EventControllerScroll::new(gtk::EventControllerScrollFlags::VERTICAL);
let step2 = step.clone();
sc.connect_scroll(move |_, _, dy| {
step2(dy);
glib::Propagation::Stop
});
popover.add_controller(sc);
}
// registrera handlet så gruppikonens skroll når stegningen
*ctx.group_list.borrow_mut() = Some(GroupList {
key: item.meta.key.clone(),
connector: connector.clone(),
step: step.clone(),
});
{
let ctx2 = ctx.clone();
let key = item.meta.key.clone();
popover.connect_closed(move |_| {
let mut gl = ctx2.group_list.borrow_mut();
if gl.as_ref().map(|h| h.key == key).unwrap_or(false) {
*gl = None;
}
});
}
popover.set_child(Some(&list));
popover.set_parent(anchor);
// säkerhetsnät: återställ peek när popovern stängs
{
let dimmed5 = dimmed.clone();
popover.connect_closed(move |_| {
peek_restore(&dimmed5.borrow());
dimmed5.borrow_mut().clear();
});
}
// 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);
popover.popup();
}
/// Högerklick på tom panelyta.
pub fn popup_panel_menu(ctx: &Rc<Ctx>, panel: &gtk::Box, x: f64, y: f64) {
let menu = gio::Menu::new();
menu.append(Some("Inställningar…"), Some("panel.settings"));
menu.append(Some("Ladda om config"), Some("panel.reload"));
menu.append(Some("Avsluta waydock"), Some("panel.quit"));
let group = gio::SimpleActionGroup::new();
let open_settings = gio::SimpleAction::new("settings", None);
{
let ctx = ctx.clone();
open_settings.connect_activate(move |_, _| settings::open(&ctx));
}
group.add_action(&open_settings);
let reload = gio::SimpleAction::new("reload", None);
{
let ctx = ctx.clone();
reload.connect_activate(move |_, _| {
let fresh = config::load_or_create(&ctx.cfg_path);
*ctx.cfg.borrow_mut() = fresh;
if let Some(cb) = ctx.on_config_changed.borrow().as_ref() {
cb();
}
});
}
group.add_action(&reload);
let quit = gio::SimpleAction::new("quit", None);
{
let ctx = ctx.clone();
quit.connect_activate(move |_, _| ctx.app.quit());
}
group.add_action(&quit);
panel.insert_action_group("panel", Some(&group));
let popover = gtk::PopoverMenu::from_model(Some(&menu));
popover.add_css_class("waydock-menu");
popover.set_parent(panel);
popover.set_pointing_to(Some(&gdk::Rectangle::new(x as i32, y as i32, 1, 1)));
register_popover(ctx, &popover);
popover.popup();
}
/// Starta en app från menyn (används inte ännu — behålls för settings-GUI:t).
#[allow(dead_code)]
pub fn launch_item(item: &Item) {
icons::launch(&item.meta);
}