from gi.repository import Gdk, GLib from loguru import logger from fabric.utils import idle_add from fabric.utils.helpers import ( get_relative_path, ) from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.label import Label from fabric.widgets.scale import Scale from fabric.widgets.svg import Svg from modules.controlcenter.bluetooth import BluetoothConnections from modules.controlcenter.wifi import WifiConnections from services.brightness import Brightness from utils.roam import audio_service, modus_service from widgets.wayland import WaylandWindow as Window from modules.controlcenter.player import PlayerBoxStack from modules.controlcenter.per_app_volume import PerAppVolumeControl from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer from services.mpris import MprisPlayerManager from fabric.widgets.svg import Svg from fabric.utils.helpers import get_relative_path brightness_service = Brightness.get_initial() # Shared MPRIS manager to reduce memory usage _shared_mpris_manager = None def get_shared_mpris_manager(): """Get shared MPRIS manager instance to reduce memory usage.""" global _shared_mpris_manager if _shared_mpris_manager is None: _shared_mpris_manager = MprisPlayerManager() return _shared_mpris_manager class ModusControlCenter(Window): def __init__(self, **kwargs): super().__init__( layer="top", title="modus", anchor="top right", margin="2px 10px 0px 0px", exclusivity="auto", keyboard_mode="on-demand", name="control-center-menu", visible=False, **kwargs, ) self.focus_mode = modus_service.dont_disturb self._updating_brightness = False self._updating_volume = False # Lazy loading flags self._music_initialized = False self._per_app_volume_initialized = False self._expanded_player_initialized = False # Store references for cleanup self._signal_connections = [] self._music_widget_content = None self._per_app_volume_widget = None self._expanded_player_widget = None self.add_keybinding("Escape", self.hide_controlcenter) volume = 100 wlan = modus_service.sc("wlan-changed", self.wlan_changed) bluetooth = modus_service.sc("bluetooth-changed", self.bluetooth_changed) music = modus_service.sc("music-changed", self.audio_changed) # Store signal connections for cleanup self._signal_connections.extend( [ audio_service.connect("changed", self.audio_changed), audio_service.connect("changed", self.volume_changed), modus_service.connect("dont-disturb-changed", self.dnd_changed), ] ) self.wlan_label = Label(wlan, name="wifi-widget-label", h_align="start") if bluetooth != "disabled": if bluetooth.startswith("connected:"): parts = bluetooth.split(":") bluetooth_display = parts[1] if len(parts) >= 2 else "Connected" else: bluetooth_display = "On" else: bluetooth_display = "Off" self.bluetooth_label = Label( bluetooth_display, name="bluetooth-widget-label", h_align="start" ) self.volume_scale = Scale( value=volume, min_value=0, max_value=100, increments=(5, 5), name="volume-widget-slider", size=30, h_expand=True, ) self.volume_scale.connect("change-value", self.set_volume) self.volume_scale.connect("scroll-event", self.on_volume_scroll) current_brightness = brightness_service.screen_brightness brightness_percentage = ( int((current_brightness / brightness_service.max_screen) * 100) if brightness_service.max_screen > 0 else 50 ) self.brightness_scale = Scale( value=brightness_percentage, min_value=0, max_value=100, increments=(5, 5), name="brightness-widget-slider", size=30, h_expand=True, ) # Only connect brightness controls if brightness service is available if brightness_service.max_screen > 0: self.brightness_scale.connect("change-value", self.set_brightness) self.brightness_scale.connect("scroll-event", self.on_brightness_scroll) self._signal_connections.append( brightness_service.connect("screen", self.brightness_changed) ) else: # Disable brightness scale if no backlight device available self.brightness_scale.set_sensitive(False) # Create placeholder music widget - lazy load content when needed self.music_widget = Box( name="music-widget", h_align="start", children=[] # Empty initially ) self.has_bluetooth_open = False self.has_wifi_open = False self.has_per_app_volume_open = False self.has_expanded_player_open = False self.bluetooth_svg = Svg( name="bluetooth-icon", svg_file=get_relative_path( "../../config/assets/icons/applets/bluetooth.svg" if bluetooth != "disabled" else "../../config/assets/icons/applets/bluetooth-off.svg" ), size=42, ) self.wifi_svg = Svg( name="wifi-icon", svg_file=get_relative_path( "../../config/assets/icons/applets/wifi.svg" if wlan != "No Connection" else "../../config/assets/icons/applets/wifi-off.svg" ), size=46, ) self.bluetooth_widget = Button( name="bluetooth-widget", child=Box( orientation="h", children=[ self.bluetooth_svg, Box( name="bluetooth-widget-info", orientation="vertical", children=[ Label( name="bluetooth-widget-name", label="Bluetooth", style_classes="ct", h_align="start", ), self.bluetooth_label, ], ), ], ), on_clicked=self.open_bluetooth, ) self.wlan_widget = Button( name="wifi-widget", child=Box( orientation="h", children=[ self.wifi_svg, Box( name="wifi-widget-info", orientation="vertical", children=[ Label( name="wifi-widget-name", label="Wi-Fi", style_classes="ct", h_align="start", ), self.wlan_label, ], ), ], ), on_clicked=self.open_wifi, ) self.focus_icon = Svg( name="focus-icon", svg_file=get_relative_path( "../../config/assets/icons/applets/dnd.svg" if self.focus_mode else "../../config/assets/icons/applets/dnd-off.svg" ), size=46, ) self.focus_widget = Button( name="focus-widget", child=Box( orientation="h", children=[ self.focus_icon, Label(label="Focus", style_classes="title ct", h_align="start"), ], ), on_clicked=self.set_dont_disturb, ) # Create main widgets directly without XML self.widgets = Box( orientation="vertical", h_expand=True, name="control-center-widgets", children=[ Box( orientation="horizontal", name="top-widget", h_expand=True, children=[ Box( orientation="vertical", name="wb-widget", style_classes="menu", spacing=5, children=[ self.wlan_widget, self.bluetooth_widget, ], ), Box( orientation="horizontal", name="dnd-widget", style_classes="menu", h_expand=True, children=[ self.focus_widget, ], ), ], ), Box( orientation="vertical", name="brightness-widget", style_classes="menu", h_expand=True, children=[ Label(label="Display", style_classes="title", h_align="start"), self.brightness_scale, Label( label="󰖨 ", name="brightness-widget-icon", h_align="start" ), ], ), Box( orientation="vertical", name="volume-widget", style_classes="menu", h_expand=True, children=[ Label(label="Sound", style_classes="title", h_align="start"), Box( orientation="horizontal", spacing=8, v_expand=True, h_expand=True, children=[ self.volume_scale, Button( name="per-app-volume-button", child=Box( h_align="center", v_align="center", h_expand=True, size=(32, 32), v_expand=True, children=[ Svg( svg_file=get_relative_path( "../../config/assets/icons/player/audio-switcher.svg" ), size=34, ) ], ), on_clicked=self.open_per_app_volume, ), ], ), Label(label=" ", name="volume-widget-icon", h_align="start"), ], ), Box( orientation="vertical", children=[ self.music_widget, ], ), ], ) # Initialize managers directly like working version self.wifi_man = WifiConnections(self) self.bluetooth_man = BluetoothConnections(self) self.has_bluetooth_open = False self.has_wifi_open = False # Lazy-loaded widgets - create placeholders self.bluetooth_widgets = None self.wifi_widgets = None self.per_app_volume_widgets = None self.expanded_player_widgets = None self.center_box = CenterBox(start_children=[self.widgets]) self.bluetooth_center_box = None self.wifi_center_box = None self.per_app_volume_center_box = None self.expanded_player_center_box = None self.widgets.set_size_request(300, -1) self.children = self.center_box # Connect to visibility changes for cleanup self.connect("notify::visible", self._on_visibility_changed) # Periodic cleanup timer (every 5 minutes when visible) GLib.timeout_add_seconds(300, self._periodic_cleanup) def destroy(self): """Proper cleanup when control center is destroyed""" # Disconnect all signal connections for connection in self._signal_connections: try: if hasattr(connection, "disconnect"): connection.disconnect() elif hasattr(connection, "handler_disconnect"): # For GObject connections connection.handler_disconnect() except Exception: pass self._signal_connections.clear() # Clean up all widgets self._cleanup_when_hidden() # Clean up managers if hasattr(self, "wifi_man") and self.wifi_man: try: if hasattr(self.wifi_man, "destroy"): self.wifi_man.destroy() except Exception: pass if hasattr(self, "bluetooth_man") and self.bluetooth_man: try: if hasattr(self.bluetooth_man, "destroy"): self.bluetooth_man.destroy() except Exception: pass super().destroy() def _on_visibility_changed(self, widget, param): """Handle visibility changes for memory management""" if not self.get_visible(): self._cleanup_when_hidden() def _cleanup_when_hidden(self): """Clean up resources when widget is hidden to reduce memory usage""" # Clean up music widget content if it exists if self._music_widget_content and hasattr( self._music_widget_content, "destroy" ): try: # Remove from parent container first current_children = list(self.music_widget.children) if self._music_widget_content in current_children: current_children.remove(self._music_widget_content) self.music_widget.children = current_children # Destroy the content to free memory self._music_widget_content.destroy() self._music_widget_content = None self._music_initialized = False except Exception as e: logger.warning(f"Failed to cleanup music widget content: {e}") # Clean up per-app volume widget if self._per_app_volume_widget and hasattr( self._per_app_volume_widget, "destroy" ): try: self._per_app_volume_widget.destroy() self._per_app_volume_widget = None self._per_app_volume_initialized = False except Exception: pass # Clean up expanded player widget if self._expanded_player_widget and hasattr( self._expanded_player_widget, "destroy" ): try: self._expanded_player_widget.destroy() self._expanded_player_widget = None self._expanded_player_initialized = False except Exception: pass # Reset widget containers to free memory self.bluetooth_widgets = None self.wifi_widgets = None self.per_app_volume_widgets = None self.expanded_player_widgets = None # Reset center boxes self.bluetooth_center_box = None self.wifi_center_box = None self.per_app_volume_center_box = None self.expanded_player_center_box = None # Force garbage collection import gc gc.collect() def _periodic_cleanup(self): """Periodic cleanup to keep memory usage low""" if not self.get_visible(): # If not visible, do aggressive cleanup self._cleanup_when_hidden() return True # Continue timer # Light cleanup when visible import gc gc.collect() return True # Continue timer def _ensure_music_widget(self): """Lazy load music widget content""" if not self._music_initialized: self._music_widget_content = PlayerBoxStack( get_shared_mpris_manager(), control_center=self ) # Add to the music widget's children list current_children = list(self.music_widget.children) current_children.append(self._music_widget_content) self.music_widget.children = current_children self._music_initialized = True def _ensure_bluetooth_widgets(self): """Lazy load bluetooth widgets""" if self.bluetooth_widgets is None: self.bluetooth_widgets = Box( orientation="vertical", h_expand=True, name="control-center-widgets", children=[ Box( orientation="horizontal", name="top-widget", h_expand=True, children=[ Box( orientation="vertical", name="wb-widget", style_classes="menu", spacing=5, children=[ self.bluetooth_man, ], ), ], ), ], ) self.bluetooth_center_box = CenterBox( start_children=[self.bluetooth_widgets] ) self.bluetooth_center_box.set_size_request(300, -1) def _ensure_wifi_widgets(self): """Lazy load wifi widgets""" if self.wifi_widgets is None: self.wifi_widgets = Box( orientation="vertical", h_expand=True, name="control-center-widgets", children=[ Box( orientation="horizontal", name="top-widget", h_expand=True, children=[ Box( orientation="vertical", name="wb-widget", style_classes="menu", spacing=5, children=[ self.wifi_man, ], ), ], ), ], ) self.wifi_center_box = CenterBox(start_children=[self.wifi_widgets]) self.wifi_center_box.set_size_request(300, -1) def _ensure_per_app_volume_widgets(self): """Lazy load per-app volume widgets""" if self.per_app_volume_widgets is None: if self._per_app_volume_widget is None: self._per_app_volume_widget = PerAppVolumeControl(self) self.per_app_volume_widgets = Box( orientation="vertical", h_expand=True, name="control-center-widgets", children=[ self._per_app_volume_widget, ], ) self.per_app_volume_center_box = CenterBox( start_children=[self.per_app_volume_widgets] ) self.per_app_volume_center_box.set_size_request(300, -1) def _ensure_expanded_player_widgets(self): """Lazy load expanded player widgets""" if self.expanded_player_widgets is None: if self._expanded_player_widget is None: self._expanded_player_widget = EmbeddedExpandedPlayer(self) self.expanded_player_widgets = Box( orientation="vertical", h_expand=True, name="control-center-widgets", children=[ self._expanded_player_widget, ], ) self.expanded_player_center_box = CenterBox( start_children=[self.expanded_player_widgets] ) self.expanded_player_center_box.set_size_request(300, -1) def set_dont_disturb(self, *_): self.focus_mode = not self.focus_mode modus_service.dont_disturb = self.focus_mode self.focus_icon.set_from_file( get_relative_path( "../../config/assets/icons/applets/dnd.svg" if self.focus_mode else "../../config/assets/icons/applets/dnd-off.svg" ) ) def set_volume(self, _, __, volume): self._updating_volume = True audio_service.speaker.volume = round(volume) self._updating_volume = False def set_brightness(self, _, __, brightness): self._updating_brightness = True brightness_value = int((brightness / 100) * brightness_service.max_screen) brightness_service.screen_brightness = brightness_value self._updating_brightness = False def brightness_changed(self, _, brightness_value): if self._updating_brightness: return if brightness_service.max_screen > 0: brightness_percentage = int( (brightness_value / brightness_service.max_screen) * 100 ) GLib.idle_add( lambda: self.brightness_scale.set_value(brightness_percentage) ) def on_volume_scroll(self, widget, event): current_value = self.volume_scale.get_value() scroll_step = 5 if event.direction == Gdk.ScrollDirection.UP: new_value = min(100, current_value + scroll_step) elif event.direction == Gdk.ScrollDirection.DOWN: new_value = max(0, current_value - scroll_step) else: return False self.volume_scale.set_value(new_value) return True def on_brightness_scroll(self, widget, event): current_value = self.brightness_scale.get_value() scroll_step = 5 if event.direction == Gdk.ScrollDirection.UP: new_value = min(100, current_value + scroll_step) elif event.direction == Gdk.ScrollDirection.DOWN: new_value = max(0, current_value - scroll_step) else: return False self.brightness_scale.set_value(new_value) return True def set_children(self, children): self.children = children def open_bluetooth(self, *_): self._ensure_bluetooth_widgets() idle_add(lambda *_: self.set_children(self.bluetooth_center_box)) self.has_bluetooth_open = True def open_wifi(self, *_): self._ensure_wifi_widgets() idle_add(lambda *_: self.set_children(self.wifi_center_box)) self.has_wifi_open = True def close_bluetooth(self, *_): idle_add(lambda *_: self.set_children(self.center_box)) self.has_bluetooth_open = False def close_wifi(self, *_): idle_add(lambda *_: self.set_children(self.center_box)) self.has_wifi_open = False def open_per_app_volume(self, *_): self._ensure_per_app_volume_widgets() idle_add(lambda *_: self.set_children(self.per_app_volume_center_box)) self.has_per_app_volume_open = True # Refresh the app list when opening if self._per_app_volume_widget: self._per_app_volume_widget.refresh() def close_per_app_volume(self, *_): idle_add(lambda *_: self.set_children(self.center_box)) self.has_per_app_volume_open = False def open_expanded_player(self, *_): self._ensure_expanded_player_widgets() idle_add(lambda *_: self.set_children(self.expanded_player_center_box)) self.has_expanded_player_open = True # Refresh the player when opening if self._expanded_player_widget: self._expanded_player_widget.refresh() def close_expanded_player(self, *_): idle_add(lambda *_: self.set_children(self.center_box)) self.has_expanded_player_open = False def _set_mousecapture(self, visible: bool): if visible: # Lazy load music widget when becoming visible self._ensure_music_widget() self.set_visible(visible) if not visible: self.close_bluetooth() self.close_wifi() self.close_per_app_volume() self.close_expanded_player() def volume_changed( self, _, ): if self._updating_volume: return GLib.idle_add( lambda: self.volume_scale.set_value(int(audio_service.speaker.volume)) ) def wlan_changed(self, _, wlan): self.wifi_svg.set_from_file( get_relative_path( "../../config/assets/icons/applets/wifi.svg" if wlan != "No Connection" else "../../config/assets/icons/applets/wifi-off.svg" ) ) if wlan != "No Connection": if wlan.startswith("connected:"): parts = wlan.split(":") if len(parts) >= 2: wifi_name = parts[1] GLib.idle_add( lambda: self.wlan_label.set_property("label", wifi_name) ) else: GLib.idle_add( lambda: self.wlan_label.set_property("label", "Connected") ) else: GLib.idle_add(lambda: self.wlan_label.set_property("label", wlan)) else: GLib.idle_add(lambda: self.wlan_label.set_property("label", wlan)) def bluetooth_changed(self, _, bluetooth): self.bluetooth_svg.set_from_file( get_relative_path( "../../config/assets/icons/applets/bluetooth.svg" if bluetooth != "disabled" else "../../config/assets/icons/applets/bluetooth-off.svg" ) ) if bluetooth != "disabled": if bluetooth.startswith("connected:"): parts = bluetooth.split(":") if len(parts) >= 2: device_name = parts[1] GLib.idle_add( lambda: self.bluetooth_label.set_property("label", device_name) ) else: GLib.idle_add( lambda: self.bluetooth_label.set_property("label", "Connected") ) elif bluetooth == "enabled": GLib.idle_add(lambda: self.bluetooth_label.set_property("label", "On")) else: GLib.idle_add(lambda: self.bluetooth_label.set_property("label", "On")) else: GLib.idle_add(lambda: self.bluetooth_label.set_property("label", "Off")) def audio_changed(self, *_): pass def dnd_changed(self, _, dnd_state): self.focus_mode = dnd_state self.focus_icon.set_from_file( get_relative_path( "../../config/assets/icons/applets/dnd.svg" if self.focus_mode else "../../config/assets/icons/applets/dnd-off.svg" ) ) def _init_mousecapture(self, mousecapture): self._mousecapture_parent = mousecapture def hide_controlcenter(self, *_): self._mousecapture_parent.toggle_mousecapture() self.set_visible(False) def destroy(self): """Clean up resources when widget is destroyed""" # Disconnect all signal connections for connection in self._signal_connections: try: connection.disconnect() except: pass # Clean up heavy components if self._wifi_man: self._wifi_man.destroy() if self._bluetooth_man: self._bluetooth_man.destroy() if self._music_widget_content: self._music_widget_content.destroy() if self._per_app_volume_widget: self._per_app_volume_widget.destroy() if self._expanded_player_widget: self._expanded_player_widget.destroy() super().destroy()