Files
Modus/modules/controlcenter/main.py
T
sankalp e97311e2b0 feat(modules): restructure battery UI and add dynamic layout rebuilding
New Features
- Introduce `get_battery_icon_file` to generate battery icons based on percentage and charging state.
- Add fallback Bluetooth control using `rfkill` when Fabric Bluetooth fails.

Enhancements
- Refactor layout management with `_rebuild_layout_from_config` and dynamic indicator instantiation.
- Replace large `EnergyModeButton` class with lightweight utility functions.
- Simplify notification icon updates using `dynamic_file`.

Bug Fixes
- No explicit bug fixes; changes focus on feature additions and refactoring.
2026-02-26 06:24:44 +05:45

1198 lines
44 KiB
Python

import subprocess
from fabric.utils import Gdk, GLib, get_relative_path, idle_add, logger
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.label import Label
from fabric.widgets.scale import Scale
from fabric.widgets.wayland import WaylandWindow as Window
from modules.controlcenter.bluetooth import (
BluetoothConnections,
set_bluetooth_enabled_with_fallback,
)
from modules.controlcenter.expanded_player import EmbeddedExpandedPlayer
from modules.controlcenter.nightlight import create_night_light_widget
from modules.controlcenter.per_app_volume import PerAppVolumeControl
from modules.controlcenter.player import PlayerBoxStack, get_shared_mpris_manager
from modules.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()
class ModusControlCenter(Window):
def __init__(self, **kwargs):
super().__init__(
layer="top",
title="modus",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
keyboard_mode="on-demand",
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
self._caffeine_process = None
# 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 = Scale(
value=volume,
min_value=0,
max_value=100,
increments=(5, 5),
name="volume-widget-slider",
size=30,
h_expand=True,
)
current_brightness = brightness_service.screen_brightness
brightness_percentage = (
int((current_brightness / brightness_service.max_screen) * 100)
if brightness_service.max_screen > 0
else 50
)
self.brightness_scale = Scale(
value=brightness_percentage,
min_value=0,
max_value=100,
increments=(5, 5),
name="brightness-widget-slider",
size=30,
h_expand=True,
)
# Only connect brightness controls if brightness service is available
if 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"),
self.brightness_scale,
Label(
label="󰖨 ", name="brightness-widget-icon", h_align="start"
),
],
),
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=[
self.volume_scale,
],
),
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,
),
],
),
Label(label=" ", name="volume-widget-icon", h_align="start"),
],
),
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():
# Just disconnect signals and reset state flags - don't destroy widgets
self._disconnect_signals_when_hidden()
else:
self._initialize_resources()
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("wifi-device-added", self.on_network_ready)
# Check initial states lazily (only when needed)
self._check_initial_states()
# Store signal connections for cleanup
self._signal_connections.extend(
[
audio_service.connect("changed", self.audio_changed),
audio_service.connect("changed", self.volume_changed),
modus_service.connect("wlan-changed", self.wlan_changed),
modus_service.connect("bluetooth-changed", self.bluetooth_changed),
modus_service.connect("dont-disturb-changed", self.dnd_changed),
]
)
# Connect brightness controls if brightness service is available
if brightness_service.max_screen > 0:
self.brightness_scale.connect("change-value", self.set_brightness)
self.brightness_scale.connect("scroll-event", self.on_brightness_scroll)
self._signal_connections.append(
brightness_service.connect("screen", self.brightness_changed)
)
# Connect volume scale signals
self.volume_scale.connect("change-value", 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:
# Just disconnect signals and reset flags - don't destroy widgets
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
# Reset current view
self.current_view = "main"
except Exception as e:
logger.warning(f"Control center signal disconnection failed: {e}")
def _ensure_bluetooth_widgets(self):
"""Lazy load bluetooth widgets"""
if self.bluetooth_widgets is None:
self.bluetooth_widgets = Box(
orientation="vertical",
h_expand=True,
v_expand=True,
children=[self.bluetooth_man],
)
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:
self.wifi_widgets = Box(
orientation="vertical",
h_expand=True,
v_expand=True,
children=[
self.wifi_man,
],
)
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):
# Check if caffeine is already running - use faster method
try:
# Use faster check with timeout
result = subprocess.run(
["pgrep", "-f", "modus-inhibit"],
capture_output=True,
text=True,
timeout=0.5, # Add timeout to prevent hanging
)
self.caffeine_mode = bool(result.stdout.strip())
except (subprocess.TimeoutExpired, Exception):
self.caffeine_mode = False
# Flight mode starts as False (normal mode)
self.flight_mode = False
# Update initial labels (will be set after widgets are created)
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:
if self.caffeine_mode:
inhibit_script = get_relative_path("../../utils/inhibit.py")
subprocess.run(["python3", inhibit_script, "off"], check=False)
self.caffeine_mode = False
if self._caffeine_process:
try:
self._caffeine_process.terminate()
except:
pass
self._caffeine_process = None
else:
inhibit_script = get_relative_path("../../utils/inhibit.py")
self._caffeine_process = subprocess.Popen(
["python3", inhibit_script, "on"], start_new_session=True
)
self.caffeine_mode = True
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) * brightness_service.max_screen)
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 brightness_service.max_screen > 0:
brightness_percentage = int(
(brightness_value / 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, *_):
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, *_):
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:
self.close_bluetooth()
self.close_wifi()
self.close_per_app_volume()
self.close_expanded_player()
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 _init_mousecapture(self, mousecapture):
self._mousecapture_parent = mousecapture
def _disconnect_all_signals(self):
"""Disconnect all signal connections to prevent memory leaks"""
try:
for connection in self._signal_connections:
try:
if connection and hasattr(connection, "disconnect"):
connection.disconnect()
except Exception as e:
logger.warning(f"Failed to disconnect signal: {e}")
self._signal_connections.clear()
try:
self.disconnect_by_func(self._on_visibility_changed)
except Exception as e:
logger.warning(f"Failed to disconnect visibility signal: {e}")
try:
self.disconnect_by_func(self.hide_controlcenter)
except Exception as e:
logger.warning(f"Failed to disconnect keybinding: {e}")
if hasattr(self, "network_service") and self.network_service:
try:
self.network_service.disconnect_by_func(self.on_network_ready)
except Exception as e:
logger.warning(f"Failed to disconnect network signal: {e}")
if hasattr(self, "brightness_service") and self.brightness_service:
try:
self.brightness_service.disconnect_by_func(self.brightness_changed)
except Exception as e:
logger.warning(f"Failed to disconnect brightness signal: {e}")
if hasattr(self, "audio_service") and audio_service:
try:
audio_service.disconnect_by_func(self.audio_changed)
audio_service.disconnect_by_func(self.volume_changed)
except Exception as e:
logger.warning(f"Failed to disconnect audio signals: {e}")
if hasattr(self, "modus_service") and modus_service:
try:
modus_service.disconnect_by_func(self.wlan_changed)
modus_service.disconnect_by_func(self.bluetooth_changed)
modus_service.disconnect_by_func(self.dnd_changed)
except Exception as e:
logger.warning(f"Failed to disconnect modus signals: {e}")
if hasattr(self, "volume_scale") and 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.warning(f"Failed to disconnect volume scale signals: {e}")
if hasattr(self, "brightness_scale") and 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.warning(
f"Failed to disconnect brightness scale signals: {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):
"""Clean up any running processes"""
try:
# Clean up caffeine process
if self._caffeine_process:
try:
self._caffeine_process.terminate()
self._caffeine_process.wait(timeout=1)
except Exception as e:
logger.warning(f"Failed to terminate caffeine process: {e}")
finally:
self._caffeine_process = None
logger.debug("All processes cleaned up successfully")
except Exception as e:
logger.warning(f"Process cleanup failed: {e}")
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
if hasattr(self, "_mousecapture_parent"):
self._mousecapture_parent.toggle_mousecapture()
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:
pass