186 lines
7.9 KiB
Python
186 lines
7.9 KiB
Python
"""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 GLib, 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=10, row_spacing=10)
|
|
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_card = bordered(self._expand_holder, radius=16, fill_bg=True)
|
|
self._expand_reveal.set_child(self._expand_card)
|
|
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)
|
|
|
|
# A second overlay: an external "takeover" widget (the taskbar's workspace/
|
|
# window panel) that, when shown, covers the whole quad region.
|
|
self._takeover_reveal = Gtk.Revealer(
|
|
transition_type=Gtk.RevealerTransitionType.CROSSFADE,
|
|
transition_duration=180, reveal_child=False)
|
|
self._takeover_reveal.set_halign(Gtk.Align.FILL)
|
|
self._takeover_reveal.set_valign(Gtk.Align.FILL)
|
|
self._takeover_holder = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
self._takeover_holder.add_css_class("quad-expanded")
|
|
self._takeover_reveal.set_child(bordered(self._takeover_holder, radius=16, fill_bg=True))
|
|
self.add_overlay(self._takeover_reveal)
|
|
self._takeover_reveal.set_visible(False)
|
|
|
|
settings.subscribe(self._on_settings_changed)
|
|
|
|
# -- external takeover (taskbar panel) ---------------------------------
|
|
def set_takeover_widget(self, widget: Gtk.Widget, on_back) -> None:
|
|
"""Mount an external widget (once) that will cover the quad region when shown.
|
|
A Back row is prepended so the covered quads can be restored."""
|
|
header = Gtk.CenterBox()
|
|
header.add_css_class("expanded-header")
|
|
back = Gtk.Button(label=" Back")
|
|
back.add_css_class("quad-action")
|
|
back.connect("clicked", lambda *_: on_back())
|
|
header.set_start_widget(back)
|
|
self._takeover_holder.append(header)
|
|
widget.set_vexpand(True)
|
|
self._takeover_holder.append(widget)
|
|
|
|
def show_takeover(self) -> None:
|
|
self._takeover_reveal.set_visible(True)
|
|
self._takeover_reveal.set_reveal_child(True)
|
|
self.grid.add_css_class("dimmed")
|
|
|
|
def hide_takeover(self) -> None:
|
|
self._takeover_reveal.set_reveal_child(False)
|
|
self._takeover_reveal.set_visible(False)
|
|
self.grid.remove_css_class("dimmed")
|
|
|
|
# -- 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)
|
|
# bouncy scale pop (CSS @keyframes). Keep the class only for the animation's
|
|
# duration, then drop it: while it's applied, any re-layout of the card (module
|
|
# content streaming in, the grid updating underneath) restarts the transform
|
|
# animation, which made the pop loop forever.
|
|
self._expand_card.add_css_class("quad-pop")
|
|
GLib.timeout_add(340, self._clear_pop)
|
|
self.grid.add_css_class("dimmed")
|
|
card.on_show()
|
|
|
|
def _clear_pop(self) -> bool:
|
|
self._expand_card.remove_css_class("quad-pop")
|
|
return GLib.SOURCE_REMOVE
|
|
|
|
def request_collapse(self) -> None:
|
|
self._expand_reveal.set_reveal_child(False)
|
|
self._expand_reveal.set_visible(False)
|
|
self._expand_card.remove_css_class("quad-pop") # reset so the pop replays
|
|
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()
|