From 0aecb28a4f394a4b344fe76dbc47bad169f4a23b Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 31 Jul 2025 21:12:26 +0545 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(wifi):=20enhance=20WiFi=20scan?= =?UTF-8?q?ning=20with=20pull-to-refresh=20and=20improved=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/controlcenter/wifi.py | 127 +++++++++++++++------------- modules/panel/components/menubar.py | 12 ++- services/network.py | 46 ++++++++-- 3 files changed, 112 insertions(+), 73 deletions(-) diff --git a/modules/controlcenter/wifi.py b/modules/controlcenter/wifi.py index cf5e890..8fc9267 100644 --- a/modules/controlcenter/wifi.py +++ b/modules/controlcenter/wifi.py @@ -1,16 +1,13 @@ +from gi.repository import Gtk, Gdk, GLib + +from services.network import NetworkService from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.centerbox import CenterBox from fabric.widgets.image import Image from fabric.widgets.label import Label from fabric.widgets.scrolledwindow import ScrolledWindow -from gi.repository import Gdk, GLib, Gtk - -from services.network import NetworkService - - -# FIX: Make sure the network only scan when the pull to refresh started -# TODO: Show visual Feeadback and animation like bluetooth +from fabric.widgets.eventbox import EventBox class WifiNetworkSlot(CenterBox): @@ -23,16 +20,14 @@ class WifiNetworkSlot(CenterBox): self.ssid = network_data.get("ssid", "Unknown") self.bssid = network_data.get("bssid", "") self.strength = network_data.get("strength", 0) - self.icon_name = network_data.get( - "icon-name", "network-wireless-signal-none-symbolic" - ) + self.icon_name = network_data.get("icon-name", "network-wireless-signal-none-symbolic") # The network service puts the same active-ap reference in all networks # We need to check if THIS specific network's BSSID matches the active AP's BSSID self.is_connected = False if self.wifi_service and self.wifi_service._ap: active_bssid = self.wifi_service._ap.get_bssid() - self.is_connected = active_bssid == self.bssid + self.is_connected = (active_bssid == self.bssid) # Initialize styles based on connection state self.styles = [ @@ -64,7 +59,7 @@ class WifiNetworkSlot(CenterBox): is_currently_connected = False if self.wifi_service and self.wifi_service._ap: active_bssid = self.wifi_service._ap.get_bssid() - is_currently_connected = active_bssid == self.bssid + is_currently_connected = (active_bssid == self.bssid) if is_currently_connected: # Disconnect from network @@ -86,7 +81,7 @@ class WifiNetworkSlot(CenterBox): is_currently_connected = False if self.wifi_service and self.wifi_service._ap: active_bssid = self.wifi_service._ap.get_bssid() - is_currently_connected = active_bssid == self.bssid + is_currently_connected = (active_bssid == self.bssid) self.is_connected = is_currently_connected self.styles = [ @@ -108,6 +103,7 @@ class WifiConnections(Box): self.parent = parent self.network_service = NetworkService() self.wifi_service = None + self.is_scanning = False # Track scanning state # Wait for network service to be ready self.network_service.connect("device-ready", self.on_network_ready) @@ -118,6 +114,7 @@ class WifiConnections(Box): label="↓ Pull to scan for networks", h_align="center", visible=False, + style="color: #fff; font-size: 12px; padding: 5px;" ) self.title = Box( @@ -138,7 +135,7 @@ class WifiConnections(Box): self.device_box = Box( spacing=2, orientation="vertical", - children=[self.refresh_indicator, self.available_networks], + children=[self.refresh_indicator, self.available_networks] ) # Create scrolled window with pull-to-refresh @@ -173,32 +170,33 @@ class WifiConnections(Box): self.toggle_button.set_active(self.wifi_service.enabled) self.toggle_button.connect( "notify::active", - lambda *_: setattr( - self.wifi_service, "enabled", self.toggle_button.get_active() - ), + lambda *_: setattr(self.wifi_service, 'enabled', self.toggle_button.get_active()), ) # Connect to WiFi service signals self.wifi_service.connect("notify::enabled", self.on_wifi_enabled_changed) - self.wifi_service.connect("scanning", self.on_scanning_changed) + self.wifi_service.connect("notify::scanning", self.on_scanning_changed) self.wifi_service.connect("changed", self.update_networks) # Also connect to access point changes to update connection states - self.wifi_service.connect( - "notify::access-points", self.refresh_network_states - ) + self.wifi_service.connect("notify::access-points", self.refresh_network_states) # Initial network update self.update_networks() + # Initialize scanning state + self.wifi_service.notify("scanning") + def on_wifi_enabled_changed(self, *_): """Handle WiFi enabled state changes""" if self.wifi_service: self.toggle_button.set_active(self.wifi_service.enabled) - def on_scanning_changed(self, service, is_scanning): + def on_scanning_changed(self, *_): """Handle scanning state changes""" - print(f"WiFi scanning state changed: {is_scanning}") # Debug output - self.update_scan_indicator(is_scanning) + if self.wifi_service: + is_scanning = self.wifi_service.scanning + self.is_scanning = is_scanning # Update our tracking state + self.update_scan_label() def update_networks(self, *_): """Update the list of available networks""" @@ -222,7 +220,7 @@ class WifiConnections(Box): def refresh_network_states(self, *_): """Refresh connection states for all network slots""" for child in self.available_networks.get_children(): - if hasattr(child, "on_changed"): + if hasattr(child, 'on_changed'): child.on_changed() def setup_pull_to_refresh(self): @@ -239,7 +237,7 @@ class WifiConnections(Box): self.bounce_timeout_id = None self.bounce_frame = 0 self.bounce_duration = 60 # Total frames for animation - self.bounce_amplitude = 15 # Maximum bounce height in pixels + self.bounce_amplitude = 30 # Maximum bounce height in pixels (increased for visibility) # Connect to scroll events self.scrolled_window.connect("scroll-event", self.on_scroll_event) @@ -249,10 +247,10 @@ class WifiConnections(Box): # Enable events self.scrolled_window.set_events( - Gdk.EventMask.SCROLL_MASK - | Gdk.EventMask.BUTTON_PRESS_MASK - | Gdk.EventMask.BUTTON_RELEASE_MASK - | Gdk.EventMask.POINTER_MOTION_MASK + Gdk.EventMask.SCROLL_MASK | + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.POINTER_MOTION_MASK ) def on_scroll_event(self, widget, event): @@ -260,12 +258,11 @@ class WifiConnections(Box): # 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 - start scan - print("WiFi scroll scan triggered") # Debug output - if self.wifi_service: + # Scrolling up at the top - start scan (only if not already scanning) + if self.wifi_service and not self.is_scanning: self.wifi_service.scan() - # Trigger smooth bounce animation for scroll - self.start_bounce_animation() + # Trigger smooth bounce animation for scroll + self.start_bounce_animation() return True # Consume the event return False # Let normal scrolling continue @@ -281,12 +278,11 @@ class WifiConnections(Box): if self.is_pulling: pull_distance = event.y - self.pull_start_y if pull_distance > self.pull_threshold: - # Trigger scan - print("WiFi pull-to-refresh scan triggered") # Debug output - if self.wifi_service: + # Start scan (only if not already scanning) + if self.wifi_service and not self.is_scanning: self.wifi_service.scan() - # Trigger elastic animation for pull gesture (most satisfying) - self.start_elastic_animation() + # Trigger elastic animation for pull gesture (most satisfying) + self.start_elastic_animation() else: # Stop any ongoing animation self.stop_bounce_animation() @@ -304,49 +300,54 @@ class WifiConnections(Box): # 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") + if self.is_scanning: + self.refresh_indicator.set_label("↑ Already scanning...") + else: + self.refresh_indicator.set_label("↑ Release to scan") self.refresh_indicator.add_style_class("ready-to-refresh") else: - self.refresh_indicator.set_label("↓ Pull to scan for networks") + if self.is_scanning: + self.refresh_indicator.set_label("↓ Scanning in progress...") + else: + self.refresh_indicator.set_label("↓ Pull to scan for networks") self.refresh_indicator.remove_style_class("ready-to-refresh") else: self.refresh_indicator.set_visible(False) return False - def update_scan_indicator(self, is_scanning): + def update_scan_label(self): """Update scanning state appearance""" - print(f"WiFi update_scan_indicator called: {is_scanning}") # Debug output - if is_scanning: + if self.wifi_service and self.wifi_service.scanning: # Show scanning feedback in refresh indicator + self.refresh_indicator.set_label("Scanning for networks...") self.refresh_indicator.set_visible(True) self.refresh_indicator.add_style_class("scanning") - print("WiFi scanning indicator shown") # Debug output + self.refresh_indicator.show_all() else: - # Hide scanning feedback - if not self.is_pulling: + # Hide scanning feedback only if not animating and not pulling + if not self.is_pulling and not self.bounce_timeout_id: self.refresh_indicator.set_visible(False) self.refresh_indicator.remove_style_class("scanning") - print("WiFi scanning indicator hidden") # Debug output def ease_out_bounce(self, t): """Smooth bounce easing function (0 to 1)""" - if t < 1 / 2.75: + import math + if t < 1/2.75: return 7.5625 * t * t - elif t < 2 / 2.75: - t -= 1.5 / 2.75 + elif t < 2/2.75: + t -= 1.5/2.75 return 7.5625 * t * t + 0.75 - elif t < 2.5 / 2.75: - t -= 2.25 / 2.75 + elif t < 2.5/2.75: + t -= 2.25/2.75 return 7.5625 * t * t + 0.9375 else: - t -= 2.625 / 2.75 + t -= 2.625/2.75 return 7.5625 * t * t + 0.984375 def ease_out_elastic(self, t): """Elastic easing function for smoother bounce""" import math - if t == 0 or t == 1: return t @@ -356,7 +357,6 @@ class WifiConnections(Box): def start_bounce_animation(self): """Start smooth bounce animation for the refresh indicator""" - print("WiFi bounce animation started") # Debug output if self.bounce_timeout_id: GLib.source_remove(self.bounce_timeout_id) @@ -370,6 +370,9 @@ class WifiConnections(Box): # Animation finished self.refresh_indicator.set_margin_top(0) self.bounce_timeout_id = None + # Hide indicator after animation if not scanning and not pulling + if not (self.wifi_service and self.wifi_service.scanning) and not self.is_pulling: + self.refresh_indicator.set_visible(False) return False # Calculate progress (0 to 1) @@ -387,13 +390,12 @@ class WifiConnections(Box): def start_elastic_animation(self): """Start elastic animation for more dramatic effect""" - print("WiFi elastic animation started") # Debug output if self.bounce_timeout_id: GLib.source_remove(self.bounce_timeout_id) self.bounce_frame = 0 self.bounce_duration = 80 # Longer for elastic effect - self.bounce_amplitude = 20 # Higher amplitude + self.bounce_amplitude = 40 # Higher amplitude (increased for visibility) self.bounce_timeout_id = GLib.timeout_add(16, self.animate_elastic) def animate_elastic(self): @@ -402,6 +404,9 @@ class WifiConnections(Box): # Animation finished self.refresh_indicator.set_margin_top(0) self.bounce_timeout_id = None + # Hide indicator after animation if not scanning and not pulling + if not (self.wifi_service and self.wifi_service.scanning) and not self.is_pulling: + self.refresh_indicator.set_visible(False) return False # Calculate progress (0 to 1) @@ -414,6 +419,7 @@ class WifiConnections(Box): bounce_offset = int(self.bounce_amplitude * (1 - eased_progress)) self.refresh_indicator.set_margin_top(max(0, bounce_offset)) + self.bounce_frame += 1 return True @@ -426,5 +432,4 @@ class WifiConnections(Box): def on_destroy(self, widget): """Cleanup animations when widget is destroyed""" - self.stop_bounce_animation() - + self.stop_bounce_animation() \ No newline at end of file diff --git a/modules/panel/components/menubar.py b/modules/panel/components/menubar.py index a4c39dc..6626158 100644 --- a/modules/panel/components/menubar.py +++ b/modules/panel/components/menubar.py @@ -367,6 +367,12 @@ class MenuBar(Box): self.dropdown_system.global_menu_button_help, ] - def show_system_dropdown(self, _): - """Show the system dropdown menu""" - self.dropdown_system.menu_button_dropdown.toggle_mousecapture() + def show_system_dropdown(self, imac_button): + self.dropdown_system.menu_button_dropdown.child_window.set_pointing_to( + imac_button + ) + mouse_capture = self.dropdown_system.menu_button_dropdown + if mouse_capture.is_visible(): + mouse_capture.set_child_window_visible(False) + else: + mouse_capture.set_child_window_visible(True) diff --git a/services/network.py b/services/network.py index 1113b3f..c6fcf4b 100644 --- a/services/network.py +++ b/services/network.py @@ -31,6 +31,8 @@ class Wifi(Service): self._device: NM.DeviceWifi = device self._ap: NM.AccessPoint | None = None self._ap_signal: int | None = None + self._scan_cancellable: Gio.Cancellable | None = None + self._is_scanning: bool = False super().__init__(**kwargs) self._client.connect( @@ -107,18 +109,44 @@ class Wifi(Service): def scan(self): """Start scanning for WiFi networks and emit scanning signal""" - if self._device: - self.emit("scanning", True) # Emit signal that scanning has started + if self._device and not self._is_scanning: + self._is_scanning = True + self._scan_cancellable = Gio.Cancellable() + self.notify("scanning") # Notify property change self._device.request_scan_async( - None, - lambda device, result: [ - device.request_scan_finish(result), - self.emit( - "scanning", False - ), # Emit signal that scanning has stopped - ], + self._scan_cancellable, + lambda device, result: self._scan_finished(device, result), ) + def _scan_finished(self, device, result): + """Handle scan completion""" + try: + device.request_scan_finish(result) + except Exception as e: + logger.error(f"WiFi scan error: {e}") + finally: + self._is_scanning = False + self._scan_cancellable = None + self.notify("scanning") # Notify property change + + def toggle_scan(self): + """Toggle WiFi scanning on/off""" + if self._is_scanning: + # Stop current scan + if self._scan_cancellable: + self._scan_cancellable.cancel() + self._scan_cancellable = None + self._is_scanning = False + self.notify("scanning") # Notify property change + else: + # Start new scan + self.scan() + + @Property(bool, "readable", default_value=False) + def scanning(self) -> bool: + """Check if currently scanning""" + return self._is_scanning + def is_active_ap(self, name) -> bool: return self._ap.get_bssid() == name if self._ap else False