Files
Modus/src/window/controlcenter/main.py
T

1238 lines
46 KiB
Python

from fabric.utils import Gdk, GLib, idle_add, logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.eventbox import EventBox
from fabric.widgets.label import Label
from fabric.widgets.overlay import Overlay
from services.brightness import Brightness
from services.modus import is_caffeine_active, toggle_caffeine
from services.network import NetworkClient
from shared.widgets.flat_scale import FlatScale
from shared.window.applet_window import AppletWindow
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,
)
from window.controlcenter.expanded_player import EmbeddedExpandedPlayer
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
_brightness_instance: "Brightness | None" = None
def get_brightness_service() -> "Brightness":
global _brightness_instance
if _brightness_instance is None:
_brightness_instance = Brightness()
return _brightness_instance
class ModusControlCenter(AppletWindow):
def __init__(self, parent=None, pointing_to=None, **kwargs):
super().__init__(
parent=parent,
pointing_to=pointing_to,
layer="top",
title="modus-controlcenter",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
name="control-center-menu",
visible=False,
**kwargs,
)
self.focus_mode = modus_service.dont_disturb
self._updating_brightness = False
self._updating_volume = False
# Flight mode and caffeine states
self.flight_mode = False
self.caffeine_mode = False
# Loading flags - music and expanded player are no longer lazy loaded
self._per_app_volume_initialized = False
self._signals_connected = False # Track if signals are connected
self._resources_initialized = False # Track if resources are initialized
# Store references for cleanup - initialize all as None
self._signal_connections = []
self._music_widget_content = None
self._per_app_volume_widget = None
self._expanded_player_widget = None
self._mpris_manager = None # Shared MPRIS manager instance
# Initialize network service for WiFi toggle - lazy load
self.network_service = None
self.wifi_service = None
# Initialize flight mode and caffeine states - lazy load
self.caffeine_mode = False
self.flight_mode = False
# Add keybinding immediately (this is fast)
self.add_keybinding("Escape", self.hide_controlcenter)
volume = 100
# Get initial values and connect signals - store connection IDs for cleanup
wlan = modus_service.wlan if modus_service.wlan else "No Connection"
bluetooth = modus_service.bluetooth if modus_service.bluetooth else "Off"
self.wlan_label = Label(
label=wlan,
name="wifi-widget-label",
max_chars_width=15,
h_align="start",
ellipsization="end",
)
if bluetooth != "disabled":
if bluetooth.startswith("connected:"):
parts = bluetooth.split(":")
bluetooth_display = parts[1] if len(parts) >= 2 else "Connected"
else:
bluetooth_display = "On"
else:
bluetooth_display = "Off"
self.bluetooth_label = Label(
label=bluetooth_display,
name="bluetooth-widget-label",
max_chars_width=15,
ellipsization="end",
h_align="start",
)
self.volume_scale = FlatScale(
value=volume,
min_value=0,
max_value=100,
step=5,
name="volume-widget-slider",
size=30,
h_expand=True,
)
current_brightness = get_brightness_service().screen_brightness
brightness_percentage = (
int((current_brightness / get_brightness_service().max_screen) * 100)
if get_brightness_service().max_screen > 0
else 50
)
self.brightness_scale = FlatScale(
value=brightness_percentage,
min_value=0,
max_value=100,
step=5,
name="brightness-widget-slider",
size=30,
h_expand=True,
)
# Only connect brightness controls if brightness service is available
if get_brightness_service().max_screen > 0:
pass
else:
# Disable brightness scale if no backlight device available
self.brightness_scale.set_sensitive(False)
self._mpris_manager = get_shared_mpris_manager()
self.music_widget = PlayerBoxStack(self._mpris_manager, control_center=self)
self.has_bluetooth_open = False
self.has_wifi_open = False
self.has_per_app_volume_open = False
self.has_expanded_player_open = False
self.bluetooth_svg = svg_file(
(
"applets/bluetooth.svg"
if bluetooth != "disabled"
else "applets/bluetooth-off.svg"
),
size=42,
)
self.wifi_svg = svg_file(
"applets/wifi.svg" if wlan != "No Connection" else "applets/wifi-off.svg",
size=42,
)
self.bluetooth_widget = Box(
name="bluetooth-widget",
orientation="h",
children=[
Button(
name="bluetooth-icon-button",
child=self.bluetooth_svg,
on_clicked=self.toggle_bluetooth,
),
Button(
name="bluetooth-info-button",
child=Box(
name="bluetooth-widget-info",
orientation="vertical",
children=[
Label(
name="bluetooth-widget-name",
label="Bluetooth",
style_classes="ct",
h_align="start",
),
self.bluetooth_label,
],
),
on_clicked=self.open_bluetooth,
),
],
)
self.wlan_widget = Box(
name="wifi-widget",
orientation="h",
children=[
Button(
name="wifi-icon-button",
child=self.wifi_svg,
on_clicked=self.toggle_wifi,
),
Button(
name="wifi-info-button",
child=Box(
name="wifi-widget-info",
orientation="vertical",
children=[
Label(
name="wifi-widget-name",
label="Wi-Fi",
style_classes="ct",
h_align="start",
),
self.wlan_label,
],
),
on_clicked=self.open_wifi,
),
],
)
self.focus_icon = svg_file(
"applets/dnd.svg" if self.focus_mode else "applets/dnd-off.svg",
size=42,
)
self.focus_status_label = Label(
label="On" if self.focus_mode else "Off",
style_classes="status-label",
h_align="start",
)
self.focus_widget = Button(
name="focus-widget",
child=Box(
spacing=4,
orientation="h",
children=[
self.focus_icon,
Box(
v_expand=True,
h_expand=True,
v_align="center",
orientation="v",
h_align="start",
children=[
Label(
label="Focus",
style_classes="title-widget",
h_align="start",
),
self.focus_status_label,
],
),
],
),
on_clicked=self.set_dont_disturb,
)
self.flight_icon = svg_file(
"applets/flight-on.svg" if self.flight_mode else "applets/flight-off.svg",
size=42,
)
self.flight_widget = Button(
name="flight-widget",
child=Box(
orientation="v",
h_expand=True,
v_expand=True,
spacing=8,
h_align="center",
v_align="center",
children=[
self.flight_icon,
Label(
label="Flight",
style_classes="title-widget",
h_align="center",
),
],
),
on_clicked=self.toggle_flight_mode,
)
self.caffeine_icon = svg_file(
(
"applets/caffeine-on.svg"
if self.caffeine_mode
else "applets/caffeine-off.svg"
),
size=42,
)
self.caffeine_status_label = Label(
label="On" if self.caffeine_mode else "Off",
style_classes="status-label",
h_align="start",
)
self.caffeine_widget = Button(
name="caffeine-widget",
child=Box(
orientation="v",
spacing=8,
h_expand=True,
v_expand=True,
h_align="center",
v_align="center",
children=[
self.caffeine_icon,
Label(
label="Caffeine",
style_classes="title-widget",
h_align="center",
),
],
),
on_clicked=self.toggle_caffeine,
)
# Create night light widget
self.night_light_widget = create_night_light_widget(self)
# Create main widgets directly without XML - defer heavy operations
self.widgets = Box(
orientation="vertical",
h_expand=True,
name="control-center-widgets",
children=[
# Top section: Wi-Fi + Bluetooth on left, DND + Flight Mode + Caffeine on right
Box(
orientation="horizontal",
h_expand=True,
name="top-widget",
children=[
# Left side: Wi-Fi and Bluetooth
Box(
orientation="vertical",
name="wb-widget",
style_classes="menu",
spacing=5,
children=[
self.wlan_widget,
self.bluetooth_widget,
self.night_light_widget,
],
),
# Right side: DND, Flight Mode, and Caffeine
Box(
orientation="vertical",
h_expand=True,
name="right-side-widget",
children=[
# DND widget
Box(
orientation="vertical",
name="dnd-widget",
style_classes="menu",
children=[
self.focus_widget,
],
),
# Flight Mode and Caffeine row
Box(
orientation="horizontal",
name="flight-caffeine-row",
children=[
Box(
orientation="vertical",
name="flight-widget-container",
style_classes="menu",
h_expand=True,
v_expand=True,
children=[
self.flight_widget,
],
),
Box(
orientation="vertical",
name="caffeine-widget-container",
h_expand=True,
v_expand=True,
style_classes="menu",
children=[
self.caffeine_widget,
],
),
],
),
],
),
],
),
Box(
orientation="vertical",
name="brightness-widget",
style_classes="menu",
h_expand=True,
children=[
Label(label="Display", style_classes="title", h_align="start"),
Overlay(
h_expand=True,
child=self.brightness_scale,
overlays=[
svg_file(
"brightness/brightness.svg",
name="brightness-widget-icon",
size=28,
h_align="start",
v_align="center",
)
],
),
],
),
Box(
orientation="vertical",
name="volume-widget",
style_classes="menu",
h_expand=True,
children=[
Label(label="Sound", style_classes="title", h_align="start"),
Box(
name="vol-box",
orientation="horizontal",
spacing=12,
v_expand=True,
children=[
Box(
name="vol-slider-box",
h_expand=True,
v_align="center",
v_expand=False,
children=[
Overlay(
h_expand=True,
child=self.volume_scale,
overlays=[
svg_file(
"volume/audio-volume.svg",
name="volume-widget-icon",
size=28,
h_align="start",
v_align="center",
)
],
),
],
),
Button(
name="per-app-volume-button",
size=(36, 36),
child=svg_file(
"player/audio-switcher.svg",
name="per-app-volume-icon",
size=32,
),
on_clicked=self.open_per_app_volume,
),
],
),
],
),
self.music_widget,
],
)
# Initialize managers directly like working version
self.wifi_man = WifiConnections(self)
self.bluetooth_man = BluetoothConnections(self)
self.has_bluetooth_open = False
self.has_wifi_open = False
# Create expanded player widgets immediately (no lazy loading)
self._expanded_player_widget = EmbeddedExpandedPlayer(self)
self.expanded_player_widgets = Box(
orientation="vertical",
h_expand=True,
name="control-center-widgets",
children=[
self._expanded_player_widget,
],
)
# Lazy-loaded widgets - create placeholders for others
self.bluetooth_widgets = None
self.wifi_widgets = None
self.per_app_volume_widgets = None
# Create main content boxes
self.center_box = CenterBox(start_children=[self.widgets])
self.bluetooth_center_box = None
self.wifi_center_box = None
self.per_app_volume_center_box = None
# Create expanded player center box immediately
self.expanded_player_center_box = CenterBox(
start_children=[self.expanded_player_widgets]
)
self.expanded_player_center_box.set_size_request(300, -1)
# Create revealers for crossfade transitions
self.widgets.set_size_request(300, -1)
self.children = self.center_box
# Track current state for smooth transitions
self.current_view = "main" # main, expanded_player
# Connect to visibility changes for cleanup
self.connect("notify::visible", self._on_visibility_changed)
def _on_visibility_changed(self, widget, param):
"""Handle visibility changes for resource management"""
if not self.get_visible():
# 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:
return
try:
logger.debug("Initializing control center resources...")
# Initialize network service lazily
if self.network_service is None:
self.network_service = NetworkClient()
# Connect network signal
self.network_service.connect("device-ready", self.on_network_ready)
# Check initial states lazily (only when needed)
self._check_initial_states()
# 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),
),
]
)
# Connect brightness controls if brightness service is available
if get_brightness_service().max_screen > 0:
self.brightness_scale.connect("value-changed", self.set_brightness)
self.brightness_scale.connect("scroll-event", self.on_brightness_scroll)
self._signal_connections.append(
(
get_brightness_service(),
get_brightness_service().connect(
"screen", self.brightness_changed
),
)
)
# Connect volume scale signals
self.volume_scale.connect("value-changed", self.set_volume)
self.volume_scale.connect("scroll-event", self.on_volume_scroll)
# Mark signals as connected
self._signals_connected = True
self._resources_initialized = True
logger.debug("Control center resources initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize control center resources: {e}")
# Reset flags on failure
self._signals_connected = False
self._resources_initialized = False
def _disconnect_signals_when_hidden(self):
"""Disconnect signals when hidden to reduce resource usage, but keep widgets intact"""
try:
# 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 as e:
logger.error(f"An error occurred: {e}")
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 as e:
logger.error(f"An error occurred: {e}")
self._signals_connected = False
self._resources_initialized = False
# Reset state flags
self.has_bluetooth_open = False
self.has_wifi_open = False
self.has_per_app_volume_open = False
self.has_expanded_player_open = False
# Instantly reset the view
self._delayed_reset_view()
logger.debug("Control center signals disconnected while hidden")
except Exception as e:
logger.warning(f"Control center signal disconnection failed: {e}")
def _delayed_reset_view(self):
if not self.get_visible():
self.has_bluetooth_open = False
self.has_wifi_open = False
self.has_per_app_volume_open = False
self.has_expanded_player_open = False
self.current_view = "main"
self.set_children(self.center_box)
return False
def _ensure_bluetooth_widgets(self):
"""Lazy load bluetooth widgets"""
if self.bluetooth_widgets is None:
inner_box = Box(
orientation="vertical",
h_expand=True,
v_expand=True,
children=[self.bluetooth_man],
)
self.bluetooth_widgets = EventBox(
events="button-press-mask",
on_button_press_event=lambda *_: True,
child=inner_box,
)
self.bluetooth_center_box = Box(
h_expand=True, v_expand=True, children=[self.bluetooth_widgets]
)
self.bluetooth_center_box.set_size_request(350, -1)
def _ensure_wifi_widgets(self):
"""Lazy load wifi widgets"""
if self.wifi_widgets is None:
inner_box = Box(
orientation="vertical",
h_expand=True,
v_expand=True,
children=[
self.wifi_man,
],
)
self.wifi_widgets = EventBox(
events="button-press-mask",
on_button_press_event=lambda *_: True,
child=inner_box,
)
self.wifi_center_box = Box(
h_expand=True, v_expand=True, children=[self.wifi_widgets]
)
self.wifi_center_box.set_size_request(350, -1)
def _ensure_per_app_volume_widgets(self):
"""Lazy load per-app volume widgets"""
if self.per_app_volume_widgets is None:
if self._per_app_volume_widget is None:
self._per_app_volume_widget = PerAppVolumeControl(self)
self.per_app_volume_widgets = Box(
orientation="vertical",
h_expand=True,
name="control-center-widgets",
children=[
self._per_app_volume_widget,
],
)
self.per_app_volume_center_box = CenterBox(
start_children=[self.per_app_volume_widgets]
)
self.per_app_volume_center_box.set_size_request(300, -1)
def _check_initial_states(self):
self.caffeine_mode = is_caffeine_active()
self.flight_mode = False
GLib.timeout_add(100, self._update_initial_labels)
def _update_initial_labels(self):
return False
def set_dont_disturb(self, *_):
self.focus_mode = not self.focus_mode
modus_service.dont_disturb = self.focus_mode
self.focus_icon.dynamic_file(
"applets/dnd.svg" if self.focus_mode else "applets/dnd-off.svg"
)
self.focus_status_label.set_label("On" if self.focus_mode else "Off")
def toggle_flight_mode(self, *_):
try:
self.flight_mode = not self.flight_mode
if self.flight_mode:
# Turn off WiFi and Bluetooth
if self.wifi_service:
self.wifi_service.wireless_enabled = False
if hasattr(self, "bluetooth_man") and hasattr(
self.bluetooth_man, "client"
):
self.bluetooth_man.client.set_enabled(False)
else:
# Turn on WiFi and Bluetooth
if self.wifi_service:
self.wifi_service.wireless_enabled = True
if hasattr(self, "bluetooth_man") and hasattr(
self.bluetooth_man, "client"
):
self.bluetooth_man.client.set_enabled(True)
# Update icon
self.flight_icon.dynamic_file(
"applets/flight-on.svg"
if self.flight_mode
else "applets/flight-off.svg"
)
except Exception as e:
logger.warning(f"Failed to toggle flight mode: {e}")
def toggle_caffeine(self, *_):
try:
self.caffeine_mode = toggle_caffeine()
self.caffeine_icon.dynamic_file(
"applets/caffeine-on.svg"
if self.caffeine_mode
else "applets/caffeine-off.svg"
)
self.caffeine_status_label.set_label("On" if self.caffeine_mode else "Off")
except Exception as e:
logger.warning(f"Failed to toggle caffeine: {e}")
def set_volume(self, _, volume):
if not self._signals_connected:
return
self._updating_volume = True
audio_service.speaker.volume = round(volume)
self._updating_volume = False
def set_brightness(self, _, brightness):
if not self._signals_connected:
return
self._updating_brightness = True
brightness_value = int((brightness / 100) * get_brightness_service().max_screen)
get_brightness_service().screen_brightness = brightness_value
self._updating_brightness = False
def brightness_changed(self, _, brightness_value):
if not self._signals_connected or self._updating_brightness:
return
if get_brightness_service().max_screen > 0:
brightness_percentage = int(
(brightness_value / get_brightness_service().max_screen) * 100
)
GLib.idle_add(
lambda: self.brightness_scale.set_value(brightness_percentage)
)
def on_volume_scroll(self, widget, event):
if not self._signals_connected:
return False
current_value = self.volume_scale.get_value()
scroll_step = 5
if event.direction == Gdk.ScrollDirection.UP:
new_value = min(100, current_value + scroll_step)
elif event.direction == Gdk.ScrollDirection.DOWN:
new_value = max(0, current_value - scroll_step)
else:
return False
self.volume_scale.set_value(new_value)
return True
def on_brightness_scroll(self, widget, event):
if not self._signals_connected:
return False
current_value = self.brightness_scale.get_value()
scroll_step = 5
if event.direction == Gdk.ScrollDirection.UP:
new_value = min(100, current_value + scroll_step)
elif event.direction == Gdk.ScrollDirection.DOWN:
new_value = max(0, current_value - scroll_step)
else:
return False
self.brightness_scale.set_value(new_value)
return True
def toggle_bluetooth(self, *_):
if not self._resources_initialized:
return
try:
if hasattr(self, "bluetooth_man") and hasattr(self.bluetooth_man, "client"):
current_state = self.bluetooth_man.client.enabled
set_bluetooth_enabled_with_fallback(
self.bluetooth_man.client, not current_state
)
else:
logger.warning("Bluetooth client not available for toggling")
except Exception as e:
logger.warning(f"Failed to toggle bluetooth: {e}")
def on_network_ready(self, *_):
self.wifi_service = self.network_service.wifi_device
if self.wifi_service:
self.wifi_service.connect("notify::wireless-enabled", self.update_wifi_icon)
def update_wifi_icon(self, *_):
try:
if self.wifi_service and hasattr(self, "wifi_svg"):
is_enabled = self.wifi_service.wireless_enabled
self.wifi_svg.dynamic_file(
"applets/wifi.svg" if is_enabled else "applets/wifi-off.svg"
)
except Exception as e:
logger.warning(f"Failed to update WiFi icon: {e}")
def toggle_wifi(self, *_):
if not self._resources_initialized:
return
try:
if self.wifi_service:
self.wifi_service.toggle_wifi()
GLib.timeout_add(100, self.update_wifi_icon)
else:
logger.warning("WiFi device not available for toggling")
except Exception as e:
logger.warning(f"Failed to toggle wifi: {e}")
def set_children(self, children):
self.children = children
def open_bluetooth(self, *_):
if not self._resources_initialized:
return
self._ensure_bluetooth_widgets()
idle_add(lambda *_: self.set_children(self.bluetooth_center_box))
self.has_bluetooth_open = True
def open_wifi(self, *_):
if not self._resources_initialized:
return
self._ensure_wifi_widgets()
idle_add(lambda *_: self.set_children(self.wifi_center_box))
self.has_wifi_open = True
def close_bluetooth(self, *_):
try:
self.bluetooth_man.close_bluetooth()
except Exception as e:
logger.error(f"An error occurred: {e}")
if self.current_view == "expanded_player":
self._crossfade_to_view("main")
else:
idle_add(lambda *_: self.set_children(self.center_box))
self.has_bluetooth_open = False
def close_wifi(self, *_):
try:
self.wifi_man.close_wifi()
except Exception as e:
logger.error(f"An error occurred: {e}")
if self.current_view == "expanded_player":
self._crossfade_to_view("main")
else:
idle_add(lambda *_: self.set_children(self.center_box))
self.has_wifi_open = False
def open_per_app_volume(self, *_):
if not self._resources_initialized:
return
self._ensure_per_app_volume_widgets()
if self.current_view == "expanded_player":
# If coming from expanded player, use crossfade
self._crossfade_to_view("main")
GLib.timeout_add(
250,
lambda: idle_add(
lambda *_: self.set_children(self.per_app_volume_center_box)
),
)
else:
idle_add(lambda *_: self.set_children(self.per_app_volume_center_box))
self.has_per_app_volume_open = True
# Refresh the app list when opening
if self._per_app_volume_widget:
self._per_app_volume_widget.refresh()
def close_per_app_volume(self, *_):
if self.current_view == "expanded_player":
self._crossfade_to_view("main")
else:
idle_add(lambda *_: self.set_children(self.center_box))
self.has_per_app_volume_open = False
def open_expanded_player(self, *_):
if not self._resources_initialized:
return
self._crossfade_to_view("expanded_player")
self.has_expanded_player_open = True
if self._expanded_player_widget:
self._expanded_player_widget.refresh()
def close_expanded_player(self, *_):
# Just hide the expanded player widget, don't destroy it
self._crossfade_to_view("main")
self.has_expanded_player_open = False
def _crossfade_to_view(self, view_name):
"""Handle transitions between views"""
if view_name == "expanded_player":
idle_add(lambda *_: self.set_children(self.expanded_player_center_box))
self.current_view = "expanded_player"
elif view_name == "main":
idle_add(lambda *_: self.set_children(self.center_box))
self.current_view = "main"
def _set_mousecapture(self, visible: bool):
if visible:
GLib.idle_add(self._initialize_resources)
self.set_visible(visible)
if not visible:
# Simply reset views without multiple overlapping set_children calls
self.has_bluetooth_open = False
self.has_wifi_open = False
self.has_per_app_volume_open = False
self.has_expanded_player_open = False
self.current_view = "main"
self.set_children(self.center_box)
def volume_changed(
self,
_,
):
if not self._signals_connected or self._updating_volume:
return
GLib.idle_add(
lambda: self.volume_scale.set_value(int(audio_service.speaker.volume))
)
def wlan_changed(self, _, wlan):
if not self._signals_connected:
return
self.wifi_svg.dynamic_file(
"applets/wifi.svg" if wlan != "No Connection" else "applets/wifi-off.svg"
)
if wlan != "No Connection":
if wlan.startswith("connected:"):
parts = wlan.split(":")
if len(parts) >= 2:
wifi_name = parts[1]
GLib.idle_add(
lambda: self.wlan_label.set_property("label", wifi_name)
)
else:
GLib.idle_add(
lambda: self.wlan_label.set_property("label", "Connected")
)
else:
GLib.idle_add(lambda: self.wlan_label.set_property("label", wlan))
else:
GLib.idle_add(lambda: self.wlan_label.set_property("label", wlan))
def bluetooth_changed(self, _, bluetooth):
if not self._signals_connected:
return
self.bluetooth_svg.dynamic_file(
"applets/bluetooth.svg"
if bluetooth != "disabled"
else "applets/bluetooth-off.svg"
)
if bluetooth != "disabled":
if bluetooth.startswith("connected:"):
parts = bluetooth.split(":")
if len(parts) >= 2:
device_name = parts[1]
GLib.idle_add(lambda: self.bluetooth_label.set_label(device_name))
else:
GLib.idle_add(lambda: self.bluetooth_label.set_label("Connected"))
elif bluetooth == "enabled":
GLib.idle_add(lambda: self.bluetooth_label.set_label("On"))
else:
GLib.idle_add(lambda: self.bluetooth_label.set_label("On"))
else:
GLib.idle_add(lambda: self.bluetooth_label.set_label("Off"))
def audio_changed(self, *_):
if not self._signals_connected:
return
pass
def dnd_changed(self, _, dnd_state):
if not self._signals_connected:
return
self.focus_mode = dnd_state
self.focus_icon.dynamic_file(
"applets/dnd.svg" if self.focus_mode else "applets/dnd-off.svg"
)
self.focus_status_label.set_label("On" if self.focus_mode else "Off")
def _disconnect_all_signals(self):
"""Disconnect all signal connections to prevent memory leaks"""
try:
# _signal_connections stores (obj, handler_id) tuples
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 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 as e:
logger.error(f"An error occurred: {e}")
try:
self.volume_scale.disconnect_by_func(self.on_volume_scroll)
except Exception as e:
logger.error(f"An error occurred: {e}")
if hasattr(self, "brightness_scale") and self.brightness_scale:
try:
self.brightness_scale.disconnect_by_func(self.set_brightness)
except Exception as e:
logger.error(f"An error occurred: {e}")
try:
self.brightness_scale.disconnect_by_func(self.on_brightness_scroll)
except Exception as e:
logger.error(f"An error occurred: {e}")
# Disconnect the visibility change signal on self
try:
self.disconnect_by_func(self._on_visibility_changed)
except Exception as e:
logger.error(f"An error occurred: {e}")
self._signals_connected = False
logger.debug("All signals disconnected successfully")
except Exception as e:
logger.warning(f"Signal disconnection failed: {e}")
def _cleanup_managers(self):
"""Clean up all manager instances"""
try:
if hasattr(self, "wifi_man") and self.wifi_man:
try:
self.wifi_man.destroy()
except Exception as e:
logger.warning(f"Failed to destroy WiFi manager: {e}")
self.wifi_man = None
if hasattr(self, "bluetooth_man") and self.bluetooth_man:
try:
self.bluetooth_man.destroy()
except Exception as e:
logger.warning(f"Failed to destroy Bluetooth manager: {e}")
self.bluetooth_man = None
if hasattr(self, "network_service") and self.network_service:
try:
self.network_service.destroy()
except Exception as e:
logger.warning(f"Failed to destroy network service: {e}")
self.network_service = None
if hasattr(self, "wifi_service") and self.wifi_service:
try:
self.wifi_service.disconnect_by_func(self.update_wifi_icon)
except Exception as e:
logger.warning(f"Failed to disconnect WiFi service: {e}")
self.wifi_service = None
self._resources_initialized = False
logger.debug("All managers cleaned up successfully")
except Exception as e:
logger.warning(f"Manager cleanup failed: {e}")
def _cleanup_widgets(self):
try:
# Clean up main widgets
if hasattr(self, "widgets") and self.widgets:
try:
self.widgets.destroy()
except Exception as e:
logger.warning(f"Failed to destroy main widgets: {e}")
self.widgets = None
# Clean up center box
if hasattr(self, "center_box") and self.center_box:
try:
self.center_box.destroy()
except Exception as e:
logger.warning(f"Failed to destroy center box: {e}")
self.center_box = None
# Clean up individual widgets
widget_attrs = [
"wlan_widget",
"bluetooth_widget",
"focus_widget",
"flight_widget",
"caffeine_widget",
"night_light_widget",
"volume_scale",
"brightness_scale",
"wlan_label",
"bluetooth_label",
"focus_status_label",
"caffeine_status_label",
"wlan_svg",
"bluetooth_svg",
"focus_icon",
"flight_icon",
"caffeine_icon",
]
for attr in widget_attrs:
if hasattr(self, attr) and getattr(self, attr):
try:
widget = getattr(self, attr)
if hasattr(widget, "destroy"):
widget.destroy()
except Exception as e:
logger.warning(f"Failed to destroy {attr}: {e}")
setattr(self, attr, None)
logger.debug("All widgets cleaned up successfully")
except Exception as e:
logger.warning(f"Widget cleanup failed: {e}")
def _cleanup_processes(self):
logger.debug("All processes cleaned up successfully")
def _complete_cleanup(self):
"""Perform complete cleanup of all resources"""
try:
logger.debug("Starting complete cleanup...")
self._disconnect_all_signals()
self._cleanup_managers()
self._cleanup_widgets()
self._cleanup_processes()
self._disconnect_signals_when_hidden()
# Force garbage collection
import gc
gc.collect()
logger.debug("Complete cleanup finished successfully")
except Exception as e:
logger.error(f"Complete cleanup failed: {e}")
def hide_controlcenter(self, *_):
try:
# Just disconnect signals when hiding, don't destroy widgets
self._disconnect_signals_when_hidden()
# Hide the control center
self.set_visible(False)
self.set_visible(False)
except Exception as e:
logger.error(f"Failed to hide control center: {e}")
# Still try to hide even if cleanup fails
self.set_visible(False)
def destroy(self):
try:
self._complete_cleanup()
super().destroy()
logger.debug("Control center destroyed successfully")
except Exception as e:
logger.error(f"Failed to destroy control center: {e}")
try:
super().destroy()
except Exception as e:
logger.error(f"An error occurred: {e}")