refactor(src/window): migrate control center windows to AppletWindow and simplify UI

Replace Window base with AppletWindow for consistency, switch to FlatScale, and streamline mouse capture handling.
This commit is contained in:
sankalp committed 2026-06-17 01:45:43 +05:45
1 parent b8d3e74afd
commit f78534dce0
11 files changed
+168 -199

No files matched your search

+7 -9
View File
@@ -1,7 +1,7 @@
import subprocess import subprocess
from fabric.bluetooth import BluetoothClient, BluetoothDevice from fabric.bluetooth import BluetoothClient, BluetoothDevice
from fabric.utils import Gdk, GLib, Gtk, exec_shell_command, logger from fabric.utils import Gdk, GLib, exec_shell_command, logger
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox from fabric.widgets.centerbox import CenterBox
@@ -12,6 +12,7 @@ from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.separator import Separator from fabric.widgets.separator import Separator
from utils.utils import svg_file from utils.utils import svg_file
from shared.widgets.smooth_switch import SmoothSwitch
def get_battery_icon_file( def get_battery_icon_file(
@@ -246,14 +247,11 @@ class BluetoothConnections(Box):
children=title_children, children=title_children,
) )
self.toggle_button = Gtk.Switch(visible=True, name="toggle-button") self.toggle_button = SmoothSwitch(
name="toggle-button",
# Safely set initial state active=self.client.enabled,
self.toggle_button.set_active(self.client.enabled) on_user_toggle=lambda active: set_bluetooth_enabled_with_fallback(
self.toggle_button.connect( self.client, active
"notify::active",
lambda *_: set_bluetooth_enabled_with_fallback(
self.client, self.toggle_button.get_active()
), ),
) )
@@ -1437,9 +1437,6 @@ class ExpandedPlayer(Window):
super().destroy() super().destroy()
def _init_mousecapture(self, mousecapture):
self._mousecapture_parent = mousecapture
def hide_controlcenter(self, *_): def hide_controlcenter(self, *_):
# self._mousecapture_parent.toggle_mousecapture() # self._mousecapture_parent.toggle_mousecapture()
self.set_visible(False) self.set_visible(False)
+17 -19
View File
@@ -5,8 +5,7 @@ from fabric.widgets.box import Box
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label from fabric.widgets.label import Label
from fabric.widgets.scale import Scale from shared.widgets.flat_scale import FlatScale
from fabric.widgets.wayland import WaylandWindow as Window
from services.brightness import Brightness from services.brightness import Brightness
from services.network import NetworkClient from services.network import NetworkClient
@@ -22,18 +21,21 @@ from window.controlcenter.per_app_volume import PerAppVolumeControl
from window.controlcenter.player import PlayerBoxStack, get_shared_mpris_manager from window.controlcenter.player import PlayerBoxStack, get_shared_mpris_manager
from window.controlcenter.wifi import WifiConnections from window.controlcenter.wifi import WifiConnections
brightness_service = Brightness.get_initial() from shared.window.applet_window import AppletWindow
brightness_service = Brightness()
class ModusControlCenter(Window): class ModusControlCenter(AppletWindow):
def __init__(self, **kwargs): def __init__(self, parent=None, pointing_to=None, **kwargs):
super().__init__( super().__init__(
parent=parent,
pointing_to=pointing_to,
layer="top", layer="top",
title="modus", title="modus",
anchor="top right", anchor="top right",
margin="2px 10px 0px 0px", margin="2px 10px 0px 0px",
exclusivity="auto", exclusivity="auto",
keyboard_mode="on-demand",
name="control-center-menu", name="control-center-menu",
visible=False, visible=False,
**kwargs, **kwargs,
@@ -98,11 +100,11 @@ class ModusControlCenter(Window):
ellipsization="end", ellipsization="end",
h_align="start", h_align="start",
) )
self.volume_scale = Scale( self.volume_scale = FlatScale(
value=volume, value=volume,
min_value=0, min_value=0,
max_value=100, max_value=100,
increments=(5, 5), step=5,
name="volume-widget-slider", name="volume-widget-slider",
size=30, size=30,
h_expand=True, h_expand=True,
@@ -115,11 +117,11 @@ class ModusControlCenter(Window):
else 50 else 50
) )
self.brightness_scale = Scale( self.brightness_scale = FlatScale(
value=brightness_percentage, value=brightness_percentage,
min_value=0, min_value=0,
max_value=100, max_value=100,
increments=(5, 5), step=5,
name="brightness-widget-slider", name="brightness-widget-slider",
size=30, size=30,
h_expand=True, h_expand=True,
@@ -555,7 +557,7 @@ class ModusControlCenter(Window):
# Connect brightness controls if brightness service is available # Connect brightness controls if brightness service is available
if brightness_service.max_screen > 0: if brightness_service.max_screen > 0:
self.brightness_scale.connect("change-value", self.set_brightness) self.brightness_scale.connect("value-changed", self.set_brightness)
self.brightness_scale.connect("scroll-event", self.on_brightness_scroll) self.brightness_scale.connect("scroll-event", self.on_brightness_scroll)
self._signal_connections.append( self._signal_connections.append(
( (
@@ -565,7 +567,7 @@ class ModusControlCenter(Window):
) )
# Connect volume scale signals # Connect volume scale signals
self.volume_scale.connect("change-value", self.set_volume) self.volume_scale.connect("value-changed", self.set_volume)
self.volume_scale.connect("scroll-event", self.on_volume_scroll) self.volume_scale.connect("scroll-event", self.on_volume_scroll)
# Mark signals as connected # Mark signals as connected
@@ -762,14 +764,14 @@ class ModusControlCenter(Window):
except Exception as e: except Exception as e:
logger.warning(f"Failed to toggle caffeine: {e}") logger.warning(f"Failed to toggle caffeine: {e}")
def set_volume(self, _, __, volume): def set_volume(self, _, volume):
if not self._signals_connected: if not self._signals_connected:
return return
self._updating_volume = True self._updating_volume = True
audio_service.speaker.volume = round(volume) audio_service.speaker.volume = round(volume)
self._updating_volume = False self._updating_volume = False
def set_brightness(self, _, __, brightness): def set_brightness(self, _, brightness):
if not self._signals_connected: if not self._signals_connected:
return return
self._updating_brightness = True self._updating_brightness = True
@@ -1023,9 +1025,6 @@ class ModusControlCenter(Window):
) )
self.focus_status_label.set_label("On" if self.focus_mode else "Off") self.focus_status_label.set_label("On" if self.focus_mode else "Off")
def _init_mousecapture(self, mousecapture):
self._mousecapture_parent = mousecapture
def _disconnect_all_signals(self): def _disconnect_all_signals(self):
"""Disconnect all signal connections to prevent memory leaks""" """Disconnect all signal connections to prevent memory leaks"""
try: try:
@@ -1206,8 +1205,7 @@ class ModusControlCenter(Window):
self._disconnect_signals_when_hidden() self._disconnect_signals_when_hidden()
# Hide the control center # Hide the control center
if hasattr(self, "_mousecapture_parent"): self.set_visible(False)
self._mousecapture_parent.toggle_mousecapture()
self.set_visible(False) self.set_visible(False)
except Exception as e: except Exception as e:
+5 -5
View File
@@ -3,7 +3,7 @@ from fabric.widgets.box import Box
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.image import Image from fabric.widgets.image import Image
from fabric.widgets.label import Label from fabric.widgets.label import Label
from fabric.widgets.scale import Scale from shared.widgets.flat_scale import FlatScale
from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.separator import Separator from fabric.widgets.separator import Separator
@@ -249,11 +249,11 @@ class PerAppVolumeControl(Box):
) )
# Volume scale (smaller and horizontal) # Volume scale (smaller and horizontal)
volume_scale = Scale( volume_scale = FlatScale(
value=volume_percent, value=volume_percent,
min_value=0, min_value=0,
max_value=max_vol, max_value=max_vol,
increments=(1, 5), step=5,
name="compact-volume-slider", name="compact-volume-slider",
size=20, size=20,
h_expand=True, h_expand=True,
@@ -261,8 +261,8 @@ class PerAppVolumeControl(Box):
# Connect volume change handler # Connect volume change handler
volume_scale.connect( volume_scale.connect(
"change-value", "value-changed",
lambda scale, scroll_type, value, app=app: self._set_app_volume(app, value), lambda scale, value, app=app: self._set_app_volume(app, value),
) )
# Create horizontal compact layout # Create horizontal compact layout
+8 -3
View File
@@ -1,6 +1,6 @@
import subprocess import subprocess
from fabric.utils import Gdk, GLib, Gtk from fabric.utils import Gdk, GLib
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox from fabric.widgets.centerbox import CenterBox
@@ -13,6 +13,7 @@ from fabric.widgets.separator import Separator
from services.network import NetworkClient from services.network import NetworkClient
from utils.functions import get_wifi_connecting_icon, get_wifi_icon_for_strength from utils.functions import get_wifi_connecting_icon, get_wifi_icon_for_strength
from utils.utils import svg_file from utils.utils import svg_file
from shared.widgets.smooth_switch import SmoothSwitch
from shared.dialogs.wifi_password_dialog import WiFiPasswordDialog from shared.dialogs.wifi_password_dialog import WiFiPasswordDialog
from gi.repository import NM from gi.repository import NM
@@ -299,7 +300,12 @@ class WifiConnections(Box):
children=title_children, children=title_children,
) )
self.toggle_button = Gtk.Switch(visible=True, name="toggle-button") self.toggle_button = SmoothSwitch(
name="toggle-button",
on_user_toggle=lambda active: self.on_toggle_changed(
self.toggle_button, active
),
)
# Create Known Network section # Create Known Network section
self.known_networks_label = Label( self.known_networks_label = Label(
@@ -393,7 +399,6 @@ class WifiConnections(Box):
if self.wifi_service: if self.wifi_service:
# Set up WiFi toggle # Set up WiFi toggle
self.toggle_button.set_active(self.wifi_service.wireless_enabled) self.toggle_button.set_active(self.wifi_service.wireless_enabled)
self.toggle_button.connect("notify::active", self.on_toggle_changed)
# Connect to WiFi service signals — track IDs for cleanup # Connect to WiFi service signals — track IDs for cleanup
self._signal_ids.append( self._signal_ids.append(
+11 -14
View File
@@ -9,11 +9,11 @@ from fabric.widgets.image import Image
from fabric.widgets.label import Label from fabric.widgets.label import Label
from fabric.widgets.revealer import Revealer from fabric.widgets.revealer import Revealer
from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.wayland import WaylandWindow as Window
import shared.data as data import shared.data as data
from services.modus import notification_service from services.modus import notification_service
from shared.widgets.clipping_box import ClippingBox from shared.widgets.clipping_box import ClippingBox
from shared.window.applet_window import AppletWindow
from shared.widgets.custom_image import CustomImage from shared.widgets.custom_image import CustomImage
from utils.functions import escape_markup_text from utils.functions import escape_markup_text
from window.notification.notification import ( from window.notification.notification import (
@@ -591,13 +591,14 @@ class NotificationCenterWidget(NotificationWidget):
return False return False
class NotificationCenter(Window): class NotificationCenter(AppletWindow):
def __init__(self): def __init__(self, parent=None, pointing_to=None):
super().__init__( super().__init__(
parent=parent,
pointing_to=pointing_to,
layer="overlay", layer="overlay",
anchor="top right", anchor="top right",
visible=False, visible=False,
keyboard_mode="on-demand",
title="modus", title="modus",
) )
@@ -840,8 +841,8 @@ class NotificationCenter(Window):
self.scrolled.set_visible(current_count > 0) self.scrolled.set_visible(current_count > 0)
# Auto-close notification center when no notifications remain # Auto-close notification center when no notifications remain
if current_count == 0 and hasattr(self, "mousecapture"): if current_count == 0:
self.mousecapture.hide_child_window() self.hide()
def clear_all_notifications(self, *_): def clear_all_notifications(self, *_):
# Clear all groups # Clear all groups
@@ -851,18 +852,14 @@ class NotificationCenter(Window):
# Clear all remaining cached notification images AND icons when clear all is clicked # Clear all remaining cached notification images AND icons when clear all is clicked
cleanup_all_notification_caches() # Clear ALL caches (icons + images) cleanup_all_notification_caches() # Clear ALL caches (icons + images)
notification_service.clear_all_cached_notifications() notification_service.clear_all_cached_notifications()
if hasattr(self, "mousecapture"): self.hide()
self.mousecapture.hide_child_window()
def _on_escape_pressed(self, *_): def _on_escape_pressed(self, *_):
if hasattr(self, "mousecapture"): self.hide()
self.mousecapture.hide_child_window()
def _init_mousecapture(self, mousecapture): def set_visible(self, visible: bool):
self.mousecapture = mousecapture
def _set_mousecapture(self, visible):
"""Control notification center visibility with slide-left animation""" """Control notification center visibility with slide-left animation"""
super().set_visible(visible)
if visible: if visible:
self.main_revealer.set_reveal_child(True) self.main_revealer.set_reveal_child(True)
else: else:
+28 -41
View File
@@ -7,7 +7,6 @@ from fabric.widgets.label import Label
from shared.dialogs.about import AboutApp, get_about_window from shared.dialogs.about import AboutApp, get_about_window
from shared.window.dropdown import ModusDropdown, dropdown_divider, dropdowns from shared.window.dropdown import ModusDropdown, dropdown_divider, dropdowns
from shared.window.mousecapture import DropDownMouseCapture
from utils.app_name_resolver import format_window from utils.app_name_resolver import format_window
from utils.roam import modus_service from utils.roam import modus_service
from utils.utils import setup_cursor_hover from utils.utils import setup_cursor_hover
@@ -41,8 +40,7 @@ def create_dropdown_with_capture(dropdown_id, parent, dropdown_children, layer="
parent=parent, parent=parent,
dropdown_children=dropdown_children, dropdown_children=dropdown_children,
) )
mouse_capture = DropDownMouseCapture(layer=layer, child_window=dropdown) return dropdown
return mouse_capture
def manage_button_style_classes(buttons, active_button=None, style_class="active"): def manage_button_style_classes(buttons, active_button=None, style_class="active"):
@@ -72,7 +70,7 @@ def on_click_subthread(button, on_clicked, on_click):
for dropdown in dropdowns: for dropdown in dropdowns:
if dropdown.is_visible(): if dropdown.is_visible():
dropdown.hide_via_mousecapture() dropdown.hide()
break break
@@ -137,15 +135,13 @@ class GlobalMenuDropdowns:
self.parent = parent self.parent = parent
self.system_dropdown = SystemDropdown(parent=parent) self.system_dropdown = SystemDropdown(parent=parent)
self.menu_button_dropdown = DropDownMouseCapture( self.menu_button_dropdown = self.system_dropdown
layer="top", child_window=self.system_dropdown
)
self.menu_button = Button( self.menu_button = Button(
label="Modus", label="Modus",
name="global-menu", name="global-menu",
on_clicked=lambda _: self.menu_button_dropdown.toggle_mousecapture(), on_clicked=lambda _: self.menu_button_dropdown.toggle(),
) )
self.menu_button_dropdown.child_window.set_pointing_to(self.menu_button) self.menu_button_dropdown.set_pointing_to(self.menu_button)
self.global_title_menu_about = dropdown_option( self.global_title_menu_about = dropdown_option(
f"About {modus_service.current_active_app_name}", f"About {modus_service.current_active_app_name}",
@@ -162,8 +158,8 @@ class GlobalMenuDropdowns:
parent, parent,
[ [
dropdown_option( dropdown_option(
"Enter Full Screen", "Fullscreen",
on_click="hyprctl dispatch 'hl.dsp.fullscreen()'", on_click="hyprctl dispatch 'hl.dsp.window.fullscreen()'",
), ),
], ],
) )
@@ -183,35 +179,35 @@ class GlobalMenuDropdowns:
), ),
dropdown_divider("---------------------"), dropdown_divider("---------------------"),
dropdown_option( dropdown_option(
"Move Window to Left", "Left",
on_click="hyprctl dispatch 'hl.dsp.movewindow([[l]])'", on_click="hyprctl dispatch 'hl.dsp.window.move([[l]])'",
), ),
dropdown_option( dropdown_option(
"Move Window to Right", "Right",
on_click="hyprctl dispatch 'hl.dsp.movewindow([[r]])'", on_click="hyprctl dispatch 'hl.dsp.window.move([[r]])'",
), ),
dropdown_option( dropdown_option(
"Cycle Through Windows", "Cycle Next",
on_click="hyprctl dispatch 'hl.dsp.cyclenext()'", on_click="hyprctl dispatch 'hl.dsp.window.cycle_next()'",
), ),
dropdown_divider("---------------------"), dropdown_divider("---------------------"),
dropdown_option( dropdown_option(
"Float", on_click="hyprctl dispatch 'hl.dsp.togglefloating()'" "Float", on_click="hyprctl dispatch 'hl.dsp.window.float()'"
), ),
dropdown_option( dropdown_option(
"Quit", on_click="hyprctl dispatch 'hl.dsp.killactive()'" "Quit", on_click="hyprctl dispatch 'hl.dsp.window.close()'"
), ),
dropdown_option( dropdown_option(
"Pseudo", on_click="hyprctl dispatch 'hl.dsp.pseudo()'" "Pseudo", on_click="hyprctl dispatch 'hl.dsp.window.pseudo()'"
), ),
dropdown_option( dropdown_option(
"Toggle Split", on_click="hyprctl dispatch 'hl.dsp.togglesplit()'" "Toggle Split", on_click="hyprctl dispatch 'hl.dsp.togglesplit()'"
), ),
dropdown_option( dropdown_option(
"Center", on_click="hyprctl dispatch 'hl.dsp.centerwindow()'" "Center", on_click="hyprctl dispatch 'hl.dsp.window.center()'"
), ),
dropdown_option( dropdown_option(
"Group", on_click="hyprctl dispatch 'hl.dsp.togglegroup()'" "Group", on_click="hyprctl dispatch 'hl.dsp.group.toggle()'"
), ),
dropdown_option( dropdown_option(
"Pin", "Pin",
@@ -252,31 +248,24 @@ class GlobalMenuDropdowns:
on_clicked=self._on_title_button_clicked, on_clicked=self._on_title_button_clicked,
) )
self.global_menu_title.child_window.set_pointing_to( self.global_menu_title.set_pointing_to(self.global_menu_button_title)
self.global_menu_button_title
)
# File, Edit and Go buttons are placeholders - no dropdowns implemented yet # File, Edit and Go buttons are placeholders - no dropdowns implemented yet
self.global_menu_button_file = create_menu_button("File") self.global_menu_button_file = create_menu_button("File")
self.global_menu_button_edit = create_menu_button("Edit") self.global_menu_button_edit = create_menu_button("Edit")
self.global_menu_button_go = create_menu_button("Go") self.global_menu_button_go = create_menu_button("Go")
self.global_menu_button_view = create_menu_button( self.global_menu_button_view = create_menu_button(
"View", "View", lambda _: self.global_menu_view.toggle()
lambda _: self.global_menu_view.toggle_mousecapture(),
) )
self.global_menu_view.child_window.set_pointing_to(self.global_menu_button_view) self.global_menu_view.set_pointing_to(self.global_menu_button_view)
self.global_menu_button_window = create_menu_button( self.global_menu_button_window = create_menu_button(
"Window", "Window", lambda _: self.global_menu_window.toggle()
lambda _: self.global_menu_window.toggle_mousecapture(),
)
self.global_menu_window.child_window.set_pointing_to(
self.global_menu_button_window
) )
self.global_menu_window.set_pointing_to(self.global_menu_button_window)
self.global_menu_button_help = create_menu_button( self.global_menu_button_help = create_menu_button(
"Help", "Help", lambda _: self.global_menu_help.toggle()
lambda _: self.global_menu_help.toggle_mousecapture(),
) )
self.global_menu_help.child_window.set_pointing_to(self.global_menu_button_help) self.global_menu_help.set_pointing_to(self.global_menu_button_help)
self.all_menu_buttons = [ self.all_menu_buttons = [
self.menu_button, self.menu_button,
@@ -305,7 +294,7 @@ class GlobalMenuDropdowns:
def _on_title_button_clicked(self, _): def _on_title_button_clicked(self, _):
if has_active_window(): if has_active_window():
self.global_menu_title.toggle_mousecapture() self.global_menu_title.set_visible(not self.global_menu_title.get_visible())
def _on_active_app_changed(self, _, value): def _on_active_app_changed(self, _, value):
self.global_title_menu_about.set_property("label", f"About {value}") self.global_title_menu_about.set_property("label", f"About {value}")
@@ -366,11 +355,9 @@ class GlobalMenu(Box):
] ]
def show_system_dropdown(self, imac_button): def show_system_dropdown(self, imac_button):
self.dropdown_system.menu_button_dropdown.child_window.set_pointing_to( self.dropdown_system.menu_button_dropdown.set_pointing_to(imac_button)
imac_button
)
mouse_capture = self.dropdown_system.menu_button_dropdown mouse_capture = self.dropdown_system.menu_button_dropdown
mouse_capture.set_child_window_visible(not mouse_capture.is_visible()) mouse_capture.toggle()
def destroy(self): def destroy(self):
"""Clean up the global menu and its dropdowns""" """Clean up the global menu and its dropdowns"""
+56 -56
View File
@@ -2,12 +2,11 @@ from fabric.bluetooth import BluetoothClient
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.button import Button from fabric.widgets.button import Button
from fabric.widgets.label import Label from fabric.widgets.label import Label
from fabric.widgets.wayland import WaylandWindow as Window
from services.battery import BatteryService, DeviceState from services.battery import Battery
from services.network import NetworkClient from services.network import NetworkClient
from shared.window.battery_widget import BatteryControl from shared.window.battery_widget import BatteryControl
from shared.window.mousecapture import DropDownMouseCapture from shared.window.applet_window import AppletWindow
from utils.functions import format_duration, get_wifi_icon_for_strength from utils.functions import format_duration, get_wifi_icon_for_strength
from utils.roam import modus_service from utils.roam import modus_service
from utils.utils import setup_cursor_hover, svg_file from utils.utils import setup_cursor_hover, svg_file
@@ -15,45 +14,41 @@ from window.controlcenter.bluetooth import BluetoothConnections
from window.controlcenter.wifi import WifiConnections from window.controlcenter.wifi import WifiConnections
def create_control_window(name_prefix): def create_control_window(name_prefix, parent=None, pointing_to=None):
return Window( return AppletWindow(
parent=parent,
pointing_to=pointing_to,
layer="overlay", layer="overlay",
title="modus", title="modus",
anchor="top right", anchor="top right",
margin="2px 10px 0px 0px", margin="2px 10px 0px 0px",
exclusivity="auto", exclusivity="auto",
keyboard_mode="on-demand",
name=f"{name_prefix}-window", name=f"{name_prefix}-window",
visible=False, visible=False,
) )
def create_mouse_capture(window):
return DropDownMouseCapture(layer="top", child_window=window)
def setup_control_center( def setup_control_center(
show_window, name_prefix, widget_class, parent, **widget_kwargs show_window, name_prefix, widget_class, parent, pointing_to=None, **widget_kwargs
): ):
if not show_window: if not show_window:
return None, None, None return None, None
window = create_control_window(name_prefix) window = create_control_window(name_prefix, parent=parent, pointing_to=pointing_to)
widget = widget_class(parent, **widget_kwargs) widget = widget_class(parent, **widget_kwargs)
window.children = [widget] window.children = [widget]
mouse_capture = create_mouse_capture(window)
return window, widget, mouse_capture return window, widget
def handle_indicator_click(mouse_capture): def handle_indicator_click(window):
if mouse_capture: if window:
mouse_capture.toggle_mousecapture() window.toggle()
def hide_control_center(mouse_capture): def hide_control_center(window):
if mouse_capture: if window:
mouse_capture.hide_child_window() window.hide()
class BluetoothIndicator(Box): class BluetoothIndicator(Box):
@@ -77,16 +72,14 @@ class BluetoothIndicator(Box):
( (
self.bluetooth_window, self.bluetooth_window,
self.bluetooth_widget, self.bluetooth_widget,
self.bluetooth_mousecapture,
) = setup_control_center( ) = setup_control_center(
self.show_window, self.show_window,
"bluetooth", "bluetooth",
BluetoothConnections, BluetoothConnections,
self, self,
pointing_to=self.bt_button,
show_back_button=False, show_back_button=False,
) )
if self.bluetooth_window:
self.bluetooth_window._pointing_widget = self.bt_button
modus_service.connect("bluetooth-changed", self.on_bluetooth_changed) modus_service.connect("bluetooth-changed", self.on_bluetooth_changed)
self.bluetooth.connect("changed", self.on_bluetooth_direct_changed) self.bluetooth.connect("changed", self.on_bluetooth_direct_changed)
@@ -155,13 +148,13 @@ class BluetoothIndicator(Box):
modus_service.bluetooth = bluetooth_state modus_service.bluetooth = bluetooth_state
def on_bluetooth_clicked(self, *args): def on_bluetooth_clicked(self, *args):
handle_indicator_click(self.bluetooth_mousecapture) handle_indicator_click(self.bluetooth_window)
def close_bluetooth(self, *args): def close_bluetooth(self, *args):
hide_control_center(self.bluetooth_mousecapture) hide_control_center(self.bluetooth_window)
def hide_controlcenter(self, *args): def hide_controlcenter(self, *args):
hide_control_center(self.bluetooth_mousecapture) hide_control_center(self.bluetooth_window)
class NetworkIndicator(Box): class NetworkIndicator(Box):
@@ -188,12 +181,12 @@ class NetworkIndicator(Box):
( (
self.wifi_window, self.wifi_window,
self.wifi_widget, self.wifi_widget,
self.wifi_mousecapture,
) = setup_control_center( ) = setup_control_center(
self.show_window, self.show_window,
"wifi", "wifi",
WifiConnections, WifiConnections,
self, self,
pointing_to=self.network_button,
show_back_button=False, show_back_button=False,
) )
if self.wifi_window: if self.wifi_window:
@@ -232,6 +225,7 @@ class NetworkIndicator(Box):
def on_network_direct_changed(self, *args): def on_network_direct_changed(self, *args):
self.update_modus_service_wlan_state() self.update_modus_service_wlan_state()
self.update_state() self.update_state()
hide_control_center(self.wifi_window)
def on_network_changed(self, *args): def on_network_changed(self, *args):
self.update_modus_service_wlan_state() self.update_modus_service_wlan_state()
@@ -309,13 +303,19 @@ class NetworkIndicator(Box):
self.network_button.set_tooltip_text(tooltip) self.network_button.set_tooltip_text(tooltip)
def on_wifi_clicked(self, *args): def on_wifi_clicked(self, *args):
handle_indicator_click(self.wifi_mousecapture) handle_indicator_click(self.wifi_window)
def close_wifi(self, *args): def close_wifi(self, *args):
hide_control_center(self.wifi_mousecapture) hide_control_center(self.wifi_window)
def on_wifi_direct_changed(self, *args):
hide_control_center(self.wifi_window)
def on_network_clicked(self, *args):
handle_indicator_click(self.wifi_window)
def hide_controlcenter(self, *args): def hide_controlcenter(self, *args):
hide_control_center(self.wifi_mousecapture) hide_control_center(self.wifi_window)
class BatteryIndicator(Box): class BatteryIndicator(Box):
@@ -323,7 +323,7 @@ class BatteryIndicator(Box):
super().__init__(name="battery-indicator", orientation="h", **kwargs) super().__init__(name="battery-indicator", orientation="h", **kwargs)
self.show_window = show_window self.show_window = show_window
self.battery_service = BatteryService() self.battery_service = Battery()
self.battery_icon = svg_file("battery/battery-100.svg", size=23) self.battery_icon = svg_file("battery/battery-100.svg", size=23)
@@ -344,12 +344,12 @@ class BatteryIndicator(Box):
( (
self.battery_window, self.battery_window,
self.battery_widget, self.battery_widget,
self.battery_mousecapture,
) = setup_control_center( ) = setup_control_center(
self.show_window, self.show_window,
"battery", "battery",
BatteryControl, BatteryControl,
self, self,
pointing_to=self.battery_button,
show_back_button=False, show_back_button=False,
) )
if self.battery_window: if self.battery_window:
@@ -367,22 +367,28 @@ class BatteryIndicator(Box):
def on_battery_direct_changed(self, *args): def on_battery_direct_changed(self, *args):
self.update_modus_service_battery_state() self.update_modus_service_battery_state()
self.update_state() self.update_state()
hide_control_center(self.battery_window)
def _get_percentage(self): def _get_percentage(self):
return int(self.battery_service.get_property("Percentage") or 0) return int(self.battery_service.percent)
def _get_state(self): def _get_state(self):
state_value = self.battery_service.get_property("State") if self.battery_service.charging:
return DeviceState.get(state_value, "UNKNOWN") return "CHARGING"
if self.battery_service.discharging:
return "DISCHARGING"
if self.battery_service.charged:
return "FULLY_CHARGED"
return "UNKNOWN"
def _is_present(self): def _is_present(self):
return bool(self.battery_service.get_property("IsPresent")) return self.battery_service.available
def _get_time_to_empty(self): def _get_time_to_empty(self):
return int(self.battery_service.get_property("TimeToEmpty") or 0) return self.battery_service.time_remaining
def _get_time_to_full(self): def _get_time_to_full(self):
return int(self.battery_service.get_property("TimeToFull") or 0) return self.battery_service.time_to_full
def _format_time(self, seconds: int) -> str: def _format_time(self, seconds: int) -> str:
return format_duration(seconds) return format_duration(seconds)
@@ -433,10 +439,10 @@ class BatteryIndicator(Box):
def update_state(self): def update_state(self):
if not self._is_present(): if not self._is_present():
print("[Battery] No battery detected, hiding indicator") print("[Battery] No battery detected, hiding indicator")
self.set_visible(False) self.hide()
return return
else: else:
self.set_visible(True) self.show()
percentage = self._get_percentage() percentage = self._get_percentage()
state = self._get_state() state = self._get_state()
@@ -451,13 +457,13 @@ class BatteryIndicator(Box):
self.battery_label.set_label(percentage_text) self.battery_label.set_label(percentage_text)
def on_battery_clicked(self, *args): def on_battery_clicked(self, *args):
handle_indicator_click(self.battery_mousecapture) handle_indicator_click(self.battery_window)
def close_battery(self, *args): def close_battery(self, *args):
hide_control_center(self.battery_mousecapture) hide_control_center(self.battery_window)
def hide_controlcenter(self, *args): def hide_controlcenter(self, *args):
hide_control_center(self.battery_mousecapture) hide_control_center(self.battery_window)
def destroy(self): def destroy(self):
"""Clean up resources when the indicator is destroyed.""" """Clean up resources when the indicator is destroyed."""
@@ -466,8 +472,8 @@ class BatteryIndicator(Box):
modus_service.disconnect_by_func(self.on_battery_changed) modus_service.disconnect_by_func(self.on_battery_changed)
if hasattr(self, "battery_service") and self.battery_service: if hasattr(self, "battery_service") and self.battery_service:
self.battery_service.disconnect_by_func(self.on_battery_direct_changed) self.battery_service.disconnect_by_func(self.on_battery_direct_changed)
except Exception: except Exception as e:
pass print(f"Error disconnecting battery indicator signals: {e}")
# Destroy window and widget # Destroy window and widget
try: try:
@@ -475,8 +481,6 @@ class BatteryIndicator(Box):
self.battery_widget.destroy() self.battery_widget.destroy()
if hasattr(self, "battery_window") and self.battery_window: if hasattr(self, "battery_window") and self.battery_window:
self.battery_window.destroy() self.battery_window.destroy()
if hasattr(self, "battery_mousecapture") and self.battery_mousecapture:
self.battery_mousecapture.destroy()
except Exception: except Exception:
pass pass
@@ -494,15 +498,13 @@ def add_destroy_to_indicators():
self.bluetooth.disconnect_by_func(self.on_bluetooth_direct_changed) self.bluetooth.disconnect_by_func(self.on_bluetooth_direct_changed)
self.bluetooth.disconnect_by_func(self.on_device_added) self.bluetooth.disconnect_by_func(self.on_device_added)
self.bluetooth.disconnect_by_func(self.on_device_removed) self.bluetooth.disconnect_by_func(self.on_device_removed)
except Exception: except Exception as e:
pass print(f"Error disconnecting bluetooth indicator signals: {e}")
try: try:
if hasattr(self, "bluetooth_widget") and self.bluetooth_widget: if hasattr(self, "bluetooth_widget") and self.bluetooth_widget:
self.bluetooth_widget.destroy() self.bluetooth_widget.destroy()
if hasattr(self, "bluetooth_window") and self.bluetooth_window: if hasattr(self, "bluetooth_window") and self.bluetooth_window:
self.bluetooth_window.destroy() self.bluetooth_window.destroy()
if hasattr(self, "bluetooth_mousecapture") and self.bluetooth_mousecapture:
self.bluetooth_mousecapture.destroy()
except Exception: except Exception:
pass pass
Box.destroy(self) Box.destroy(self)
@@ -539,15 +541,13 @@ def add_destroy_to_indicators():
) )
except Exception: except Exception:
pass pass
except Exception: except Exception as e:
pass print(f"Error disconnecting network indicator signals: {e}")
try: try:
if hasattr(self, "wifi_widget") and self.wifi_widget: if hasattr(self, "wifi_widget") and self.wifi_widget:
self.wifi_widget.destroy() self.wifi_widget.destroy()
if hasattr(self, "wifi_window") and self.wifi_window: if hasattr(self, "wifi_window") and self.wifi_window:
self.wifi_window.destroy() self.wifi_window.destroy()
if hasattr(self, "wifi_mousecapture") and self.wifi_mousecapture:
self.wifi_mousecapture.destroy()
except Exception: except Exception:
pass pass
Box.destroy(self) Box.destroy(self)
+11 -20
View File
@@ -8,7 +8,7 @@ from fabric.widgets.wayland import WaylandWindow as Window
from services.config import on_config_change, get_config_all from services.config import on_config_change, get_config_all
from services.modus import notification_service from services.modus import notification_service
from shared.window.mousecapture import MouseCapture
from utils.roam import modus_service from utils.roam import modus_service
from utils.utils import setup_cursor_hover, svg_file from utils.utils import setup_cursor_hover, svg_file
from window.controlcenter.main import ModusControlCenter from window.controlcenter.main import ModusControlCenter
@@ -78,20 +78,18 @@ class Panel(Window):
) )
setup_cursor_hover(self.search, "pointer") setup_cursor_hover(self.search, "pointer")
self.control_center = MouseCapture(
layer="top", child_window=ModusControlCenter()
)
self.control_center_btn = Button( self.control_center_btn = Button(
name="panel-button", name="panel-button",
child=svg_file("misc/control-center.svg", size=22), child=svg_file("misc/control-center.svg", size=22),
on_clicked=self.control_center.toggle_mousecapture,
) )
setup_cursor_hover(self.control_center_btn, "pointer") setup_cursor_hover(self.control_center_btn, "pointer")
self.control_center.child_window._pointing_widget = self.control_center_btn
self.notification_center = MouseCapture( self.control_center = ModusControlCenter(
layer="overlay", child_window=NotificationCenter() parent=self, pointing_to=self.control_center_btn
)
self.control_center_btn.connect(
"clicked",
lambda *_: self.control_center.toggle(),
) )
self.notification_icon = svg_file( self.notification_icon = svg_file(
@@ -104,8 +102,9 @@ class Panel(Window):
on_clicked=self.on_notification_icon_clicked, on_clicked=self.on_notification_icon_clicked,
) )
setup_cursor_hover(self.notification_center_btn, "pointer") setup_cursor_hover(self.notification_center_btn, "pointer")
self.notification_center.child_window._pointing_widget = (
self.notification_center_btn self.notification_center = NotificationCenter(
parent=self, pointing_to=self.notification_center_btn
) )
self.datetime_btn = Button( self.datetime_btn = Button(
@@ -174,9 +173,7 @@ class Panel(Window):
def on_notification_icon_clicked(self, *args): def on_notification_icon_clicked(self, *args):
count = notification_service.count count = notification_service.count
if count > 0: if count > 0:
# Only open notification center if there are notifications self.notification_center.toggle()
self.notification_center.toggle_mousecapture()
# Do nothing if no notifications
def update_notification_icon(self): def update_notification_icon(self):
count = notification_service.count count = notification_service.count
@@ -198,8 +195,6 @@ class Panel(Window):
if config_data is None: if config_data is None:
config_data = get_config_all() config_data = get_config_all()
print(f"[Panel] Rebuilding layout with config: {config_data}")
# Update indicator visibility directly # Update indicator visibility directly
battery_visible = config_data.get("battery", True) battery_visible = config_data.get("battery", True)
network_visible = config_data.get("network", True) network_visible = config_data.get("network", True)
@@ -258,10 +253,6 @@ class Panel(Window):
# Force a layout recalculation # Force a layout recalculation
self.queue_resize() self.queue_resize()
print(
f"[Panel] Battery visibility state: {self.battery_indicator.get_visible()}"
)
def _on_config_changed(self, new_config, old_config): def _on_config_changed(self, new_config, old_config):
print("[Panel] Config changed, checking keys...") print("[Panel] Config changed, checking keys...")
keys = { keys = {
+14 -3
View File
@@ -1,7 +1,7 @@
import json import json
from fabric.hyprland.widgets import get_hyprland_connection from fabric.hyprland.widgets import get_hyprland_connection
from fabric.utils import Gdk from fabric.utils import Gdk, GLib
from fabric.widgets.box import Box from fabric.widgets.box import Box
from fabric.widgets.eventbox import EventBox from fabric.widgets.eventbox import EventBox
from fabric.widgets.image import Image from fabric.widgets.image import Image
@@ -198,6 +198,10 @@ class ApplicationSwitcher(Window):
name="window-button", name="window-button",
style_classes=["active"] if i == self.current_index else None, style_classes=["active"] if i == self.current_index else None,
child=button_content, child=button_content,
events=["button-press-event"],
)
event_box.connect(
"button-press-event", lambda w, e, idx=i: self._on_item_clicked(idx)
) )
current_row.add(event_box) current_row.add(event_box)
@@ -311,11 +315,18 @@ class ApplicationSwitcher(Window):
address = window.get("address") address = window.get("address")
if address: if address:
try: try:
command = f"/dispatch focuswindow address:{address}" GLib.spawn_command_line_async(
self.conn.send_command(command) f"hyprctl dispatch 'hl.dsp.focus({{ window = \"address:{address}\" }})'"
)
except Exception as e: except Exception as e:
print(f"Failed to focus window: {e}") print(f"Failed to focus window: {e}")
def _on_item_clicked(self, index):
self.current_index = index
self.update_selection()
self.activate_selected()
self.hide_switcher()
def grab_keyboard(self): def grab_keyboard(self):
try: try:
display = Gdk.Display.get_default() display = Gdk.Display.get_default()
+11 -26
View File
@@ -6,10 +6,9 @@ from fabric.widgets.button import Button
from fabric.widgets.entry import Entry from fabric.widgets.entry import Entry
from fabric.widgets.label import Label from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow from fabric.widgets.scrolledwindow import ScrolledWindow
from fabric.widgets.wayland import WaylandWindow as Window
from services.todo import get_todo_service from services.todo import get_todo_service
from shared.window.mousecapture import MouseCapture from shared.window.applet_window import AppletWindow
from utils.utils import svg_file from utils.utils import svg_file
@@ -224,18 +223,20 @@ class TodoItem(Box):
self.date_label.set_label(date_text) self.date_label.set_label(date_text)
class TodoListWidget(Window): class TodoListWidget(AppletWindow):
"""Main todo list widget window""" """Main todo list window"""
def __init__(self, **kwargs): def __init__(self, parent=None, pointing_to=None, **kwargs):
super().__init__( super().__init__(
parent=parent,
pointing_to=pointing_to,
layer="top",
title="modus-todo", title="modus-todo",
anchor="top right", anchor="top right",
margin="2px 10px 0px 0px", margin="2px 10px 0px 0px",
exclusivity="auto", exclusivity="auto",
keyboard_mode="on-demand", name="todo-list-menu",
name="todo-list-window", visible=False,
visible=False, # Back to hidden by default
**kwargs, **kwargs,
) )
@@ -416,13 +417,8 @@ class TodoListWidget(Window):
def hide_todo_list(self, *_): def hide_todo_list(self, *_):
"""Hide the todo list""" """Hide the todo list"""
if hasattr(self, "_mousecapture_parent"): self.hide()
self._mousecapture_parent.toggle_mousecapture() self.hide()
self.set_visible(False)
def _init_mousecapture(self, mousecapture):
"""Initialize mousecapture parent reference"""
self._mousecapture_parent = mousecapture
def destroy(self): def destroy(self):
"""Clean up when destroyed""" """Clean up when destroyed"""
@@ -430,14 +426,3 @@ class TodoListWidget(Window):
get_todo_service().remove_callback(self._on_todo_event) get_todo_service().remove_callback(self._on_todo_event)
super().destroy() super().destroy()
class TodoListCapture(MouseCapture):
"""MouseCapture wrapper for the todo list"""
def __init__(self, **kwargs):
super().__init__(
layer="top",
child_window=TodoListWidget(),
**kwargs,
)