360 lines
11 KiB
Python
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],
|
|
}
|