// 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, } /// Målet för den senast öppnade ikon-menyn (actions läser härifrån). pub struct MenuTarget { pub key: String, pub windows: Vec, 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, win: >k::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::()) 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, 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| { 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 = 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, popover: &impl IsA) { 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, anchor: >k::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, widget: >k::Button) -> String { ctx.docks .borrow() .iter() .find(|d| { widget .ancestor(gtk::ApplicationWindow::static_type()) .map(|w| w == *d.window.upcast_ref::()) .unwrap_or(false) }) .map(|d| d.connector.clone()) .unwrap_or_default() } /// Högerklicksmenyn på en app-ikon. pub fn popup_item_menu(ctx: &Rc, item: &Rc, anchor: >k::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 { 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, item: &Rc, anchor: >k::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>> = Rc::new(std::cell::RefCell::new(Vec::new())); // bygg raderna först, koppla handlers efteråt (select behöver alla) let windows: Rc> = 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> = Rc::new(rows_vec); // markera rad idx: visuell markering + förhandsvisning (peek/fokus) let selected = Rc::new(std::cell::Cell::new(None::)); let select: Rc = 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 = 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, panel: >k::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); }