"""Generic chrome around any module: a header (icon, title, expand, settings) and a body that is either the module's compact widget or an 'enable me' placeholder. A disabled quad never calls the module's build(), so a disabled Bluetooth/Network quad spawns no backend work at all. """ from __future__ import annotations import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk # noqa: E402 from module_base import ModuleContext, ModuleInstance, ModuleSpec class QuadCard(Gtk.Box): def __init__(self, spec: ModuleSpec, settings, services, request_expand, request_collapse): super().__init__(orientation=Gtk.Orientation.VERTICAL) self.add_css_class("quad-card") self.spec = spec self.settings = settings self.services = services self.ctx = ModuleContext(spec, settings, services, request_expand, request_collapse) self.instance: ModuleInstance | None = None self._header = self._build_header() self.append(self._header) self._body_holder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._body_holder.add_css_class("quad-body") self._body_holder.set_vexpand(True) self.append(self._body_holder) self._rebuild_body() # -- header ------------------------------------------------------------ def _build_header(self) -> Gtk.Widget: header = Gtk.CenterBox() header.add_css_class("quad-header") title = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8) icon = Gtk.Label(label=self.spec.icon) icon.add_css_class("quad-icon") name = Gtk.Label(label=self.spec.title, xalign=0.0) name.add_css_class("quad-title") title.append(icon) title.append(name) header.set_start_widget(title) actions = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) self._expand_btn = Gtk.Button(label="") # nf-fa-expand self._expand_btn.add_css_class("quad-action") self._expand_btn.set_tooltip_text("Expand") self._expand_btn.connect("clicked", lambda *_: self.ctx.expand()) actions.append(self._build_settings_button()) actions.append(self._expand_btn) header.set_end_widget(actions) return header def _build_settings_button(self) -> Gtk.Widget: btn = Gtk.MenuButton(label="") # nf-fa-cog btn.add_css_class("quad-action") btn.set_tooltip_text("Settings") pop = Gtk.Popover() pop.add_css_class("quad-settings") box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) box.append(self._switch_row("Enabled", self.settings.quad_enabled( self.spec.id, self.spec.default_enabled), lambda v: self.settings.set_quad_enabled(self.spec.id, v))) for feat in self.spec.features: box.append(Gtk.Separator()) box.append(self._switch_row( feat.label, self.settings.feature(self.spec.id, feat.id, feat.default), lambda v, fid=feat.id: self.settings.set_feature(self.spec.id, fid, v))) pop.set_child(box) btn.set_popover(pop) return btn @staticmethod def _switch_row(label: str, value: bool, on_change) -> Gtk.Widget: row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12) row.add_css_class("switch-row") lbl = Gtk.Label(label=label, xalign=0.0, hexpand=True) sw = Gtk.Switch(active=value) sw.connect("state-set", lambda _sw, v: (on_change(v), False)[1]) row.append(lbl) row.append(sw) return row # -- body -------------------------------------------------------------- def _clear_body(self) -> None: if self.instance and self.instance.destroy: self.instance.destroy() self.instance = None child = self._body_holder.get_first_child() while child: self._body_holder.remove(child) child = self._body_holder.get_first_child() def _rebuild_body(self) -> None: self._clear_body() enabled = self.settings.quad_enabled(self.spec.id, self.spec.default_enabled) if enabled: inst = self.spec.build(self.ctx) self.instance = inst self._body_holder.append(inst.compact) self._expand_btn.set_sensitive(inst.expanded is not None) else: self._expand_btn.set_sensitive(False) self._body_holder.append(self._disabled_placeholder()) def _disabled_placeholder(self) -> Gtk.Widget: box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10, valign=Gtk.Align.CENTER, halign=Gtk.Align.CENTER) box.add_css_class("quad-disabled") box.append(Gtk.Label(label=f"{self.spec.title} is off")) btn = Gtk.Button(label="Enable") btn.add_css_class("enable-btn") btn.connect("clicked", lambda *_: self.settings.set_quad_enabled(self.spec.id, True)) box.append(btn) return box # -- lifecycle --------------------------------------------------------- def on_settings_changed(self) -> None: """Called by the grid when settings.json changes; rebuild if enablement flipped.""" enabled = self.settings.quad_enabled(self.spec.id, self.spec.default_enabled) has_module = self.instance is not None if enabled != has_module: self._rebuild_body() @property def expanded_widget(self) -> Gtk.Widget | None: return self.instance.expanded if self.instance else None def on_show(self) -> None: if self.instance and self.instance.on_show: self.instance.on_show() def on_hide(self) -> None: if self.instance and self.instance.on_hide: self.instance.on_hide()