From 7eb21daf2f561f9c8f57dd51ffb274652bc1c953 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 22 Apr 2026 21:18:39 +0545 Subject: [PATCH] hope it fix memory leak --- src/shared/styles/controlcenter.css | 61 ++++++++++++---- src/window/controlcenter/battery.py | 77 +++++++++++++++----- src/window/controlcenter/bluetooth.py | 77 +++++++++++++++----- src/window/controlcenter/main.py | 82 +++++++--------------- src/window/controlcenter/per_app_volume.py | 22 ++++-- src/window/controlcenter/wifi.py | 52 ++++++++++---- 6 files changed, 245 insertions(+), 126 deletions(-) diff --git a/src/shared/styles/controlcenter.css b/src/shared/styles/controlcenter.css index a60738d..863966f 100644 --- a/src/shared/styles/controlcenter.css +++ b/src/shared/styles/controlcenter.css @@ -3,6 +3,7 @@ font-family: "SF Pro Rounded"; font-weight: bold; } + #control-center-menu { background-color: transparent; border-radius: 12px; @@ -32,6 +33,7 @@ font-size: 16px; font-weight: bold; } + #control-center-widgets { background-color: alpha(#010101, 0.01); box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4); @@ -39,8 +41,9 @@ border-radius: 12px; padding: 0.5rem; } -#focus-widget { -} + +#focus-widget {} + #wb-widget, #brightness-menu { background-color: alpha(#000, 0.08); @@ -48,6 +51,7 @@ border: 1px solid alpha(#111, 0.4); border-radius: 12px; } + /* Widgets */ #wifi-widget, #bluetooth-widget, @@ -301,9 +305,11 @@ margin-top: -10px; /* border: 1px solid alpha(#fff, 1); */ } + #volume-widget-slider { /* margin-bottom: 2px; */ } + #volume-widget-icon { font-size: 22px; margin-top: -35px; @@ -344,6 +350,7 @@ /* min-height: 40px; */ /* margin: -9px; */ } + #control-center-menu scale { background-color: transparent; margin-top: 10px; @@ -377,7 +384,7 @@ /* Per-app volume control styles */ #per-app-volume-control { min-height: 140px; - min-width: 350px; + min-width: 328px; /* background-color: alpha(#000, 0.3); */ /* background-color: alpha(#999, 0.1); */ /* box-shadow: inset 0 0 200px 0 alpha(#111, 0.3); */ @@ -392,6 +399,7 @@ min-height: 120px; min-width: 120px; } + #back-button { padding: 8px 12px; background-color: alpha(#fff, 0.1); @@ -402,6 +410,7 @@ font-weight: 500; transition: background-color 0.2s ease; } + #back-button:hover { background-color: alpha(#fff, 0.18); } @@ -617,8 +626,10 @@ /* macOS-style expanded player - more compact */ #macos-outer-player-box { - min-height: 80px; /* Reduced height */ - min-width: 380px; /* Slightly wider to accommodate expanded track info */ + min-height: 80px; + /* Reduced height */ + min-width: 380px; + /* Slightly wider to accommodate expanded track info */ /* margin: 10px; */ padding: 10px; /* background-color: alpha(#000, 0.4); */ @@ -633,6 +644,7 @@ min-height: 70px; border-radius: 9px; } + #macos-album-image { min-width: 70px; min-height: 70px; @@ -651,6 +663,7 @@ background-size: cover; box-shadow: 0 2px 8px alpha(#000, 0.3); } + #macos-album-image image { min-width: 70px; min-height: 70px; @@ -686,6 +699,7 @@ color: alpha(#999, 0.8); margin-bottom: 8px; } + /* macOS seek bar styling */ #macos-seek-bar { background-color: transparent; @@ -735,17 +749,20 @@ /* macOS control buttons - more compact */ #macos-button-box { - margin-top: -1px; /* Reduced top margin */ + margin-top: -1px; + /* Reduced top margin */ } #macos-control-button { background: transparent; border: none; border-radius: 50%; - min-width: 28px; /* Slightly smaller */ + min-width: 28px; + /* Slightly smaller */ opacity: 0.7; min-height: 28px; - padding: 5px; /* Reduced padding */ + padding: 5px; + /* Reduced padding */ color: #ffffff; transition: all 0.15s ease; } @@ -758,10 +775,12 @@ background: transparent; border: none; border-radius: 50%; - min-width: 36px; /* Slightly smaller */ + min-width: 36px; + /* Slightly smaller */ opacity: 0.7; min-height: 36px; - padding: 7px; /* Reduced padding */ + padding: 7px; + /* Reduced padding */ color: #ffffff; transition: all 0.15s ease; } @@ -772,16 +791,19 @@ /* macOS player switcher dots - compact horizontal layout */ #macos-stack-buttons-box { - margin: 2px 5px; /* Minimal margins */ + margin: 2px 5px; + /* Minimal margins */ } .macos-switcher-dot { background: alpha(#fff, 0.3); /* border: none; */ border-radius: 50%; - min-width: 12px; /* Smaller dots */ + min-width: 12px; + /* Smaller dots */ min-height: 12px; - margin: 2px; /* Reduced margins */ + margin: 2px; + /* Reduced margins */ /* transition: all 0.2s ease; */ } @@ -791,7 +813,8 @@ .macos-switcher-dot.active { background: #ffffff; - box-shadow: 0 0 0 1px alpha(#fff, 0.3); /* Smaller glow */ + box-shadow: 0 0 0 1px alpha(#fff, 0.3); + /* Smaller glow */ } /* Per app-volume-item */ @@ -801,9 +824,11 @@ padding-right: 14px; border-bottom: 1px solid alpha(#fff, 0.1); } + #app-icon { margin-top: 8px; } + .app-name-compact { font-size: 14px; font-weight: bold; @@ -914,6 +939,7 @@ #wifi-other-button:hover { background-color: rgba(255, 255, 255, 0.1); } + #wifi-other-button:last-child { margin-bottom: 3px; } @@ -922,6 +948,7 @@ /* background-color: #000; */ border-radius: 8px; } + .device-slot:hover { background-color: rgba(255, 255, 255, 0.05); } @@ -935,6 +962,7 @@ font-size: 12px; font-weight: bold; } + #app-volume-header { font-size: 16px; padding-left: 5px; @@ -949,6 +977,7 @@ padding: 2px; /* margin-right: 8px; */ } + .wifi-icon-box-connected { background-color: #007aff; border-radius: 50%; @@ -967,6 +996,7 @@ font-family: "SF Pro Rounded"; font-weight: bold; } + .status-label { font-size: 11px; font-weight: 500; @@ -981,8 +1011,9 @@ #app-control-box { min-width: 100px; } + #caffeine-widget, #flight-widget { padding: 0px 0 5px 0; min-width: 50px; -} +} \ No newline at end of file diff --git a/src/window/controlcenter/battery.py b/src/window/controlcenter/battery.py index e8c32ec..6d175eb 100644 --- a/src/window/controlcenter/battery.py +++ b/src/window/controlcenter/battery.py @@ -61,9 +61,13 @@ class BluetoothDeviceSlot(CenterBox): def __init__(self, device: BluetoothDevice, **kwargs): super().__init__(h_expand=True, name="device-button", **kwargs) self.device = device - self.device.connect("changed", self.on_changed) - self.device.connect( - "notify::closed", lambda *_: self.device.closed and self.destroy() + self._destroyed = False + self._signal_ids = [] + self._signal_ids.append(self.device.connect("changed", self.on_changed)) + self._signal_ids.append( + self.device.connect( + "notify::closed", lambda *_: self.device.closed and self.destroy() + ) ) self.styles = [ @@ -112,6 +116,20 @@ class BluetoothDeviceSlot(CenterBox): self.device_button.connect("leave-notify-event", self.on_button_leave) self.device.emit("changed") # to update display status + def destroy(self): + """Clean up device signal connections to prevent memory leaks.""" + if self._destroyed: + return + self._destroyed = True + for sig_id in self._signal_ids: + try: + self.device.disconnect(sig_id) + except Exception: + pass + self._signal_ids.clear() + self.device = None + super().destroy() + def on_button_enter(self, widget, event): self.add_style_class("button-hovered") @@ -123,6 +141,8 @@ class BluetoothDeviceSlot(CenterBox): self.device.set_connecting(not self.device.connected) def on_changed(self, *_): + if self._destroyed or self.device is None: + return try: # Update connection and pairing status new_styles = [ @@ -197,6 +217,7 @@ class BluetoothConnections(Box): self.refresh_timer = None # Timer for periodic device refresh self._update_in_progress = False # Prevent concurrent updates self._destroyed = False # Track if widget is destroyed + self._client_signal_ids = [] # Track BluetoothClient signal IDs self.client = BluetoothClient(on_device_added=self.on_device_added) @@ -236,19 +257,29 @@ class BluetoothConnections(Box): ), ) - # Connect client signals - self.client.connect( - "notify::enabled", - lambda *_: self.toggle_button.set_active(self.client.enabled), + # Connect client signals — track IDs for disconnect on destroy + self._client_signal_ids.append( + self.client.connect( + "notify::enabled", + lambda *_: self.toggle_button.set_active(self.client.enabled), + ) + ) + self._client_signal_ids.append( + self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) ) - self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) # Connect to device changes - self.client.connect("device-added", self.update_devices) - self.client.connect("device-removed", self.update_devices) + self._client_signal_ids.append( + self.client.connect("device-added", self.update_devices) + ) + self._client_signal_ids.append( + self.client.connect("device-removed", self.update_devices) + ) # Connect to additional signals for better real-time monitoring - self.client.connect("changed", self.on_client_changed) + self._client_signal_ids.append( + self.client.connect("changed", self.on_client_changed) + ) # Create Devices section self.paired_devices_label = Label( @@ -473,20 +504,21 @@ class BluetoothConnections(Box): def periodic_device_refresh(self): """Periodically refresh device list to catch external connections""" - # Skip if update in progress, destroyed, or client not available + # If destroyed, remove the GLib source by returning False + if self._destroyed: + self.refresh_timer = None + return False + + # Skip if update in progress or client not available/enabled if ( self._update_in_progress - or self._destroyed or not self.client or not self.client.enabled ): return True # Continue monitoring try: - # Simple check - just trigger update_devices which has its own safety checks - # Don't force signal emissions as that can cause race conditions self.update_devices() - except Exception: pass @@ -513,13 +545,20 @@ class BluetoothConnections(Box): """Cleanup when widget is destroyed""" # Mark as destroyed to prevent further updates self._destroyed = True - # Stop monitoring + # Stop monitoring timer self.stop_device_monitoring() - # Make sure other devices revealer is collapsed when closing + # Disconnect all BluetoothClient signals + for sig_id in self._client_signal_ids: + try: + self.client.disconnect(sig_id) + except Exception: + pass + self._client_signal_ids.clear() + # Collapse other devices revealer try: self.other_devices_revealer.child_revealed = False except Exception: - pass # Widget might already be destroyed + pass def close_bluetooth(self): """Called when Bluetooth panel is being closed""" diff --git a/src/window/controlcenter/bluetooth.py b/src/window/controlcenter/bluetooth.py index 10fc050..972fd51 100644 --- a/src/window/controlcenter/bluetooth.py +++ b/src/window/controlcenter/bluetooth.py @@ -61,9 +61,13 @@ class BluetoothDeviceSlot(CenterBox): def __init__(self, device: BluetoothDevice, **kwargs): super().__init__(h_expand=True, name="device-button", **kwargs) self.device = device - self.device.connect("changed", self.on_changed) - self.device.connect( - "notify::closed", lambda *_: self.device.closed and self.destroy() + self._destroyed = False + self._signal_ids = [] + self._signal_ids.append(self.device.connect("changed", self.on_changed)) + self._signal_ids.append( + self.device.connect( + "notify::closed", lambda *_: self.device.closed and self.destroy() + ) ) self.styles = [ @@ -112,6 +116,20 @@ class BluetoothDeviceSlot(CenterBox): self.device_button.connect("leave-notify-event", self.on_button_leave) self.device.emit("changed") # to update display status + def destroy(self): + """Clean up device signal connections to prevent memory leaks.""" + if self._destroyed: + return + self._destroyed = True + for sig_id in self._signal_ids: + try: + self.device.disconnect(sig_id) + except Exception: + pass + self._signal_ids.clear() + self.device = None + super().destroy() + def on_button_enter(self, widget, event): self.add_style_class("button-hovered") @@ -123,6 +141,8 @@ class BluetoothDeviceSlot(CenterBox): self.device.set_connecting(not self.device.connected) def on_changed(self, *_): + if self._destroyed or self.device is None: + return try: # Update connection and pairing status new_styles = [ @@ -197,6 +217,7 @@ class BluetoothConnections(Box): self.refresh_timer = None # Timer for periodic device refresh self._update_in_progress = False # Prevent concurrent updates self._destroyed = False # Track if widget is destroyed + self._client_signal_ids = [] # Track BluetoothClient signal IDs self.client = BluetoothClient(on_device_added=self.on_device_added) @@ -236,19 +257,29 @@ class BluetoothConnections(Box): ), ) - # Connect client signals - self.client.connect( - "notify::enabled", - lambda *_: self.toggle_button.set_active(self.client.enabled), + # Connect client signals — track IDs for disconnect on destroy + self._client_signal_ids.append( + self.client.connect( + "notify::enabled", + lambda *_: self.toggle_button.set_active(self.client.enabled), + ) + ) + self._client_signal_ids.append( + self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) ) - self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) # Connect to device changes - self.client.connect("device-added", self.update_devices) - self.client.connect("device-removed", self.update_devices) + self._client_signal_ids.append( + self.client.connect("device-added", self.update_devices) + ) + self._client_signal_ids.append( + self.client.connect("device-removed", self.update_devices) + ) # Connect to additional signals for better real-time monitoring - self.client.connect("changed", self.on_client_changed) + self._client_signal_ids.append( + self.client.connect("changed", self.on_client_changed) + ) # Create Devices section self.paired_devices_label = Label( @@ -473,20 +504,21 @@ class BluetoothConnections(Box): def periodic_device_refresh(self): """Periodically refresh device list to catch external connections""" - # Skip if update in progress, destroyed, or client not available + # If destroyed, remove the GLib source by returning False + if self._destroyed: + self.refresh_timer = None + return False + + # Skip if update in progress or client not available/enabled if ( self._update_in_progress - or self._destroyed or not self.client or not self.client.enabled ): return True # Continue monitoring try: - # Simple check - just trigger update_devices which has its own safety checks - # Don't force signal emissions as that can cause race conditions self.update_devices() - except Exception: pass @@ -513,13 +545,20 @@ class BluetoothConnections(Box): """Cleanup when widget is destroyed""" # Mark as destroyed to prevent further updates self._destroyed = True - # Stop monitoring + # Stop monitoring timer self.stop_device_monitoring() - # Make sure other devices revealer is collapsed when closing + # Disconnect all BluetoothClient signals + for sig_id in self._client_signal_ids: + try: + self.client.disconnect(sig_id) + except Exception: + pass + self._client_signal_ids.clear() + # Collapse other devices revealer try: self.other_devices_revealer.child_revealed = False except Exception: - pass # Widget might already be destroyed + pass def close_bluetooth(self): """Called when Bluetooth panel is being closed""" diff --git a/src/window/controlcenter/main.py b/src/window/controlcenter/main.py index fe8646a..477eb90 100644 --- a/src/window/controlcenter/main.py +++ b/src/window/controlcenter/main.py @@ -509,14 +509,14 @@ class ModusControlCenter(Window): # Check initial states lazily (only when needed) self._check_initial_states() - # Store signal connections for cleanup + # Store signal connections as (obj, handler_id) tuples for proper 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), + (audio_service, audio_service.connect("changed", self.audio_changed)), + (audio_service, audio_service.connect("changed", self.volume_changed)), + (modus_service, modus_service.connect("wlan-changed", self.wlan_changed)), + (modus_service, modus_service.connect("bluetooth-changed", self.bluetooth_changed)), + (modus_service, modus_service.connect("dont-disturb-changed", self.dnd_changed)), ] ) @@ -525,7 +525,7 @@ class ModusControlCenter(Window): 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) + (brightness_service, brightness_service.connect("screen", self.brightness_changed)) ) # Connect volume scale signals @@ -970,70 +970,42 @@ class ModusControlCenter(Window): def _disconnect_all_signals(self): """Disconnect all signal connections to prevent memory leaks""" try: - for connection in self._signal_connections: + # _signal_connections stores (obj, handler_id) tuples + for obj, handler_id in self._signal_connections: try: - if connection and hasattr(connection, "disconnect"): - connection.disconnect() + obj.disconnect(handler_id) 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}") - + # Disconnect scale widget signals (connected directly, not tracked) if hasattr(self, "volume_scale") and self.volume_scale: try: self.volume_scale.disconnect_by_func(self.set_volume) + except Exception: + pass + try: self.volume_scale.disconnect_by_func(self.on_volume_scroll) - except Exception as e: - logger.warning(f"Failed to disconnect volume scale signals: {e}") + except Exception: + pass if hasattr(self, "brightness_scale") and self.brightness_scale: try: self.brightness_scale.disconnect_by_func(self.set_brightness) + except Exception: + pass + try: self.brightness_scale.disconnect_by_func(self.on_brightness_scroll) - except Exception as e: - logger.warning( - f"Failed to disconnect brightness scale signals: {e}" - ) + except Exception: + pass + + # Disconnect the visibility change signal on self + try: + self.disconnect_by_func(self._on_visibility_changed) + except Exception: + pass self._signals_connected = False - logger.debug("All signals disconnected successfully") except Exception as e: diff --git a/src/window/controlcenter/per_app_volume.py b/src/window/controlcenter/per_app_volume.py index bf7223b..3445b8a 100644 --- a/src/window/controlcenter/per_app_volume.py +++ b/src/window/controlcenter/per_app_volume.py @@ -26,6 +26,8 @@ class PerAppVolumeControl(Box): self._updating_volumes = set() self._app_widgets = {} self._signal_connections = [] + self._destroyed = False + self._refresh_timer = None # Header with back button self.header = Box( @@ -80,6 +82,9 @@ class PerAppVolumeControl(Box): def _auto_refresh(self): """Auto-refresh the application list every 2 seconds""" + if self._destroyed: + self._refresh_timer = None + return False # Remove GLib source self._populate_apps() return True # Continue the timer @@ -306,7 +311,8 @@ class PerAppVolumeControl(Box): def _on_stream_changed(self, *_): """Handle when audio streams are added or removed""" - GLib.idle_add(self._populate_apps) + if not self._destroyed: + GLib.idle_add(self._populate_apps) def refresh(self): """Manually refresh the application list""" @@ -314,10 +320,17 @@ class PerAppVolumeControl(Box): def destroy(self): """Clean up resources""" - if hasattr(self, "_refresh_timer"): - GLib.source_remove(self._refresh_timer) + self._destroyed = True - # Disconnect fabric audio service signals + # Stop the auto-refresh timer + if self._refresh_timer is not None: + try: + GLib.source_remove(self._refresh_timer) + except Exception: + pass + self._refresh_timer = None + + # Disconnect audio service signals if audio_service: for connection in self._signal_connections: try: @@ -328,5 +341,6 @@ class PerAppVolumeControl(Box): self._signal_connections.clear() self._app_widgets.clear() self._updating_volumes.clear() + self.control_center = None super().destroy() diff --git a/src/window/controlcenter/wifi.py b/src/window/controlcenter/wifi.py index 441d23f..10870bb 100644 --- a/src/window/controlcenter/wifi.py +++ b/src/window/controlcenter/wifi.py @@ -262,9 +262,12 @@ class WifiConnections(Box): self.refresh_timer = None # Timer for periodic network refresh self._update_in_progress = False # Prevent concurrent updates self._destroyed = False # Track if widget is destroyed + self._signal_ids = [] # Track all service signal IDs for cleanup - # Wait for network service to be ready - self.network_service.connect("wifi-device-added", self.on_network_ready) + # Wait for network service to be ready — track signal ID + self._signal_ids.append( + (self.network_service, self.network_service.connect("wifi-device-added", self.on_network_ready)) + ) # Create pull-to-refresh indicator self.refresh_indicator = Label( @@ -387,13 +390,21 @@ class WifiConnections(Box): 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 - self.wifi_service.connect( - "notify::wireless-enabled", self.on_wifi_enabled_changed + # Connect to WiFi service signals — track IDs for cleanup + self._signal_ids.append( + (self.wifi_service, self.wifi_service.connect( + "notify::wireless-enabled", self.on_wifi_enabled_changed + )) + ) + self._signal_ids.append( + (self.wifi_service, self.wifi_service.connect("changed", self.update_networks)) + ) + self._signal_ids.append( + (self.wifi_service, self.wifi_service.connect("ap-added", self.update_networks)) + ) + self._signal_ids.append( + (self.wifi_service, self.wifi_service.connect("ap-removed", self.update_networks)) ) - self.wifi_service.connect("changed", self.update_networks) - self.wifi_service.connect("ap-added", self.update_networks) - self.wifi_service.connect("ap-removed", self.update_networks) # Initial network update self.update_networks() @@ -581,17 +592,20 @@ class WifiConnections(Box): def periodic_network_refresh(self): """Periodically refresh network list to catch external connections""" - # Skip if update in progress, destroyed, or wifi service not available + # If destroyed, remove the GLib source by returning False + if self._destroyed: + self.refresh_timer = None + return False + + # Skip if update in progress or wifi not available/enabled if ( self._update_in_progress - or self._destroyed or not self.wifi_service or not self.wifi_service.wireless_enabled ): return True # Continue monitoring try: - # Simple check - just trigger update_networks which has its own safety checks self.update_networks() except Exception: pass @@ -692,13 +706,23 @@ class WifiConnections(Box): """Cleanup when widget is destroyed""" # Mark as destroyed to prevent further updates self._destroyed = True - # Stop monitoring + # Stop monitoring timer self.stop_network_monitoring() - # Make sure other networks revealer is collapsed when closing + # Disconnect all tracked service signals + for obj, sig_id in self._signal_ids: + try: + obj.disconnect(sig_id) + except Exception: + pass + self._signal_ids.clear() + # Drop strong references + self.wifi_service = None + self.network_service = None + # Collapse other networks revealer try: self.other_networks_revealer.child_revealed = False except Exception: - pass # Widget might already be destroyed + pass def close_wifi(self): """Called when WiFi panel is being closed"""