202 lines
7.6 KiB
Python
202 lines
7.6 KiB
Python
import json
|
|
from gi.repository import GLib, Gdk
|
|
from fabric.hyprland.widgets import get_hyprland_connection
|
|
from utils.occlusion import check_occlusion
|
|
|
|
|
|
class AutoHideBarController:
|
|
"""Controller for bar auto-hide behavior."""
|
|
|
|
# Constants for configuration
|
|
OCCLUSION_CHECK_INTERVAL = 250 # ms
|
|
HIDE_DELAY = 300 # ms
|
|
|
|
def __init__(self, bar):
|
|
self.bar = bar
|
|
self.is_hidden = False
|
|
self.hide_id = None
|
|
self.is_hovered = False
|
|
self.conn = get_hyprland_connection()
|
|
self.last_occlusion_state = False
|
|
|
|
self._setup_event_handlers()
|
|
self._start_occlusion_checking()
|
|
|
|
def _setup_event_handlers(self):
|
|
"""Initialize event handlers for window management"""
|
|
# Initial state check
|
|
if self.conn.ready:
|
|
self.check_hide()
|
|
else:
|
|
self.conn.connect("event::ready", lambda *_: self.check_hide())
|
|
|
|
# Monitor window events
|
|
window_events = ("activewindow", "openwindow", "closewindow", "changefloatingmode")
|
|
for event in window_events:
|
|
self.conn.connect(f"event::{event}", lambda *_: self.check_hide())
|
|
self.conn.connect("event::workspace", lambda *_: GLib.timeout_add(50, self.check_hide))
|
|
|
|
def _start_occlusion_checking(self):
|
|
"""Start periodic occlusion check"""
|
|
GLib.timeout_add(self.OCCLUSION_CHECK_INTERVAL, self.check_occlusion_state)
|
|
|
|
def _get_bar_height(self):
|
|
"""Get the height of the bar with fallback"""
|
|
try:
|
|
height = self.bar.get_allocated_height()
|
|
return height if height > 0 else 30
|
|
except Exception:
|
|
return 30 # Default fallback
|
|
|
|
def _get_occlusion_region(self):
|
|
"""Get the region to check for occlusion"""
|
|
return ("top", self._get_bar_height())
|
|
|
|
def check_occlusion_state(self):
|
|
"""Periodic occlusion check"""
|
|
# Skip occlusion check if hovered
|
|
if self.is_hovered:
|
|
self.bar.main_box.remove_style_class("occluded")
|
|
return True
|
|
|
|
# Check occlusion using the top region
|
|
is_occluded = check_occlusion(self._get_occlusion_region())
|
|
|
|
# Only apply changes if state has changed to avoid unnecessary CSS operations
|
|
if is_occluded != self.last_occlusion_state:
|
|
self.last_occlusion_state = is_occluded
|
|
if is_occluded:
|
|
self.bar.main_box.add_style_class("occluded")
|
|
else:
|
|
self.bar.main_box.remove_style_class("occluded")
|
|
if not self.is_hovered and self.is_hidden:
|
|
# Debounce showing the bar to prevent flickering
|
|
GLib.timeout_add(100, lambda: self.toggle_bar(not self._should_stay_hidden()))
|
|
|
|
return True
|
|
|
|
def _should_stay_hidden(self):
|
|
"""Determine if the bar should stay hidden based on window state"""
|
|
clients = self.get_clients()
|
|
current_ws = self.get_workspace()
|
|
ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws]
|
|
|
|
# If we have normal windows and are not being hovered, stay hidden
|
|
return self._has_normal_windows(ws_clients) and not self.is_hovered
|
|
|
|
def on_bar_enter(self, widget, event):
|
|
"""Handle hover over bar content."""
|
|
self.is_hovered = True
|
|
self.bar.main_box.remove_style_class("occluded")
|
|
|
|
# Cancel any pending hide operations
|
|
self._cancel_hide_timer()
|
|
self.toggle_bar(show=True)
|
|
return True # Stop event propagation
|
|
|
|
def on_bar_leave(self, widget, event):
|
|
"""Handle leave from bar content."""
|
|
# Only trigger if mouse actually left the entire bar area
|
|
if event.detail == Gdk.NotifyType.INFERIOR:
|
|
return False # Ignore child-to-child mouse movements
|
|
|
|
self.is_hovered = False
|
|
|
|
# Schedule hide if needed
|
|
if self._should_stay_hidden() or check_occlusion(self._get_occlusion_region()):
|
|
self.delay_hide()
|
|
|
|
# Update occlusion state
|
|
if check_occlusion(self._get_occlusion_region()):
|
|
self.last_occlusion_state = True
|
|
self.bar.main_box.add_style_class("occluded")
|
|
|
|
return True # Stop event propagation
|
|
|
|
def on_hover_enter(self, *args):
|
|
"""Handle hover over top activation area."""
|
|
self.bar.main_box.remove_style_class("occluded")
|
|
self.toggle_bar(show=True)
|
|
return False
|
|
|
|
def _cancel_hide_timer(self):
|
|
"""Cancel any pending hide timer"""
|
|
if self.hide_id:
|
|
GLib.source_remove(self.hide_id)
|
|
self.hide_id = None
|
|
|
|
def toggle_bar(self, show):
|
|
"""Show or hide the bar immediately."""
|
|
if show:
|
|
if self.is_hidden:
|
|
self.is_hidden = False
|
|
self.bar.main_box.remove_style_class("hide-bar")
|
|
self.bar.main_box.add_style_class("show-bar")
|
|
self._cancel_hide_timer()
|
|
else:
|
|
if not self.is_hidden:
|
|
self.is_hidden = True
|
|
self.bar.main_box.remove_style_class("show-bar")
|
|
self.bar.main_box.add_style_class("hide-bar")
|
|
|
|
def delay_hide(self):
|
|
"""Schedule hiding after short delay."""
|
|
self._cancel_hide_timer()
|
|
self.hide_id = GLib.timeout_add(self.HIDE_DELAY, self.hide_bar)
|
|
|
|
def hide_bar(self):
|
|
"""Finalize hiding procedure."""
|
|
if not self.is_hovered:
|
|
self.toggle_bar(show=False)
|
|
self.hide_id = None
|
|
return False
|
|
|
|
def get_clients(self):
|
|
"""Get current client list."""
|
|
try:
|
|
return json.loads(self.conn.send_command("j/clients").reply.decode())
|
|
except (json.JSONDecodeError, AttributeError):
|
|
return []
|
|
|
|
def get_workspace(self):
|
|
"""Get current workspace ID."""
|
|
try:
|
|
return json.loads(
|
|
self.conn.send_command("j/activeworkspace").reply.decode()
|
|
).get("id", 0)
|
|
except (json.JSONDecodeError, AttributeError):
|
|
return 0
|
|
|
|
def _has_normal_windows(self, clients):
|
|
"""Check if any non-floating, non-fullscreen windows exist"""
|
|
return any(not w.get("floating") and not w.get("fullscreen") for w in clients)
|
|
|
|
def check_hide(self, *args):
|
|
"""Determine if bar should auto-hide based on window state."""
|
|
# Get window data
|
|
clients = self.get_clients()
|
|
current_ws = self.get_workspace()
|
|
ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws]
|
|
|
|
# Check for occlusion
|
|
is_occluded = check_occlusion(self._get_occlusion_region())
|
|
self.last_occlusion_state = is_occluded
|
|
|
|
# Determine visibility state
|
|
if not ws_clients:
|
|
# No windows in current workspace, show the bar
|
|
self.bar.main_box.remove_style_class("occluded")
|
|
self.toggle_bar(show=True)
|
|
elif self._has_normal_windows(ws_clients):
|
|
# Normal windows exist, handle occlusion and hide if not hovered
|
|
if is_occluded:
|
|
self.bar.main_box.add_style_class("occluded")
|
|
|
|
if not self.is_hovered:
|
|
self.toggle_bar(show=False)
|
|
else:
|
|
# Only floating/fullscreen windows, show the bar
|
|
self.bar.main_box.remove_style_class("occluded")
|
|
self.toggle_bar(show=True)
|
|
|
|
return False |