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>
530 lines
20 KiB
Rust
530 lines
20 KiB
Rust
// Settings-GUI:t: ett GTK4-fönster som redigerar hela JSON-configen.
|
|
// Varje widgetändring uppdaterar delade configen, applicerar direkt
|
|
// (snabb feedback) och sparar filen debouncat — samma fil som kan
|
|
// handredigeras, filewatchern håller allt i synk.
|
|
|
|
use std::cell::Cell;
|
|
use std::rc::Rc;
|
|
use std::time::Duration;
|
|
|
|
use gtk4 as gtk;
|
|
use gtk4::gdk;
|
|
use gtk4::glib;
|
|
use gtk4::prelude::*;
|
|
use log::warn;
|
|
|
|
use crate::config::{
|
|
self, AutohideMode, ClickAction, HotspotAction, IndicatorStyle, Layer, MenuItem,
|
|
MiddleClick, Position, ScrollAction,
|
|
};
|
|
use crate::dock::Ctx;
|
|
|
|
thread_local! {
|
|
static OPEN_WINDOW: std::cell::RefCell<Option<glib::WeakRef<gtk::Window>>> =
|
|
const { std::cell::RefCell::new(None) };
|
|
}
|
|
|
|
/// Uppdatera config + applicera + spara (debouncat 300 ms).
|
|
fn commit(ctx: &Rc<Ctx>, mutate: impl FnOnce(&mut config::Config)) {
|
|
mutate(&mut ctx.cfg.borrow_mut());
|
|
if let Some(cb) = ctx.on_config_changed.borrow().as_ref() {
|
|
cb();
|
|
}
|
|
let generation = ctx.save_generation.get() + 1;
|
|
ctx.save_generation.set(generation);
|
|
let ctx = ctx.clone();
|
|
glib::timeout_add_local_once(Duration::from_millis(300), move || {
|
|
if ctx.save_generation.get() == generation {
|
|
if let Err(e) = config::save(&ctx.cfg_path, &ctx.cfg.borrow()) {
|
|
warn!("kunde inte spara config: {e}");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
fn hex_of(rgba: &gdk::RGBA) -> String {
|
|
format!(
|
|
"#{:02x}{:02x}{:02x}",
|
|
(rgba.red() * 255.0) as u8,
|
|
(rgba.green() * 255.0) as u8,
|
|
(rgba.blue() * 255.0) as u8
|
|
)
|
|
}
|
|
|
|
// -- widgethjälpare ------------------------------------------------------
|
|
|
|
fn page() -> (gtk::ScrolledWindow, gtk::Box) {
|
|
let list = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
|
list.set_margin_top(12);
|
|
list.set_margin_bottom(12);
|
|
list.set_margin_start(14);
|
|
list.set_margin_end(14);
|
|
let scroll = gtk::ScrolledWindow::new();
|
|
scroll.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
|
|
scroll.set_child(Some(&list));
|
|
(scroll, list)
|
|
}
|
|
|
|
fn row(list: >k::Box, label: &str, widget: &impl IsA<gtk::Widget>) {
|
|
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 10);
|
|
let lbl = gtk::Label::new(Some(label));
|
|
lbl.set_halign(gtk::Align::Start);
|
|
lbl.set_hexpand(true);
|
|
hbox.append(&lbl);
|
|
hbox.append(widget);
|
|
list.append(&hbox);
|
|
}
|
|
|
|
fn heading(list: >k::Box, text: &str) {
|
|
let lbl = gtk::Label::new(None);
|
|
lbl.set_markup(&format!("<b>{text}</b>"));
|
|
lbl.set_halign(gtk::Align::Start);
|
|
lbl.set_margin_top(10);
|
|
list.append(&lbl);
|
|
}
|
|
|
|
fn switch(ctx: &Rc<Ctx>, active: bool, set: impl Fn(&mut config::Config, bool) + 'static) -> gtk::Switch {
|
|
let sw = gtk::Switch::new();
|
|
sw.set_active(active);
|
|
sw.set_valign(gtk::Align::Center);
|
|
let ctx = ctx.clone();
|
|
let set = Rc::new(set);
|
|
sw.connect_active_notify(move |sw| {
|
|
let v = sw.is_active();
|
|
let set = set.clone();
|
|
commit(&ctx, move |c| set(c, v));
|
|
});
|
|
sw
|
|
}
|
|
|
|
fn spin(
|
|
ctx: &Rc<Ctx>,
|
|
value: f64,
|
|
range: (f64, f64),
|
|
step: f64,
|
|
set: impl Fn(&mut config::Config, f64) + 'static,
|
|
) -> gtk::SpinButton {
|
|
let sb = gtk::SpinButton::with_range(range.0, range.1, step);
|
|
sb.set_value(value);
|
|
let ctx = ctx.clone();
|
|
let set = Rc::new(set);
|
|
sb.connect_value_changed(move |sb| {
|
|
let v = sb.value();
|
|
let set = set.clone();
|
|
commit(&ctx, move |c| set(c, v));
|
|
});
|
|
sb
|
|
}
|
|
|
|
fn scale01(ctx: &Rc<Ctx>, value: f64, set: impl Fn(&mut config::Config, f64) + 'static) -> gtk::Scale {
|
|
let sc = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 1.0, 0.01);
|
|
sc.set_value(value);
|
|
sc.set_size_request(160, -1);
|
|
sc.set_draw_value(false);
|
|
let ctx = ctx.clone();
|
|
let set = Rc::new(set);
|
|
sc.connect_value_changed(move |sc| {
|
|
let v = sc.value();
|
|
let set = set.clone();
|
|
commit(&ctx, move |c| set(c, v));
|
|
});
|
|
sc
|
|
}
|
|
|
|
fn color_button(
|
|
ctx: &Rc<Ctx>,
|
|
hex: &str,
|
|
set: impl Fn(&mut config::Config, String) + 'static,
|
|
) -> gtk::ColorDialogButton {
|
|
let btn = gtk::ColorDialogButton::new(Some(gtk::ColorDialog::new()));
|
|
if let Ok(rgba) = gdk::RGBA::parse(hex) {
|
|
btn.set_rgba(&rgba);
|
|
}
|
|
let ctx = ctx.clone();
|
|
let set = Rc::new(set);
|
|
btn.connect_rgba_notify(move |btn| {
|
|
let hex = hex_of(&btn.rgba());
|
|
let set = set.clone();
|
|
commit(&ctx, move |c| set(c, hex));
|
|
});
|
|
btn
|
|
}
|
|
|
|
fn dropdown(
|
|
ctx: &Rc<Ctx>,
|
|
options: &[&str],
|
|
selected: usize,
|
|
set: impl Fn(&mut config::Config, usize) + 'static,
|
|
) -> gtk::DropDown {
|
|
let dd = gtk::DropDown::from_strings(options);
|
|
dd.set_selected(selected as u32);
|
|
let ctx = ctx.clone();
|
|
let set = Rc::new(set);
|
|
dd.connect_selected_notify(move |dd| {
|
|
let i = dd.selected() as usize;
|
|
let set = set.clone();
|
|
commit(&ctx, move |c| set(c, i));
|
|
});
|
|
dd
|
|
}
|
|
|
|
// -- hotspot-hjälpare ----------------------------------------------------
|
|
|
|
fn hotspot_to_text(a: &Option<HotspotAction>) -> String {
|
|
match a {
|
|
Some(HotspotAction::Exec(cmd)) => format!("exec:{cmd}"),
|
|
Some(HotspotAction::Ipc(m)) => format!("ipc:{m}"),
|
|
None => String::new(),
|
|
}
|
|
}
|
|
|
|
fn text_to_hotspot(text: &str) -> Option<HotspotAction> {
|
|
let t = text.trim();
|
|
if let Some(cmd) = t.strip_prefix("exec:") {
|
|
Some(HotspotAction::Exec(cmd.trim().to_string()))
|
|
} else if let Some(m) = t.strip_prefix("ipc:") {
|
|
Some(HotspotAction::Ipc(m.trim().to_string()))
|
|
} else if t.is_empty() {
|
|
None
|
|
} else {
|
|
Some(HotspotAction::Exec(t.to_string()))
|
|
}
|
|
}
|
|
|
|
fn hotspot_entry(
|
|
ctx: &Rc<Ctx>,
|
|
current: &Option<HotspotAction>,
|
|
set: impl Fn(&mut config::Config, Option<HotspotAction>) + 'static,
|
|
) -> gtk::Entry {
|
|
let entry = gtk::Entry::new();
|
|
entry.set_text(&hotspot_to_text(current));
|
|
entry.set_placeholder_text(Some("exec:kommando eller ipc:scale/toggle"));
|
|
entry.set_width_chars(24);
|
|
let ctx = ctx.clone();
|
|
let set = Rc::new(set);
|
|
entry.connect_changed(move |e| {
|
|
let action = text_to_hotspot(&e.text());
|
|
let set = set.clone();
|
|
commit(&ctx, move |c| set(c, action));
|
|
});
|
|
entry
|
|
}
|
|
|
|
// -- sidorna -------------------------------------------------------------
|
|
|
|
fn build_appearance_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
|
|
let (scroll, list) = page();
|
|
let a = ctx.cfg.borrow().appearance.clone();
|
|
|
|
heading(&list, "Panel");
|
|
row(&list, "Panelfärg", &color_button(ctx, &a.background, |c, v| c.appearance.background = v));
|
|
row(&list, "Transparens", &scale01(ctx, a.opacity, |c, v| c.appearance.opacity = v));
|
|
row(&list, "Hörnradie", &spin(ctx, a.corner_radius as f64, (0.0, 40.0), 1.0, |c, v| c.appearance.corner_radius = v as u32));
|
|
row(&list, "Accentfärg", &color_button(ctx, &a.accent, |c, v| c.appearance.accent = v));
|
|
|
|
heading(&list, "Kant & skugga");
|
|
row(&list, "Kantfärg", &color_button(ctx, &a.border.color, |c, v| c.appearance.border.color = v));
|
|
row(&list, "Kantopacitet", &scale01(ctx, a.border.opacity, |c, v| c.appearance.border.opacity = v));
|
|
row(&list, "Kantbredd", &spin(ctx, a.border.width as f64, (0.0, 8.0), 1.0, |c, v| c.appearance.border.width = v as u32));
|
|
row(&list, "Skugga", &switch(ctx, a.shadow.enabled, |c, v| c.appearance.shadow.enabled = v));
|
|
row(&list, "Skuggstorlek", &spin(ctx, a.shadow.blur as f64, (0.0, 60.0), 1.0, |c, v| c.appearance.shadow.blur = v as u32));
|
|
row(&list, "Skuggopacitet", &scale01(ctx, a.shadow.opacity, |c, v| c.appearance.shadow.opacity = v));
|
|
|
|
heading(&list, "Ikoner");
|
|
row(&list, "Ikonstorlek", &spin(ctx, a.icon_size as f64, (16.0, 96.0), 2.0, |c, v| c.appearance.icon_size = v as i32));
|
|
row(&list, "Ikonavstånd", &spin(ctx, a.icon_spacing as f64, (0.0, 32.0), 1.0, |c, v| c.appearance.icon_spacing = v as i32));
|
|
row(&list, "Panel-padding", &spin(ctx, a.padding as f64, (0.0, 32.0), 1.0, |c, v| c.appearance.padding = v as i32));
|
|
row(&list, "Hover-zoom", &switch(ctx, a.hover_zoom, |c, v| c.appearance.hover_zoom = v));
|
|
row(&list, "Animationer", &switch(ctx, a.animations, |c, v| c.appearance.animations = v));
|
|
let ind_idx = match a.indicator.style {
|
|
IndicatorStyle::Dot => 0,
|
|
IndicatorStyle::Line => 1,
|
|
IndicatorStyle::None => 2,
|
|
};
|
|
row(&list, "Indikatorstil", &dropdown(ctx, &["Prickar", "Linje", "Ingen"], ind_idx, |c, i| {
|
|
c.appearance.indicator.style = match i {
|
|
1 => IndicatorStyle::Line,
|
|
2 => IndicatorStyle::None,
|
|
_ => IndicatorStyle::Dot,
|
|
};
|
|
}));
|
|
row(&list, "Max indikatorer", &spin(ctx, a.indicator.max as f64, (1.0, 5.0), 1.0, |c, v| c.appearance.indicator.max = v as u32));
|
|
|
|
scroll
|
|
}
|
|
|
|
fn build_layout_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
|
|
let (scroll, list) = page();
|
|
let cfg = ctx.cfg.borrow().clone();
|
|
|
|
heading(&list, "Placering");
|
|
let pos_idx = match cfg.position {
|
|
Position::Bottom => 0,
|
|
Position::Top => 1,
|
|
Position::Left => 2,
|
|
Position::Right => 3,
|
|
};
|
|
row(&list, "Kant", &dropdown(ctx, &["Nere", "Uppe", "Vänster", "Höger"], pos_idx, |c, i| {
|
|
c.position = match i {
|
|
1 => Position::Top,
|
|
2 => Position::Left,
|
|
3 => Position::Right,
|
|
_ => Position::Bottom,
|
|
};
|
|
}));
|
|
row(&list, "Centrerad ö", &switch(ctx, cfg.centered, |c, v| c.centered = v));
|
|
row(&list, "Marginal från kant", &spin(ctx, cfg.margin as f64, (0.0, 64.0), 1.0, |c, v| c.margin = v as i32));
|
|
let layer_idx = match cfg.layer {
|
|
Layer::Top => 0,
|
|
Layer::Overlay => 1,
|
|
Layer::Bottom => 2,
|
|
};
|
|
row(&list, "Lager", &dropdown(ctx, &["Ovanpå fönster", "Alltid överst (overlay)", "Under fönster"], layer_idx, |c, i| {
|
|
c.layer = match i {
|
|
1 => Layer::Overlay,
|
|
2 => Layer::Bottom,
|
|
_ => Layer::Top,
|
|
};
|
|
}));
|
|
row(&list, "Reservera plats (undvik fönster)", &switch(ctx, cfg.avoid_windows, |c, v| c.avoid_windows = v));
|
|
|
|
heading(&list, "Autohide");
|
|
row(&list, "Aktiverad", &switch(ctx, cfg.autohide.enabled, |c, v| c.autohide.enabled = v));
|
|
let mode_idx = match cfg.autohide.mode {
|
|
AutohideMode::Dodge => 0,
|
|
AutohideMode::Always => 1,
|
|
};
|
|
row(&list, "Läge", &dropdown(ctx, &["Dodge (göm vid överlapp)", "Göm alltid"], mode_idx, |c, i| {
|
|
c.autohide.mode = if i == 1 { AutohideMode::Always } else { AutohideMode::Dodge };
|
|
}));
|
|
row(&list, "Fördröjning (ms)", &spin(ctx, cfg.autohide.hide_delay_ms as f64, (0.0, 3000.0), 50.0, |c, v| c.autohide.hide_delay_ms = v as u32));
|
|
row(&list, "Sensorbredd (px)", &spin(ctx, cfg.autohide.reveal_px as f64, (1.0, 16.0), 1.0, |c, v| c.autohide.reveal_px = v as i32));
|
|
row(&list, "Animationstid (ms)", &spin(ctx, cfg.autohide.animation_ms as f64, (0.0, 1000.0), 25.0, |c, v| c.autohide.animation_ms = v as u32));
|
|
|
|
let note = gtk::Label::new(Some("Per-skärm-inställningar (unika pins, only_this_output)\nredigeras i config.json under \"outputs\"."));
|
|
note.add_css_class("dim-label");
|
|
note.set_halign(gtk::Align::Start);
|
|
note.set_margin_top(12);
|
|
list.append(¬e);
|
|
|
|
scroll
|
|
}
|
|
|
|
fn build_behavior_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
|
|
let (scroll, list) = page();
|
|
let b = ctx.cfg.borrow().behavior.clone();
|
|
let sp = ctx.cfg.borrow().special_buttons.clone();
|
|
|
|
heading(&list, "Klick");
|
|
let click_idx = match b.click {
|
|
ClickAction::FocusOrMinimize => 0,
|
|
ClickAction::Focus => 1,
|
|
ClickAction::Cycle => 2,
|
|
};
|
|
row(&list, "Vänsterklick", &dropdown(ctx, &["Fokusera/minimera", "Bara fokusera", "Växla fönster"], click_idx, |c, i| {
|
|
c.behavior.click = match i {
|
|
1 => ClickAction::Focus,
|
|
2 => ClickAction::Cycle,
|
|
_ => ClickAction::FocusOrMinimize,
|
|
};
|
|
}));
|
|
let mid_idx = match b.middle_click {
|
|
MiddleClick::LaunchNew => 0,
|
|
MiddleClick::Close => 1,
|
|
MiddleClick::None => 2,
|
|
};
|
|
row(&list, "Mittklick", &dropdown(ctx, &["Starta ny instans", "Stäng", "Inget"], mid_idx, |c, i| {
|
|
c.behavior.middle_click = match i {
|
|
1 => MiddleClick::Close,
|
|
2 => MiddleClick::None,
|
|
_ => MiddleClick::LaunchNew,
|
|
};
|
|
}));
|
|
let scroll_idx = match b.scroll {
|
|
ScrollAction::CycleWindows => 0,
|
|
ScrollAction::None => 1,
|
|
};
|
|
row(&list, "Skroll", &dropdown(ctx, &["Växla fönster", "Inget"], scroll_idx, |c, i| {
|
|
c.behavior.scroll = if i == 1 { ScrollAction::None } else { ScrollAction::CycleWindows };
|
|
}));
|
|
|
|
heading(&list, "Övrigt");
|
|
row(&list, "Gruppera appar", &switch(ctx, b.group_apps, |c, v| c.behavior.group_apps = v));
|
|
row(&list, "Tooltips", &switch(ctx, b.tooltips, |c, v| c.behavior.tooltips = v));
|
|
row(&list, "Urgency-puls", &switch(ctx, b.urgent_pulse, |c, v| c.behavior.urgent_pulse = v));
|
|
|
|
heading(&list, "Specialknappar");
|
|
row(&list, "Visa skrivbordet", &switch(ctx, sp.show_desktop, |c, v| c.special_buttons.show_desktop = v));
|
|
row(&list, "Översikt (scale)", &switch(ctx, sp.overview, |c, v| c.special_buttons.overview = v));
|
|
|
|
heading(&list, "Högerklicksmenyn");
|
|
let entries: [(&str, MenuItem); 5] = [
|
|
("Minimera", MenuItem::Minimize),
|
|
("Maximera", MenuItem::Maximize),
|
|
("Flytta till skärm", MenuItem::MoveToOutput),
|
|
("Pinna/släpp", MenuItem::Pin),
|
|
("Stäng", MenuItem::Close),
|
|
];
|
|
for (label, mi) in entries {
|
|
let present = ctx.cfg.borrow().context_menu.contains(&mi);
|
|
let mi2 = mi.clone();
|
|
row(&list, label, &switch(ctx, present, move |c, v| {
|
|
if v {
|
|
if !c.context_menu.contains(&mi2) {
|
|
c.context_menu.push(mi2.clone());
|
|
}
|
|
} else {
|
|
c.context_menu.retain(|m| *m != mi2);
|
|
}
|
|
}));
|
|
}
|
|
|
|
scroll
|
|
}
|
|
|
|
fn build_pins_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
|
|
let (scroll, list) = page();
|
|
|
|
heading(&list, "Globala pins (.desktop-id, i ordning)");
|
|
let pins_box = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
|
list.append(&pins_box);
|
|
|
|
fn rebuild_rows(ctx: &Rc<Ctx>, pins_box: >k::Box) {
|
|
while let Some(c) = pins_box.first_child() {
|
|
pins_box.remove(&c);
|
|
}
|
|
let pins = ctx.cfg.borrow().pins.clone();
|
|
for (i, pin) in pins.iter().enumerate() {
|
|
let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
|
let name = crate::icons::lookup_pin(pin)
|
|
.map(|m| m.display_name)
|
|
.unwrap_or_else(|| "⚠ hittas inte".into());
|
|
let lbl = gtk::Label::new(Some(&format!("{pin} ({name})")));
|
|
lbl.set_halign(gtk::Align::Start);
|
|
lbl.set_hexpand(true);
|
|
hbox.append(&lbl);
|
|
|
|
let up = gtk::Button::from_icon_name("go-up-symbolic");
|
|
up.add_css_class("flat");
|
|
let remove = gtk::Button::from_icon_name("edit-delete-symbolic");
|
|
remove.add_css_class("flat");
|
|
hbox.append(&up);
|
|
hbox.append(&remove);
|
|
pins_box.append(&hbox);
|
|
|
|
let ctx2 = ctx.clone();
|
|
let pb2 = pins_box.clone();
|
|
up.connect_clicked(move |_| {
|
|
if i > 0 {
|
|
commit(&ctx2, |c| c.pins.swap(i, i - 1));
|
|
rebuild_rows(&ctx2, &pb2);
|
|
}
|
|
});
|
|
let ctx3 = ctx.clone();
|
|
let pb3 = pins_box.clone();
|
|
remove.connect_clicked(move |_| {
|
|
commit(&ctx3, |c| {
|
|
c.pins.remove(i);
|
|
});
|
|
rebuild_rows(&ctx3, &pb3);
|
|
});
|
|
}
|
|
}
|
|
rebuild_rows(ctx, &pins_box);
|
|
|
|
let add_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
|
let entry = gtk::Entry::new();
|
|
entry.set_placeholder_text(Some("t.ex. org.kde.konsole"));
|
|
entry.set_hexpand(true);
|
|
let add = gtk::Button::with_label("Lägg till");
|
|
add_box.append(&entry);
|
|
add_box.append(&add);
|
|
list.append(&add_box);
|
|
{
|
|
let ctx = ctx.clone();
|
|
let pins_box = pins_box.clone();
|
|
add.connect_clicked(move |_| {
|
|
let id = entry.text().trim().to_string();
|
|
if id.is_empty() {
|
|
return;
|
|
}
|
|
if crate::icons::lookup_pin(&id).is_none() {
|
|
entry.add_css_class("error");
|
|
return;
|
|
}
|
|
entry.remove_css_class("error");
|
|
entry.set_text("");
|
|
let id2 = id.clone();
|
|
commit(&ctx, move |c| {
|
|
if !c.pins.contains(&id2) {
|
|
c.pins.push(id2.clone());
|
|
}
|
|
});
|
|
rebuild_rows(&ctx, &pins_box);
|
|
});
|
|
}
|
|
|
|
let note = gtk::Label::new(Some("Tips: hitta id med ls /usr/share/applications | grep -i <app>\nPer-skärm-pins redigeras i config.json under \"outputs\"."));
|
|
note.add_css_class("dim-label");
|
|
note.set_halign(gtk::Align::Start);
|
|
note.set_margin_top(12);
|
|
list.append(¬e);
|
|
|
|
scroll
|
|
}
|
|
|
|
fn build_hotspots_page(ctx: &Rc<Ctx>) -> gtk::ScrolledWindow {
|
|
let (scroll, list) = page();
|
|
let h = ctx.cfg.borrow().hotspots.clone();
|
|
|
|
heading(&list, "Hot corners");
|
|
row(&list, "Uppe vänster", &hotspot_entry(ctx, &h.corners.top_left, |c, v| c.hotspots.corners.top_left = v));
|
|
row(&list, "Uppe höger", &hotspot_entry(ctx, &h.corners.top_right, |c, v| c.hotspots.corners.top_right = v));
|
|
row(&list, "Nere vänster", &hotspot_entry(ctx, &h.corners.bottom_left, |c, v| c.hotspots.corners.bottom_left = v));
|
|
row(&list, "Nere höger", &hotspot_entry(ctx, &h.corners.bottom_right, |c, v| c.hotspots.corners.bottom_right = v));
|
|
|
|
heading(&list, "Hot edges");
|
|
row(&list, "Uppe", &hotspot_entry(ctx, &h.edges.top, |c, v| c.hotspots.edges.top = v));
|
|
row(&list, "Nere", &hotspot_entry(ctx, &h.edges.bottom, |c, v| c.hotspots.edges.bottom = v));
|
|
row(&list, "Vänster", &hotspot_entry(ctx, &h.edges.left, |c, v| c.hotspots.edges.left = v));
|
|
row(&list, "Höger", &hotspot_entry(ctx, &h.edges.right, |c, v| c.hotspots.edges.right = v));
|
|
|
|
row(&list, "Dwell-tid (ms)", &spin(ctx, h.dwell_ms as f64, (0.0, 2000.0), 50.0, |c, v| c.hotspots.dwell_ms = v as u32));
|
|
|
|
let note = gtk::Label::new(Some("Format: exec:<kommando> eller ipc:<metod> (t.ex. ipc:scale/toggle).\nTomt fält = av. IPC kräver Wayfires ipc-plugin."));
|
|
note.add_css_class("dim-label");
|
|
note.set_halign(gtk::Align::Start);
|
|
note.set_margin_top(12);
|
|
list.append(¬e);
|
|
|
|
scroll
|
|
}
|
|
|
|
/// Öppna settings-fönstret (återanvänder ett redan öppet).
|
|
pub fn open(ctx: &Rc<Ctx>) {
|
|
let existing = OPEN_WINDOW.with(|w| w.borrow().as_ref().and_then(|w| w.upgrade()));
|
|
if let Some(win) = existing {
|
|
win.present();
|
|
return;
|
|
}
|
|
|
|
let win = gtk::Window::new();
|
|
win.set_title(Some("waydock — inställningar"));
|
|
win.set_default_size(520, 620);
|
|
|
|
let notebook = gtk::Notebook::new();
|
|
notebook.append_page(&build_appearance_page(ctx), Some(>k::Label::new(Some("Utseende"))));
|
|
notebook.append_page(&build_layout_page(ctx), Some(>k::Label::new(Some("Layout"))));
|
|
notebook.append_page(&build_behavior_page(ctx), Some(>k::Label::new(Some("Beteende"))));
|
|
notebook.append_page(&build_pins_page(ctx), Some(>k::Label::new(Some("Pins"))));
|
|
notebook.append_page(&build_hotspots_page(ctx), Some(>k::Label::new(Some("Hotspots"))));
|
|
win.set_child(Some(¬ebook));
|
|
|
|
OPEN_WINDOW.with(|w| *w.borrow_mut() = Some(win.downgrade()));
|
|
win.present();
|
|
}
|
|
|
|
// hindra att Cell-importen städas bort av misstag när sidor ändras
|
|
#[allow(unused)]
|
|
fn _keep(_: Cell<u8>) {}
|