394 lines
14 KiB
Python
394 lines
14 KiB
Python
from fabric.hyprland.service import Hyprland, HyprlandEvent
|
|
from fabric.hyprland.widgets import HyprlandWorkspaces, WorkspaceButton
|
|
from fabric.system_tray.widgets import SystemTray
|
|
from fabric.utils import Gdk, GLib
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.centerbox import CenterBox
|
|
from fabric.widgets.datetime import DateTime
|
|
from fabric.widgets.eventbox import EventBox
|
|
from fabric.widgets.revealer import Revealer
|
|
from fabric.widgets.wayland import WaylandWindow as Window
|
|
|
|
import config.data as data
|
|
from modules.panel.components import (
|
|
Battery,
|
|
TaskBar,
|
|
apply_enhanced_system_tray,
|
|
)
|
|
from services import has_config_changed, on_config_change
|
|
from utils.corners import MyCorner
|
|
from utils.functions import is_special_workspace_id
|
|
from utils.occlusion import check_occlusion, is_mouse_in_region, get_screen_dimensions
|
|
|
|
|
|
def config_dependent(func):
|
|
def wrapper(self, *args, **kwargs):
|
|
return func(self, *args, **kwargs)
|
|
|
|
wrapper._config_dependent = True
|
|
return wrapper
|
|
|
|
|
|
class Panel(Window):
|
|
def __init__(self, **kwargs):
|
|
panel_autohide = data.DATA()["panel_autohide"]
|
|
panel_position = data.DATA().get("panel_position") or "bottom"
|
|
|
|
super().__init__(
|
|
name="panel-window",
|
|
layer="top",
|
|
anchor=panel_position,
|
|
exclusivity="auto" if not panel_autohide else "none",
|
|
pass_through=False,
|
|
visible=False,
|
|
all_visible=False,
|
|
)
|
|
|
|
self.hide_id = None
|
|
self.is_mouse_over_panel_area = False
|
|
self.effective_occlusion_size = 40
|
|
self.panel_position = panel_position
|
|
|
|
# Set corner types based on position
|
|
if panel_position == "top":
|
|
corner_left_type = "top-right"
|
|
corner_right_type = "top-left"
|
|
# For top position, corner goes first (at top)
|
|
corner_left_children = [
|
|
MyCorner(corner_left_type),
|
|
Box(v_expand=True, v_align="fill"),
|
|
]
|
|
corner_right_children = [
|
|
MyCorner(corner_right_type),
|
|
Box(v_expand=True, v_align="fill"),
|
|
]
|
|
else: # bottom
|
|
corner_left_type = "bottom-right"
|
|
corner_right_type = "bottom-left"
|
|
# For bottom position, corner goes last (at bottom)
|
|
corner_left_children = [
|
|
Box(v_expand=True, v_align="fill"),
|
|
MyCorner(corner_left_type),
|
|
]
|
|
corner_right_children = [
|
|
Box(v_expand=True, v_align="fill"),
|
|
MyCorner(corner_right_type),
|
|
]
|
|
|
|
self.corner_left = Box(
|
|
name="corner-left",
|
|
orientation="v",
|
|
h_align="start",
|
|
children=corner_left_children,
|
|
)
|
|
self.corner_right = Box(
|
|
name="corner-right",
|
|
orientation="v",
|
|
h_align="end",
|
|
children=corner_right_children,
|
|
)
|
|
|
|
use_12hr = data.DATA()["datetime_12hrs"]
|
|
formatter = "%a %-d %b %I:%M %P" if use_12hr else "%a %-d %b %H:%M"
|
|
|
|
self.datetime = DateTime(
|
|
name="date-time", formatters=[formatter], interval=30000
|
|
)
|
|
self.tray = SystemTray(name="panel-button", spacing=4, icon_size=20)
|
|
apply_enhanced_system_tray(self.tray)
|
|
self.battery = Battery()
|
|
self.taskbar = TaskBar()
|
|
self.taskbar.set_visibility_callback(self._rebuild_layout_from_config)
|
|
self.workspaces = HyprlandWorkspaces(
|
|
name="workspaces",
|
|
spacing=4,
|
|
buttons_factory=lambda ws_id: (
|
|
None
|
|
if (
|
|
data.DATA()["workspace_hide_special"]
|
|
and is_special_workspace_id(ws_id)
|
|
)
|
|
else WorkspaceButton(id=ws_id, label=str(ws_id))
|
|
),
|
|
)
|
|
|
|
self.items = Box()
|
|
|
|
# Create panel CenterBox with position-based CSS class
|
|
self.panel_centerbox = CenterBox(
|
|
name="panel",
|
|
center_children=self.items,
|
|
)
|
|
|
|
# Add CSS class for top position using GTK style context
|
|
if panel_position == "top":
|
|
self.panel_centerbox.get_style_context().add_class("panel-top")
|
|
|
|
self.panel_content = Box(
|
|
name="panel-container",
|
|
orientation="h",
|
|
h_expand=True,
|
|
children=[
|
|
self.corner_left,
|
|
self.panel_centerbox,
|
|
self.corner_right,
|
|
],
|
|
)
|
|
|
|
# Set revealer transition based on position
|
|
transition = "slide-down" if panel_position == "top" else "slide-up"
|
|
|
|
self.panel_revealer = Revealer(
|
|
name="panel-revealer",
|
|
transition_type=transition,
|
|
transition_duration=350,
|
|
child_revealed=False,
|
|
child=self.panel_content,
|
|
)
|
|
|
|
self.hover_activator = EventBox(name="hover-activator")
|
|
self.hover_activator.set_size_request(-1, 8)
|
|
|
|
# Arrange children based on position
|
|
if panel_position == "top":
|
|
panel_children = [self.panel_revealer, self.hover_activator]
|
|
else: # bottom
|
|
panel_children = [self.hover_activator, self.panel_revealer]
|
|
|
|
self.main_container = Box(
|
|
name="panel-main",
|
|
orientation="v",
|
|
children=panel_children,
|
|
)
|
|
|
|
self.event_box = EventBox(
|
|
name="panel-event-box",
|
|
child=self.main_container,
|
|
)
|
|
self.event_box.connect("enter-notify-event", self._on_mouse_enter)
|
|
self.event_box.connect("leave-notify-event", self._on_mouse_leave)
|
|
|
|
self.children = self.event_box
|
|
|
|
self.battery.connect(
|
|
"notify::visible", lambda *_: self._rebuild_layout_from_config()
|
|
)
|
|
|
|
self._rebuild_layout_from_config()
|
|
on_config_change(self._on_config_change)
|
|
self._update_panel_visibility()
|
|
|
|
self.hyprland = Hyprland()
|
|
self.hyprland.connect("event::openwindow", self._on_window_event)
|
|
self.hyprland.connect("event::closewindow", self._on_window_event)
|
|
self.hyprland.connect("event::movewindow", self._on_window_event)
|
|
self.hyprland.connect("event::changefloatingmode", self._on_window_event)
|
|
self.hyprland.connect("event::fullscreen", self._on_window_event)
|
|
self.hyprland.connect("event::workspacev2", self._on_window_event)
|
|
|
|
if panel_autohide:
|
|
self.panel_revealer.set_reveal_child(False)
|
|
self.check_occlusion_state()
|
|
|
|
def _get_enabled_widgets(self):
|
|
components = data.DATA()
|
|
enabled_widgets = {
|
|
"workspace": components["workspace"],
|
|
"taskbar": components["taskbar"],
|
|
"systray": components["systray"],
|
|
"battery": components["battery"],
|
|
"datetime": components["datetime"],
|
|
}
|
|
return enabled_widgets
|
|
|
|
@config_dependent
|
|
def _rebuild_layout_from_config(self):
|
|
enabled_widgets = self._get_enabled_widgets()
|
|
|
|
items = []
|
|
if enabled_widgets["workspace"]:
|
|
items.append(self.workspaces)
|
|
if enabled_widgets["taskbar"]:
|
|
items.append(self.taskbar)
|
|
if enabled_widgets["systray"]:
|
|
items.append(self.tray)
|
|
if enabled_widgets["battery"]:
|
|
items.append(self.battery)
|
|
if enabled_widgets["datetime"]:
|
|
items.append(self.datetime)
|
|
|
|
filtered_items = [w for w in items if w.get_visible()]
|
|
|
|
self.items.children = filtered_items
|
|
|
|
if data.DATA()["panel_enabled"]:
|
|
self.show_all()
|
|
|
|
def _update_panel_visibility(self):
|
|
panel_enabled = data.DATA()["panel_enabled"]
|
|
self.set_visible(panel_enabled)
|
|
|
|
def _on_mouse_enter(self, widget, event):
|
|
self.is_mouse_over_panel_area = True
|
|
if self.hide_id:
|
|
GLib.source_remove(self.hide_id)
|
|
self.hide_id = None
|
|
if data.DATA()["panel_autohide"]:
|
|
self.panel_revealer.set_reveal_child(True)
|
|
return True
|
|
|
|
def _on_mouse_leave(self, widget, event):
|
|
if event.detail == Gdk.NotifyType.INFERIOR:
|
|
return False
|
|
self.is_mouse_over_panel_area = False
|
|
if data.DATA()["panel_autohide"]:
|
|
self.delay_hide()
|
|
return True
|
|
|
|
def delay_hide(self):
|
|
if self.hide_id:
|
|
GLib.source_remove(self.hide_id)
|
|
self.hide_id = GLib.timeout_add(250, self.hide_panel_if_not_hovered)
|
|
|
|
def hide_panel_if_not_hovered(self):
|
|
self.hide_id = None
|
|
if not self.is_mouse_over_panel_area:
|
|
occlusion_region = (self.panel_position, self.effective_occlusion_size)
|
|
if check_occlusion(occlusion_region):
|
|
self.panel_revealer.set_reveal_child(False)
|
|
return False
|
|
|
|
def _on_window_event(self, _, event: HyprlandEvent):
|
|
GLib.idle_add(self.check_occlusion_state)
|
|
|
|
def check_occlusion_state(self):
|
|
try:
|
|
# Robust hover check as fallback for high refresh rate monitors
|
|
# If GTK events were missed, we verify the mouse position manually
|
|
if not self.is_mouse_over_panel_area:
|
|
# Determine the region we care about for hover
|
|
# If the panel is hidden, it's just the activator area
|
|
# If shown, it's the whole panel area
|
|
is_revealed = self.panel_revealer.get_reveal_child()
|
|
size = (
|
|
48 if is_revealed else 8
|
|
) # Approximate panel height when revealed
|
|
|
|
# Get current width of the panel to avoid full-width hover detection
|
|
width = self.main_container.get_allocated_width()
|
|
if width > 1:
|
|
screen_width, screen_height = get_screen_dimensions()
|
|
x = (screen_width - width) // 2
|
|
y = 0 if self.panel_position == "top" else screen_height - size
|
|
occlusion_region = (x, y, width, size)
|
|
else:
|
|
occlusion_region = (self.panel_position, size)
|
|
|
|
if is_mouse_in_region(occlusion_region):
|
|
self.is_mouse_over_panel_area = True
|
|
if self.hide_id:
|
|
GLib.source_remove(self.hide_id)
|
|
self.hide_id = None
|
|
|
|
if self.is_mouse_over_panel_area:
|
|
if not self.panel_revealer.get_reveal_child():
|
|
self.panel_revealer.set_reveal_child(True)
|
|
return True
|
|
|
|
if not data.DATA()["panel_autohide"]:
|
|
if not self.panel_revealer.get_reveal_child():
|
|
self.panel_revealer.set_reveal_child(True)
|
|
return True
|
|
|
|
# Use actual width for occlusion check too
|
|
width = self.main_container.get_allocated_width()
|
|
if width > 1:
|
|
screen_width, screen_height = get_screen_dimensions()
|
|
x = (screen_width - width) // 2
|
|
y = (
|
|
0
|
|
if self.panel_position == "top"
|
|
else screen_height - self.effective_occlusion_size
|
|
)
|
|
occlusion_region = (
|
|
x,
|
|
y,
|
|
width,
|
|
self.effective_occlusion_size,
|
|
)
|
|
else:
|
|
occlusion_region = (self.panel_position, self.effective_occlusion_size)
|
|
|
|
is_occluded_by_window = check_occlusion(occlusion_region)
|
|
|
|
self.panel_revealer.set_reveal_child(not is_occluded_by_window)
|
|
except Exception as e:
|
|
from fabric.utils import logger
|
|
|
|
logger.warning(f"Error in check_occlusion_state: {e}")
|
|
|
|
return False
|
|
|
|
@config_dependent
|
|
def _on_config_change(self, new_config, old_config):
|
|
config_paths = [
|
|
"panel.enabled",
|
|
"panel.autohide",
|
|
"panel.position",
|
|
"panel.systray.enabled",
|
|
"panel.datetime.enabled",
|
|
"panel.datetime.12hrs",
|
|
"panel.workspace.enabled",
|
|
"panel.workspace.hide_special_ws",
|
|
"panel.taskbar.enabled",
|
|
"panel.taskbar.icon_size",
|
|
"panel.taskbar.hide_empty",
|
|
"panel.taskbar.hide_special",
|
|
"panel.battery.enabled",
|
|
"panel.battery.label",
|
|
"panel.battery.hide_on_full",
|
|
"panel.battery.icon_size",
|
|
]
|
|
|
|
config_changed = any(
|
|
has_config_changed(old_config, new_config, path) for path in config_paths
|
|
)
|
|
|
|
if config_changed:
|
|
# Check if position changed (requires application restart)
|
|
new_position = data.DATA().get("panel_position") or "bottom"
|
|
if new_position != self.panel_position:
|
|
# Position change requires full restart due to Wayland limitations
|
|
print("Panel position changed - restarting application...")
|
|
import os
|
|
import sys
|
|
|
|
# Restart the Python script
|
|
os.execv(sys.executable, ["python"] + sys.argv)
|
|
return
|
|
|
|
# Position didn't change, handle other config changes
|
|
panel_autohide = data.DATA()["panel_autohide"]
|
|
|
|
# Set exclusivity based on autohide setting
|
|
exclusivity = "none" if panel_autohide else "auto"
|
|
self.set_property("exclusivity", exclusivity)
|
|
|
|
self._update_panel_visibility()
|
|
|
|
use_12hr = data.DATA()["datetime_12hrs"]
|
|
formatter = "%a %-d %b %I:%M %P" if use_12hr else "%a %-d %b %H:%M"
|
|
self.datetime.formatters = [formatter]
|
|
|
|
hide_special = data.DATA()["workspace_hide_special"]
|
|
self.workspaces.buttons_factory = lambda ws_id: (
|
|
None
|
|
if (hide_special and is_special_workspace_id(ws_id))
|
|
else WorkspaceButton(id=ws_id, label=str(ws_id))
|
|
)
|
|
|
|
self._rebuild_layout_from_config()
|
|
|
|
if panel_autohide:
|
|
self.check_occlusion_state()
|