412 lines
14 KiB
Python
412 lines
14 KiB
Python
from fabric.system_tray.widgets import SystemTray
|
|
from fabric.utils import Gdk, logger
|
|
from fabric.widgets.box import Box
|
|
from fabric.widgets.button import Button
|
|
from fabric.widgets.centerbox import CenterBox
|
|
from fabric.widgets.datetime import DateTime
|
|
from fabric.widgets.revealer import Revealer
|
|
from fabric.widgets.wayland import WaylandWindow as Window
|
|
from gi.repository import Gtk
|
|
|
|
from services.config import get_config_all, on_config_change
|
|
from services.modus import notification_service
|
|
from utils.gtk_utils import setup_cursor_hover, svg_file
|
|
from utils.roam import modus_service
|
|
from window.controlcenter.main import ModusControlCenter
|
|
from window.notification.notification_center import NotificationCenter
|
|
from window.panel.components.custom_mods import CustomMods
|
|
from window.panel.components.enhanced_system_tray import apply_enhanced_system_tray
|
|
from window.panel.components.globalmenu import GlobalMenu
|
|
from window.panel.components.indicators import (
|
|
BatteryIndicator,
|
|
BluetoothIndicator,
|
|
NetworkIndicator,
|
|
)
|
|
from window.panel.components.workspace import WorkspaceIndicator
|
|
from window.panel.notch import Notch
|
|
|
|
apply_enhanced_system_tray()
|
|
|
|
|
|
class Panel(Window):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(
|
|
title="mdous-bar",
|
|
name="bar",
|
|
layer="top",
|
|
anchor="left top right",
|
|
exclusivity="auto",
|
|
visible=False,
|
|
all_visible=False,
|
|
)
|
|
self.globalmenu = GlobalMenu(parent_window=self)
|
|
|
|
self.imac = Gtk.MenuButton(name="panel-button")
|
|
self.imac.get_style_context().add_class("flat")
|
|
self.imac.add(svg_file("misc/logo.svg", size=18))
|
|
setup_cursor_hover(self.imac, "pointer")
|
|
self.globalmenu.set_imac_button(self.imac)
|
|
|
|
self.tray = SystemTray(name="panel-button", spacing=4, icon_size=20)
|
|
|
|
self.tray_revealer = Revealer(
|
|
name="tray-revealer",
|
|
child=self.tray,
|
|
child_revealed=False,
|
|
transition_type="slide-left",
|
|
transition_duration=300,
|
|
)
|
|
|
|
self.chevron_button = Button(
|
|
name="panel-button",
|
|
child=svg_file("misc/chevron-right.svg", size=16),
|
|
on_clicked=self.toggle_tray,
|
|
)
|
|
setup_cursor_hover(self.chevron_button, "pointer")
|
|
|
|
# Hide tray elements if empty
|
|
self.tray.connect("add", self._update_tray_visibility)
|
|
self.tray.connect("remove", self._update_tray_visibility)
|
|
|
|
self._spotlight_toggle_cb = None
|
|
self.search = Button(
|
|
name="panel-button",
|
|
child=svg_file("misc/search.svg", size=22),
|
|
on_clicked=lambda *_: (
|
|
self._spotlight_toggle_cb() if self._spotlight_toggle_cb else None
|
|
),
|
|
)
|
|
setup_cursor_hover(self.search, "pointer")
|
|
|
|
self.control_center_btn = Button(
|
|
name="panel-button",
|
|
child=svg_file("misc/control-center.svg", size=22),
|
|
)
|
|
setup_cursor_hover(self.control_center_btn, "pointer")
|
|
|
|
self._control_center_instance = None
|
|
self.control_center_btn.connect(
|
|
"clicked",
|
|
lambda *_: self.control_center.toggle(),
|
|
)
|
|
|
|
self.notification_icon = svg_file(
|
|
"notifications/notification-inactive.svg", size=22
|
|
)
|
|
|
|
self.notification_center_btn = Button(
|
|
name="panel-button",
|
|
child=self.notification_icon,
|
|
on_clicked=self.on_notification_icon_clicked,
|
|
)
|
|
setup_cursor_hover(self.notification_center_btn, "pointer")
|
|
|
|
self._notification_center_instance = None
|
|
|
|
self.datetime_btn = Button(
|
|
name="panel-button",
|
|
child=DateTime(name="date-time", formatters=["%a %-d %b %I:%M %P"]),
|
|
)
|
|
self.datetime_btn.add_events(
|
|
Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK
|
|
)
|
|
setup_cursor_hover(self.datetime_btn, "pointer")
|
|
|
|
self._pomodoro_instance = None
|
|
self._todo_instance = None
|
|
self.datetime_btn.connect("clicked", self.on_datetime_clicked)
|
|
self.datetime_btn.connect("button-press-event", self.on_datetime_right_click)
|
|
|
|
self.custom_mods = CustomMods(parent_window=self)
|
|
self.workspace_indicator = WorkspaceIndicator()
|
|
|
|
# Create persistent indicators
|
|
self.battery_indicator = BatteryIndicator()
|
|
self.network_indicator = NetworkIndicator()
|
|
self.bluetooth_indicator = BluetoothIndicator()
|
|
|
|
self.indicators = Box(
|
|
name="indicators",
|
|
orientation="h",
|
|
spacing=4,
|
|
children=[
|
|
self.battery_indicator,
|
|
self.network_indicator,
|
|
self.bluetooth_indicator,
|
|
],
|
|
)
|
|
|
|
# Create boxes and mount
|
|
self.left_box = Box(name="window-left")
|
|
self.notch = Notch()
|
|
self.center_box = Box(name="window-center")
|
|
self.right_box = Box(name="window-right", spacing=4, orientation="h")
|
|
|
|
self.children = CenterBox(
|
|
name="panel",
|
|
start_children=self.left_box,
|
|
center_children=self.center_box,
|
|
end_children=self.right_box,
|
|
)
|
|
|
|
# Connect to DND state changes for notification icon
|
|
modus_service.connect("dont-disturb-changed", self.on_dnd_changed)
|
|
|
|
# Hide/show panel on fullscreen toggle
|
|
modus_service.connect("fullscreen-changed", self._on_fullscreen_changed)
|
|
|
|
# Connect to notification service for icon state updates
|
|
notification_service.connect(
|
|
"notify::count", self.on_notification_count_changed
|
|
)
|
|
|
|
# Set initial notification icon state
|
|
self.update_notification_icon()
|
|
|
|
# Initial layout build
|
|
self._rebuild_layout_from_config(get_config_all())
|
|
|
|
# Live updates
|
|
on_config_change(self._on_config_changed)
|
|
|
|
self._update_tray_visibility()
|
|
self.show()
|
|
|
|
def set_spotlight_toggle(self, callback):
|
|
self._spotlight_toggle_cb = callback
|
|
|
|
def on_dnd_changed(self, _, dnd_state):
|
|
self.update_notification_icon() # Update notification icon when DND changes
|
|
|
|
def _on_fullscreen_changed(self, _, is_fullscreen):
|
|
self.set_visible(not is_fullscreen)
|
|
|
|
def on_notification_count_changed(self, service, *args):
|
|
self.update_notification_icon()
|
|
|
|
def on_notification_icon_clicked(self, *_):
|
|
self.notification_center.toggle()
|
|
|
|
def on_datetime_clicked(self, *_):
|
|
self.pomodoro.toggle()
|
|
|
|
def on_datetime_right_click(self, widget, event):
|
|
logger.info(f"[Panel] Right click detected: button={event.button}")
|
|
if event.button == 3: # Right click
|
|
self.todo_widget.toggle()
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def pomodoro(self):
|
|
if self._pomodoro_instance is None:
|
|
from window.panel.components.pomodoro import Pomodoro
|
|
|
|
self._pomodoro_instance = Pomodoro(
|
|
parent=self, pointing_to=self.datetime_btn
|
|
)
|
|
return self._pomodoro_instance
|
|
|
|
@property
|
|
def todo_widget(self):
|
|
if self._todo_instance is None:
|
|
from window.panel.components.todo import TodoListWidget
|
|
|
|
self._todo_instance = TodoListWidget(
|
|
parent=self, pointing_to=self.datetime_btn
|
|
)
|
|
return self._todo_instance
|
|
|
|
@property
|
|
def control_center(self):
|
|
if self._control_center_instance is None:
|
|
self._control_center_instance = ModusControlCenter(
|
|
parent=self, pointing_to=self.control_center_btn
|
|
)
|
|
return self._control_center_instance
|
|
|
|
@property
|
|
def notification_center(self):
|
|
if self._notification_center_instance is None:
|
|
self._notification_center_instance = NotificationCenter(
|
|
parent=self, pointing_to=self.notification_center_btn
|
|
)
|
|
return self._notification_center_instance
|
|
|
|
def update_notification_icon(self):
|
|
count = notification_service.count
|
|
dnd_enabled = modus_service.dont_disturb
|
|
|
|
if dnd_enabled:
|
|
# DND is enabled - show disabled icon
|
|
icon_file = "notification-disabled.svg"
|
|
elif count > 0:
|
|
# Has notifications - show active icon
|
|
icon_file = "notification-active.svg"
|
|
else:
|
|
# No notifications - show inactive icon
|
|
icon_file = "notification-inactive.svg"
|
|
|
|
self.notification_icon.dynamic_file(f"notifications/{icon_file}")
|
|
|
|
def _rebuild_layout_from_config(self, config_data=None):
|
|
if config_data is None:
|
|
config_data = get_config_all()
|
|
|
|
# Update indicator visibility directly
|
|
battery_visible = config_data.get("battery", True)
|
|
network_visible = config_data.get("network", True)
|
|
bluetooth_visible = config_data.get("bluetooth", True)
|
|
|
|
self.battery_indicator.set_visible(battery_visible)
|
|
self.network_indicator.set_visible(network_visible)
|
|
self.bluetooth_indicator.set_visible(bluetooth_visible)
|
|
|
|
# Ensure they update their internal state
|
|
if battery_visible:
|
|
self.battery_indicator.update_state()
|
|
if network_visible:
|
|
self.network_indicator.update_state()
|
|
if bluetooth_visible:
|
|
self.bluetooth_indicator.update_state()
|
|
|
|
# Center (notch)
|
|
if config_data.get("notch", True):
|
|
self.center_box.children = [self.notch]
|
|
else:
|
|
self.center_box.children = []
|
|
|
|
# Left
|
|
left_children = []
|
|
if config_data.get("imac_button", True):
|
|
left_children.append(self.imac)
|
|
if config_data.get("global_menu", True):
|
|
left_children.append(self.globalmenu)
|
|
|
|
for child in left_children:
|
|
child.show()
|
|
self.left_box.children = left_children
|
|
|
|
# Right
|
|
right_children = []
|
|
if config_data.get("custom_mods", True):
|
|
right_children.append(self.custom_mods)
|
|
if config_data.get("workspace_indicator", True):
|
|
right_children.append(self.workspace_indicator)
|
|
|
|
if config_data.get("systray", True):
|
|
right_children.extend([self.tray_revealer, self.chevron_button])
|
|
|
|
right_children.append(self.indicators)
|
|
|
|
if config_data.get("search", True):
|
|
right_children.append(self.search)
|
|
if config_data.get("control_center", True):
|
|
right_children.append(self.control_center_btn)
|
|
if config_data.get("date_time", True):
|
|
right_children.append(self.datetime_btn)
|
|
if config_data.get("notification_center", True):
|
|
right_children.append(self.notification_center_btn)
|
|
|
|
for child in right_children:
|
|
child.show()
|
|
self.right_box.children = right_children
|
|
|
|
self.indicators.show()
|
|
self._update_tray_visibility()
|
|
self.show()
|
|
|
|
# Force a layout recalculation
|
|
self.queue_resize()
|
|
|
|
def _on_config_changed(self, new_config, old_config):
|
|
logger.debug("Config changed, checking keys...")
|
|
keys = {
|
|
"imac_button",
|
|
"global_menu",
|
|
"notch",
|
|
"custom_mods",
|
|
"workspace_indicator",
|
|
"systray",
|
|
"battery",
|
|
"network",
|
|
"bluetooth",
|
|
"search",
|
|
"control_center",
|
|
"date_time",
|
|
"notification_center",
|
|
}
|
|
changed_keys = [k for k in keys if new_config.get(k) != old_config.get(k)]
|
|
if changed_keys:
|
|
logger.debug(f"Rebuilding due to changes in: {changed_keys}")
|
|
self._rebuild_layout_from_config(new_config)
|
|
|
|
self._update_tray_visibility()
|
|
|
|
def _update_tray_visibility(self, *_):
|
|
# We check if there are any visible children in the tray
|
|
visible_children = [
|
|
child for child in self.tray.get_children() if child.get_visible()
|
|
]
|
|
has_items = len(visible_children) > 0
|
|
|
|
self.tray_revealer.set_visible(has_items)
|
|
self.chevron_button.set_visible(has_items)
|
|
|
|
if not has_items:
|
|
# Reset state if it becomes hidden
|
|
self.tray_revealer.child_revealed = False
|
|
self.chevron_button.get_child().dynamic_file("misc/chevron-right.svg")
|
|
# Add extra spacing between workspace indicator and indicators when tray is hidden
|
|
self.indicators.set_margin_left(10)
|
|
else:
|
|
self.indicators.set_margin_left(0)
|
|
|
|
def toggle_tray(self, *_):
|
|
current_state = self.tray_revealer.child_revealed
|
|
self.tray_revealer.child_revealed = not current_state
|
|
|
|
if self.tray_revealer.child_revealed:
|
|
self.chevron_button.get_child().dynamic_file("misc/chevron-left.svg")
|
|
else:
|
|
self.chevron_button.get_child().dynamic_file("misc/chevron-right.svg")
|
|
|
|
def destroy(self):
|
|
"""Clean up all signals and components"""
|
|
try:
|
|
modus_service.disconnect_by_func(self.on_dnd_changed)
|
|
notification_service.disconnect_by_func(self.on_notification_count_changed)
|
|
from services.config import off_config_change
|
|
|
|
off_config_change(self._on_config_changed)
|
|
except Exception as e:
|
|
logger.error(f"An error occurred: {e}")
|
|
|
|
# Destroy components
|
|
for component in [
|
|
self.globalmenu,
|
|
self.notch,
|
|
self.custom_mods,
|
|
self.workspace_indicator,
|
|
self.indicators,
|
|
]:
|
|
try:
|
|
component.destroy()
|
|
except Exception as e:
|
|
logger.error(f"An error occurred: {e}")
|
|
|
|
# Destroy MouseCapture windows
|
|
for mc in [
|
|
self.control_center,
|
|
self.notification_center,
|
|
self._pomodoro_instance,
|
|
self._todo_instance,
|
|
]:
|
|
try:
|
|
if mc:
|
|
mc.destroy()
|
|
except Exception as e:
|
|
logger.error(f"An error occurred: {e}")
|
|
|
|
super().destroy()
|