453 lines
16 KiB
Python
453 lines
16 KiB
Python
"""Appearance page — borders, focus ring, shadows, corner radius."""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "4.0")
|
|
gi.require_version("Adw", "1")
|
|
from gi.repository import Adw, Gdk, Gtk
|
|
|
|
from nirimod.kdl_parser import KdlNode, find_or_create, set_child_arg, set_node_flag
|
|
from nirimod.pages.base import BasePage
|
|
from nirimod.window_effects import (
|
|
blur_effects_enabled,
|
|
focused_window_blur_enabled,
|
|
get_global_draw_border_with_background,
|
|
get_global_corner_radius,
|
|
get_global_window_opacity,
|
|
global_window_blur_enabled,
|
|
global_window_xray_enabled,
|
|
set_focused_window_blur,
|
|
set_global_draw_border_with_background,
|
|
set_global_corner_radius,
|
|
set_global_window_blur,
|
|
set_global_window_opacity,
|
|
set_global_window_xray,
|
|
set_blur_effects_enabled,
|
|
)
|
|
|
|
|
|
def _parse_color(color_str: str) -> Gdk.RGBA:
|
|
rgba = Gdk.RGBA()
|
|
if color_str and not rgba.parse(color_str):
|
|
rgba.parse("#7fc8ff")
|
|
return rgba
|
|
|
|
|
|
class AppearancePage(BasePage):
|
|
def build(self) -> Gtk.Widget:
|
|
tb, _, _, content = self._make_toolbar_page("Appearance")
|
|
self._content = content
|
|
self._build_content()
|
|
return tb
|
|
|
|
def _build_content(self):
|
|
content = self._content
|
|
nodes = self._nodes
|
|
layout = find_or_create(nodes, "layout")
|
|
|
|
fr_node = layout.get_child("focus-ring") or KdlNode("focus-ring")
|
|
fr_group = self._build_border_group("Focus Ring", "focus-ring", fr_node, layout)
|
|
content.append(fr_group)
|
|
|
|
b_node = layout.get_child("border") or KdlNode("border")
|
|
b_group = self._build_border_group("Border", "border", b_node, layout)
|
|
content.append(b_group)
|
|
|
|
shadow_grp = Adw.PreferencesGroup(title="Shadow")
|
|
shadow_node = layout.get_child("shadow") or KdlNode("shadow")
|
|
|
|
shadow_on_row = Adw.SwitchRow(title="Enable Shadows")
|
|
shadow_on_row.set_active(shadow_node.get_child("on") is not None)
|
|
shadow_on_row.connect(
|
|
"notify::active", lambda r, _: self._set_shadow_flag("on", r.get_active())
|
|
)
|
|
shadow_grp.add(shadow_on_row)
|
|
|
|
soft_val = int(shadow_node.child_arg("softness") or 30)
|
|
softness_adj = Gtk.Adjustment(
|
|
value=soft_val, lower=0, upper=100, step_increment=1
|
|
)
|
|
softness_row = Adw.SpinRow(
|
|
title="Softness (blur radius)", adjustment=softness_adj, digits=0
|
|
)
|
|
|
|
softness_row._last_val = soft_val
|
|
|
|
def _on_soft_changed(r, _):
|
|
new_val = int(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_shadow("softness", new_val)
|
|
|
|
softness_row.connect("notify::value", _on_soft_changed)
|
|
shadow_grp.add(softness_row)
|
|
|
|
spread_val = int(shadow_node.child_arg("spread") or 5)
|
|
spread_adj = Gtk.Adjustment(
|
|
value=spread_val, lower=-50, upper=100, step_increment=1
|
|
)
|
|
spread_row = Adw.SpinRow(title="Spread", adjustment=spread_adj, digits=0)
|
|
|
|
spread_row._last_val = spread_val
|
|
|
|
def _on_spread_changed(r, _):
|
|
new_val = int(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_shadow("spread", new_val)
|
|
|
|
spread_row.connect("notify::value", _on_spread_changed)
|
|
shadow_grp.add(spread_row)
|
|
|
|
color_str = shadow_node.child_arg("color") or "#0007"
|
|
color_row = Adw.ActionRow(title="Shadow Color")
|
|
color_btn = Gtk.ColorDialogButton(
|
|
dialog=Gtk.ColorDialog(title="Shadow Color", with_alpha=True)
|
|
)
|
|
color_btn.set_rgba(_parse_color(color_str))
|
|
color_btn.set_valign(Gtk.Align.CENTER)
|
|
color_btn.connect(
|
|
"notify::rgba", lambda b, _: self._set_shadow_color(b.get_rgba())
|
|
)
|
|
color_row.add_suffix(color_btn)
|
|
shadow_grp.add(color_row)
|
|
|
|
draw_behind_row = Adw.SwitchRow(
|
|
title="Draw Behind Window",
|
|
subtitle="Fixes corner artifacts with non-CSD apps",
|
|
)
|
|
draw_behind_row.set_active(
|
|
shadow_node.get_child("draw-behind-window") is not None
|
|
)
|
|
draw_behind_row.connect(
|
|
"notify::active",
|
|
lambda r, _: self._set_shadow_flag("draw-behind-window", r.get_active()),
|
|
)
|
|
shadow_grp.add(draw_behind_row)
|
|
content.append(shadow_grp)
|
|
|
|
blur_grp = Adw.PreferencesGroup(
|
|
title="Blur (Global)",
|
|
description=(
|
|
"Requires Niri 26.04 or later. Sets blur quality and optional "
|
|
"window blur rules."
|
|
),
|
|
)
|
|
blur_node = next((n for n in nodes if n.name == "blur"), None)
|
|
|
|
blur_effects_row = Adw.SwitchRow(
|
|
title="Enable Blur Effects",
|
|
subtitle="Controls the compositor-level blur { off } setting",
|
|
)
|
|
blur_effects_row.set_active(blur_effects_enabled(nodes))
|
|
blur_effects_row.connect(
|
|
"notify::active",
|
|
lambda r, _: self._set_blur_effects_enabled(r.get_active()),
|
|
)
|
|
blur_grp.add(blur_effects_row)
|
|
|
|
blur_enabled_row = Adw.SwitchRow(
|
|
title="Force Blur on Windows",
|
|
subtitle="Adds background-effect { blur true } to the global window rule",
|
|
)
|
|
blur_enabled_row.set_active(global_window_blur_enabled(nodes))
|
|
blur_enabled_row.connect(
|
|
"notify::active",
|
|
lambda r, _: self._set_window_blur_enabled(r.get_active()),
|
|
)
|
|
blur_grp.add(blur_enabled_row)
|
|
|
|
focused_blur_row = Adw.SwitchRow(
|
|
title="Keep Focused Windows Blurred",
|
|
subtitle="Adds a focused-window rule that forces blur on",
|
|
)
|
|
focused_blur_row.set_active(focused_window_blur_enabled(nodes))
|
|
focused_blur_row.connect(
|
|
"notify::active",
|
|
lambda r, _: self._set_focused_window_blur_enabled(r.get_active()),
|
|
)
|
|
blur_grp.add(focused_blur_row)
|
|
|
|
xray_row = Adw.SwitchRow(
|
|
title="Use Xray Wallpaper Blur",
|
|
subtitle="Use wallpaper-only blur; disable for regular background blur",
|
|
)
|
|
xray_row.set_active(global_window_xray_enabled(nodes))
|
|
xray_row.connect(
|
|
"notify::active",
|
|
lambda r, _: self._set_window_blur_xray(r.get_active()),
|
|
)
|
|
blur_grp.add(xray_row)
|
|
|
|
opacity_val = get_global_window_opacity(nodes)
|
|
opacity_adj = Gtk.Adjustment(
|
|
value=opacity_val, lower=0.1, upper=1.0, step_increment=0.05
|
|
)
|
|
opacity_row = Adw.SpinRow(
|
|
title="Window Opacity (1 = unset)", adjustment=opacity_adj, digits=2
|
|
)
|
|
|
|
opacity_row._last_val = opacity_val
|
|
|
|
def _on_opacity_changed(r, _):
|
|
new_val = round(float(r.get_value()), 2)
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_window_opacity(new_val)
|
|
|
|
opacity_row.connect("notify::value", _on_opacity_changed)
|
|
blur_grp.add(opacity_row)
|
|
|
|
border_bg_row = Adw.SwitchRow(
|
|
title="Draw Border With Background",
|
|
subtitle="Disable to avoid focus colors behind translucent windows",
|
|
)
|
|
border_bg_row.set_active(get_global_draw_border_with_background(nodes))
|
|
border_bg_row.connect(
|
|
"notify::active",
|
|
lambda r, _: self._set_draw_border_with_background(r.get_active()),
|
|
)
|
|
blur_grp.add(border_bg_row)
|
|
|
|
passes_val = int(blur_node.child_arg("passes", 0) if blur_node else 0)
|
|
passes_adj = Gtk.Adjustment(
|
|
value=passes_val, lower=0, upper=10, step_increment=1
|
|
)
|
|
passes_row = Adw.SpinRow(
|
|
title="Passes", adjustment=passes_adj, digits=0
|
|
)
|
|
|
|
passes_row._last_val = passes_val
|
|
|
|
def _on_passes_changed(r, _):
|
|
new_val = int(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_blur("passes", new_val)
|
|
|
|
passes_row.connect("notify::value", _on_passes_changed)
|
|
blur_grp.add(passes_row)
|
|
|
|
offset_val = float(blur_node.child_arg("offset", 2.0) if blur_node else 2.0)
|
|
offset_adj = Gtk.Adjustment(
|
|
value=offset_val, lower=0.0, upper=20.0, step_increment=0.1
|
|
)
|
|
offset_row = Adw.SpinRow(title="Offset", adjustment=offset_adj, digits=1)
|
|
|
|
offset_row._last_val = offset_val
|
|
|
|
def _on_offset_changed(r, _):
|
|
new_val = float(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_blur("offset", new_val)
|
|
|
|
offset_row.connect("notify::value", _on_offset_changed)
|
|
blur_grp.add(offset_row)
|
|
|
|
noise_val = float(blur_node.child_arg("noise", 0.0) if blur_node else 0.0)
|
|
noise_adj = Gtk.Adjustment(
|
|
value=noise_val, lower=0.0, upper=1.0, step_increment=0.01
|
|
)
|
|
noise_row = Adw.SpinRow(title="Noise", adjustment=noise_adj, digits=2)
|
|
|
|
noise_row._last_val = noise_val
|
|
|
|
def _on_noise_changed(r, _):
|
|
new_val = float(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_blur("noise", new_val)
|
|
|
|
noise_row.connect("notify::value", _on_noise_changed)
|
|
blur_grp.add(noise_row)
|
|
|
|
saturation_val = float(blur_node.child_arg("saturation", 1.0) if blur_node else 1.0)
|
|
saturation_adj = Gtk.Adjustment(
|
|
value=saturation_val, lower=0.0, upper=5.0, step_increment=0.1
|
|
)
|
|
saturation_row = Adw.SpinRow(
|
|
title="Saturation", adjustment=saturation_adj, digits=1
|
|
)
|
|
|
|
saturation_row._last_val = saturation_val
|
|
|
|
def _on_saturation_changed(r, _):
|
|
new_val = float(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_blur("saturation", new_val)
|
|
|
|
saturation_row.connect("notify::value", _on_saturation_changed)
|
|
blur_grp.add(saturation_row)
|
|
|
|
content.append(blur_grp)
|
|
|
|
misc_grp = Adw.PreferencesGroup(title="Window Geometry")
|
|
|
|
cr_val = get_global_corner_radius(nodes)
|
|
cr_adj = Gtk.Adjustment(value=cr_val, lower=0, upper=40, step_increment=1)
|
|
cr_row = Adw.SpinRow(
|
|
title="Corner Radius (px)",
|
|
adjustment=cr_adj,
|
|
digits=0,
|
|
)
|
|
|
|
cr_row._last_val = cr_val
|
|
|
|
def _on_cr_changed(r, _):
|
|
new_val = int(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_corner_radius(new_val)
|
|
|
|
cr_row.connect("notify::value", _on_cr_changed)
|
|
misc_grp.add(cr_row)
|
|
content.append(misc_grp)
|
|
|
|
def _build_border_group(
|
|
self, title: str, key: str, node: KdlNode, layout: KdlNode
|
|
) -> Adw.PreferencesGroup:
|
|
grp = Adw.PreferencesGroup(title=title)
|
|
|
|
off_row = Adw.SwitchRow(title="Enable")
|
|
off_row.set_active(node.get_child("off") is None)
|
|
off_row.connect(
|
|
"notify::active",
|
|
lambda r, _, k=key: self._set_layout_border_flag(
|
|
k, "off", not r.get_active()
|
|
),
|
|
)
|
|
grp.add(off_row)
|
|
|
|
width_val = int(node.child_arg("width") or 4)
|
|
width_adj = Gtk.Adjustment(value=width_val, lower=1, upper=20, step_increment=1)
|
|
width_row = Adw.SpinRow(title="Width (px)", adjustment=width_adj, digits=0)
|
|
|
|
width_row._last_val = width_val
|
|
|
|
def _on_width_changed(r, _, k=key):
|
|
new_val = int(r.get_value())
|
|
if new_val != getattr(r, "_last_val", None):
|
|
r._last_val = new_val
|
|
self._set_layout_border(k, "width", new_val)
|
|
|
|
width_row.connect("notify::value", _on_width_changed)
|
|
grp.add(width_row)
|
|
|
|
for color_key, color_label in [
|
|
("active-color", "Active Color"),
|
|
("inactive-color", "Inactive Color"),
|
|
]:
|
|
c_str = node.child_arg(color_key) or (
|
|
"#7fc8ff" if "active" in color_key else "#202020"
|
|
)
|
|
c_row = Adw.ActionRow(title=color_label)
|
|
c_btn = Gtk.ColorDialogButton(
|
|
dialog=Gtk.ColorDialog(title=color_label, with_alpha=True)
|
|
)
|
|
c_btn.set_rgba(_parse_color(c_str))
|
|
c_btn.set_valign(Gtk.Align.CENTER)
|
|
c_btn.connect(
|
|
"notify::rgba",
|
|
lambda b, _, k=key, ck=color_key: self._set_layout_border(
|
|
k, ck, self._rgba_to_hex(b.get_rgba())
|
|
),
|
|
)
|
|
c_row.add_suffix(c_btn)
|
|
grp.add(c_row)
|
|
|
|
return grp
|
|
|
|
@staticmethod
|
|
def _rgba_to_hex(rgba: Gdk.RGBA) -> str:
|
|
r = int(rgba.red * 255)
|
|
g = int(rgba.green * 255)
|
|
b = int(rgba.blue * 255)
|
|
a = int(rgba.alpha * 255)
|
|
if a == 255:
|
|
return f"#{r:02x}{g:02x}{b:02x}"
|
|
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
|
|
|
|
def _get_layout(self):
|
|
return find_or_create(self._nodes, "layout")
|
|
|
|
def _get_border_node(self, key: str) -> KdlNode:
|
|
layout = self._get_layout()
|
|
node = layout.get_child(key)
|
|
if node is None:
|
|
node = KdlNode(key)
|
|
layout.children.append(node)
|
|
return node
|
|
|
|
def _set_layout_border(self, bkey: str, prop: str, value):
|
|
node = self._get_border_node(bkey)
|
|
set_child_arg(node, prop, value)
|
|
self._commit(f"{bkey} {prop}")
|
|
|
|
def _set_layout_border_flag(self, bkey: str, flag: str, enabled: bool):
|
|
node = self._get_border_node(bkey)
|
|
set_node_flag(node, flag, enabled)
|
|
self._commit(f"{bkey} {flag}")
|
|
|
|
def _get_shadow_node(self) -> KdlNode:
|
|
layout = self._get_layout()
|
|
node = layout.get_child("shadow")
|
|
if node is None:
|
|
node = KdlNode("shadow")
|
|
layout.children.append(node)
|
|
return node
|
|
|
|
def _set_shadow(self, prop: str, value):
|
|
set_child_arg(self._get_shadow_node(), prop, value)
|
|
self._commit(f"shadow {prop}")
|
|
|
|
def _set_shadow_flag(self, flag: str, enabled: bool):
|
|
set_node_flag(self._get_shadow_node(), flag, enabled)
|
|
self._commit(f"shadow {flag}")
|
|
|
|
def _set_shadow_color(self, rgba: Gdk.RGBA):
|
|
set_child_arg(self._get_shadow_node(), "color", self._rgba_to_hex(rgba))
|
|
self._commit("shadow color")
|
|
|
|
def _set_blur(self, prop: str, value):
|
|
blur_node = find_or_create(self._nodes, "blur")
|
|
set_child_arg(blur_node, prop, value)
|
|
self._commit(f"blur {prop}")
|
|
|
|
def _set_blur_effects_enabled(self, enabled: bool):
|
|
set_blur_effects_enabled(self._nodes, enabled)
|
|
self._commit("blur effects")
|
|
|
|
def _set_window_blur_enabled(self, enabled: bool):
|
|
set_global_window_blur(self._nodes, enabled)
|
|
self._commit("window blur")
|
|
|
|
def _set_focused_window_blur_enabled(self, enabled: bool):
|
|
set_focused_window_blur(self._nodes, enabled)
|
|
self._commit("focused window blur")
|
|
|
|
def _set_window_blur_xray(self, enabled: bool):
|
|
set_global_window_xray(self._nodes, enabled)
|
|
self._commit("window blur xray")
|
|
|
|
def _set_window_opacity(self, opacity: float):
|
|
set_global_window_opacity(self._nodes, opacity)
|
|
self._commit("window opacity")
|
|
|
|
def _set_draw_border_with_background(self, enabled: bool):
|
|
set_global_draw_border_with_background(self._nodes, enabled)
|
|
self._commit("draw border with background")
|
|
|
|
def _set_corner_radius(self, radius: int):
|
|
set_global_corner_radius(self._nodes, radius)
|
|
self._commit("corner radius")
|
|
|
|
def refresh(self):
|
|
for child in list(self._content):
|
|
self._content.remove(child)
|
|
self._build_content()
|