Files
niritings/nirimod/pages/layout.py
2026-05-29 00:41:12 +00:00

285 lines
11 KiB
Python

"""Layout settings page."""
from __future__ import annotations
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
from nirimod.kdl_parser import KdlNode, find_or_create, set_child_arg, remove_child
from nirimod.pages.base import BasePage
CENTER_OPTIONS = ["never", "always", "on-overflow"]
class LayoutPage(BasePage):
def build(self) -> Gtk.Widget:
tb, _, _, content = self._make_toolbar_page("Layout")
self._content = content
self._build_content()
return tb
def _build_content(self):
content = self._content
nodes = self._nodes
layout = find_or_create(nodes, "layout")
basic_grp = Adw.PreferencesGroup(title="General")
gaps_val = int(layout.child_arg("gaps") or 16)
gaps_adj = Gtk.Adjustment(value=gaps_val, lower=0, upper=200, step_increment=2)
gaps_row = Adw.SpinRow(title="Window Gaps (px)", adjustment=gaps_adj, digits=0)
gaps_row._last_val = gaps_val
def _on_gaps_changed(r, _):
new_val = int(r.get_value())
if new_val != getattr(r, "_last_val", None):
r._last_val = new_val
self._set_layout("gaps", new_val)
gaps_row.connect("notify::value", _on_gaps_changed)
basic_grp.add(gaps_row)
cfc_model = Gtk.StringList.new(CENTER_OPTIONS)
cfc_row = Adw.ComboRow(title="Center Focused Column", model=cfc_model)
cur_cfc = layout.child_arg("center-focused-column") or "never"
if cur_cfc in CENTER_OPTIONS:
cfc_row.set_selected(CENTER_OPTIONS.index(cur_cfc))
cfc_row.connect(
"notify::selected",
lambda r, _: self._set_layout(
"center-focused-column", CENTER_OPTIONS[r.get_selected()]
),
)
basic_grp.add(cfc_row)
prefer_csd_row = Adw.SwitchRow(
title="Prefer No CSD", subtitle="Ask apps to omit client-side decorations"
)
prefer_csd_row.set_active(any(n.name == "prefer-no-csd" for n in nodes))
prefer_csd_row.connect(
"notify::active",
lambda r, _: self._toggle_top("prefer-no-csd", r.get_active()),
)
basic_grp.add(prefer_csd_row)
bg_color_val = str(layout.child_arg("background-color") or "transparent")
bg_row = Adw.EntryRow(title="Background Color (e.g. transparent, #000000)")
bg_row.set_text(bg_color_val)
bg_row.set_show_apply_button(True)
bg_row.connect(
"apply",
lambda r: self._set_layout("background-color", r.get_text().strip()),
)
basic_grp.add(bg_row)
content.append(basic_grp)
dcw_grp = Adw.PreferencesGroup(title="Default Column Width")
dcw_node = layout.get_child("default-column-width")
prop_val = 0.5
fixed_val = 800
use_fixed = False
if dcw_node:
fc = dcw_node.get_child("fixed")
pc = dcw_node.get_child("proportion")
if fc and fc.args:
fixed_val = int(fc.args[0])
use_fixed = True
elif pc and pc.args:
prop_val = float(pc.args[0])
mode_model = Gtk.StringList.new(["Proportion", "Fixed (px)"])
mode_row = Adw.ComboRow(title="Mode", model=mode_model)
mode_row.set_selected(1 if use_fixed else 0)
dcw_grp.add(mode_row)
prop_adj = Gtk.Adjustment(value=prop_val, lower=0.05, upper=1.0, step_increment=0.05)
prop_spin = Gtk.SpinButton(adjustment=prop_adj, digits=2, climb_rate=1)
prop_spin.set_valign(Gtk.Align.CENTER)
prop_spin.connect("value-changed", lambda s: self._set_dcw_proportion(s.get_value()))
prop_row = Adw.ActionRow(title="Proportion")
prop_row.add_suffix(prop_spin)
prop_row.set_visible(not use_fixed)
dcw_grp.add(prop_row)
fixed_adj = Gtk.Adjustment(value=fixed_val, lower=100, upper=7680, step_increment=10)
fixed_spin = Gtk.SpinButton(adjustment=fixed_adj, digits=0, climb_rate=1)
fixed_spin.set_valign(Gtk.Align.CENTER)
fixed_spin.connect("value-changed", lambda s: self._set_dcw_fixed(int(s.get_value())))
fixed_row = Adw.ActionRow(title="Fixed Width (px)")
fixed_row.add_suffix(fixed_spin)
fixed_row.set_visible(use_fixed)
dcw_grp.add(fixed_row)
def _on_mode_changed(r, _):
is_fixed = r.get_selected() == 1
prop_row.set_visible(not is_fixed)
fixed_row.set_visible(is_fixed)
if is_fixed:
self._set_dcw_fixed(int(fixed_spin.get_value()))
else:
self._set_dcw_proportion(prop_spin.get_value())
mode_row.connect("notify::selected", _on_mode_changed)
content.append(dcw_grp)
pw_grp = Adw.PreferencesGroup(title="Preset Column Widths (proportions)")
pw_grp.set_description("Cycled through by Mod+R")
pcw_node = layout.get_child("preset-column-widths")
presets = []
if pcw_node:
for c in pcw_node.children:
if c.name == "proportion" and c.args:
presets.append(float(c.args[0]))
self._preset_spins: list[Gtk.SpinButton] = []
for val in presets or [0.333, 0.5, 0.667]:
self._add_preset_row(pw_grp, val)
add_preset_btn = Gtk.Button(label="Add Preset")
add_preset_btn.add_css_class("flat")
add_preset_btn.connect("clicked", lambda *_: self._add_preset_row(pw_grp, 0.5))
pw_grp.set_header_suffix(add_preset_btn)
content.append(pw_grp)
struts_grp = Adw.PreferencesGroup(title="Struts (outer gaps, px)")
struts_node = layout.get_child("struts")
for side in ["left", "right", "top", "bottom"]:
val = int(struts_node.child_arg(side) or 0) if struts_node else 0
adj = Gtk.Adjustment(value=val, lower=0, upper=500, step_increment=4)
row = Adw.SpinRow(title=side.capitalize(), adjustment=adj, digits=0)
row._last_val = val
def _on_strut_changed(r, _, s=side):
new_val = int(r.get_value())
if new_val != getattr(r, "_last_val", None):
r._last_val = new_val
self._set_strut(s, new_val)
row.connect("notify::value", _on_strut_changed)
struts_grp.add(row)
content.append(struts_grp)
def _add_preset_row(self, grp: Adw.PreferencesGroup, val: float):
spin_adj = Gtk.Adjustment(value=val, lower=0.05, upper=1.0, step_increment=0.05)
spin = Gtk.SpinButton(adjustment=spin_adj, digits=3, climb_rate=1)
spin.set_valign(Gtk.Align.CENTER)
self._preset_spins.append(spin)
row = Adw.ActionRow(title=f"Proportion {val:.3f}")
spin.connect(
"value-changed",
lambda s, r=row: (
r.set_title(f"Proportion {s.get_value():.3f}"),
self._save_presets(),
),
)
del_btn = Gtk.Button(icon_name="user-trash-symbolic")
del_btn.set_valign(Gtk.Align.CENTER)
del_btn.add_css_class("flat")
del_btn.add_css_class("error")
def _on_delete(s=spin):
self._preset_spins.remove(s)
grp.remove(row)
self._save_presets()
del_btn.connect("clicked", lambda *_: _on_delete())
row.add_suffix(spin)
row.add_suffix(del_btn)
grp.add(row)
def _save_presets(self):
layout = find_or_create(self._nodes, "layout")
pcw = layout.get_child("preset-column-widths")
if pcw is None:
pcw = KdlNode("preset-column-widths")
layout.children.append(pcw)
new_children = []
for i, s in enumerate(self._preset_spins):
if i < len(pcw.children):
child = pcw.children[i]
child.name = "proportion"
child.args = [round(s.get_value(), 5)]
new_children.append(child)
else:
new_children.append(KdlNode("proportion", args=[round(s.get_value(), 5)]))
salvaged = ""
for i in range(len(self._preset_spins), len(pcw.children)):
salvaged += pcw.children[i].leading_trivia
if salvaged and new_children:
new_children[-1].trailing_trivia += salvaged
pcw.children = new_children
self._commit("preset column widths")
def _set_layout(self, key: str, value):
layout = find_or_create(self._nodes, "layout")
set_child_arg(layout, key, value)
self._commit(f"layout {key}")
def _set_dcw_proportion(self, val: float):
layout = find_or_create(self._nodes, "layout")
dcw = layout.get_child("default-column-width")
if dcw is None:
dcw = KdlNode("default-column-width")
layout.children.append(dcw)
dcw.children = [KdlNode("proportion", args=[round(val, 4)])]
self._commit("default column width proportion")
def _set_dcw_fixed(self, px: int):
layout = find_or_create(self._nodes, "layout")
dcw = layout.get_child("default-column-width")
if dcw is None:
dcw = KdlNode("default-column-width")
layout.children.append(dcw)
dcw.children = [KdlNode("fixed", args=[px])]
self._commit("default column width fixed")
def _set_strut(self, side: str, val: int):
layout = find_or_create(self._nodes, "layout")
struts = layout.get_child("struts")
if struts is None:
struts = KdlNode("struts")
layout.children.append(struts)
if val > 0:
set_child_arg(struts, side, val)
else:
remove_child(struts, side)
self._commit(f"strut {side}")
def _toggle_top(self, key: str, enabled: bool):
nodes = self._nodes
existing = next((n for n in reversed(nodes) if n.name == key), None)
app_state = self._win.app_state
if enabled and not existing:
cache = getattr(app_state, "_removed_top_nodes", {})
if key in cache:
idx, node = cache[key]
nodes.insert(min(idx, len(nodes)), node)
else:
nodes.append(KdlNode(key))
elif not enabled and existing:
if not hasattr(app_state, "_removed_top_nodes"):
app_state._removed_top_nodes = {}
app_state._removed_top_nodes[key] = (nodes.index(existing), existing)
nodes.remove(existing)
self._commit(f"toggle {key}")
def refresh(self):
for child in list(self._content):
self._content.remove(child)
self._build_content()