related to memory leak

This commit is contained in:
sankalp committed 2026-04-23 16:09:52 +05:45
1 parent 7eb21daf2f
commit 6577c19dcb
25 files changed
+610 -68

No files matched your search

+13
View File
@@ -88,3 +88,16 @@ class ModusDropdown(PopupWindow):
return
self.set_visible(False)
modus_service.dropdowns_hide = not modus_service.dropdowns_hide
def destroy(self):
"""Clean up resources and global references"""
global dropdowns
if self in dropdowns:
dropdowns.remove(self)
try:
modus_service.disconnect_by_func(self.hide_dropdown)
except Exception:
pass
super().destroy()
+22
View File
@@ -299,3 +299,25 @@ class DropDownMouseCapture(MouseCapture):
if modus_service.current_dropdown == self.child_window.id:
return
return self.hide_child_window(widget, event)
def destroy(self):
"""Clean up signal connections"""
try:
modus_service.disconnect_by_func(self.dropdowns_hide_changed)
except Exception:
pass
super().destroy()
def add_destroy_to_mousecapture():
# Base MouseCapture
def mc_destroy(self):
# Child window should be destroyed by its owner, but we should null references
self.child_window = None
Window.destroy(self)
MouseCapture.destroy = mc_destroy
# Apply destroy to base class
add_destroy_to_mousecapture()
+1 -5
View File
@@ -510,11 +510,7 @@ class BluetoothConnections(Box):
return False
# Skip if update in progress or client not available/enabled
if (
self._update_in_progress
or not self.client
or not self.client.enabled
):
if self._update_in_progress or not self.client or not self.client.enabled:
return True # Continue monitoring
try:
+1 -5
View File
@@ -510,11 +510,7 @@ class BluetoothConnections(Box):
return False
# Skip if update in progress or client not available/enabled
if (
self._update_in_progress
or not self.client
or not self.client.enabled
):
if self._update_in_progress or not self.client or not self.client.enabled:
return True # Continue monitoring
try:
+41 -2
View File
@@ -184,6 +184,20 @@ class EmbeddedExpandedPlayer(Box):
# This will automatically update as MPRIS players change
pass
def suspend(self):
"""Suspend updates when control center is hidden"""
if hasattr(self, "player_stack") and self.player_stack:
for child in self.player_stack.get_children():
if hasattr(child, "suspend"):
child.suspend()
def resume(self):
"""Resume updates when control center is shown"""
if hasattr(self, "player_stack") and self.player_stack:
for child in self.player_stack.get_children():
if hasattr(child, "resume"):
child.resume()
def destroy(self):
"""Clean up resources and prevent memory leaks"""
logger.debug("🗑️ EmbeddedExpandedPlayer cleanup starting")
@@ -787,7 +801,9 @@ class PlayerBox(Box):
"label",
GObject.BindingFlags.DEFAULT,
lambda _, x: (
re.sub(r"\r?\n", " ", x) if x != "" and x is not None else "No Title"
re.sub(r"\r?\n", " ", x)
if x != "" and x is not None
else "No Title"
), # type: ignore
)
)
@@ -811,7 +827,9 @@ class PlayerBox(Box):
"label",
GObject.BindingFlags.DEFAULT,
lambda _, x: (
re.sub(r"\r?\n", " ", x) if x != "" and x is not None else "No Album"
re.sub(r"\r?\n", " ", x)
if x != "" and x is not None
else "No Album"
), # type: ignore
)
)
@@ -1097,6 +1115,27 @@ class PlayerBox(Box):
# Start new timer and store its ID
self._seekbar_timer_id = invoke_repeater(1000, self._move_seekbar)
def suspend(self):
"""Stop timer and other active updates when hidden"""
if self._seekbar_timer_id:
try:
GLib.source_remove(self._seekbar_timer_id)
except Exception:
pass
self._seekbar_timer_id = None
logger.debug(
f"[PlayerBox] Suspended timer for {self.player.player_name if self.player else 'unknown'}"
)
def resume(self):
"""Restart timer and updates when shown"""
if self.exit or self.player is None:
return
if not self._seekbar_timer_id:
self._seekbar_timer_id = invoke_repeater(1000, self._move_seekbar)
logger.debug(f"[PlayerBox] Resumed timer for {self.player.player_name}")
def _cleanup_temp_files(self):
"""Clean up temporary artwork files."""
for temp_file in self.temp_artwork_files:
+70 -11
View File
@@ -8,6 +8,10 @@ from fabric.widgets.label import Label
from fabric.widgets.scale import Scale
from fabric.widgets.wayland import WaylandWindow as Window
from services.brightness import Brightness
from services.network import NetworkClient
from utils.roam import audio_service, modus_service
from utils.utils import svg_file
from window.controlcenter.bluetooth import (
BluetoothConnections,
set_bluetooth_enabled_with_fallback,
@@ -17,10 +21,6 @@ from window.controlcenter.nightlight import create_night_light_widget
from window.controlcenter.per_app_volume import PerAppVolumeControl
from window.controlcenter.player import PlayerBoxStack, get_shared_mpris_manager
from window.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()
@@ -487,11 +487,27 @@ class ModusControlCenter(Window):
def _on_visibility_changed(self, widget, param):
"""Handle visibility changes for resource management"""
if not self.get_visible():
# Suspend player updates
if hasattr(self, "music_widget") and hasattr(self.music_widget, "suspend"):
self.music_widget.suspend()
if hasattr(self, "_expanded_player_widget") and hasattr(
self._expanded_player_widget, "suspend"
):
self._expanded_player_widget.suspend()
# Just disconnect signals and reset state flags - don't destroy widgets
self._disconnect_signals_when_hidden()
else:
self._initialize_resources()
# Resume player updates
if hasattr(self, "music_widget") and hasattr(self.music_widget, "resume"):
self.music_widget.resume()
if hasattr(self, "_expanded_player_widget") and hasattr(
self._expanded_player_widget, "resume"
):
self._expanded_player_widget.resume()
def _initialize_resources(self):
"""Initialize resources and connect signals when the control center becomes visible."""
if self._resources_initialized:
@@ -512,11 +528,28 @@ class ModusControlCenter(Window):
# Store signal connections as (obj, handler_id) tuples for proper cleanup
self._signal_connections.extend(
[
(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)),
(
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 +558,10 @@ 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, brightness_service.connect("screen", self.brightness_changed))
(
brightness_service,
brightness_service.connect("screen", self.brightness_changed),
)
)
# Connect volume scale signals
@@ -547,7 +583,28 @@ class ModusControlCenter(Window):
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
# Actually disconnect the signals we tracked
for obj, handler_id in self._signal_connections:
try:
obj.disconnect(handler_id)
except Exception as e:
logger.warning(f"Failed to disconnect signal: {e}")
self._signal_connections.clear()
# Disconnect direct scale signals (connected in _initialize_resources)
if hasattr(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:
pass
if hasattr(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:
pass
self._signals_connected = False
self._resources_initialized = False
@@ -560,6 +617,8 @@ class ModusControlCenter(Window):
# Reset current view
self.current_view = "main"
logger.debug("Control center signals disconnected while hidden")
except Exception as e:
logger.warning(f"Control center signal disconnection failed: {e}")
+6 -1
View File
@@ -170,7 +170,12 @@ class PerAppVolumeControl(Box):
def _populate_apps(self):
"""Populate the widget with current audio applications"""
# Clear existing widgets
# Clear and DESTROY existing widgets to prevent memory leaks
for child in list(self.apps_container.get_children()):
try:
child.destroy()
except Exception:
pass
self.apps_container.children = []
self._app_widgets.clear()
+48 -1
View File
@@ -259,6 +259,20 @@ class PlayerBoxStack(Box):
super().destroy()
def suspend(self):
"""Suspend all child players when hidden"""
for child in self.player_stack.get_children():
if hasattr(child, "suspend"):
child.suspend()
logger.debug("[PlayerBoxStack] Suspended child players")
def resume(self):
"""Resume all child players when shown"""
for child in self.player_stack.get_children():
if hasattr(child, "resume"):
child.resume()
logger.debug("[PlayerBoxStack] Resumed child players")
def _periodic_cleanup(self):
"""Enhanced cleanup for reuse - clean internal state and free memory"""
try:
@@ -776,7 +790,9 @@ class PlayerBox(Box):
"label",
GObject.BindingFlags.DEFAULT,
lambda _, x: (
re.sub(r"\r?\n", " ", x) if x != "" and x is not None else "No Title"
re.sub(r"\r?\n", " ", x)
if x != "" and x is not None
else "No Title"
), # type: ignore
)
)
@@ -904,6 +920,37 @@ class PlayerBox(Box):
for handler_id in connections:
self._signal_connections.append((self.player, handler_id))
def suspend(self):
"""Disconnect signals when hidden"""
self.exit = True
for obj, handler_id in self._signal_connections:
try:
obj.disconnect(handler_id)
except Exception:
pass
self._signal_connections.clear()
logger.debug(
f"[PlayerBox] Suspended signals for {self.player.player_name if self.player else 'unknown'}"
)
def resume(self):
"""Reconnect signals when shown"""
if self.player is None:
return
self.exit = False
if not self._signal_connections:
connections = bulk_connect(
self.player,
{
"closed": self._on_player_exit,
"notify::playback-status": self._on_playback_change,
"notify::metadata": self._on_metadata,
},
)
for handler_id in connections:
self._signal_connections.append((self.player, handler_id))
logger.debug(f"[PlayerBox] Resumed signals for {self.player.player_name}")
def destroy(self):
"""Clean up all resources when the widget is destroyed."""
# Set exit flag first — stops all callbacks from doing work
+24 -7
View File
@@ -266,7 +266,12 @@ class WifiConnections(Box):
# 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))
(
self.network_service,
self.network_service.connect(
"wifi-device-added", self.on_network_ready
),
)
)
# Create pull-to-refresh indicator
@@ -392,18 +397,30 @@ class WifiConnections(Box):
# 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.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.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.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,
self.wifi_service.connect("ap-removed", self.update_networks),
)
)
# Initial network update
+53 -8
View File
@@ -43,7 +43,12 @@ def http_get_json(
) -> Optional[Dict]:
"""Helper to perform GET requests and return JSON using built-in urllib."""
try:
req = urllib.request.Request(url, headers=headers or {})
# Some APIs require a User-Agent or they will return 403 Forbidden
default_headers = {"User-Agent": "Modus-Desktop/1.0"}
if headers:
default_headers.update(headers)
req = urllib.request.Request(url, headers=default_headers)
with urllib.request.urlopen(req, timeout=timeout) as response:
if response.status == 200:
return json.loads(response.read().decode("utf-8"))
@@ -53,13 +58,23 @@ def http_get_json(
def get_location() -> str:
"""Get current location using multiple IP geolocation APIs with fallback."""
"""Get current location from config or multiple IP geolocation APIs with fallback."""
# Try to get location from config first
try:
config = load_config()
manual_location = config.get("weather_location")
if manual_location:
return manual_location
except Exception:
pass
# Fallback to IP geolocation APIs
for api_url in LOCATION_APIS:
data = http_get_json(api_url, timeout=2)
if data:
city = data.get("city", "")
if city:
return city.replace(" ", "")
return city
print("All location APIs failed")
return ""
@@ -197,8 +212,14 @@ def update_weather(widget):
debounced_perform_fetch()
return True
GLib.timeout_add_seconds(WEATHER_UPDATE_INTERVAL, fetch_and_update)
fetch_and_update()
def initial_fetch():
debounced_perform_fetch()
return False
# Trigger first fetch immediately - MUST return False to not loop in idle
GLib.idle_add(initial_fetch)
return GLib.timeout_add_seconds(WEATHER_UPDATE_INTERVAL, fetch_and_update)
def update_widget(widget, weather_info):
@@ -221,9 +242,10 @@ class Weather(Box):
)
self.parent = parent
self.weatherinfo = None
self._weather_timer_id = None
self._create_labels()
self._layout_labels()
update_weather(self)
self._weather_timer_id = update_weather(self)
def _create_labels(self):
self.header = Box(orientation="h", h_expand=True)
@@ -291,6 +313,13 @@ class Weather(Box):
self.parent.set_visible(True)
def destroy(self):
"""Cleanup weather update timer"""
if self._weather_timer_id:
GLib.source_remove(self._weather_timer_id)
self._weather_timer_id = None
super().destroy()
class WeatherContainer(Box):
def __init__(self, **kwargs):
@@ -363,7 +392,16 @@ class Calendar(Box):
self.add(self.month_label)
self.add(self.days_header)
self.add(self.calendar_grid)
invoke_repeater(CALENDAR_UPDATE_INTERVAL, self.update_calendar_if_needed)
self._calendar_timer_id = invoke_repeater(
CALENDAR_UPDATE_INTERVAL, self.update_calendar_if_needed
)
def destroy(self):
"""Cleanup calendar update timer"""
if hasattr(self, "_calendar_timer_id") and self._calendar_timer_id:
GLib.source_remove(self._calendar_timer_id)
self._calendar_timer_id = None
super().destroy()
def _update_current_date(self):
now = datetime.datetime.now()
@@ -520,7 +558,14 @@ class SystemInfoBase(Box):
)
def start_updates(self):
invoke_repeater(SYSTEM_UPDATE_INTERVAL, self.update)
self._system_timer_id = invoke_repeater(SYSTEM_UPDATE_INTERVAL, self.update)
def destroy(self):
"""Cleanup system info update timer"""
if hasattr(self, "_system_timer_id") and self._system_timer_id:
GLib.source_remove(self._system_timer_id)
self._system_timer_id = None
super().destroy()
def create_info_line(
self, indicator_name: str, info_text: str, value_text: str
+25 -2
View File
@@ -84,9 +84,21 @@ class AppBar(Box):
logger.error(f"[AppBar] Error updating apps: {e}")
return True
GLib.timeout_add(250, update_running_apps)
self._app_monitor_timer_id = GLib.timeout_add(250, update_running_apps)
GLib.idle_add(self.update_dock_apps)
def destroy(self):
"""Clean up timers and children"""
if hasattr(self, "_app_monitor_timer_id") and self._app_monitor_timer_id:
GLib.source_remove(self._app_monitor_timer_id)
self._app_monitor_timer_id = None
# Destroy context menu
if self.menu:
self.menu.destroy()
super().destroy()
def _populate_pinned_apps(self):
clear_children(self.pinned_apps_container)
@@ -851,4 +863,15 @@ class Dock(Window):
return True
GLib.timeout_add(300, check_dock_occlusion)
self._occlusion_timer_id = GLib.timeout_add(300, check_dock_occlusion)
def destroy(self):
"""Clean up timers and children"""
if hasattr(self, "_occlusion_timer_id") and self._occlusion_timer_id:
GLib.source_remove(self._occlusion_timer_id)
self._occlusion_timer_id = None
if hasattr(self, "app_bar") and self.app_bar:
self.app_bar.destroy()
super().destroy()
+8 -2
View File
@@ -720,7 +720,10 @@ class Launcher(Window):
# Clear existing results
for child in self.results_box.get_children():
self.results_box.remove(child)
if isinstance(child, ResultItem):
child.destroy()
else:
self.results_box.remove(child)
# Add new results
for i, result in enumerate(self.results):
@@ -790,7 +793,10 @@ class Launcher(Window):
self.results = []
self.selected_index = 0
for child in self.results_box.get_children():
self.results_box.remove(child)
if isinstance(child, ResultItem):
child.destroy()
else:
self.results_box.remove(child)
# Keep the results scroll visible even when empty
def _handle_escape_key(self) -> bool:
+26 -19
View File
@@ -95,29 +95,36 @@ class OTPPlugin(PluginBase):
def _selective_force_refresh(self):
"""Update time display in existing OTP result items."""
try:
import gc
from fabric import Application
def do_update():
try:
for obj in gc.get_objects():
if (
hasattr(obj, "__class__")
and obj.__class__.__name__ == "Launcher"
and hasattr(obj, "results_box")
and hasattr(obj, "visible")
and obj.visible
and hasattr(obj, "results")
and obj.results
):
has_otp_results = any(
result.data and result.data.get("type") == "totp"
for result in obj.results
if hasattr(result, "data") and result.data
)
app = Application.get_default()
if not app:
return False
if has_otp_results:
self._update_existing_result_labels(obj.results_box)
return False
launcher = None
for window in app.get_windows():
if window.get_name() == "launcher-window":
launcher = window
break
if (
launcher
and hasattr(launcher, "results_box")
and hasattr(launcher, "visible")
and launcher.visible
and hasattr(launcher, "results")
and launcher.results
):
has_otp_results = any(
result.data and result.data.get("type") == "totp"
for result in launcher.results
if hasattr(result, "data") and result.data
)
if has_otp_results:
self._update_existing_result_labels(launcher.results_box)
except Exception:
pass
return False
+29 -3
View File
@@ -771,6 +771,7 @@ class NotificationRevealer(SlideRevealer):
# Animation state
self._animation_in_progress = False
self._spring_timer_id = None
self._anim_timeout_id = None
self._css_provider = None
# Wrap notification in EventBox for swipe detection
@@ -791,8 +792,8 @@ class NotificationRevealer(SlideRevealer):
smooth_revealer_animation(self)
# Connect our own handler that manages the slide animation
self.notification.connect("closed", self.on_resolved)
# Connect our own handler that manages the slide animation - track ID for cleanup
self._closed_handler_id = self.notification.connect("closed", self.on_resolved)
self._animation_in_progress = True
@@ -924,12 +925,37 @@ class NotificationRevealer(SlideRevealer):
self.hide()
# Consistent timing for smooth transitions
timeout_duration = self.duration + 50
GLib.timeout_add(timeout_duration, lambda: self._on_animation_complete(True))
self._anim_timeout_id = GLib.timeout_add(
timeout_duration, lambda: self._on_animation_complete(True)
)
def destroy(self):
# Clean up CSS provider and timers
if self._spring_timer_id:
GLib.source_remove(self._spring_timer_id)
self._spring_timer_id = None
if self._anim_timeout_id:
GLib.source_remove(self._anim_timeout_id)
self._anim_timeout_id = None
# Disconnect notification signal
if hasattr(self, "_closed_handler_id") and self._closed_handler_id:
try:
self.notification.disconnect(self._closed_handler_id)
except Exception:
pass
self._closed_handler_id = 0
# Clean up CSS provider from style context
if self._css_provider:
try:
style_context = self.notif_box.get_style_context()
if style_context:
style_context.remove_provider(self._css_provider)
except Exception:
pass
self._css_provider = None
super().destroy()
+10
View File
@@ -98,3 +98,13 @@ class AudioOSDContainer(BaseOSDContainer):
def update(self, *_):
self._update_display()
super().update()
def destroy(self):
"""Disconnect signals from Audio service"""
try:
self.audio.disconnect_by_func(self._on_speaker_changed)
if self.audio.speaker:
self.audio.speaker.disconnect_by_func(self._on_speaker_stream_changed)
except Exception:
pass
super().destroy()
+4
View File
@@ -29,6 +29,10 @@ class BaseOSDContainer(Box):
self.window.connect("leave-notify-event", self._on_leave_notify)
return False
def destroy(self):
self.cleanup_all_handlers()
super().destroy()
def _on_enter_notify(self, widget, event):
if event.detail != Gdk.NotifyType.INFERIOR:
self._is_hovered = True
+10
View File
@@ -57,3 +57,13 @@ class BrightnessOSDContainer(BaseOSDContainer):
def update(self, *_):
self._update_display()
super().update()
def destroy(self):
"""Disconnect signals from Brightness service"""
try:
self.brightness_service.disconnect_by_func(
self._on_screen_brightness_changed
)
except Exception:
pass
super().destroy()
+8
View File
@@ -45,3 +45,11 @@ class CapsLockOSDContainer(BaseOSDContainer):
def update(self, *_):
# We don't need a separate display update here as it's handled in the signal
super().update()
def destroy(self):
"""Disconnect signals from CapsLock service"""
try:
self.capslock.disconnect_by_func(self._on_caps_lock_state_changed)
except Exception:
pass
super().destroy()
@@ -37,3 +37,11 @@ class KeyboardLayoutOSDContainer(BaseOSDContainer):
def update(self, *_):
self.label.set_text(self.keyboard_layout.current_layout)
super().update()
def destroy(self):
"""Disconnect signals from KeyboardLayout service"""
try:
self.keyboard_layout.disconnect_by_func(self._on_layout_changed)
except Exception:
pass
super().destroy()
+12
View File
@@ -98,3 +98,15 @@ class MicrophoneOSDContainer(BaseOSDContainer):
def update(self, *_):
self._update_display()
super().update()
def destroy(self):
"""Disconnect signals from Audio service"""
try:
self.audio.disconnect_by_func(self._on_microphone_changed)
if self.audio.microphone:
self.audio.microphone.disconnect_by_func(
self._on_microphone_stream_changed
)
except Exception:
pass
super().destroy()
+35
View File
@@ -307,6 +307,32 @@ class GlobalMenuDropdowns:
active_button = self.dropdown_button_map.get(dropdown_id)
manage_button_style_classes(self.all_menu_buttons, active_button)
def destroy(self):
"""Clean up all dropdowns and signal connections"""
try:
modus_service.disconnect_by_func(self._on_active_app_changed)
modus_service.disconnect_by_func(self.changed_dropdown)
modus_service.disconnect_by_func(self.hide_dropdowns)
except Exception:
pass
# Destroy all dropdown captures
dropdown_captures = [
getattr(self, "menu_button_dropdown", None),
getattr(self, "global_menu_title", None),
getattr(self, "global_menu_view", None),
getattr(self, "global_menu_window", None),
getattr(self, "global_menu_help", None),
]
for capture in dropdown_captures:
try:
if capture and hasattr(capture, "destroy"):
capture.destroy()
except Exception:
pass
super().destroy()
class GlobalMenu(Box):
def __init__(self, parent_window=None, **kwargs):
@@ -335,3 +361,12 @@ class GlobalMenu(Box):
)
mouse_capture = self.dropdown_system.menu_button_dropdown
mouse_capture.set_child_window_visible(not mouse_capture.is_visible())
def destroy(self):
"""Clean up the global menu and its dropdowns"""
if hasattr(self, "dropdown_system") and self.dropdown_system:
try:
self.dropdown_system.destroy()
except Exception:
pass
super().destroy()
+99
View File
@@ -457,3 +457,102 @@ class BatteryIndicator(Box):
def hide_controlcenter(self, *args):
hide_control_center(self.battery_mousecapture)
def destroy(self):
"""Clean up resources when the indicator is destroyed."""
# Disconnect signals
try:
modus_service.disconnect_by_func(self.on_battery_changed)
if hasattr(self, "battery_service") and self.battery_service:
self.battery_service.disconnect_by_func(self.on_battery_direct_changed)
except Exception:
pass
# Destroy window and widget
try:
if hasattr(self, "battery_widget") and self.battery_widget:
self.battery_widget.destroy()
if hasattr(self, "battery_window") and self.battery_window:
self.battery_window.destroy()
if hasattr(self, "battery_mousecapture") and self.battery_mousecapture:
self.battery_mousecapture.destroy()
except Exception:
pass
super().destroy()
# Add destroy methods to other classes in this file as well
def add_destroy_to_indicators():
# BluetoothIndicator
def bt_destroy(self):
try:
modus_service.disconnect_by_func(self.on_bluetooth_changed)
if hasattr(self, "bluetooth") and self.bluetooth:
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_removed)
except Exception:
pass
try:
if hasattr(self, "bluetooth_widget") and self.bluetooth_widget:
self.bluetooth_widget.destroy()
if hasattr(self, "bluetooth_window") and self.bluetooth_window:
self.bluetooth_window.destroy()
if hasattr(self, "bluetooth_mousecapture") and self.bluetooth_mousecapture:
self.bluetooth_mousecapture.destroy()
except Exception:
pass
Box.destroy(self)
BluetoothIndicator.destroy = bt_destroy
# NetworkIndicator
def net_destroy(self):
try:
modus_service.disconnect_by_func(self.on_wlan_changed)
if hasattr(self, "network_service") and self.network_service:
self.network_service.disconnect_by_func(self.on_wifi_device_added)
self.network_service.disconnect_by_func(self.on_ethernet_device_added)
self.network_service.disconnect_by_func(self.on_network_changed)
# Also disconnect from the devices themselves if they exist
if (
hasattr(self.network_service, "wifi_device")
and self.network_service.wifi_device
):
try:
self.network_service.wifi_device.disconnect_by_func(
self.on_network_direct_changed
)
except Exception:
pass
if (
hasattr(self.network_service, "ethernet_device")
and self.network_service.ethernet_device
):
try:
self.network_service.ethernet_device.disconnect_by_func(
self.on_network_direct_changed
)
except Exception:
pass
except Exception:
pass
try:
if hasattr(self, "wifi_widget") and self.wifi_widget:
self.wifi_widget.destroy()
if hasattr(self, "wifi_window") and self.wifi_window:
self.wifi_window.destroy()
if hasattr(self, "wifi_mousecapture") and self.wifi_mousecapture:
self.wifi_mousecapture.destroy()
except Exception:
pass
Box.destroy(self)
NetworkIndicator.destroy = net_destroy
# Apply the destroy methods
add_destroy_to_indicators()
+15 -1
View File
@@ -69,7 +69,9 @@ class WorkspaceIndicator(Box):
def update_config(self, new_config: dict):
if "hide_special_workspace" in new_config:
button_factory = self._get_button_factory()
self.remove(self.workspaces)
if hasattr(self, "workspaces") and self.workspaces:
self.workspaces.destroy()
self.workspaces = HyprlandWorkspaces(
name="workspaces",
spacing=4,
@@ -77,3 +79,15 @@ class WorkspaceIndicator(Box):
)
self.add(self.workspaces)
self.workspaces.show_all()
def destroy(self):
try:
from services.config import _config_handlers
if self._on_config_changed in _config_handlers:
_config_handlers.remove(self._on_config_changed)
except Exception:
pass
if hasattr(self, "workspaces") and self.workspaces:
self.workspaces.destroy()
Box.destroy(self)
+41
View File
@@ -188,6 +188,14 @@ class Panel(Window):
self.left_box.children = left_children
# Indicators (create fresh instances on each rebuild)
# First, destroy old indicators to prevent memory leaks
for child in list(self.indicators.get_children()):
try:
child.destroy()
except Exception:
pass
self.indicators.children = []
indicators_children = []
if get_config("battery", True):
indicators_children.append(BatteryIndicator())
@@ -267,3 +275,36 @@ class Panel(Window):
self.chevron_button.get_child().dynamic_file("misc/chevron-left.svg")
else:
self.chevron_button.get_child().dynamic_file("misc/chevron-right.svg")
def destroy(self):
"""Clean up all signals and components"""
try:
modus_service.disconnect_by_func(self.on_dnd_changed)
notification_service.disconnect_by_func(self.on_notification_count_changed)
from services.config import _config_handlers
if self._on_config_changed in _config_handlers:
_config_handlers.remove(self._on_config_changed)
except Exception:
pass
# Destroy components
for component in [
self.globalmenu,
self.workspace_indicator,
self.recording_indicator,
self.indicators,
]:
try:
component.destroy()
except Exception:
pass
# Destroy MouseCapture windows
for mc in [self.control_center, self.notification_center]:
try:
mc.destroy()
except Exception:
pass
super().destroy()
+1 -1
View File
@@ -109,7 +109,7 @@ class ApplicationSwitcher(Window):
def update_windows(self) -> None:
for child in self.view.get_children():
self.view.remove(child)
child.destroy()
try:
clients_data = self.conn.send_command("j/clients").reply