"""The 2x2 quad region and its expand-over-the-others behaviour. A Gtk.Overlay stacks two things in the same space: * base : a 2x2 Gtk.Grid of QuadCards * overlay: a Revealer that, when a quad expands, fills the whole region (the full content width, covering all four cells) with that module's expanded view wrapped in a small header carrying a collapse button. Because the overlay fills the region exactly, the expanded quad is as wide as the appdrawer below it, and the outer letterbox margins (applied further up the tree) are untouched — so letterboxing stays identical in every state. """ from __future__ import annotations import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk # noqa: E402 from lib.border import bordered from module_base import ModuleSpec from ui.quadcard import QuadCard class QuadGrid(Gtk.Overlay): def __init__(self, specs: list[ModuleSpec], settings, services): super().__init__() self.add_css_class("quad-region") self.set_vexpand(True) self.settings = settings self._expanded_id: str | None = None self.grid = Gtk.Grid(column_homogeneous=True, row_homogeneous=True, column_spacing=14, row_spacing=14) self.grid.add_css_class("quad-grid") self.set_child(self.grid) self.cards: dict[str, QuadCard] = {} for index, spec in enumerate(specs[:4]): card = QuadCard(spec, settings, services, self.request_expand, self.request_collapse) self.cards[spec.id] = card self.grid.attach(bordered(card, radius=16, fill_bg=True), index % 2, index // 2, 1, 1) # overlay used for the expanded quad self._expand_reveal = Gtk.Revealer( transition_type=Gtk.RevealerTransitionType.CROSSFADE, transition_duration=180, reveal_child=False) self._expand_reveal.set_halign(Gtk.Align.FILL) self._expand_reveal.set_valign(Gtk.Align.FILL) self._expand_holder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self._expand_holder.add_css_class("quad-expanded") self._expand_reveal.set_child(bordered(self._expand_holder, radius=16, fill_bg=True)) self.add_overlay(self._expand_reveal) # The overlay's holder is opaque; keep the whole overlay hidden unless a quad # is actually expanded, otherwise it paints over the 2x2 grid. self._expand_reveal.set_visible(False) settings.subscribe(self._on_settings_changed) # -- expansion --------------------------------------------------------- def request_expand(self, module_id: str) -> None: card = self.cards.get(module_id) if not card or card.instance is None or card.instance.expanded is None: return # A module opts into expansion by supplying a distinct expanded widget # (a separate instance), so no reparenting of the compact cell is needed. content = card.instance.expanded # reparent content into the expanded holder self._clear_expand_holder() self._expand_reveal.set_visible(True) self._expand_holder.append(self._expanded_header(card.spec.title)) if card.instance.scroll_expanded: wrap = Gtk.ScrolledWindow(vexpand=True, hscrollbar_policy=Gtk.PolicyType.NEVER) wrap.set_child(content) self._expand_holder.append(wrap) else: content.set_vexpand(True) self._expand_holder.append(content) self._expanded_id = module_id self._expand_reveal.set_reveal_child(True) self.grid.add_css_class("dimmed") card.on_show() def request_collapse(self) -> None: self._expand_reveal.set_reveal_child(False) self._expand_reveal.set_visible(False) self._expanded_id = None self.grid.remove_css_class("dimmed") # Drop the reference to the reparented widget so the card can reuse it. self._clear_expand_holder() def _clear_expand_holder(self) -> None: child = self._expand_holder.get_first_child() while child: # detach any ScrolledWindow's child so it survives for the card if isinstance(child, Gtk.ScrolledWindow): inner = child.get_child() if inner: child.set_child(None) self._expand_holder.remove(child) child = self._expand_holder.get_first_child() def _expanded_header(self, title: str) -> Gtk.Widget: header = Gtk.CenterBox() header.add_css_class("expanded-header") back = Gtk.Button(label=" Back") # nf arrow back.add_css_class("quad-action") back.connect("clicked", lambda *_: self.request_collapse()) header.set_start_widget(back) lbl = Gtk.Label(label=title) lbl.add_css_class("quad-title") header.set_center_widget(lbl) return header @property def is_expanded(self) -> bool: return self._expanded_id is not None # -- lifecycle --------------------------------------------------------- def _on_settings_changed(self) -> None: for card in self.cards.values(): card.on_settings_changed() def on_show(self) -> None: for card in self.cards.values(): card.on_show() def on_hide(self) -> None: if self.is_expanded: self.request_collapse() for card in self.cards.values(): card.on_hide()