diff --git a/src/window/desktop/main.py b/src/window/desktop/main.py index cabd850..95ad36e 100644 --- a/src/window/desktop/main.py +++ b/src/window/desktop/main.py @@ -2,6 +2,7 @@ import importlib import os import pkgutil import sys +from functools import partial import cairo import tomlkit @@ -45,7 +46,7 @@ def _load_user_widgets() -> set[str]: module_name = fname[:-3] full_path = os.path.join(_CONFIG_DESKTOP_DIR, fname) try: - before_keys = set(DesktopWidgetRegistry._widgets) + before_keys = set(DesktopWidgetRegistry.keys()) spec = importlib.util.spec_from_file_location( f"user_widget_{module_name}", full_path ) @@ -53,7 +54,7 @@ def _load_user_widgets() -> set[str]: sys.modules[spec.name] = mod spec.loader.exec_module(mod) _user_widget_modules[module_name] = spec.name - new_keys = set(DesktopWidgetRegistry._widgets) - before_keys + new_keys = set(DesktopWidgetRegistry.keys()) - before_keys for k in new_keys: _user_key_to_file[k] = module_name DesktopWidgetRegistry.register_user(k) @@ -100,13 +101,13 @@ class PositionManager: mid_str = str(monitor_id) if mid_str not in state or not state[mid_str]: state[mid_str] = [] - for key in DesktopWidgetRegistry._widgets: + for key in DesktopWidgetRegistry.widgets(): pos = DesktopWidgetRegistry.get_default_position(key) px, py = pos if pos else (0.0, 0.0) state[mid_str].append({"key": key, "px": px, "py": py}) cls._write(state) else: - registered = set(DesktopWidgetRegistry._widgets) + registered = set(DesktopWidgetRegistry.keys()) before = len(state[mid_str]) state[mid_str] = [e for e in state[mid_str] if e["key"] in registered] for key in registered - {e["key"] for e in state[mid_str]}: @@ -173,25 +174,8 @@ class DesktopWidgetWindow(WaylandWindow): self._win_w = max(1, geom.width) self._win_h = max(1, geom.height) - self._fade_animator = Animator( - bezier_curve=(0.4, 0.0, 0.2, 1.0), - duration=0.2, - min_value=0.0, - max_value=1.0, - tick_widget=self._root, - ) - self._fade_animator.connect("notify::value", self._on_fade_value) - self._fade_animator.connect("finished", self._on_fade_finished) - - self._fade_out_animator = Animator( - bezier_curve=(0.4, 0.0, 0.2, 1.0), - duration=0.2, - min_value=0.0, - max_value=1.0, - tick_widget=self._root, - ) - self._fade_out_animator.connect("notify::value", self._on_fade_out_value) - self._fade_out_animator.connect("finished", self._on_fade_out_finished) + self._fade_in_animator = self._make_fade_animator(1.0) + self._fade_out_animator = self._make_fade_animator(0.0) self._fixed.set_opacity(0.0) super().__init__( @@ -229,34 +213,42 @@ class DesktopWidgetWindow(WaylandWindow): # fade + def _make_fade_animator(self, target_opacity: float) -> Animator: + animator = Animator( + bezier_curve=(0.4, 0.0, 0.2, 1.0), + duration=0.2, + min_value=0.0, + max_value=1.0, + tick_widget=self._root, + ) + animator.connect("notify::value", self._on_fade_value) + animator.connect("finished", partial(self._on_fade_finished, target_opacity)) + return animator + def _on_fade_value(self, animator, _) -> None: self._fixed.set_opacity(animator.value) - def _on_fade_finished(self, animator) -> None: - self._fixed.set_opacity(1.0) + def _on_fade_finished(self, target_opacity: float, _animator) -> None: + self._fixed.set_opacity(target_opacity) def _fade_in(self) -> None: - if self._fade_animator.playing: + if self._fade_in_animator.playing: return self._fade_out_animator.pause() - self._fade_animator.value = self._fixed.get_opacity() - self._fade_animator.min_value = self._fixed.get_opacity() - self._fade_animator.max_value = 1.0 - self._fade_animator.play() + opacity = self._fixed.get_opacity() + self._fade_in_animator.value = opacity + self._fade_in_animator.min_value = opacity + self._fade_in_animator.max_value = 1.0 + self._fade_in_animator.play() def _fade_out(self) -> None: - self._fade_animator.pause() + self._fade_in_animator.pause() + opacity = self._fixed.get_opacity() self._fade_out_animator.min_value = 0.0 - self._fade_out_animator.max_value = self._fixed.get_opacity() - self._fade_out_animator.value = self._fixed.get_opacity() + self._fade_out_animator.max_value = opacity + self._fade_out_animator.value = opacity self._fade_out_animator.play() - def _on_fade_out_value(self, animator, _) -> None: - self._fixed.set_opacity(animator.value) - - def _on_fade_out_finished(self, animator) -> None: - self._fixed.set_opacity(0.0) - # fallback poll — catches deletions that Gio file monitor misses def _poll_user_widgets(self) -> bool: diff --git a/src/window/desktop/registry.py b/src/window/desktop/registry.py index 25bf9ef..a8f6c17 100644 --- a/src/window/desktop/registry.py +++ b/src/window/desktop/registry.py @@ -1,42 +1,76 @@ +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +from fabric.utils import logger + + +@dataclass(slots=True, frozen=True) +class WidgetInfo: + widget_cls: type[Any] + size: tuple[int, int] + default_position: tuple[float, float] | None = None + + class DesktopWidgetRegistry: - _widgets = {} - _sizes = {} - _default_positions = {} + DEFAULT_SIZE: tuple[int, int] = (174, 174) + + _registry: dict[str, WidgetInfo] = {} _user_keys: set[str] = set() @classmethod def register( cls, key: str, - widget_cls, + widget_cls: type[Any], size: tuple[int, int], default_position: tuple[float, float] | None = None, - ): - cls._widgets[key] = widget_cls - cls._sizes[key] = size - if default_position is not None: - cls._default_positions[key] = default_position + ) -> None: + if key in cls._registry: + logger.warning("Replacing widget '%s'", key) + cls._registry[key] = WidgetInfo(widget_cls, size, default_position) @classmethod def register_user(cls, key: str) -> None: cls._user_keys.add(key) @classmethod - def unregister_user_keys(cls, keys: set[str]) -> None: - for key in keys: - cls._widgets.pop(key, None) - cls._sizes.pop(key, None) - cls._default_positions.pop(key, None) - cls._user_keys.discard(key) + def unregister(cls, key: str) -> None: + cls._registry.pop(key, None) + cls._user_keys.discard(key) @classmethod - def get_widget_class(cls, key: str): - return cls._widgets.get(key) + def unregister_user_keys(cls, keys: set[str]) -> None: + for key in keys: + cls.unregister(key) + + @classmethod + def clear_user_widgets(cls) -> None: + cls.unregister_user_keys(set(cls._user_keys)) + + @classmethod + def get_widget_class(cls, key: str) -> type[Any] | None: + info = cls._registry.get(key) + return info.widget_cls if info else None @classmethod def get_widget_size(cls, key: str) -> tuple[int, int]: - return cls._sizes.get(key, (174, 174)) + info = cls._registry.get(key) + return info.size if info else cls.DEFAULT_SIZE @classmethod def get_default_position(cls, key: str) -> tuple[float, float] | None: - return cls._default_positions.get(key) + info = cls._registry.get(key) + return info.default_position if info else None + + @classmethod + def is_registered(cls, key: str) -> bool: + return key in cls._registry + + @classmethod + def keys(cls) -> tuple[str, ...]: + return tuple(cls._registry) + + @classmethod + def widgets(cls) -> MappingProxyType[str, WidgetInfo]: + return MappingProxyType(cls._registry)