related to memory leak
This commit is contained in:
25 files changed
+610
-68
No files matched your search
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user