hope it fix memory leak

This commit is contained in:
sankalp committed 2026-04-22 21:18:39 +05:45
1 parent 19575b5138
commit 7eb21daf2f
6 files changed
+245 -126

No files matched your search

+58 -19
View File
@@ -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"""
+58 -19
View File
@@ -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"""
+27 -55
View File
@@ -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:
+18 -4
View File
@@ -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()
+38 -14
View File
@@ -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"""