✨ feat(wifi): enhance WiFi scanning with pull-to-refresh and improved feedback
This commit is contained in:
3 files changed
+112
-73
No files matched your search
@@ -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()
|
||||
@@ -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)
|
||||
+37
-9
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user