Files
Modus/src/window/dock/main.py
T

188 lines
6.3 KiB
Python

from fabric.utils import GLib, logger, random
from fabric.widgets.box import Box
from fabric.widgets.eventbox import EventBox
from fabric.widgets.revealer import Revealer
from fabric.widgets.wayland import WaylandWindow as Window
from services.config import config, on_config_change
from services.modus import check_occlusion, get_modus_service
from .canvas import DockCanvas
class Dock(Window):
def __init__(self):
super().__init__(
layer="top",
anchor="bottom center",
exclusivity="none",
title="modus-dock",
name="dock",
)
screen = self.get_screen()
if screen:
visual = screen.get_rgba_visual()
if visual:
self.set_visual(visual)
self.set_app_paintable(True)
self.canvas = DockCanvas(self)
canvas_box = Box(
name="dock-canvas-box",
children=[self.canvas],
)
self.revealer = Revealer(
child=canvas_box,
transition_duration=200,
transition_type="slide-up",
)
hover_strip = Box(
name="dock-hover-strip",
)
outer_box = Box(
name="dock-outer-box",
orientation="v",
children=[self.revealer, hover_strip],
)
self.children = EventBox(
events=["enter-notify", "leave-notify"],
child=outer_box,
on_enter_notify_event=lambda *_: self.on_hover_enter(),
on_leave_notify_event=lambda *_: self.on_hover_leave(),
)
self.dock_height = 100
self.is_hovered = False
self.hide_ticket = 0
self._occlusion_timer_id = None
self._hyprland_handlers = []
self._destroyed = False
self.revealer.set_reveal_child(True)
self.show_all()
on_config_change(self._on_config_change)
if config().get("dock.auto_hide", True):
self._setup_occlusion_signals()
self._check_occlusion_deferred()
def _setup_occlusion_signals(self) -> None:
"""Subscribe to Hyprland events that affect occlusion."""
hyprland = get_modus_service()._hyprland_connection
events = [
"event::openwindow",
"event::closewindow",
"event::movewindow",
"event::workspace",
"event::fullscreen",
]
for event in events:
handler_id = hyprland.connect(
event, lambda *_: self._check_occlusion_deferred()
)
self._hyprland_handlers.append(handler_id)
def _check_occlusion_deferred(self) -> None:
"""Debounce occlusion check to avoid rapid successive checks."""
if hasattr(self, "_occlusion_check_pending") and self._occlusion_check_pending:
return
self._occlusion_check_pending = True
GLib.timeout_add(50, self._run_occlusion_check)
def _run_occlusion_check(self) -> bool:
"""Run the actual occlusion check."""
self._occlusion_check_pending = False
if self._destroyed:
return False
try:
if not config().get("dock_auto_hide", True):
return False
is_occ = config().get("dock.always_occluded", False) or check_occlusion(
("bottom", self.dock_height)
)
if is_occ and not self.is_hovered and self.revealer.get_reveal_child():
self.revealer.set_reveal_child(False)
elif not is_occ and not self.revealer.get_reveal_child():
self.revealer.set_reveal_child(True)
elif is_occ and self.is_hovered:
if not self.revealer.get_reveal_child():
self.revealer.set_reveal_child(True)
except Exception as e:
logger.error(f"[Dock] Occlusion check error: {e}")
return False
def _on_config_change(self, new_config, old_config) -> None:
if config().has_changed("dock.enabled", old_config):
self._update_visibility()
if config().has_changed("dock.icon_size", old_config):
self.canvas.update_icon_size()
if config().has_changed("dock.auto_hide", old_config):
if new_config.get("dock.auto_hide", True):
self._setup_occlusion_signals()
else:
self._disconnect_occlusion_signals()
self.revealer.set_reveal_child(True)
if config().has_changed("dock.hide_special_workspace_apps", old_config):
self.canvas._rebuild_model()
if config().has_changed("dock.hover_scale", old_config):
self.canvas.update_icon_size()
def _update_visibility(self) -> None:
if config().get("dock.enabled", True):
self.show()
self.revealer.set_reveal_child(True)
else:
self.hide()
def on_hover_enter(self) -> None:
self.is_hovered = True
self.hide_ticket = random.getrandbits(32)
self.revealer.set_reveal_child(True)
def on_hover_leave(self) -> None:
self.is_hovered = False
ticket = random.getrandbits(32)
self.hide_ticket = ticket
def delayed_hide(t):
if t == self.hide_ticket and not self.is_hovered:
if config().get("dock.auto_hide", True):
is_occ = config().get(
"dock.always_occluded", False
) or check_occlusion(("bottom", self.dock_height))
if is_occ:
self.revealer.set_reveal_child(False)
return False
GLib.timeout_add(500, delayed_hide, ticket)
def _disconnect_occlusion_signals(self) -> None:
"""Disconnect all Hyprland occlusion signal handlers."""
hyprland = get_modus_service()._hyprland_connection
for handler_id in self._hyprland_handlers:
try:
hyprland.disconnect(handler_id)
except Exception as e:
logger.warning(f"[Dock] Error disconnecting occlusion handler: {e}")
self._hyprland_handlers.clear()
def destroy(self) -> None:
self._destroyed = True
self._disconnect_occlusion_signals()
if hasattr(self, "canvas") and self.canvas:
self.canvas.destroy()
super().destroy()