Files
SDG-DRIFT/Ax-Shell/widgets/wayland.py
T
2026-06-03 21:32:45 +02:00

360 lines
11 KiB
Python

from gi.repository import Gdk, GObject, Gtk
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 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],
}