import re from collections.abc import Iterable from enum import Enum from typing import Literal, cast import cairo import gi from fabric.core.service import Property from fabric.utils.helpers import extract_css_values, get_enum_member from fabric.widgets.window import Window from gi.repository import Gdk, GObject, Gtk from loguru import logger gi.require_version("Gtk", "3.0") try: gi.require_version("GtkLayerShell", "0.1") from gi.repository import GtkLayerShell except: raise ImportError( "looks like we don't have gtk-layer-shell installed, make sure to install it first (as well as using wayland)" ) class WaylandWindowExclusivity(Enum): NONE = 1 NORMAL = 2 AUTO = 3 class Layer(GObject.GEnum): BACKGROUND = 0 BOTTOM = 1 TOP = 2 OVERLAY = 3 ENTRY_NUMBER = 4 class KeyboardMode(GObject.GEnum): NONE = 0 EXCLUSIVE = 1 ON_DEMAND = 2 ENTRY_NUMBER = 3 class Edge(GObject.GEnum): LEFT = 0 RIGHT = 1 TOP = 2 BOTTOM = 3 ENTRY_NUMBER = 4 class WaylandWindow(Window): @Property( Layer, flags="read-write", default_value=Layer.TOP, ) def layer(self) -> Layer: # type: ignore return self._layer # type: ignore @layer.setter def layer( self, value: Literal["background", "bottom", "top", "overlay"] | Layer, ) -> None: self._layer = get_enum_member(Layer, value, default=Layer.TOP) return GtkLayerShell.set_layer(self, self._layer) @Property(int, "read-write") def monitor(self) -> int: if not (monitor := cast(Gdk.Monitor, GtkLayerShell.get_monitor(self))): return -1 display = monitor.get_display() or Gdk.Display.get_default() for i in range(0, display.get_n_monitors()): if display.get_monitor(i) is monitor: return i return -1 @monitor.setter def monitor(self, monitor: int | Gdk.Monitor) -> bool: if isinstance(monitor, int): display = Gdk.Display().get_default() monitor = display.get_monitor(monitor) return ( (GtkLayerShell.set_monitor(self, monitor), True)[1] if monitor is not None else False ) @Property(WaylandWindowExclusivity, "read-write") def exclusivity(self) -> WaylandWindowExclusivity: return self._exclusivity @exclusivity.setter def exclusivity( self, value: Literal["none", "normal", "auto"] | WaylandWindowExclusivity ) -> None: value = get_enum_member( WaylandWindowExclusivity, value, default=WaylandWindowExclusivity.NONE ) self._exclusivity = value match value: case WaylandWindowExclusivity.NORMAL: return GtkLayerShell.set_exclusive_zone(self, True) case WaylandWindowExclusivity.AUTO: return GtkLayerShell.auto_exclusive_zone_enable(self) case _: return GtkLayerShell.set_exclusive_zone(self, False) @Property(bool, "read-write", default_value=False) def pass_through(self) -> bool: return self._pass_through @pass_through.setter def pass_through(self, pass_through: bool = False): self._pass_through = pass_through region = cairo.Region() if pass_through is True else None self.input_shape_combine_region(region) del region return @Property( KeyboardMode, "read-write", default_value=KeyboardMode.NONE, ) def keyboard_mode(self) -> KeyboardMode: return self._keyboard_mode @keyboard_mode.setter def keyboard_mode( self, value: ( Literal[ "none", "exclusive", "on-demand", "entry-number", ] | KeyboardMode ), ): self._keyboard_mode = get_enum_member( KeyboardMode, value, default=KeyboardMode.NONE ) return GtkLayerShell.set_keyboard_mode(self, self._keyboard_mode) @Property(tuple[Edge, ...], "read-write") def anchor(self): return tuple( x for x in [ Edge.TOP, Edge.RIGHT, Edge.BOTTOM, Edge.LEFT, ] if GtkLayerShell.get_anchor(self, x) ) @anchor.setter def anchor(self, value: str | Iterable[Edge]) -> None: self._anchor = value if isinstance(value, (list, tuple)) and all( isinstance(edge, Edge) for edge in value ): for edge in [ Edge.TOP, Edge.RIGHT, Edge.BOTTOM, Edge.LEFT, ]: if edge not in value: GtkLayerShell.set_anchor(self, edge, False) GtkLayerShell.set_anchor(self, edge, True) return elif isinstance(value, str): for edge, anchored in WaylandWindow.extract_edges_from_string( value ).items(): GtkLayerShell.set_anchor(self, edge, anchored) return @Property(tuple[int, ...], flags="read-write") def margin(self) -> tuple[int, ...]: return tuple( GtkLayerShell.get_margin(self, x) for x in [ Edge.TOP, Edge.RIGHT, Edge.BOTTOM, Edge.LEFT, ] ) @margin.setter def margin(self, value: str | Iterable[int]) -> None: for edge, mrgv in WaylandWindow.extract_margin(value).items(): GtkLayerShell.set_margin(self, edge, mrgv) return @Property(object, "read-write") def keyboard_mode(self): kb_mode = GtkLayerShell.get_keyboard_mode(self) if GtkLayerShell.get_keyboard_interactivity(self): kb_mode = KeyboardMode.EXCLUSIVE return kb_mode @keyboard_mode.setter def keyboard_mode( self, value: Literal["none", "exclusive", "on-demand"] | KeyboardMode, ): return GtkLayerShell.set_keyboard_mode( self, get_enum_member( KeyboardMode, value, default=KeyboardMode.NONE, ), ) def __init__( self, layer: Literal["background", "bottom", "top", "overlay"] | Layer = Layer.TOP, anchor: str = "", margin: str | Iterable[int] = "0px 0px 0px 0px", exclusivity: ( Literal["auto", "normal", "none"] | WaylandWindowExclusivity ) = WaylandWindowExclusivity.NONE, keyboard_mode: ( Literal["none", "exclusive", "on-demand"] | KeyboardMode ) = KeyboardMode.NONE, pass_through: bool = False, monitor: int | Gdk.Monitor | None = None, title: str = "fabric", type: Literal["top-level", "popup"] | Gtk.WindowType = Gtk.WindowType.TOPLEVEL, child: Gtk.Widget | None = None, name: str | None = None, visible: bool = True, all_visible: bool = False, style: str | None = None, style_classes: Iterable[str] | str | None = None, tooltip_text: str | None = None, tooltip_markup: str | None = None, h_align: ( Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None ) = None, v_align: ( Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None ) = None, h_expand: bool = False, v_expand: bool = False, size: Iterable[int] | int | None = None, **kwargs, ): Window.__init__( self, title=title, type=type, child=child, name=name, visible=False, all_visible=False, style=style, style_classes=style_classes, tooltip_text=tooltip_text, tooltip_markup=tooltip_markup, h_align=h_align, v_align=v_align, h_expand=h_expand, v_expand=v_expand, size=size, **kwargs, ) self._layer = Layer.ENTRY_NUMBER self._keyboard_mode = KeyboardMode.NONE self._anchor = anchor self._exclusivity = WaylandWindowExclusivity.NONE self._pass_through = pass_through GtkLayerShell.init_for_window(self) GtkLayerShell.set_namespace(self, title) self.connect( "notify::title", lambda *_: GtkLayerShell.set_namespace(self, self.get_title()), ) if monitor is not None: self.monitor = monitor self.layer = layer self.anchor = anchor self.margin = margin self.keyboard_mode = keyboard_mode self.exclusivity = exclusivity self.pass_through = pass_through ( self.show_all() if all_visible is True else self.show() if visible is True else None ) def steal_input(self) -> None: return GtkLayerShell.set_keyboard_interactivity(self, True) def return_input(self) -> None: return GtkLayerShell.set_keyboard_interactivity(self, False) # custom overrides def show(self) -> None: super().show() return self.do_handle_post_show_request() def show_all(self) -> None: super().show_all() return self.do_handle_post_show_request() def do_handle_post_show_request(self) -> None: if not self.get_children(): logger.warning( "[WaylandWindow] showing an empty window is not recommended, some compositors might freak out." ) self.pass_through = self._pass_through return @staticmethod def extract_anchor_values(string: str) -> tuple[str, ...]: """ extracts the geometry values from a given geometry string. :param string: the string containing the geometry values. :type string: str :return: a list of unique directions extracted from the geometry string. :rtype: list """ direction_map = {"l": "left", "t": "top", "r": "right", "b": "bottom"} pattern = re.compile(r"\b(left|right|top|bottom)\b", re.IGNORECASE) matches = pattern.findall(string) return tuple(set(tuple(direction_map[match.lower()[0]] for match in matches))) @staticmethod def extract_edges_from_string(string: str) -> dict["Edge", bool]: anchor_values = WaylandWindow.extract_anchor_values(string.lower()) return { Edge.TOP: "top" in anchor_values, Edge.RIGHT: "right" in anchor_values, Edge.BOTTOM: "bottom" in anchor_values, Edge.LEFT: "left" in anchor_values, } @staticmethod def extract_margin(input: str | Iterable[int]) -> dict["Edge", int]: margins = ( extract_css_values(input.lower()) if isinstance(input, str) else ( input if isinstance(input, (tuple, list)) and len(input) == 4 else (0, 0, 0, 0) ) ) return { Edge.TOP: margins[0], Edge.RIGHT: margins[1], Edge.BOTTOM: margins[2], Edge.LEFT: margins[3], }