Files
Modus/mine/modules/panel/main.py
T
2026-07-12 18:56:10 +05:45

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()