✨ feat: Introduce customizable modules and settings for enhanced user experience

This commit is contained in:
sankalp committed 2025-07-29 21:03:08 +05:45
1 parent eaa3b7e17d
commit 5789013e2e
67 files changed
-29663

No files matched your search

-4
View File
@@ -1,4 +0,0 @@
from .custom_image import *
from .icon_resolver import *
from .occlusion import *
from .animator import *
-179
View File
@@ -1,179 +0,0 @@
from typing import cast
from fabric import Property, Service, Signal
from gi.repository import GLib, Gtk
class Animator(Service):
@Signal
def finished(self) -> None: ...
@Property(tuple[float, float, float, float], "read-write")
def bezier_curve(self) -> tuple[float, float, float, float]:
return self._bezier_curve
@bezier_curve.setter
def bezier_curve(self, value: tuple[float, float, float, float]):
self._bezier_curve = value
return
@Property(float, "read-write")
def value(self):
return self._value
@value.setter
def value(self, value: float):
self._value = value
return
@Property(float, "read-write")
def max_value(self):
return self._max_value
@max_value.setter
def max_value(self, value: float):
self._max_value = value
return
@Property(float, "read-write")
def min_value(self):
return self._min_value
@min_value.setter
def min_value(self, value: float):
self._min_value = value
return
@Property(bool, "read-write", default_value=False)
def playing(self):
return self._playing
@playing.setter
def playing(self, value: bool):
self._playing = value
return
@Property(bool, "read-write", default_value=False)
def repeat(self):
return self._repeat
@repeat.setter
def repeat(self, value: bool):
self._repeat = value
return
def __init__(
self,
bezier_curve: tuple[float, float, float, float],
duration: float,
min_value: float = 0.0,
max_value: float = 1.0,
repeat: bool = False,
tick_widget: Gtk.Widget | None = None,
**kwargs,
):
super().__init__(**kwargs)
self._bezier_curve = (1, 0, 1, 1)
self._duration = 5
self._value = 0.0
self._min_value = 0.0
self._max_value = 1.0
self._repeat = False
self.bezier_curve = bezier_curve
self.duration = duration
self.value = min_value
self.min_value = min_value
self.max_value = max_value
self.repeat = repeat
self.playing = False
self._start_time = None
self._tick_handler = None
self._timeline_pos = 0
self._tick_widget = tick_widget
def do_get_time_now(self):
return GLib.get_monotonic_time() / 1_000_000
def do_lerp(self, start: float, end: float, time: float) -> float:
return start + (end - start) * time
def do_interpolate_cubic_bezier(self, time: float) -> float:
y_points = (0, self.bezier_curve[1], self.bezier_curve[3], 1)
return (
(1 - time) ** 3 * y_points[0]
+ 3 * (1 - time) ** 2 * time * y_points[1]
+ 3 * (1 - time) * time**2 * y_points[2]
+ time**3 * y_points[3]
)
def do_ease(self, time: float) -> float:
return self.do_lerp(
self.min_value, self.max_value, self.do_interpolate_cubic_bezier(time)
)
def do_update_value(self, delta_time: float):
if not self.playing:
return
elapsed_time = delta_time - cast(float, self._start_time)
self._timeline_pos = min(1, elapsed_time / self.duration)
self.value = self.do_ease(self._timeline_pos)
if not self._timeline_pos >= 1:
return
if not self.repeat:
self.value = self.max_value
self.finished()
self.pause()
return
self._start_time = delta_time
self._timeline_pos = 0
return
def do_handle_tick(self, *_):
current_time = self.do_get_time_now()
self.do_update_value(current_time)
return True
def do_remove_tick_handlers(self):
if self._tick_handler:
if self._tick_widget:
self._tick_widget.remove_tick_callback(self._tick_handler)
else:
GLib.source_remove(self._tick_handler)
self._tick_handler = None
return
def play(self):
if self.playing:
return
self._start_time = self.do_get_time_now()
if not self._tick_handler:
if self._tick_widget:
self._tick_handler = self._tick_widget.add_tick_callback(
self.do_handle_tick
)
else:
self._tick_handler = GLib.timeout_add(16, self.do_handle_tick)
self.playing = True
return
def pause(self):
self.playing = False
return self.do_remove_tick_handlers()
def stop(self):
if not self._tick_handler:
self._timeline_pos = 0
self.playing = False
return
return self.do_remove_tick_handlers()
-39
View File
@@ -1,39 +0,0 @@
import math
from typing import cast
import cairo
from gi.repository import Gtk
from fabric.widgets.image import Image
class CustomImage(Image):
def do_render_rectangle(
self, cr: cairo.Context, width: int, height: int, radius: int = 0
):
cr.move_to(radius, 0)
cr.line_to(width - radius, 0)
cr.arc(width - radius, radius, radius, -(math.pi / 2), 0)
cr.line_to(width, height - radius)
cr.arc(width - radius, height - radius, radius, 0, (math.pi / 2))
cr.line_to(radius, height)
cr.arc(radius, height - radius, radius, (math.pi / 2), math.pi)
cr.line_to(0, radius)
cr.arc(radius, radius, radius, math.pi, (3 * (math.pi / 2)))
cr.close_path()
def do_draw(self, cr: cairo.Context):
context = self.get_style_context()
width, height = self.get_allocated_width(), self.get_allocated_height()
cr.save()
self.do_render_rectangle(
cr,
width,
height,
cast(int, context.get_property("border-radius", Gtk.StateFlags.NORMAL)),
)
cr.clip()
Image.do_draw(self, cr)
cr.restore()
-102
View File
@@ -1,102 +0,0 @@
import json
import os
import re
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
from loguru import logger
import config.data as data
ICON_CACHE_FILE = data.CACHE_DIR + "/icons.json"
if not os.path.exists(data.CACHE_DIR):
os.makedirs(data.CACHE_DIR)
class IconResolver:
def __init__(self, default_applicaiton_icon: str = "application-x-executable-symbolic"):
if os.path.exists(ICON_CACHE_FILE):
with open(ICON_CACHE_FILE) as f:
try:
self._icon_dict = json.load(f)
except json.JSONDecodeError:
logger.info("[ICONS] Cache file does not exist or is corrupted")
self._icon_dict = {}
else:
self._icon_dict = {}
self.default_applicaiton_icon = default_applicaiton_icon
def get_icon_name(self, app_id: str):
if app_id in self._icon_dict:
return self._icon_dict[app_id]
new_icon = self._compositor_find_icon(app_id)
logger.info(
f"[ICONS] found new icon: '{new_icon}' for app id: '{app_id}', storing..."
)
self._store_new_icon(app_id, new_icon)
return new_icon
def get_icon_pixbuf(self, app_id: str, size: int = 16):
icon_theme = Gtk.IconTheme.get_default()
icon_name = self.get_icon_name(app_id)
try:
# Try to load the resolved icon.
return icon_theme.load_icon(icon_name, size, Gtk.IconLookupFlags.FORCE_SIZE)
except GLib.Error as primary_error:
logger.warning(
f"Warning: Icon '{icon_name}' not found in theme. Error: {primary_error}"
)
try:
# Fallback to the default application icon.
return icon_theme.load_icon(
self.default_applicaiton_icon, size, Gtk.IconLookupFlags.FORCE_SIZE
)
except GLib.Error as fallback_error:
logger.error(
f"Error: Fallback icon '{self.default_applicaiton_icon}' also not found. Error: {fallback_error}"
)
return None
def _store_new_icon(self, app_id: str, icon: str):
self._icon_dict[app_id] = icon
with open(ICON_CACHE_FILE, "w") as f:
json.dump(self._icon_dict, f)
def _get_icon_from_desktop_file(self, desktop_file_path: str):
# Retrieve the icon specified in the [Desktop Entry] section.
with open(desktop_file_path) as f:
for line in f.readlines():
if "Icon=" in line:
return "".join(line[5:].split())
return self.default_applicaiton_icon
def _get_desktop_file(self, app_id: str) -> str | None:
data_dirs = GLib.get_system_data_dirs()
for data_dir in data_dirs:
data_dir = os.path.join(data_dir, "applications")
if os.path.exists(data_dir):
files = os.listdir(data_dir)
matching = [s for s in files if "".join(app_id.lower().split()) in s.lower()]
if matching:
return os.path.join(data_dir, matching[0])
for word in list(filter(None, re.split(r"-|\.|_|\s", app_id))):
matching = [s for s in files if word.lower() in s.lower()]
if matching:
return os.path.join(data_dir, matching[0])
return None
def _compositor_find_icon(self, app_id: str):
icon_theme = Gtk.IconTheme.get_default()
if icon_theme.has_icon(app_id):
return app_id
if icon_theme.has_icon(app_id + "-desktop"):
return app_id + "-desktop"
desktop_file = self._get_desktop_file(app_id)
return (
self._get_icon_from_desktop_file(desktop_file)
if desktop_file
else self.default_applicaiton_icon
)
-185
View File
@@ -1,185 +0,0 @@
# Parameters
font_family: str = "tabler-icons"
font_weight: str = "normal"
span: str = f"<span font-family='{font_family}' font-weight='{font_weight}'>"
# Panels
apps: str = "&#xebde;"
dashboard: str = "&#xea87;"
chat: str = "&#xf59f;"
wallpapers: str = "&#xeb01;"
windows: str = "&#xefe6;"
# Bar
colorpicker: str = "&#xebe6;"
media: str = "&#xf00d;"
# Circles
temp: str = "&#xeb38;"
disk: str = "&#xea88;"
battery: str = "&#xea38;"
memory: str = "&#xfa97;"
cpu: str = "&#xef8e;"
swap: str = "&#xeb31;"
# Toolbox
toolbox: str = "&#xebca;"
ssfull: str = "&#xec3c;"
ssregion: str = "&#xf201;"
screenshots: str = "&#xeb0a;"
screenrecord: str = "&#xed22;"
recordings: str = "&#xeafa;"
ocr: str = "&#xfcc3;"
gamemode: str = "&#xf026;"
gamemode_off: str = "&#xf111;"
close: str = "&#xeb55;"
bell: str = "&#xeb55;"
bell_off: str = "&#xece9"
notifications_on: str = "&#xea35;"
notifications_off: str = "&#xece9;"
notifications_clear: str = "&#xf814;"
notifications: str = "&#xeafe;"
# Wallpapers
add: str = "&#xeb0b;"
sort: str = "&#xeb5a;"
circle: str = "&#xf671;"
check_box: str = "&#xf63d;"
box: str = "&#xf59a;"
scan: str = "&#xeb13;"
scan_stop: str = "&#xed4a;"
record: str = "&#xf692;"
update: str = "&#xfa0a;"
updated: str = "&#xf704;"
# Chevrons
chevron_up: str = "&#xea62;"
chevron_down: str = "&#xea5f;"
chevron_left: str = "&#xea60;"
chevron_right: str = "&#xea61;"
# Power
lock: str = "&#xeae2;"
suspend: str = "&#xece7;"
logout: str = "&#xeba8;"
reboot: str = "&#xeb13;"
shutdown: str = "&#xeb0d;"
# Power Manager
power_saving: str = "&#xed4f;"
power_balanced: str = "&#xfa77;"
power_performance: str = "&#xec45;"
charging: str = "&#xefef;"
discharging: str = "&#xefe9;"
alert: str = "&#xea06;"
bat_charging: str = "&#xeeca;"
bat_discharging: str = "&#xf0a1;"
bat_low: str = "&#xff1d;"
bat_full: str = "&#xea38;"
moderate_signal: str = "&#xeba4;"
good_signal: str = "&#xeba5;"
no_signal: str = "&#xf063;"
weak_signal: str = "&#xeba3;"
# Applets
lan: str = "&#xeb9d;"
wifi_0: str = "&#xeba3;"
wifi_1: str = "&#xeba4;"
wifi_2: str = "&#xeba5;"
wifi_3: str = "&#xeb52;"
wifi: str = "&#xeb52;"
world: str = "&#xeb54;"
world_off: str = "&#xf1ca;"
bluetooth: str = "&#xea37;"
night: str = "&#xeaf8;"
coffee: str = "&#xef0e;"
dnd: str = "&#xea35;"
darkmode: str = "&#xfe56;"
wifi_off: str = "&#xecfa;"
bluetooth_off: str = "&#xeceb;"
night_off: str = "&#xf162;"
dnd_off: str = "&#xece9;"
# Bluetooth
bluetooth_connected: str = "&#xecea;"
bluetooth_disconnected: str = "&#xf081;"
# Player
pause: str = "&#xf690;"
play: str = "&#xf691;"
stop: str = "&#xf695;"
skip_back: str = "&#xf693;"
skip_forward: str = "&#xf694;"
prev: str = "&#xf697;"
next: str = "&#xf696;"
shuffle: str = "&#xf000;"
repeat: str = "&#xeb72;"
music: str = "&#xeafc;"
rewind_backward_5: str = "&#xfabf;"
rewind_forward_5: str = "&#xfac7;"
# Volume
vol_off: str = "&#xf1c3;"
vol_mute: str = "&#xeb50;"
vol_medium: str = "&#xeb4f;"
vol_high: str = "&#xeb51;"
mic: str = "&#xeaf0;"
mic_muted: str = "&#xed16;"
# Overview
circle_plus: str = "&#xea69;"
# Pins
copy_plus: str = "&#xfdae;"
# Clipboard Manager
clipboard: str = "&#xea6f;"
clip_text: str = "&#xf089;"
# Confirm
accept: str = "&#xea5e;"
cancel: str = "&#xeb55;"
trash: str = "&#xeb41;"
clear: str = "&#xef88;"
# Config
config: str = "&#xeb20;"
# Icons
firefox: str = "&#xecfd;"
chromium: str = "&#xec18;"
spotify: str = "&#xfe86;"
disc: str = "&#x1003e;"
disc_off: str = "&#xf118;"
# Misc
dot: str = "&#xf698;"
palette: str = "&#xeb01;"
cloud_off: str = "&#xed3e;"
loader: str = "&#xeca3;"
radar: str = "&#xf017;"
emoji: str = "&#xeaf7;"
keyboard: str = "&#xebd6;"
terminal: str = "&#xebef;"
exceptions: list[str] = ["font_family", "font_weight", "span"]
def apply_span() -> None:
global_dict = globals()
for key in global_dict:
if key not in exceptions and not key.startswith("__"):
global_dict[key] = f"{span}{global_dict[key]}</span>"
apply_span()
-141
View File
@@ -1,141 +0,0 @@
import subprocess
import json
import config.data as data
def get_current_workspace():
"""
Get the current workspace ID using hyprctl.
"""
try:
result = subprocess.run(
["hyprctl", "activeworkspace"], capture_output=True, text=True
)
# Assume the output similar to: "ID <number>"
# Extracting the number from the output
parts = result.stdout.split()
for i, part in enumerate(parts):
if part == "ID" and i + 1 < len(parts):
return int(parts[i + 1])
except Exception as e:
print(f"Error getting current workspace: {e}")
return -1
def get_screen_dimensions():
"""
Get screen dimensions from hyprctl.
Returns:
tuple: (width, height) of the monitor containing the current workspace
"""
try:
# Get current workspace
workspace_id = get_current_workspace()
# Get monitor information
result = subprocess.run(
["hyprctl", "-j", "monitors"], capture_output=True, text=True
)
monitors = json.loads(result.stdout)
# Find the monitor containing our workspace
for monitor in monitors:
if monitor.get("activeWorkspace", {}).get("id") == workspace_id:
return monitor.get("width", data.CURRENT_WIDTH), monitor.get(
"height", data.CURRENT_HEIGHT
)
# Fallback to first monitor
if monitors:
return monitors[0].get("width", data.CURRENT_WIDTH), monitors[0].get(
"height", data.CURRENT_HEIGHT
)
except Exception as e:
print(f"Error getting screen dimensions: {e}")
# Default fallback values
return data.CURRENT_WIDTH, data.CURRENT_HEIGHT
def check_occlusion(occlusion_region, workspace=None):
"""
Check if a region is occupied by any window on a given workspace.
Parameters:
occlusion_region: Can be one of:
- tuple (side, size): where side is "top", "bottom", "left", or "right"
and size is the pixel width of the region
- tuple (x, y, width, height): The full region coordinates (legacy format)
workspace (int, optional): The workspace ID to check. If None, the current workspace is used.
Returns:
bool: True if any window overlaps with the occlusion region, False otherwise.
"""
if workspace is None:
workspace = get_current_workspace()
# Handle simplified side-based format
if isinstance(occlusion_region, tuple) and len(occlusion_region) == 2:
side, size = occlusion_region
if isinstance(side, str):
# Convert side-based format to coordinates
screen_width, screen_height = get_screen_dimensions()
if side.lower() == "bottom":
occlusion_region = (0, screen_height - size, screen_width, size)
elif side.lower() == "top":
occlusion_region = (0, 0, screen_width, size)
elif side.lower() == "left":
occlusion_region = (0, 0, size, screen_height)
elif side.lower() == "right":
occlusion_region = (screen_width - size, 0, size, screen_height)
# Ensure occlusion_region is in the correct format (x, y, width, height)
if not isinstance(occlusion_region, tuple) or len(occlusion_region) != 4:
print(f"Invalid occlusion region format: {occlusion_region}")
return False
try:
result = subprocess.run(
["hyprctl", "-j", "clients"], capture_output=True, text=True
)
clients = json.loads(result.stdout)
except Exception as e:
print(f"Error retrieving client windows: {e}")
return False
occ_x, occ_y, occ_width, occ_height = occlusion_region
occ_x2 = occ_x + occ_width
occ_y2 = occ_y + occ_height
for client in clients:
# Check if client is mapped
if not client.get("mapped", False):
continue
# Ensure client has proper workspace information and matches the workspace
client_workspace = client.get("workspace", {})
if client_workspace.get("id") != workspace:
continue
# Ensure client has position and size info
position = client.get("at")
size = client.get("size")
if not position or not size:
continue
x, y = position
width, height = size
win_x1, win_y1 = x, y
win_x2, win_y2 = x + width, y + height
# Check for intersection between the window and occlusion region
if not (
win_x2 <= occ_x or win_x1 >= occ_x2 or win_y2 <= occ_y or win_y1 >= occ_y2
):
return True # Occlusion region is occupied
return False # No window overlaps the occlusion region
-359
View File
@@ -1,359 +0,0 @@
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],
}