"""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._features: dict[str, bool] = {} 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, valign=Gtk.Align.CENTER) 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() self._features = self._feature_snapshot() 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 _feature_snapshot(self) -> dict[str, bool]: return {f.id: self.settings.feature(self.spec.id, f.id, f.default) for f in self.spec.features} 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 this quad's enablement flipped or any of its feature toggles changed, so every switch in the settings popover takes effect live (the popover is only reachable while the grid is collapsed, so no expanded view is ever reparented mid-rebuild).""" enabled = self.settings.quad_enabled(self.spec.id, self.spec.default_enabled) has_module = self.instance is not None features_changed = enabled and self._feature_snapshot() != self._features if enabled != has_module or features_changed: 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()