import subprocess from fabric.utils import Gdk, GLib, get_relative_path, idle_add, logger 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.wayland import WaylandWindow as Window from modules.controlcenter.bluetooth import ( BluetoothConnections, set_bluetooth_enabled_with_fallback, ) from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer from modules.controlcenter.nightlight import create_night_light_widget from modules.controlcenter.per_app_volume import PerAppVolumeControl from modules.controlcenter.player import PlayerBoxStack, get_shared_mpris_manager from modules.controlcenter.wifi import WifiConnections from services.brightness import Brightness from services.network import NetworkClient from utils.roam import audio_service, modus_service from utils.utils import svg_file brightness_service = Brightness.get_initial() 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 # Flight mode and caffeine states self.flight_mode = False self.caffeine_mode = False self._caffeine_process = None # Loading flags - music and expanded player are no longer lazy loaded self._per_app_volume_initialized = False self._signals_connected = False # Track if signals are connected self._resources_initialized = False # Track if resources are initialized # Store references for cleanup - initialize all as None self._signal_connections = [] self._music_widget_content = None self._per_app_volume_widget = None self._expanded_player_widget = None self._mpris_manager = None # Shared MPRIS manager instance # Initialize network service for WiFi toggle - lazy load self.network_service = None self.wifi_service = None # Initialize flight mode and caffeine states - lazy load self.caffeine_mode = False self.flight_mode = False # Add keybinding immediately (this is fast) self.add_keybinding("Escape", self.hide_controlcenter) volume = 100 # Get initial values and connect signals - store connection IDs for cleanup wlan = modus_service.wlan if modus_service.wlan else "No Connection" bluetooth = modus_service.bluetooth if modus_service.bluetooth else "Off" self.wlan_label = Label( label=wlan, name="wifi-widget-label", max_chars_width=15, h_align="start", ellipsization="end", ) 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( label=bluetooth_display, name="bluetooth-widget-label", max_chars_width=15, ellipsization="end", 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, ) 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: pass else: # Disable brightness scale if no backlight device available self.brightness_scale.set_sensitive(False) self._mpris_manager = get_shared_mpris_manager() self.music_widget = PlayerBoxStack(self._mpris_manager, control_center=self) 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_file( ( "applets/bluetooth.svg" if bluetooth != "disabled" else "applets/bluetooth-off.svg" ), size=42, ) self.wifi_svg = svg_file( "applets/wifi.svg" if wlan != "No Connection" else "applets/wifi-off.svg", size=42, ) self.bluetooth_widget = Box( name="bluetooth-widget", orientation="h", children=[ Button( name="bluetooth-icon-button", child=self.bluetooth_svg, on_clicked=self.toggle_bluetooth, ), Button( name="bluetooth-info-button", child=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 = Box( name="wifi-widget", orientation="h", children=[ Button( name="wifi-icon-button", child=self.wifi_svg, on_clicked=self.toggle_wifi, ), Button( name="wifi-info-button", child=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_file( "applets/dnd.svg" if self.focus_mode else "applets/dnd-off.svg", size=42, ) self.focus_status_label = Label( label="On" if self.focus_mode else "Off", style_classes="status-label", h_align="start", ) self.focus_widget = Button( name="focus-widget", child=Box( spacing=4, orientation="h", children=[ self.focus_icon, Box( v_expand=True, h_expand=True, v_align="center", orientation="v", h_align="start", children=[ Label( label="Focus", style_classes="title-widget", h_align="start", ), self.focus_status_label, ], ), ], ), on_clicked=self.set_dont_disturb, ) self.flight_icon = svg_file( "applets/flight-on.svg" if self.flight_mode else "applets/flight-off.svg", size=42, ) self.flight_widget = Button( name="flight-widget", child=Box( orientation="v", h_expand=True, v_expand=True, spacing=8, h_align="center", v_align="center", children=[ self.flight_icon, Label( label="Flight", style_classes="title-widget", h_align="center", ), ], ), on_clicked=self.toggle_flight_mode, ) self.caffeine_icon = svg_file( ( "applets/caffeine-on.svg" if self.caffeine_mode else "applets/caffeine-off.svg" ), size=42, ) self.caffeine_status_label = Label( label="On" if self.caffeine_mode else "Off", style_classes="status-label", h_align="start", ) self.caffeine_widget = Button( name="caffeine-widget", child=Box( orientation="v", spacing=8, h_expand=True, v_expand=True, h_align="center", v_align="center", children=[ self.caffeine_icon, Label( label="Caffeine", style_classes="title-widget", h_align="center", ), ], ), on_clicked=self.toggle_caffeine, ) # Create night light widget self.night_light_widget = create_night_light_widget(self) # Create main widgets directly without XML - defer heavy operations self.widgets = Box( orientation="vertical", h_expand=True, name="control-center-widgets", children=[ # Top section: Wi-Fi + Bluetooth on left, DND + Flight Mode + Caffeine on right Box( orientation="horizontal", h_expand=True, name="top-widget", children=[ # Left side: Wi-Fi and Bluetooth Box( orientation="vertical", name="wb-widget", style_classes="menu", spacing=5, children=[ self.wlan_widget, self.bluetooth_widget, self.night_light_widget, ], ), # Right side: DND, Flight Mode, and Caffeine Box( orientation="vertical", h_expand=True, name="right-side-widget", children=[ # DND widget Box( orientation="vertical", name="dnd-widget", style_classes="menu", children=[ self.focus_widget, ], ), # Flight Mode and Caffeine row Box( orientation="horizontal", name="flight-caffeine-row", children=[ Box( orientation="vertical", name="flight-widget-container", style_classes="menu", h_expand=True, v_expand=True, children=[ self.flight_widget, ], ), Box( orientation="vertical", name="caffeine-widget-container", h_expand=True, v_expand=True, style_classes="menu", children=[ self.caffeine_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( name="vol-box", orientation="horizontal", spacing=12, v_expand=True, children=[ Box( name="vol-slider-box", h_expand=True, v_align="center", v_expand=False, children=[ self.volume_scale, ], ), Button( name="per-app-volume-button", size=(36, 36), child=svg_file( "player/audio-switcher.svg", name="per-app-volume-icon", size=32, ), on_clicked=self.open_per_app_volume, ), ], ), Label(label=" ", name="volume-widget-icon", h_align="start"), ], ), 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 # Create expanded player widgets immediately (no lazy loading) 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, ], ) # Lazy-loaded widgets - create placeholders for others self.bluetooth_widgets = None self.wifi_widgets = None self.per_app_volume_widgets = None # Create main content boxes 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 # Create expanded player center box immediately self.expanded_player_center_box = CenterBox( start_children=[self.expanded_player_widgets] ) self.expanded_player_center_box.set_size_request(300, -1) # Create revealers for crossfade transitions self.widgets.set_size_request(300, -1) self.children = self.center_box # Track current state for smooth transitions self.current_view = "main" # main, expanded_player # Connect to visibility changes for cleanup self.connect("notify::visible", self._on_visibility_changed) def _on_visibility_changed(self, widget, param): """Handle visibility changes for resource management""" if not self.get_visible(): # Just disconnect signals and reset state flags - don't destroy widgets self._disconnect_signals_when_hidden() else: self._initialize_resources() def _initialize_resources(self): """Initialize resources and connect signals when the control center becomes visible.""" if self._resources_initialized: return try: logger.debug("Initializing control center resources...") # Initialize network service lazily if self.network_service is None: self.network_service = NetworkClient() # Connect network signal self.network_service.connect("wifi-device-added", self.on_network_ready) # Check initial states lazily (only when needed) self._check_initial_states() # 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("wlan-changed", self.wlan_changed), modus_service.connect("bluetooth-changed", self.bluetooth_changed), modus_service.connect("dont-disturb-changed", self.dnd_changed), ] ) # 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) ) # Connect volume scale signals self.volume_scale.connect("change-value", self.set_volume) self.volume_scale.connect("scroll-event", self.on_volume_scroll) # Mark signals as connected self._signals_connected = True self._resources_initialized = True logger.debug("Control center resources initialized successfully") except Exception as e: logger.error(f"Failed to initialize control center resources: {e}") # Reset flags on failure self._signals_connected = False self._resources_initialized = False def _disconnect_signals_when_hidden(self): """Disconnect signals when hidden to reduce resource usage, but keep widgets intact""" try: # Just disconnect signals and reset flags - don't destroy widgets self._signals_connected = False self._resources_initialized = False # Reset state flags self.has_bluetooth_open = False self.has_wifi_open = False self.has_per_app_volume_open = False self.has_expanded_player_open = False # Reset current view self.current_view = "main" except Exception as e: logger.warning(f"Control center signal disconnection failed: {e}") def _ensure_bluetooth_widgets(self): """Lazy load bluetooth widgets""" if self.bluetooth_widgets is None: self.bluetooth_widgets = Box( orientation="vertical", h_expand=True, v_expand=True, children=[self.bluetooth_man], ) self.bluetooth_center_box = Box( h_expand=True, v_expand=True, children=[self.bluetooth_widgets] ) self.bluetooth_center_box.set_size_request(350, -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, v_expand=True, children=[ self.wifi_man, ], ) self.wifi_center_box = Box( h_expand=True, v_expand=True, children=[self.wifi_widgets] ) self.wifi_center_box.set_size_request(350, -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 _check_initial_states(self): # Check if caffeine is already running - use faster method try: # Use faster check with timeout result = subprocess.run( ["pgrep", "-f", "modus-inhibit"], capture_output=True, text=True, timeout=0.5, # Add timeout to prevent hanging ) self.caffeine_mode = bool(result.stdout.strip()) except (subprocess.TimeoutExpired, Exception): self.caffeine_mode = False # Flight mode starts as False (normal mode) self.flight_mode = False # Update initial labels (will be set after widgets are created) GLib.timeout_add(100, self._update_initial_labels) def _update_initial_labels(self): return False def set_dont_disturb(self, *_): self.focus_mode = not self.focus_mode modus_service.dont_disturb = self.focus_mode self.focus_icon.dynamic_file( "applets/dnd.svg" if self.focus_mode else "applets/dnd-off.svg" ) self.focus_status_label.set_label("On" if self.focus_mode else "Off") def toggle_flight_mode(self, *_): try: self.flight_mode = not self.flight_mode if self.flight_mode: # Turn off WiFi and Bluetooth if self.wifi_service: self.wifi_service.wireless_enabled = False if hasattr(self, "bluetooth_man") and hasattr( self.bluetooth_man, "client" ): self.bluetooth_man.client.set_enabled(False) else: # Turn on WiFi and Bluetooth if self.wifi_service: self.wifi_service.wireless_enabled = True if hasattr(self, "bluetooth_man") and hasattr( self.bluetooth_man, "client" ): self.bluetooth_man.client.set_enabled(True) # Update icon self.flight_icon.dynamic_file( "applets/flight-on.svg" if self.flight_mode else "applets/flight-off.svg" ) except Exception as e: logger.warning(f"Failed to toggle flight mode: {e}") def toggle_caffeine(self, *_): try: if self.caffeine_mode: inhibit_script = get_relative_path("../../utils/inhibit.py") subprocess.run(["python3", inhibit_script, "off"], check=False) self.caffeine_mode = False if self._caffeine_process: try: self._caffeine_process.terminate() except Exception: pass self._caffeine_process = None else: inhibit_script = get_relative_path("../../utils/inhibit.py") self._caffeine_process = subprocess.Popen( ["python3", inhibit_script, "on"], start_new_session=True ) self.caffeine_mode = True self.caffeine_icon.dynamic_file( "applets/caffeine-on.svg" if self.caffeine_mode else "applets/caffeine-off.svg" ) self.caffeine_status_label.set_label("On" if self.caffeine_mode else "Off") except Exception as e: logger.warning(f"Failed to toggle caffeine: {e}") def set_volume(self, _, __, volume): if not self._signals_connected: return self._updating_volume = True audio_service.speaker.volume = round(volume) self._updating_volume = False def set_brightness(self, _, __, brightness): if not self._signals_connected: return 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 not self._signals_connected or 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): if not self._signals_connected: return False 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): if not self._signals_connected: return False 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 toggle_bluetooth(self, *_): if not self._resources_initialized: return try: if hasattr(self, "bluetooth_man") and hasattr(self.bluetooth_man, "client"): current_state = self.bluetooth_man.client.enabled set_bluetooth_enabled_with_fallback( self.bluetooth_man.client, not current_state ) else: logger.warning("Bluetooth client not available for toggling") except Exception as e: logger.warning(f"Failed to toggle bluetooth: {e}") def on_network_ready(self, *_): self.wifi_service = self.network_service.wifi_device if self.wifi_service: self.wifi_service.connect("notify::wireless-enabled", self.update_wifi_icon) def update_wifi_icon(self, *_): try: if self.wifi_service and hasattr(self, "wifi_svg"): is_enabled = self.wifi_service.wireless_enabled self.wifi_svg.dynamic_file( "applets/wifi.svg" if is_enabled else "applets/wifi-off.svg" ) except Exception as e: logger.warning(f"Failed to update WiFi icon: {e}") def toggle_wifi(self, *_): if not self._resources_initialized: return try: if self.wifi_service: self.wifi_service.toggle_wifi() GLib.timeout_add(100, self.update_wifi_icon) else: logger.warning("WiFi device not available for toggling") except Exception as e: logger.warning(f"Failed to toggle wifi: {e}") def set_children(self, children): self.children = children def open_bluetooth(self, *_): if not self._resources_initialized: return self._ensure_bluetooth_widgets() idle_add(lambda *_: self.set_children(self.bluetooth_center_box)) self.has_bluetooth_open = True def open_wifi(self, *_): if not self._resources_initialized: return self._ensure_wifi_widgets() idle_add(lambda *_: self.set_children(self.wifi_center_box)) self.has_wifi_open = True def close_bluetooth(self, *_): if self.current_view == "expanded_player": self._crossfade_to_view("main") else: idle_add(lambda *_: self.set_children(self.center_box)) self.has_bluetooth_open = False def close_wifi(self, *_): if self.current_view == "expanded_player": self._crossfade_to_view("main") else: idle_add(lambda *_: self.set_children(self.center_box)) self.has_wifi_open = False def open_per_app_volume(self, *_): if not self._resources_initialized: return self._ensure_per_app_volume_widgets() if self.current_view == "expanded_player": # If coming from expanded player, use crossfade self._crossfade_to_view("main") GLib.timeout_add( 250, lambda: idle_add( lambda *_: self.set_children(self.per_app_volume_center_box) ), ) else: 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, *_): if self.current_view == "expanded_player": self._crossfade_to_view("main") else: idle_add(lambda *_: self.set_children(self.center_box)) self.has_per_app_volume_open = False def open_expanded_player(self, *_): if not self._resources_initialized: return self._crossfade_to_view("expanded_player") self.has_expanded_player_open = True if self._expanded_player_widget: self._expanded_player_widget.refresh() def close_expanded_player(self, *_): # Just hide the expanded player widget, don't destroy it self._crossfade_to_view("main") self.has_expanded_player_open = False def _crossfade_to_view(self, view_name): """Handle transitions between views""" if view_name == "expanded_player": idle_add(lambda *_: self.set_children(self.expanded_player_center_box)) self.current_view = "expanded_player" elif view_name == "main": idle_add(lambda *_: self.set_children(self.center_box)) self.current_view = "main" def _set_mousecapture(self, visible: bool): if visible: GLib.idle_add(self._initialize_resources) 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 not self._signals_connected or self._updating_volume: return GLib.idle_add( lambda: self.volume_scale.set_value(int(audio_service.speaker.volume)) ) def wlan_changed(self, _, wlan): if not self._signals_connected: return self.wifi_svg.dynamic_file( "applets/wifi.svg" if wlan != "No Connection" else "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): if not self._signals_connected: return self.bluetooth_svg.dynamic_file( "applets/bluetooth.svg" if bluetooth != "disabled" else "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_label(device_name)) else: GLib.idle_add(lambda: self.bluetooth_label.set_label("Connected")) elif bluetooth == "enabled": GLib.idle_add(lambda: self.bluetooth_label.set_label("On")) else: GLib.idle_add(lambda: self.bluetooth_label.set_label("On")) else: GLib.idle_add(lambda: self.bluetooth_label.set_label("Off")) def audio_changed(self, *_): if not self._signals_connected: return pass def dnd_changed(self, _, dnd_state): if not self._signals_connected: return self.focus_mode = dnd_state self.focus_icon.dynamic_file( "applets/dnd.svg" if self.focus_mode else "applets/dnd-off.svg" ) 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): """Disconnect all signal connections to prevent memory leaks""" try: for connection in self._signal_connections: try: if connection and hasattr(connection, "disconnect"): connection.disconnect() except Exception as e: logger.warning(f"Failed to disconnect signal: {e}") self._signal_connections.clear() try: self.disconnect_by_func(self._on_visibility_changed) except Exception as e: logger.warning(f"Failed to disconnect visibility signal: {e}") try: self.disconnect_by_func(self.hide_controlcenter) except Exception as e: logger.warning(f"Failed to disconnect keybinding: {e}") if hasattr(self, "network_service") and self.network_service: try: self.network_service.disconnect_by_func(self.on_network_ready) except Exception as e: logger.warning(f"Failed to disconnect network signal: {e}") if hasattr(self, "brightness_service") and self.brightness_service: try: self.brightness_service.disconnect_by_func(self.brightness_changed) except Exception as e: logger.warning(f"Failed to disconnect brightness signal: {e}") if hasattr(self, "audio_service") and audio_service: try: audio_service.disconnect_by_func(self.audio_changed) audio_service.disconnect_by_func(self.volume_changed) except Exception as e: logger.warning(f"Failed to disconnect audio signals: {e}") if hasattr(self, "modus_service") and modus_service: try: modus_service.disconnect_by_func(self.wlan_changed) modus_service.disconnect_by_func(self.bluetooth_changed) modus_service.disconnect_by_func(self.dnd_changed) except Exception as e: logger.warning(f"Failed to disconnect modus signals: {e}") if hasattr(self, "volume_scale") and self.volume_scale: try: self.volume_scale.disconnect_by_func(self.set_volume) self.volume_scale.disconnect_by_func(self.on_volume_scroll) except Exception as e: logger.warning(f"Failed to disconnect volume scale signals: {e}") if hasattr(self, "brightness_scale") and self.brightness_scale: try: self.brightness_scale.disconnect_by_func(self.set_brightness) self.brightness_scale.disconnect_by_func(self.on_brightness_scroll) except Exception as e: logger.warning( f"Failed to disconnect brightness scale signals: {e}" ) self._signals_connected = False logger.debug("All signals disconnected successfully") except Exception as e: logger.warning(f"Signal disconnection failed: {e}") def _cleanup_managers(self): """Clean up all manager instances""" try: if hasattr(self, "wifi_man") and self.wifi_man: try: self.wifi_man.destroy() except Exception as e: logger.warning(f"Failed to destroy WiFi manager: {e}") self.wifi_man = None if hasattr(self, "bluetooth_man") and self.bluetooth_man: try: self.bluetooth_man.destroy() except Exception as e: logger.warning(f"Failed to destroy Bluetooth manager: {e}") self.bluetooth_man = None if hasattr(self, "network_service") and self.network_service: try: self.network_service.destroy() except Exception as e: logger.warning(f"Failed to destroy network service: {e}") self.network_service = None if hasattr(self, "wifi_service") and self.wifi_service: try: self.wifi_service.disconnect_by_func(self.update_wifi_icon) except Exception as e: logger.warning(f"Failed to disconnect WiFi service: {e}") self.wifi_service = None self._resources_initialized = False logger.debug("All managers cleaned up successfully") except Exception as e: logger.warning(f"Manager cleanup failed: {e}") def _cleanup_widgets(self): try: # Clean up main widgets if hasattr(self, "widgets") and self.widgets: try: self.widgets.destroy() except Exception as e: logger.warning(f"Failed to destroy main widgets: {e}") self.widgets = None # Clean up center box if hasattr(self, "center_box") and self.center_box: try: self.center_box.destroy() except Exception as e: logger.warning(f"Failed to destroy center box: {e}") self.center_box = None # Clean up individual widgets widget_attrs = [ "wlan_widget", "bluetooth_widget", "focus_widget", "flight_widget", "caffeine_widget", "night_light_widget", "volume_scale", "brightness_scale", "wlan_label", "bluetooth_label", "focus_status_label", "caffeine_status_label", "wlan_svg", "bluetooth_svg", "focus_icon", "flight_icon", "caffeine_icon", ] for attr in widget_attrs: if hasattr(self, attr) and getattr(self, attr): try: widget = getattr(self, attr) if hasattr(widget, "destroy"): widget.destroy() except Exception as e: logger.warning(f"Failed to destroy {attr}: {e}") setattr(self, attr, None) logger.debug("All widgets cleaned up successfully") except Exception as e: logger.warning(f"Widget cleanup failed: {e}") def _cleanup_processes(self): """Clean up any running processes""" try: # Clean up caffeine process if self._caffeine_process: try: self._caffeine_process.terminate() self._caffeine_process.wait(timeout=1) except Exception as e: logger.warning(f"Failed to terminate caffeine process: {e}") finally: self._caffeine_process = None logger.debug("All processes cleaned up successfully") except Exception as e: logger.warning(f"Process cleanup failed: {e}") def _complete_cleanup(self): """Perform complete cleanup of all resources""" try: logger.debug("Starting complete cleanup...") self._disconnect_all_signals() self._cleanup_managers() self._cleanup_widgets() self._cleanup_processes() self._disconnect_signals_when_hidden() # Force garbage collection import gc gc.collect() logger.debug("Complete cleanup finished successfully") except Exception as e: logger.error(f"Complete cleanup failed: {e}") def hide_controlcenter(self, *_): try: # Just disconnect signals when hiding, don't destroy widgets self._disconnect_signals_when_hidden() # Hide the control center if hasattr(self, "_mousecapture_parent"): self._mousecapture_parent.toggle_mousecapture() self.set_visible(False) except Exception as e: logger.error(f"Failed to hide control center: {e}") # Still try to hide even if cleanup fails self.set_visible(False) def destroy(self): try: self._complete_cleanup() super().destroy() logger.debug("Control center destroyed successfully") except Exception as e: logger.error(f"Failed to destroy control center: {e}") try: super().destroy() except Exception: pass