Dotfiles/desktopenvs/hyprlua/astal-menu/window.py

164 lines
6.6 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.taskbar import Taskbar
PANEL_WIDTH_FRACTION = 0.5 # of the monitor width...
MAX_PANEL_WIDTH = 1100 # ...clamped to this
TOP_MARGIN = 46 # gap below the top bar
BOTTOM_MARGIN = 34 # gap from the screen bottom when expanded
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._init_layer_shell()
self.root = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
self.root.set_name("panel-root")
self.root.add_css_class("panel")
# taskbar (full-width, top)
self.taskbar = Taskbar(on_activate=self.hide_menu)
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))
self.root.append(self.taskbar_reveal)
# quads
specs = list(ordered_specs(settings))
self.grid = QuadGrid(specs, settings, services)
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)
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")
# Anchor the top edge only: the surface is horizontally centred and its
# height follows the content (no opposite anchors = no full-screen fill).
LayerShell.set_anchor(self, LayerShell.Edge.TOP, True)
LayerShell.set_margin(self, LayerShell.Edge.TOP, TOP_MARGIN)
LayerShell.set_keyboard_mode(self, LayerShell.KeyboardMode.ON_DEMAND)
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)
# -- appdrawer expansion ----------------------------------------------
def _on_appdrawer_expand(self, expanded: bool) -> None:
self._drawer_expanded = expanded
# Stretch to the bottom only while the drawer is expanded.
LayerShell.set_anchor(self, LayerShell.Edge.BOTTOM, expanded)
LayerShell.set_margin(self, LayerShell.Edge.BOTTOM, BOTTOM_MARGIN if expanded else 0)
# Hide (not just un-reveal) the taskbar + quads so they reserve zero space
# and the drawer fills the whole panel from the top.
self.quad_reveal.set_reveal_child(not expanded)
self.taskbar_reveal.set_reveal_child(not expanded)
self.quad_reveal.set_visible(not expanded)
self.taskbar_reveal.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.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