212 lines
9.0 KiB
Python
212 lines
9.0 KiB
Python
"""The popup: a content-sized floating layer-shell panel anchored top-centre.
|
|
|
|
Earlier this was a full-monitor overlay with a dim backdrop, but that blocks the
|
|
whole screen — the invisible full-screen surface intercepts every click. Instead the
|
|
window now sizes to its own content and only occupies that area, leaving the rest of
|
|
the screen usable. It is dismissed with the launcher toggle, Esc, or the ✕ button
|
|
(there is no click-outside-to-close, since that would require a blocking full-screen
|
|
surface).
|
|
|
|
Gtk.Window (layer TOP, anchored TOP → horizontally centred, height = content)
|
|
└ Gtk.Overlay
|
|
main : #panel-root (Taskbar / QuadGrid / AppDrawer, each drawn-bordered)
|
|
over : ✕ close button (top-right)
|
|
|
|
Expanding the app drawer additionally anchors the BOTTOM edge so the panel stretches
|
|
down and the drawer fills to the bottom; collapsing removes that anchor.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "4.0")
|
|
gi.require_version("Gdk", "4.0")
|
|
gi.require_version("Gtk4LayerShell", "1.0")
|
|
from gi.repository import Gdk, GLib, Gtk # noqa: E402
|
|
from gi.repository import Gtk4LayerShell as LayerShell # noqa: E402
|
|
|
|
from appservices import Services
|
|
from lib.border import bordered
|
|
from registry import ordered_specs
|
|
from ui.appdrawer import AppDrawer
|
|
from ui.quadgrid import QuadGrid
|
|
from ui.statsbar import Statsbar
|
|
from ui.taskbar import Taskbar
|
|
|
|
PANEL_WIDTH_FRACTION = 0.5 # of the monitor width...
|
|
MAX_PANEL_WIDTH = 1100 # ...clamped to this
|
|
EDGE_MARGIN = 28 # gap from the anchored edge
|
|
BOTTOM_MARGIN = 34 # gap from the far edge when the drawer is expanded
|
|
SIDES = ("top", "bottom", "left", "right")
|
|
|
|
|
|
class MenuWindow(Gtk.ApplicationWindow):
|
|
def __init__(self, app, settings, services: Services):
|
|
super().__init__(application=app)
|
|
self.set_name("menu-window")
|
|
self.add_css_class("menu-window")
|
|
self.settings = settings
|
|
self.services = services
|
|
self._drawer_expanded = False
|
|
self._side = "top"
|
|
|
|
self._init_layer_shell()
|
|
|
|
self.root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
self.root.set_name("panel-root")
|
|
self.root.add_css_class("panel")
|
|
|
|
# minimalist system-stats line (full-width, very top)
|
|
self.statsbar = Statsbar()
|
|
self.statsbar_reveal = Gtk.Revealer(
|
|
transition_type=Gtk.RevealerTransitionType.SLIDE_DOWN,
|
|
transition_duration=160, reveal_child=True)
|
|
self.statsbar_reveal.set_child(bordered(self.statsbar, border=2, radius=12, fill_bg=True))
|
|
self.root.append(self.statsbar_reveal)
|
|
|
|
# taskbar (full-width, top)
|
|
self.taskbar = Taskbar(on_activate=self.hide_menu,
|
|
on_toggle_panel=self._on_taskbar_panel)
|
|
self.taskbar_reveal = Gtk.Revealer(
|
|
transition_type=Gtk.RevealerTransitionType.SLIDE_DOWN,
|
|
transition_duration=160, reveal_child=True)
|
|
self.taskbar_reveal.set_child(bordered(self.taskbar, fill_bg=True))
|
|
self.root.append(self.taskbar_reveal)
|
|
|
|
# quads
|
|
specs = list(ordered_specs(settings))
|
|
self.grid = QuadGrid(specs, settings, services)
|
|
# The taskbar's workspace/window panel is mounted over the quad region so that
|
|
# expanding it collapses the taskbar strip and covers the 2x2 quads.
|
|
self.grid.set_takeover_widget(self.taskbar.panel_widget,
|
|
on_back=self.taskbar.collapse_panel)
|
|
self.quad_reveal = Gtk.Revealer(
|
|
transition_type=Gtk.RevealerTransitionType.SLIDE_UP,
|
|
transition_duration=200, reveal_child=True)
|
|
self.quad_reveal.set_child(self.grid)
|
|
self.root.append(self.quad_reveal)
|
|
|
|
# appdrawer
|
|
self.appdrawer = AppDrawer(settings, on_launch=self.hide_menu,
|
|
on_toggle_expand=self._on_appdrawer_expand)
|
|
self.appdrawer_wrap = bordered(self.appdrawer, fill_bg=True)
|
|
self.appdrawer_wrap.set_valign(Gtk.Align.FILL)
|
|
self.root.append(self.appdrawer_wrap)
|
|
|
|
overlay = Gtk.Overlay()
|
|
overlay.set_child(self.root)
|
|
close = Gtk.Button(label="✕")
|
|
close.add_css_class("close-btn")
|
|
close.set_halign(Gtk.Align.END)
|
|
close.set_valign(Gtk.Align.START)
|
|
close.connect("clicked", lambda *_: self.hide_menu())
|
|
overlay.add_overlay(close)
|
|
self.set_child(overlay)
|
|
|
|
key = Gtk.EventControllerKey()
|
|
key.connect("key-pressed", self._on_key)
|
|
self.add_controller(key)
|
|
|
|
self.connect("map", lambda *_: self._apply_size())
|
|
self.set_visible(False)
|
|
|
|
# -- layer shell / size -----------------------------------------------
|
|
def _init_layer_shell(self) -> None:
|
|
LayerShell.init_for_window(self)
|
|
LayerShell.set_layer(self, LayerShell.Layer.TOP)
|
|
LayerShell.set_namespace(self, "astal-menu")
|
|
LayerShell.set_keyboard_mode(self, LayerShell.KeyboardMode.ON_DEMAND)
|
|
self._apply_anchor()
|
|
|
|
def set_side(self, side: str) -> None:
|
|
"""Anchor the panel to the given monitor edge ('top'|'bottom'|'left'|'right').
|
|
The compositor's layer 'slide' animation then slides it in from that edge, and
|
|
the panel sits flush against it. Set before showing so the map animates."""
|
|
if side in SIDES:
|
|
self._side = side
|
|
self._apply_anchor()
|
|
|
|
def _apply_anchor(self) -> None:
|
|
"""Pin the panel to its side edge; while the drawer is expanded, also pin the
|
|
perpendicular edges so it fills the screen height."""
|
|
E = LayerShell.Edge
|
|
for edge in (E.TOP, E.BOTTOM, E.LEFT, E.RIGHT):
|
|
LayerShell.set_anchor(self, edge, False)
|
|
LayerShell.set_margin(self, edge, 0)
|
|
primary = {"top": E.TOP, "bottom": E.BOTTOM, "left": E.LEFT, "right": E.RIGHT}[self._side]
|
|
LayerShell.set_anchor(self, primary, True)
|
|
LayerShell.set_margin(self, primary, EDGE_MARGIN)
|
|
if self._drawer_expanded:
|
|
# fill vertically so the expanded drawer reaches top and bottom
|
|
for edge in (E.TOP, E.BOTTOM):
|
|
LayerShell.set_anchor(self, edge, True)
|
|
if edge != primary:
|
|
LayerShell.set_margin(self, edge, BOTTOM_MARGIN)
|
|
|
|
def _monitor_width(self) -> int:
|
|
display = Gdk.Display.get_default()
|
|
surface = self.get_surface()
|
|
mon = display.get_monitor_at_surface(surface) if surface is not None else None
|
|
if mon is None:
|
|
monitors = display.get_monitors()
|
|
mon = monitors.get_item(0) if monitors.get_n_items() else None
|
|
return mon.get_geometry().width if mon is not None else 1920
|
|
|
|
def _apply_size(self) -> None:
|
|
width = min(int(self._monitor_width() * PANEL_WIDTH_FRACTION), MAX_PANEL_WIDTH)
|
|
self.root.set_size_request(width, -1)
|
|
|
|
# -- taskbar panel expansion ------------------------------------------
|
|
def _on_taskbar_panel(self, expanded: bool) -> None:
|
|
# Cover the quads with the taskbar's workspace/window panel. Fully collapse the
|
|
# taskbar strip (header included) so the expanded panel reads as ONE module
|
|
# rather than a thin strip on top plus a detached card. Collapse any quad first.
|
|
if expanded and self.grid.is_expanded:
|
|
self.grid.request_collapse()
|
|
self.taskbar_reveal.set_reveal_child(not expanded)
|
|
self.taskbar_reveal.set_visible(not expanded)
|
|
self.grid.show_takeover() if expanded else self.grid.hide_takeover()
|
|
|
|
# -- appdrawer expansion ----------------------------------------------
|
|
def _on_appdrawer_expand(self, expanded: bool) -> None:
|
|
self._drawer_expanded = expanded
|
|
self._apply_anchor()
|
|
# Hide (not just un-reveal) the stats/taskbar/quads so they reserve zero space
|
|
# and the drawer fills the whole panel.
|
|
for rev in (self.statsbar_reveal, self.quad_reveal, self.taskbar_reveal):
|
|
rev.set_reveal_child(not expanded)
|
|
rev.set_visible(not expanded)
|
|
self.appdrawer_wrap.set_vexpand(expanded)
|
|
|
|
# -- visibility --------------------------------------------------------
|
|
def show_menu(self, focus_appdrawer: bool = False) -> None:
|
|
self.appdrawer.set_expanded(False)
|
|
self.taskbar.refresh()
|
|
self.grid.on_show()
|
|
self.appdrawer.on_show()
|
|
self.set_visible(True)
|
|
self.present()
|
|
GLib.timeout_add(30, lambda: (self._apply_size(), False)[1])
|
|
if focus_appdrawer:
|
|
self.appdrawer.set_expanded(True)
|
|
|
|
def hide_menu(self) -> None:
|
|
self.grid.on_hide()
|
|
self.taskbar.collapse_panel()
|
|
self.grid.hide_takeover()
|
|
self.appdrawer.set_expanded(False)
|
|
self.set_visible(False)
|
|
|
|
def toggle(self, focus_appdrawer: bool = False) -> None:
|
|
if self.get_visible():
|
|
self.hide_menu()
|
|
else:
|
|
self.show_menu(focus_appdrawer)
|
|
|
|
def _on_key(self, _c, keyval, _kc, _state) -> bool:
|
|
if keyval == Gdk.KEY_Escape:
|
|
self.hide_menu()
|
|
return True
|
|
return False
|