Files
Modus/src/window/panel/main.py
T
sankalp 3f66c5e55c style: add flat class to imac MenuButton
Ensures consistent styling with other panel buttons.
2026-07-28 10:00:31 +05:45

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