waydock v0.1: panel, pins, fönstergrupper, menyer, settings-GUI, live-config
Some checks failed
check / check (push) Failing after 52s
Some checks failed
check / check (push) Failing after 52s
M0–M5 ur doc/plan.md: layer-shell-dock per skärm med transparent #222222-ö, pins + körande appar (gruppering, badges, indikatorer), klick/skroll/mittklick, konfigurerbar högerklicksmeny, tomyta-meny, settings-GUI som redigerar JSON-configen live, filewatcher, Wayfire-IPC-stubbar (scale, flytta-till-skärm), CI + release-flöde. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
316
src/menu.rs
Normal file
316
src/menu.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
// 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, MenuItem};
|
||||
use crate::dock::{Ctx, Item};
|
||||
use crate::icons;
|
||||
use crate::settings;
|
||||
use crate::toplevel::ToplevelInfo;
|
||||
use crate::wayfire_ipc;
|
||||
|
||||
/// 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 output-egna pins om skärmen har egna, annars i de globala
|
||||
let has_own = cfg
|
||||
.outputs
|
||||
.get(connector)
|
||||
.map(|o| o.pins.is_some())
|
||||
.unwrap_or(false);
|
||||
let pins: &mut Vec<String> = if has_own {
|
||||
cfg.outputs
|
||||
.get_mut(connector)
|
||||
.and_then(|o| o.pins.as_mut())
|
||||
.expect("has_own kollades nyss")
|
||||
} else {
|
||||
&mut cfg.pins
|
||||
};
|
||||
if pinned {
|
||||
pins.retain(|p| p.to_lowercase() != key);
|
||||
} else if !pins.iter().any(|p| p.to_lowercase() == key) {
|
||||
pins.push(key.clone());
|
||||
}
|
||||
if let Err(e) = config::save(&ctx.cfg_path, &cfg) {
|
||||
warn!("kunde inte spara config: {e}");
|
||||
}
|
||||
}
|
||||
crate::dock::refresh_items(ctx);
|
||||
}
|
||||
|
||||
/// Popover som städar bort sig själv när den stängs.
|
||||
fn autodispose(popover: &impl IsA<gtk::Popover>) {
|
||||
let p = popover.as_ref().clone();
|
||||
p.connect_closed(|p| {
|
||||
let p = p.clone();
|
||||
glib::idle_add_local_once(move || p.unparent());
|
||||
});
|
||||
}
|
||||
|
||||
/// 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 = ctx
|
||||
.docks
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|d| anchor.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();
|
||||
|
||||
*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_parent(anchor);
|
||||
autodispose(&popover);
|
||||
popover.popup();
|
||||
}
|
||||
|
||||
/// Vänsterklick 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");
|
||||
let list = gtk::Box::new(gtk::Orientation::Vertical, 2);
|
||||
|
||||
for w in &item.windows {
|
||||
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));
|
||||
let ctx2 = ctx.clone();
|
||||
let w2 = w.clone();
|
||||
let pop2 = popover.clone();
|
||||
row.connect_clicked(move |_| {
|
||||
if let Some(s) = ctx2.service.borrow().as_ref() {
|
||||
s.activate(&w2);
|
||||
}
|
||||
pop2.popdown();
|
||||
});
|
||||
list.append(&row);
|
||||
}
|
||||
popover.set_child(Some(&list));
|
||||
popover.set_parent(anchor);
|
||||
autodispose(&popover);
|
||||
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)));
|
||||
autodispose(&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);
|
||||
}
|
||||
Reference in New Issue
Block a user