All checks were successful
check / check (push) Successful in 1m42s
popdown() emittar closed synkront, och closed-hanteraren lånar också popover-stacken — register_popover höll kvar sitt borrow_mut under drain-loopen → 'RefCell already borrowed'-panik vid högerklick när en tidigare popover låg kvar i stacken. Stacken töms nu till en lokal Vec innan popdown körs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
645 lines
22 KiB
Rust
645 lines
22 KiB
Rust
// Kontextmenyer: högerklick 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: >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::<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 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>) {
|
||
let p = popover.as_ref().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 {
|
||
// töm stacken FÖRE popdown — closed-hanterarna lånar också
|
||
// stacken (popdown emittar closed synkront → dubbellåning annars)
|
||
let old: Vec<gtk::Popover> = ctx.popover_stack.borrow_mut().drain(..).collect();
|
||
for p in old {
|
||
p.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));
|
||
ctx.popover_stack.borrow_mut().retain(|x| x != p);
|
||
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);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/// Ä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: >k::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: >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<Ctx>, widget: >k::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: >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);
|
||
setup_autoclose(ctx, &popover, 700, None);
|
||
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: >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<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);
|
||
}
|
||
// 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å
|
||
{
|
||
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();
|
||
});
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
/// Högerklick på tom panelyta.
|
||
pub fn popup_panel_menu(ctx: &Rc<Ctx>, 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);
|
||
setup_autoclose(ctx, &popover, 700, None);
|
||
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);
|
||
}
|