From 260d215b6bb0f6d7a78913f1ed7c678f734e3049 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 17 Jun 2026 05:15:03 +0545 Subject: [PATCH] refactor(src/window/controlcenter): simplify bluetooth status handling and UI updates Cleanup duplicated UI updates and improve signal handling logic for better stability. --- src/window/controlcenter/bluetooth.py | 105 ++++++------------------- src/window/controlcenter/wifi.py | 106 ++++---------------------- 2 files changed, 41 insertions(+), 170 deletions(-) diff --git a/src/window/controlcenter/bluetooth.py b/src/window/controlcenter/bluetooth.py index c595f75..1ee8970 100644 --- a/src/window/controlcenter/bluetooth.py +++ b/src/window/controlcenter/bluetooth.py @@ -1,7 +1,6 @@ import subprocess - from enum import Enum, auto -from services.bluetooth import BluetoothClient, BluetoothDevice + from fabric.utils import Gdk, GLib, exec_shell_command, logger from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -10,9 +9,10 @@ from fabric.widgets.image import Image from fabric.widgets.label import Label from fabric.widgets.separator import Separator -from utils.utils import svg_file +from services.bluetooth import BluetoothClient, BluetoothDevice from shared.widgets.smooth_switch import SmoothSwitch from shared.window.animated_scrollwindow import AnimatedScrollable +from utils.utils import svg_file def get_battery_icon_file( @@ -36,8 +36,6 @@ def get_battery_icon_file( def set_bluetooth_enabled_with_fallback(client, enabled: bool): - # Always run rfkill first because if it's blocked, BlueZ will fail and - # our custom client catches the exception (so it won't trigger an except block here). if enabled: command = "rfkill unblock bluetooth" else: @@ -49,9 +47,7 @@ def set_bluetooth_enabled_with_fallback(client, enabled: bool): elif isinstance(result, str) and result.strip(): logger.warning(f"rfkill command output: {result.strip()}") - # Try setting enabled state via client as well try: - # Our custom client has enabled as a property, but setting it sets it on adapters if hasattr(client, "set_enabled"): client.set_enabled(enabled) else: @@ -94,7 +90,6 @@ class BluetoothDeviceSlot(CenterBox): style_classes=" ".join(self.styles), ) - # Status icon for connection state self.status_icon = Image( icon_name="process-working-symbolic", size=5, @@ -118,7 +113,6 @@ class BluetoothDeviceSlot(CenterBox): ) self.start_children = [self.device_button] - # Add battery info if available if hasattr(device, "battery_percentage") and device.battery_percentage > 0: battery_box = Box(orientation="h", spacing=4) @@ -143,7 +137,6 @@ class BluetoothDeviceSlot(CenterBox): self.device.emit("changed") def _get_icon_for_device(self, device) -> str: - # Fallback to symbolic icons instead of duotone dtype = getattr(device, "type", None) or getattr( device, "icon_name", "bluetooth" ) @@ -210,7 +203,6 @@ class BluetoothDeviceSlot(CenterBox): except Exception: pass - # Update battery info if available if ( hasattr(self.device, "battery_percentage") and self.device.battery_percentage > 0 @@ -226,20 +218,18 @@ class BluetoothDeviceSlot(CenterBox): ) ) - # Create battery percentage label battery_label = Label( label=f"{self.device.battery_percentage:.0f}%", name="battery-label" ) battery_box.children = [battery_icon, battery_label] self.end_children = [battery_box] - else: # Update existing battery display + else: battery_box = self.end_children[0] if hasattr(battery_box, "children") and len(battery_box.children) >= 2: battery_icon = battery_box.children[0] battery_label = battery_box.children[1] - # Update battery icon battery_icon.dynamic_file( get_battery_icon_file( self.device.battery_percentage, @@ -247,9 +237,8 @@ class BluetoothDeviceSlot(CenterBox): ) ) - # Update battery percentage battery_label.set_label(f"{self.device.battery_percentage:.0f}%") - elif self.end_children: # Remove battery display if no longer available + elif self.end_children: self.end_children = [] def _refresh_state(self): @@ -320,7 +309,6 @@ class BluetoothConnections(Box): style="color: #fff; font-size: 12px; padding: 5px;", ) - # Create title with optional back button title_children = [] if show_back_button: title_children.append( @@ -344,7 +332,6 @@ class BluetoothConnections(Box): on_user_toggle=self.handle_user_toggle, ) - # Connect client signals — track IDs for disconnect on destroy self._client_signal_ids.append( self.client.connect( "notify::enabled", @@ -355,7 +342,6 @@ class BluetoothConnections(Box): self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) ) - # Connect to device changes self._client_signal_ids.append( self.client.connect("device-added", self.update_devices) ) @@ -363,7 +349,6 @@ class BluetoothConnections(Box): self.client.connect("device-removed", self.update_devices) ) - # Connect to additional signals for better real-time monitoring self._client_signal_ids.append( self.client.connect("changed", self.on_client_changed) ) @@ -382,7 +367,6 @@ class BluetoothConnections(Box): overlay_scroll=True, ) - # Create "No devices available" message self.no_devices_label = Label( label="No devices available", h_align="center", @@ -390,7 +374,6 @@ class BluetoothConnections(Box): visible=False, ) - # Create Other Devices section with clickable title self.other_devices_button = Button( child=Label("Other Devices", h_align="start"), name="wifi-other-button", @@ -399,7 +382,6 @@ class BluetoothConnections(Box): self.other_devices = Box(spacing=4, orientation="vertical") self.other_devices.set_size_request(-1, 150) - # Create scrolled window for other devices self.other_devices_scrolled = AnimatedScrollable( min_content_size=(303, 0), max_content_size=(303, 300), @@ -411,7 +393,6 @@ class BluetoothConnections(Box): # Add pull-to-refresh functionality to scrolled window self.setup_pull_to_refresh() - # Create More Settings button (same style as Other Devices button) self.more_settings_button = Button( child=Label("More Settings", h_align="start"), name="wifi-other-button", @@ -436,22 +417,18 @@ class BluetoothConnections(Box): self.more_settings_button, ] - # Connect cleanup on destroy self.connect("destroy", self.on_destroy) + self.connect("unmap", self.on_hide) self.client.notify("scanning") self.client.notify("enabled") - - # Initial device update self.update_devices() - # Start periodic device monitoring for real-time updates self.start_device_monitoring() def handle_user_toggle(self, active: bool): self._switch_lock = True set_bluetooth_enabled_with_fallback(self.client, active) - # Lock the switch UI for 1.5 seconds to prevent DBus propagation flicker GLib.timeout_add(1500, self._unlock_switch) def _unlock_switch(self): @@ -460,18 +437,30 @@ class BluetoothConnections(Box): self.toggle_button.set_active(self.client.enabled) return False + def on_hide(self, *_): + """Called when the widget is hidden (popup closed)""" + if self.other_devices.get_visible(): + self.other_devices.set_visible(False) + self.other_devices_scrolled.snap_to_size(0) + if ( + self.client + and self.client.scanning + and hasattr(self.client, "stop_scan") + ): + self.client.stop_scan() + + self.update_scan_label() + def toggle_other_devices(self, *_): """Toggle the visibility of other devices section""" current_state = self.other_devices.get_visible() self.other_devices.set_visible(not current_state) + self.update_scan_label() - # Handle scanning based on section visibility if self.client: if self.other_devices.get_visible(): - # Start scanning when revealing other devices section if not self.client.scanning and hasattr(self.client, "scan"): GLib.timeout_add(100, self.client.scan) - # Also force an immediate device refresh to catch any missed connections if hasattr(self, "_refresh_timeout_id") and self._refresh_timeout_id: GLib.source_remove(self._refresh_timeout_id) @@ -482,7 +471,6 @@ class BluetoothConnections(Box): self._refresh_timeout_id = GLib.timeout_add(150, _do_refresh) else: - # Stop scanning when hiding other devices section if self.client.scanning and hasattr(self.client, "stop_scan"): self.client.stop_scan() @@ -499,26 +487,22 @@ class BluetoothConnections(Box): def update_scan_label(self): """Update scanning state appearance""" - if self.client.scanning: - # Show scanning feedback in refresh indicator + if self.client and self.client.scanning and self.other_devices.get_visible(): self.refresh_indicator.set_label("Scanning for devices...") self.refresh_indicator.set_visible(True) self.refresh_indicator.add_style_class("scanning") else: - # Hide scanning feedback self.refresh_indicator.set_visible(False) self.refresh_indicator.remove_style_class("scanning") def update_devices(self, *_): """Update the list of available devices""" - # Prevent concurrent updates and check if destroyed if self._update_in_progress or self._destroyed or not self.client: return self._update_in_progress = True try: - # Store current device addresses to detect changes current_paired_addresses = { child.device.address for child in self.paired_devices.get_children() @@ -530,7 +514,6 @@ class BluetoothConnections(Box): if hasattr(child, "device") } - # Get current devices safely devices = self.client.devices paired_devices = [] other_devices = [] @@ -540,8 +523,6 @@ class BluetoothConnections(Box): for device in devices: try: if device.name and device.name != "Unknown": - # Categorize devices: paired devices go to "Devices (Paired)" - # All others go to "Other Devices" if device.paired: paired_devices.append(device) new_paired_addresses.add(device.address) @@ -551,11 +532,9 @@ class BluetoothConnections(Box): except Exception: continue - # Check if we need to update (devices added/removed) paired_changed = current_paired_addresses != new_paired_addresses other_changed = current_other_addresses != new_other_addresses - # Only rebuild if something actually changed if paired_changed or other_changed: existing_paired = { child.device.address: child @@ -568,7 +547,6 @@ class BluetoothConnections(Box): if hasattr(child, "device") } - # Add paired devices for device in paired_devices: if not self._destroyed: if device.address in existing_paired: @@ -584,7 +562,6 @@ class BluetoothConnections(Box): device_slot = BluetoothDeviceSlot(device) self.paired_devices.add(device_slot) - # Add other devices for device in other_devices: if not self._destroyed: if device.address in existing_other: @@ -599,30 +576,19 @@ class BluetoothConnections(Box): device_slot = BluetoothDeviceSlot(device) self.other_devices.add(device_slot) - # We deliberately DO NOT destroy slots that disappear from the current scan. - # Destroying slots causes the window to shrink abruptly, which can cause - # the mouse cursor to fall outside the window bounds, triggering Hyprland - # to instantly dismiss the popup. - # Instead, they remain in the list until the control center is closed. for slot in existing_paired.values(): pass for slot in existing_other.values(): pass - # Show/hide sections based on available devices if not self._destroyed: has_paired_devices = len(paired_devices) > 0 has_other_devices = len(other_devices) > 0 has_any_devices = has_paired_devices or has_other_devices - # Always keep scrolled window visible so it animates to 0 smoothly self.paired_devices_scrolled.set_visible(True) - - # Show "No devices available" message if no devices at all self.no_devices_label.set_visible(not has_any_devices) - - # Always show the other devices button, regardless of available devices self.other_devices_button.set_visible(True) # Always visible except Exception: @@ -632,8 +598,6 @@ class BluetoothConnections(Box): def start_device_monitoring(self): """Start periodic monitoring for device changes""" - # Monitor for device changes every 5 seconds (less aggressive) - # This helps catch devices that connect from external sources self.refresh_timer = GLib.timeout_add_seconds(5, self.periodic_device_refresh) def stop_device_monitoring(self): @@ -644,7 +608,6 @@ class BluetoothConnections(Box): def periodic_device_refresh(self): """Periodically refresh device list to catch external connections""" - # If destroyed, remove the GLib source by returning False if self._destroyed: self.refresh_timer = None return False @@ -658,7 +621,7 @@ class BluetoothConnections(Box): except Exception: pass - return True # Continue monitoring + return True def force_device_refresh(self): """Force an immediate refresh of the device list""" @@ -666,38 +629,28 @@ class BluetoothConnections(Box): return try: - # Simply trigger update_devices which has its own safety checks - # Avoid forcing signal emissions to prevent race conditions self.update_devices() except Exception: pass def on_client_changed(self, *_): """Handle when the bluetooth client state changes""" - # Sync switch state and scanning label directly since custom BluetoothClient - # doesn't emit notify::enabled or notify::scanning when adapters update. if getattr(self, "toggle_button", None) and not self._switch_lock: self.toggle_button.set_active(self.client.enabled) self.update_scan_label() - - # Update devices when client state changes self.update_devices() def on_destroy(self, widget): """Cleanup when widget is destroyed""" - # Mark as destroyed to prevent further updates self._destroyed = True - # Stop monitoring timer self.stop_device_monitoring() - # Disconnect all BluetoothClient signals for sig_id in self._client_signal_ids: try: self.client.disconnect(sig_id) except Exception: pass self._client_signal_ids.clear() - # Collapse other devices revealer try: self.other_devices_revealer.child_revealed = False except Exception: @@ -705,12 +658,10 @@ class BluetoothConnections(Box): def close_bluetooth(self): """Called when Bluetooth panel is being closed""" - # Collapse the other devices section when closing self.other_devices_revealer.child_revealed = False def setup_pull_to_refresh(self): """Setup pull-to-refresh gesture for the scrolled window""" - # Get the scrolled window's vertical adjustment self.vadjustment = self.other_devices_scrolled.get_vadjustment() # Track gesture state @@ -738,15 +689,13 @@ class BluetoothConnections(Box): def on_scroll_event(self, widget, event): """Handle scroll events for pull-to-refresh""" - # Only handle pull-to-refresh when at the top if self.vadjustment.get_value() <= 0: if event.direction == Gdk.ScrollDirection.UP: - # Scrolling up at the top - force scan and refresh if not self.client.scanning and hasattr(self.client, "scan"): self.client.scan() self.force_device_refresh() - return True # Consume the event - return False # Let normal scrolling continue + return True + return False def on_button_press(self, widget, event): """Handle button press for touch/drag gestures""" @@ -760,11 +709,9 @@ class BluetoothConnections(Box): if self.is_pulling: pull_distance = event.y - self.pull_start_y if pull_distance > self.pull_threshold: - # Force scan and refresh if not self.client.scanning and hasattr(self.client, "scan"): self.client.scan() self.force_device_refresh() - # Hide refresh indicator self.refresh_indicator.set_visible(False) self.refresh_indicator.remove_style_class("ready-to-refresh") self.is_pulling = False @@ -775,7 +722,6 @@ class BluetoothConnections(Box): if self.is_pulling and self.vadjustment.get_value() <= 0: pull_distance = event.y - self.pull_start_y if pull_distance > 0: - # Show refresh indicator when pulling down self.refresh_indicator.set_visible(True) if pull_distance >= self.pull_threshold: if self.client.scanning: @@ -795,5 +741,4 @@ class BluetoothConnections(Box): def on_device_added(self, client: BluetoothClient, address: str): """Handle when a new device is added""" - # Update the device list when devices are added self.update_devices() diff --git a/src/window/controlcenter/wifi.py b/src/window/controlcenter/wifi.py index 6159f0c..9f7a63d 100644 --- a/src/window/controlcenter/wifi.py +++ b/src/window/controlcenter/wifi.py @@ -9,11 +9,11 @@ from fabric.widgets.label import Label from fabric.widgets.separator import Separator from services.network import NetworkClient +from shared.dialogs.wifi_password_dialog import WiFiPasswordDialog +from shared.widgets.smooth_switch import SmoothSwitch +from shared.window.animated_scrollwindow import AnimatedScrollable from utils.functions import get_wifi_connecting_icon, get_wifi_icon_for_strength from utils.utils import svg_file -from shared.widgets.smooth_switch import SmoothSwitch -from shared.dialogs.wifi_password_dialog import WiFiPasswordDialog -from shared.window.animated_scrollwindow import AnimatedScrollable class WifiNetworkSlot(Box): @@ -26,32 +26,24 @@ class WifiNetworkSlot(Box): self.network_service = network_service self.parent = parent # Reference to control center - # Get network info from AccessPoint object self.ssid = access_point.ssid self.bssid = access_point.bssid self.strength = access_point.strength self.icon_name = access_point.icon - # Check if this network is currently connected self.is_connected = access_point.is_active - # Initialize styles based on connection state self.styles = [ "connected" if self.is_connected else "", ] - # Create connection status indicator using dynamic WiFi icon based on signal strength wifi_icon_path = get_wifi_icon_for_strength(self.strength) self.dimage = svg_file(wifi_icon_path, size=28) self.wifi_icon_box = Box( children=[self.dimage], style_classes=["wifi-icon-box"], ) - # if self.is_connected: - # self.wifi_icon_box.remove_style_class("wifi-icon-box") - # self.wifi_icon_box.add_style_class("wifi-icon-box-connected") - # - # Set initial style classes + if self.is_connected: self.dimage.add_style_class("connected") @@ -68,10 +60,8 @@ class WifiNetworkSlot(Box): name="wifi-lock-icon", ) - # Initialize password dialog self.password_dialog = None - # Create the start section with WiFi icon and network name start_box = Box( orientation="h", spacing=8, @@ -79,7 +69,6 @@ class WifiNetworkSlot(Box): start_box.children = [self.wifi_icon_box, self.network_label] if self.lock_icon: start_box.children.append(self.lock_icon) - # Create end section with lock icon if needed self.children = [ Button( @@ -90,7 +79,6 @@ class WifiNetworkSlot(Box): ) ] - # Emit initial change to update display self.on_changed() def update_ap(self, access_point): @@ -100,7 +88,6 @@ class WifiNetworkSlot(Box): self.strength = access_point.strength self.is_connected = access_point.is_active - # Update icon only if not currently animating a connection state if not self.dimage.has_style_class( "connecting" ) and not self.dimage.has_style_class("disconnecting"): @@ -112,16 +99,13 @@ class WifiNetworkSlot(Box): self.on_changed() def toggle_connecting(self): - # Check if this network is currently connected is_currently_connected = self.access_point.is_active if is_currently_connected: - # Show disconnecting state using connecting icon connecting_icon = get_wifi_connecting_icon() self.dimage.set_from_file(connecting_icon) self.dimage.add_style_class("disconnecting") - # Disconnect from network if self.wifi_service and self.wifi_service._device: subprocess.Popen( [ @@ -132,16 +116,13 @@ class WifiNetworkSlot(Box): ] ) self.is_connected = False - # Remove disconnecting state after a short delay to show feedback GLib.timeout_add(500, lambda: self._reset_disconnect_state()) else: - # Try to connect - check if password is required and not saved is_saved = self.network_service and self.network_service.is_network_saved( self.ssid ) if self.access_point.requires_password and not is_saved: - # Show password dialog immediately self._show_password_dialog() else: # Try to connect without password (for open or saved networks) @@ -171,11 +152,9 @@ class WifiNetworkSlot(Box): self.access_point.bssid, callback=on_open_connection_result ) except Exception: - # Handle any connection errors gracefully self._reset_connect_state() self.on_changed() - # Update display after connection attempt self.on_changed() def _reset_disconnect_state(self): @@ -184,7 +163,7 @@ class WifiNetworkSlot(Box): wifi_icon_path = get_wifi_icon_for_strength(self.strength) self.dimage.set_from_file(wifi_icon_path) self.on_changed() - return False # Remove timeout + return False def _reset_connect_state(self): """Reset visual state after connect operation""" @@ -192,15 +171,14 @@ class WifiNetworkSlot(Box): wifi_icon_path = get_wifi_icon_for_strength(self.strength) self.dimage.set_from_file(wifi_icon_path) self.on_changed() - return False # Remove timeout + return False def on_changed(self, *_): - # Check if this network is currently connected using the access point's is_active property self.is_connected = self.access_point.is_active self.styles = [ "connected" if self.is_connected else "", ] - # Update style classes for SVG widget + if self.is_connected: self.wifi_icon_box.add_style_class("wifi-icon-box-connected") else: @@ -209,11 +187,9 @@ class WifiNetworkSlot(Box): def _show_password_dialog(self): """Show the WiFi password dialog""" - # Close the control center first if self.parent and hasattr(self.parent, "hide_controlcenter"): self.parent.hide_controlcenter() - # Create a new dialog each time to ensure clean state if self.password_dialog: self.password_dialog.destroy_dialog() @@ -228,21 +204,17 @@ class WifiNetworkSlot(Box): def _on_password_connect(self, ssid, password): """Handle password dialog connect action""" if password.strip(): - # Show connecting state using connecting icon connecting_icon = get_wifi_connecting_icon() self.dimage.set_from_file(connecting_icon) self.dimage.add_style_class("connecting") - # Try to connect with password using callback def on_connection_result(success, message): """Handle the connection result""" if success: self.is_connected = True - # Remove connecting state after a short delay GLib.timeout_add(500, lambda: self._reset_connect_state()) - # Clear any timeout in the password dialog if ( self.password_dialog and self.password_dialog.connection_timeout_id @@ -251,12 +223,10 @@ class WifiNetworkSlot(Box): self.password_dialog.connection_timeout_id = None self.password_dialog.is_connecting = False else: - # Connection failed - show error in dialog self._reset_connect_state() if self.password_dialog: self._show_connection_error(message) - # Update display after connection attempt self.on_changed() try: @@ -265,7 +235,6 @@ class WifiNetworkSlot(Box): self.access_point.bssid, password, callback=on_connection_result ) except Exception: - # Handle any connection errors gracefully self._reset_connect_state() if self.password_dialog: self._show_connection_error("Connection failed. Please try again.") @@ -279,7 +248,6 @@ class WifiNetworkSlot(Box): def _on_password_cancel(self): """Handle password dialog cancel action""" - # Reset any connecting state self._reset_connect_state() @@ -301,7 +269,6 @@ class WifiConnections(Box): self._destroyed = False # Track if widget is destroyed self._signal_ids = [] # Track all service signal IDs for cleanup - # Wait for network service to be ready — track signal ID self._signal_ids.append( ( self.network_service, @@ -318,7 +285,6 @@ class WifiConnections(Box): style="color: #fff; font-size: 12px; padding: 5px;", ) - # Create title with optional back button title_children = [] if show_back_button: title_children.append( @@ -409,22 +375,25 @@ class WifiConnections(Box): self.more_settings_button, ] - # Connect cleanup on destroy self.connect("destroy", self.on_destroy) + self.connect("unmap", self.on_hide) - # Start periodic network monitoring for real-time updates self.start_network_monitoring() + def on_hide(self, *_): + """Called when the widget is hidden (popup closed)""" + if self.other_networks.get_visible(): + self.other_networks.set_visible(False) + self.other_networks_scrolled.snap_to_size(0) + def toggle_other_networks(self, *_): """Toggle the visibility of other networks section""" current_state = self.other_networks.get_visible() self.other_networks.set_visible(not current_state) if self.other_networks.get_visible(): - # Trigger a scan when revealing other networks and force refresh if self.wifi_service: self.wifi_service.scan() - # Defer network refresh to avoid layout-shift killing the window focus during click if hasattr(self, "_refresh_timeout_id") and self._refresh_timeout_id: GLib.source_remove(self._refresh_timeout_id) @@ -439,10 +408,8 @@ class WifiConnections(Box): """Called when network service is ready""" self.wifi_service = self.network_service.wifi_device if self.wifi_service: - # Set up WiFi toggle self.toggle_button.set_active(self.wifi_service.enabled) - # Connect to WiFi service signals — track IDs for cleanup self._signal_ids.append( ( self.wifi_service, @@ -492,7 +459,6 @@ class WifiConnections(Box): self._update_in_progress = True try: - # Store current network SSIDs to detect changes current_known_ssids = { child.ssid for child in self.known_networks.get_children() @@ -504,7 +470,6 @@ class WifiConnections(Box): if hasattr(child, "ssid") } - # Get current networks access_points = self.wifi_service.access_points known_networks = [] other_networks = [] @@ -514,8 +479,6 @@ class WifiConnections(Box): for access_point in access_points: try: if access_point.ssid and access_point.ssid != "Unknown": - # Categorize networks: connected or saved networks go to "Known Network" - # All others go to "Other Networks" if access_point.is_active or self._is_saved_network( access_point ): @@ -527,11 +490,9 @@ class WifiConnections(Box): except Exception: continue - # Check if we need to update (networks added/removed) known_changed = current_known_ssids != new_known_ssids other_changed = current_other_ssids != new_other_ssids - # Only rebuild if something actually changed if known_changed or other_changed: # Get existing networks existing_known = { @@ -541,7 +502,6 @@ class WifiConnections(Box): child.ssid: child for child in self.other_networks.get_children() } - # Process known networks for access_point in known_networks: if not self._destroyed: if access_point.ssid in existing_known: @@ -561,7 +521,6 @@ class WifiConnections(Box): ) self.known_networks.add(network_slot) - # Process other networks for access_point in other_networks: if not self._destroyed: if access_point.ssid in existing_other: @@ -581,13 +540,7 @@ class WifiConnections(Box): ) self.other_networks.add(network_slot) - # We deliberately DO NOT destroy slots that disappear from the current scan. - # Destroying slots causes the window to shrink abruptly, which can cause - # the mouse cursor to fall outside the window bounds, triggering Hyprland - # to instantly dismiss the popup. - # Instead, they remain in the list until the control center is closed. for slot in existing_known.values(): - # Update to 0 strength if it's completely gone if hasattr(slot, "access_point"): slot.strength = 0 from shared.widgets.wifi_icon import get_wifi_icon_for_strength @@ -607,22 +560,14 @@ class WifiConnections(Box): ) and not slot.dimage.has_style_class("disconnecting"): slot.dimage.set_from_file(get_wifi_icon_for_strength(0)) - # Show/hide sections based on available networks if not self._destroyed: has_known_networks = len(known_networks) > 0 has_other_networks = len(other_networks) > 0 has_any_networks = has_known_networks or has_other_networks - # Always keep scrolled window visible so it animates to 0 smoothly instead of abruptly skipping layout self.known_networks_scrolled.set_visible(True) - - # Show "No networks available" message if no networks at all self.no_networks_label.set_visible(not has_any_networks) - - # Always show the other networks button, regardless of available networks self.other_networks_button.set_visible(True) # Always visible - - # Update all network connection states self.refresh_network_states() except Exception: @@ -641,20 +586,16 @@ class WifiConnections(Box): def refresh_network_states(self, *_): """Refresh connection states for all network slots""" - # Refresh known networks for child in self.known_networks.get_children(): if hasattr(child, "on_changed"): child.on_changed() - # Refresh other networks for child in self.other_networks.get_children(): if hasattr(child, "on_changed"): child.on_changed() def start_network_monitoring(self): """Start periodic monitoring for network changes""" - # Monitor for network changes every 5 seconds - # This helps catch networks that connect from external sources self.refresh_timer = GLib.timeout_add_seconds(5, self.periodic_network_refresh) def stop_network_monitoring(self): @@ -665,12 +606,10 @@ class WifiConnections(Box): def periodic_network_refresh(self): """Periodically refresh network list to catch external connections""" - # If destroyed, remove the GLib source by returning False if self._destroyed: self.refresh_timer = None return False - # Skip if update in progress or wifi not available/enabled if ( self._update_in_progress or not self.wifi_service @@ -691,14 +630,12 @@ class WifiConnections(Box): return try: - # Simply trigger update_networks which has its own safety checks self.update_networks() except Exception: pass def setup_pull_to_refresh(self): """Setup pull-to-refresh gesture for the scrolled window""" - # Get the scrolled window's vertical adjustment self.vadjustment = self.other_networks_scrolled.get_vadjustment() # Track gesture state @@ -726,15 +663,13 @@ class WifiConnections(Box): def on_scroll_event(self, widget, event): """Handle scroll events for pull-to-refresh""" - # Only handle pull-to-refresh when at the top if self.vadjustment.get_value() <= 0: if event.direction == Gdk.ScrollDirection.UP: - # Scrolling up at the top - trigger scan and force refresh if self.wifi_service: self.wifi_service.scan() self.force_network_refresh() - return True # Consume the event - return False # Let normal scrolling continue + return True + return False def on_button_press(self, widget, event): """Handle button press for touch/drag gestures""" @@ -748,11 +683,9 @@ class WifiConnections(Box): if self.is_pulling: pull_distance = event.y - self.pull_start_y if pull_distance > self.pull_threshold: - # Trigger scan and force refresh if self.wifi_service: self.wifi_service.scan() self.force_network_refresh() - # Hide refresh indicator self.refresh_indicator.set_visible(False) self.refresh_indicator.remove_style_class("ready-to-refresh") self.is_pulling = False @@ -763,7 +696,6 @@ class WifiConnections(Box): if self.is_pulling and self.vadjustment.get_value() <= 0: pull_distance = event.y - self.pull_start_y if pull_distance > 0: - # Show refresh indicator when pulling down self.refresh_indicator.set_visible(True) if pull_distance >= self.pull_threshold: self.refresh_indicator.set_label("↑ Release to scan") @@ -777,21 +709,16 @@ class WifiConnections(Box): def on_destroy(self, widget): """Cleanup when widget is destroyed""" - # Mark as destroyed to prevent further updates self._destroyed = True - # Stop monitoring timer self.stop_network_monitoring() - # Disconnect all tracked service signals for obj, sig_id in self._signal_ids: try: obj.disconnect(sig_id) except Exception: pass self._signal_ids.clear() - # Drop strong references self.wifi_service = None self.network_service = None - # Collapse other networks revealer try: self.other_networks_revealer.child_revealed = False except Exception: @@ -799,5 +726,4 @@ class WifiConnections(Box): def close_wifi(self): """Called when WiFi panel is being closed""" - # Collapse the other networks section when closing self.other_networks_revealer.child_revealed = False