Ensure monitoring starts only when the widget is shown and stops on hide, preventing timer leaks and unnecessary refreshes
739 lines
28 KiB
Python
739 lines
28 KiB
Python
import subprocess
|
|
|
|
from fabric.utils import Gdk, GLib, logger
|
|
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.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
|
|
|
|
|
|
class WifiNetworkSlot(Box):
|
|
def __init__(
|
|
self, access_point, wifi_service, network_service=None, parent=None, **kwargs
|
|
):
|
|
super().__init__(name="wifi-network-slot", **kwargs)
|
|
self.access_point = access_point
|
|
self.wifi_service = wifi_service
|
|
self.network_service = network_service
|
|
self.parent = parent # Reference to control center
|
|
|
|
self.ssid = access_point.ssid
|
|
self.bssid = access_point.bssid
|
|
self.strength = access_point.strength
|
|
self.icon_name = access_point.icon
|
|
|
|
self.is_connected = access_point.is_active
|
|
|
|
self.styles = [
|
|
"connected" if self.is_connected else "",
|
|
]
|
|
|
|
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.dimage.add_style_class("connected")
|
|
|
|
self.network_label = Label(
|
|
label=self.ssid, name="wifi-network-name", h_align="start", h_expand=True
|
|
)
|
|
|
|
# Create lock icon for secured networks
|
|
self.lock_icon = None
|
|
if self.access_point.requires_password:
|
|
self.lock_icon = Image(
|
|
icon_name="changes-prevent-symbolic",
|
|
size=12,
|
|
name="wifi-lock-icon",
|
|
)
|
|
|
|
self.password_dialog = None
|
|
|
|
start_box = Box(
|
|
orientation="h",
|
|
spacing=8,
|
|
)
|
|
start_box.children = [self.wifi_icon_box, self.network_label]
|
|
if self.lock_icon:
|
|
start_box.children.append(self.lock_icon)
|
|
|
|
self.children = [
|
|
Button(
|
|
child=start_box,
|
|
h_expand=True,
|
|
name="wifi-network-button",
|
|
on_clicked=lambda *_: self.toggle_connecting(),
|
|
)
|
|
]
|
|
|
|
self.on_changed()
|
|
|
|
def update_ap(self, access_point):
|
|
"""Update existing slot with new AP data without recreating it"""
|
|
self.access_point = access_point
|
|
self.bssid = access_point.bssid
|
|
self.strength = access_point.strength
|
|
self.is_connected = access_point.is_active
|
|
|
|
if not self.dimage.has_style_class(
|
|
"connecting"
|
|
) and not self.dimage.has_style_class("disconnecting"):
|
|
from shared.widgets.wifi_icon import get_wifi_icon_for_strength
|
|
|
|
wifi_icon_path = get_wifi_icon_for_strength(self.strength)
|
|
self.dimage.set_from_file(wifi_icon_path)
|
|
|
|
self.on_changed()
|
|
|
|
def toggle_connecting(self):
|
|
is_currently_connected = self.access_point.is_active
|
|
|
|
if is_currently_connected:
|
|
connecting_icon = get_wifi_connecting_icon()
|
|
self.dimage.set_from_file(connecting_icon)
|
|
self.dimage.add_style_class("disconnecting")
|
|
|
|
if self.wifi_service and self.wifi_service._device:
|
|
subprocess.Popen(
|
|
[
|
|
"nmcli",
|
|
"device",
|
|
"disconnect",
|
|
self.wifi_service._device.get_iface(),
|
|
]
|
|
)
|
|
self.is_connected = False
|
|
GLib.timeout_add(500, lambda: self._reset_disconnect_state())
|
|
else:
|
|
is_saved = self.network_service and self.network_service.is_network_saved(
|
|
self.ssid
|
|
)
|
|
|
|
if self.access_point.requires_password and not is_saved:
|
|
self._show_password_dialog()
|
|
else:
|
|
# Try to connect without password (for open or saved networks)
|
|
connecting_icon = get_wifi_connecting_icon()
|
|
self.dimage.set_from_file(connecting_icon)
|
|
self.dimage.add_style_class("connecting")
|
|
|
|
def on_open_connection_result(success, message):
|
|
"""Handle the connection result for open networks"""
|
|
if success:
|
|
self.is_connected = True
|
|
# Remove connecting state after a short delay
|
|
GLib.timeout_add(500, lambda: self._reset_connect_state())
|
|
else:
|
|
# Connection failed
|
|
self._reset_connect_state()
|
|
# If password was wrong or auth failed, prompt the user
|
|
if message == "Wrong password":
|
|
self._show_password_dialog()
|
|
|
|
# Update display after connection attempt
|
|
self.on_changed()
|
|
|
|
try:
|
|
if self.network_service:
|
|
self.network_service.connect_wifi_bssid(
|
|
self.access_point.bssid, callback=on_open_connection_result
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[WiFi] Failed to connect to {self.access_point.ssid}: {e}"
|
|
)
|
|
self._reset_connect_state()
|
|
self.on_changed()
|
|
|
|
self.on_changed()
|
|
|
|
def _reset_disconnect_state(self):
|
|
"""Reset visual state after disconnect operation"""
|
|
self.dimage.remove_style_class("disconnecting")
|
|
wifi_icon_path = get_wifi_icon_for_strength(self.strength)
|
|
self.dimage.set_from_file(wifi_icon_path)
|
|
self.on_changed()
|
|
return False
|
|
|
|
def _reset_connect_state(self):
|
|
"""Reset visual state after connect operation"""
|
|
self.dimage.remove_style_class("connecting")
|
|
wifi_icon_path = get_wifi_icon_for_strength(self.strength)
|
|
self.dimage.set_from_file(wifi_icon_path)
|
|
self.on_changed()
|
|
return False
|
|
|
|
def on_changed(self, *_):
|
|
self.is_connected = self.access_point.is_active
|
|
self.styles = [
|
|
"connected" if self.is_connected else "",
|
|
]
|
|
|
|
if self.is_connected:
|
|
self.wifi_icon_box.add_style_class("wifi-icon-box-connected")
|
|
else:
|
|
self.wifi_icon_box.remove_style_class("wifi-icon-box-connected")
|
|
return
|
|
|
|
def _show_password_dialog(self):
|
|
"""Show the WiFi password dialog"""
|
|
if self.parent and hasattr(self.parent, "hide_controlcenter"):
|
|
self.parent.hide_controlcenter()
|
|
|
|
if self.password_dialog:
|
|
self.password_dialog.destroy_dialog()
|
|
|
|
self.password_dialog = WiFiPasswordDialog(
|
|
ssid=self.ssid,
|
|
on_connect_callback=self._on_password_connect,
|
|
on_cancel_callback=self._on_password_cancel,
|
|
)
|
|
|
|
self.password_dialog.show_dialog()
|
|
|
|
def _on_password_connect(self, ssid, password):
|
|
"""Handle password dialog connect action"""
|
|
if password.strip():
|
|
connecting_icon = get_wifi_connecting_icon()
|
|
self.dimage.set_from_file(connecting_icon)
|
|
self.dimage.add_style_class("connecting")
|
|
|
|
def on_connection_result(success, message):
|
|
"""Handle the connection result"""
|
|
if success:
|
|
self.is_connected = True
|
|
|
|
GLib.timeout_add(500, lambda: self._reset_connect_state())
|
|
|
|
if (
|
|
self.password_dialog
|
|
and self.password_dialog.connection_timeout_id
|
|
):
|
|
GLib.source_remove(self.password_dialog.connection_timeout_id)
|
|
self.password_dialog.connection_timeout_id = None
|
|
self.password_dialog.is_connecting = False
|
|
else:
|
|
self._reset_connect_state()
|
|
if self.password_dialog:
|
|
self._show_connection_error(message)
|
|
|
|
self.on_changed()
|
|
|
|
try:
|
|
if self.network_service:
|
|
self.network_service.connect_wifi_with_password(
|
|
self.access_point.bssid, password, callback=on_connection_result
|
|
)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"[WiFi] Failed to initiate connection to {self.access_point.ssid}: {e}"
|
|
)
|
|
self._reset_connect_state()
|
|
if self.password_dialog:
|
|
self._show_connection_error("Connection failed. Please try again.")
|
|
self.on_changed()
|
|
|
|
def _show_connection_error(self, message="Incorrect password. Please try again."):
|
|
"""Show connection error in a separate thread to prevent UI blocking"""
|
|
if self.password_dialog:
|
|
self.password_dialog.show_error(message)
|
|
return False # Don't repeat if called from GLib.timeout_add
|
|
|
|
def _on_password_cancel(self):
|
|
"""Handle password dialog cancel action"""
|
|
self._reset_connect_state()
|
|
|
|
|
|
class WifiConnections(Box):
|
|
def __init__(self, parent, show_back_button=True, **kwargs):
|
|
super().__init__(
|
|
spacing=8,
|
|
orientation="vertical",
|
|
name="wifi-connections",
|
|
**kwargs,
|
|
)
|
|
|
|
self.parent = parent
|
|
self.network_service = NetworkClient()
|
|
self.wifi_service = None
|
|
self.is_scanning = False # Track scanning state
|
|
self.refresh_timer = None # Timer for periodic network refresh
|
|
self._update_in_progress = False # Prevent concurrent updates
|
|
self._destroyed = False # Track if widget is destroyed
|
|
self._signal_ids = [] # Track all service signal IDs for cleanup
|
|
|
|
self._signal_ids.append(
|
|
(
|
|
self.network_service,
|
|
self.network_service.connect("device-ready", self.on_network_ready),
|
|
)
|
|
)
|
|
|
|
# Create pull-to-refresh indicator
|
|
self.refresh_indicator = Label(
|
|
name="wifi-refresh-indicator",
|
|
label="↓ Pull to scan for networks",
|
|
h_align="center",
|
|
visible=False,
|
|
style="color: #fff; font-size: 12px; padding: 5px;",
|
|
)
|
|
|
|
title_children = []
|
|
if show_back_button:
|
|
title_children.append(
|
|
Button(
|
|
image=Image(icon_name="back", size=10),
|
|
on_clicked=lambda *_: self.parent.close_wifi(),
|
|
)
|
|
)
|
|
title_children.append(Label("Wi-Fi", name="wifi-title"))
|
|
|
|
self.title = Box(
|
|
orientation="h",
|
|
children=title_children,
|
|
)
|
|
|
|
self.toggle_button = SmoothSwitch(
|
|
name="toggle-button",
|
|
on_user_toggle=lambda active: self.on_toggle_changed(
|
|
self.toggle_button, active
|
|
),
|
|
)
|
|
|
|
# Create Known Network section
|
|
self.known_networks_label = Label(
|
|
label="Known Network", h_align="start", name="networks-title"
|
|
)
|
|
self.known_networks = Box(
|
|
spacing=4, orientation="vertical", name="known-networks"
|
|
)
|
|
self.known_networks_scrolled = AnimatedScrollable(
|
|
min_content_size=(303, 0),
|
|
max_content_size=(303, 200),
|
|
child=self.known_networks,
|
|
overlay_scroll=True,
|
|
)
|
|
|
|
# Create "No networks available" message
|
|
self.no_networks_label = Label(
|
|
label="No networks available",
|
|
h_align="center",
|
|
name="no-networks-label",
|
|
visible=False,
|
|
)
|
|
|
|
# Create Other Networks section with clickable title
|
|
self.other_networks_button = Button(
|
|
child=Label("Other Networks", h_align="start"),
|
|
name="wifi-other-button",
|
|
on_clicked=self.toggle_other_networks,
|
|
)
|
|
self.other_networks = Box(spacing=4, orientation="vertical")
|
|
self.other_networks.set_size_request(-1, 150)
|
|
|
|
# Create scrolled window for other networks
|
|
self.other_networks_scrolled = AnimatedScrollable(
|
|
min_content_size=(303, 0),
|
|
max_content_size=(303, 300),
|
|
child=self.other_networks,
|
|
overlay_scroll=True,
|
|
)
|
|
self.other_networks.set_visible(False)
|
|
|
|
# Add pull-to-refresh functionality to scrolled window
|
|
self.setup_pull_to_refresh()
|
|
|
|
# Create More Settings button (same style as Other Networks button)
|
|
self.more_settings_button = Button(
|
|
child=Label("More Settings", h_align="start"),
|
|
name="wifi-other-button",
|
|
on_clicked=self.open_network_settings,
|
|
)
|
|
|
|
self.children = [
|
|
CenterBox(
|
|
start_children=self.title,
|
|
end_children=self.toggle_button,
|
|
name="wifi-widget-top",
|
|
),
|
|
self.refresh_indicator,
|
|
Separator(orientation="h", name="separator"),
|
|
self.known_networks_label,
|
|
self.known_networks_scrolled,
|
|
self.no_networks_label,
|
|
Separator(orientation="h", name="separator"),
|
|
self.other_networks_button,
|
|
self.other_networks_scrolled,
|
|
Separator(orientation="h", name="separator"),
|
|
self.more_settings_button,
|
|
]
|
|
|
|
self.connect("destroy", self.on_destroy)
|
|
self.connect("unmap", self.on_hide)
|
|
self.connect("map", lambda *_: self.start_network_monitoring())
|
|
|
|
def on_hide(self, *_):
|
|
"""Called when the widget is hidden (popup closed)"""
|
|
self.stop_network_monitoring()
|
|
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():
|
|
if self.wifi_service:
|
|
self.wifi_service.scan()
|
|
if hasattr(self, "_refresh_timeout_id") and self._refresh_timeout_id:
|
|
GLib.source_remove(self._refresh_timeout_id)
|
|
|
|
def _do_refresh():
|
|
self.force_network_refresh()
|
|
self._refresh_timeout_id = None
|
|
return False
|
|
|
|
self._refresh_timeout_id = GLib.timeout_add(150, _do_refresh)
|
|
|
|
def on_network_ready(self, *_):
|
|
"""Called when network service is ready"""
|
|
self.wifi_service = self.network_service.wifi_device
|
|
if self.wifi_service:
|
|
self.toggle_button.set_active(self.wifi_service.enabled)
|
|
|
|
self._signal_ids.append(
|
|
(
|
|
self.wifi_service,
|
|
self.wifi_service.connect(
|
|
"notify::enabled", self.on_wifi_enabled_changed
|
|
),
|
|
)
|
|
)
|
|
self._signal_ids.append(
|
|
(
|
|
self.wifi_service,
|
|
self.wifi_service.connect("changed", self.update_networks),
|
|
)
|
|
)
|
|
|
|
# Initial network update
|
|
self.update_networks()
|
|
|
|
def on_toggle_changed(self, toggle_button, *_):
|
|
"""Handle WiFi toggle button changes"""
|
|
if self.wifi_service:
|
|
new_state = toggle_button.get_active()
|
|
self.wifi_service.enabled = new_state
|
|
|
|
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 open_network_settings(self, *_):
|
|
"""Open NetworkManager connection editor"""
|
|
try:
|
|
subprocess.Popen(["nm-connection-editor"], start_new_session=True)
|
|
if self.parent and hasattr(self.parent, "hide_controlcenter"):
|
|
self.parent.hide_controlcenter()
|
|
except FileNotFoundError:
|
|
pass
|
|
except Exception as e:
|
|
logger.error(f"[WiFi] Failed to open network settings: {e}")
|
|
|
|
def update_networks(self, *_):
|
|
"""Update the list of available networks"""
|
|
# Prevent concurrent updates and check if destroyed
|
|
if self._update_in_progress or self._destroyed or not self.wifi_service:
|
|
return
|
|
|
|
self._update_in_progress = True
|
|
|
|
try:
|
|
current_known_ssids = {
|
|
child.ssid
|
|
for child in self.known_networks.get_children()
|
|
if hasattr(child, "ssid")
|
|
}
|
|
current_other_ssids = {
|
|
child.ssid
|
|
for child in self.other_networks.get_children()
|
|
if hasattr(child, "ssid")
|
|
}
|
|
|
|
access_points = self.wifi_service.access_points
|
|
known_networks = []
|
|
other_networks = []
|
|
new_known_ssids = set()
|
|
new_other_ssids = set()
|
|
|
|
for access_point in access_points:
|
|
try:
|
|
if access_point.ssid and access_point.ssid != "Unknown":
|
|
if access_point.is_active or self._is_saved_network(
|
|
access_point
|
|
):
|
|
known_networks.append(access_point)
|
|
new_known_ssids.add(access_point.ssid)
|
|
else:
|
|
other_networks.append(access_point)
|
|
new_other_ssids.add(access_point.ssid)
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"[WiFi] Failed to process access point {access_point}: {e}"
|
|
)
|
|
continue
|
|
|
|
known_changed = current_known_ssids != new_known_ssids
|
|
other_changed = current_other_ssids != new_other_ssids
|
|
|
|
if known_changed or other_changed:
|
|
# Get existing networks
|
|
existing_known = {
|
|
child.ssid: child for child in self.known_networks.get_children()
|
|
}
|
|
existing_other = {
|
|
child.ssid: child for child in self.other_networks.get_children()
|
|
}
|
|
|
|
for access_point in known_networks:
|
|
if not self._destroyed:
|
|
if access_point.ssid in existing_known:
|
|
slot = existing_known.pop(access_point.ssid)
|
|
slot.update_ap(access_point)
|
|
elif access_point.ssid in existing_other:
|
|
slot = existing_other.pop(access_point.ssid)
|
|
slot.update_ap(access_point)
|
|
self.other_networks.remove(slot)
|
|
self.known_networks.add(slot)
|
|
else:
|
|
network_slot = WifiNetworkSlot(
|
|
access_point,
|
|
self.wifi_service,
|
|
network_service=self.network_service,
|
|
parent=self.parent,
|
|
)
|
|
self.known_networks.add(network_slot)
|
|
|
|
for access_point in other_networks:
|
|
if not self._destroyed:
|
|
if access_point.ssid in existing_other:
|
|
slot = existing_other.pop(access_point.ssid)
|
|
slot.update_ap(access_point)
|
|
elif access_point.ssid in existing_known:
|
|
slot = existing_known.pop(access_point.ssid)
|
|
slot.update_ap(access_point)
|
|
self.known_networks.remove(slot)
|
|
self.other_networks.add(slot)
|
|
else:
|
|
network_slot = WifiNetworkSlot(
|
|
access_point,
|
|
self.wifi_service,
|
|
network_service=self.network_service,
|
|
parent=self.parent,
|
|
)
|
|
self.other_networks.add(network_slot)
|
|
|
|
for slot in existing_known.values():
|
|
if hasattr(slot, "access_point"):
|
|
slot.strength = 0
|
|
from shared.widgets.wifi_icon import get_wifi_icon_for_strength
|
|
|
|
if not slot.dimage.has_style_class(
|
|
"connecting"
|
|
) and not slot.dimage.has_style_class("disconnecting"):
|
|
slot.dimage.set_from_file(get_wifi_icon_for_strength(0))
|
|
|
|
for slot in existing_other.values():
|
|
if hasattr(slot, "access_point"):
|
|
slot.strength = 0
|
|
from shared.widgets.wifi_icon import get_wifi_icon_for_strength
|
|
|
|
if not slot.dimage.has_style_class(
|
|
"connecting"
|
|
) and not slot.dimage.has_style_class("disconnecting"):
|
|
slot.dimage.set_from_file(get_wifi_icon_for_strength(0))
|
|
|
|
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
|
|
|
|
self.known_networks_scrolled.set_visible(True)
|
|
self.no_networks_label.set_visible(not has_any_networks)
|
|
self.other_networks_button.set_visible(True) # Always visible
|
|
self.refresh_network_states()
|
|
|
|
except Exception as e:
|
|
logger.error(f"[WiFi] Error during update_networks: {e}")
|
|
finally:
|
|
self._update_in_progress = False
|
|
|
|
def _is_saved_network(self, access_point):
|
|
"""Check if a network is saved/known using NetworkManager connections"""
|
|
if not self.network_service:
|
|
return False
|
|
ssid = access_point.ssid
|
|
if not ssid or ssid == "Unknown":
|
|
return False
|
|
return self.network_service.is_network_saved(ssid)
|
|
|
|
def refresh_network_states(self, *_):
|
|
"""Refresh connection states for all network slots"""
|
|
for child in self.known_networks.get_children():
|
|
if hasattr(child, "on_changed"):
|
|
child.on_changed()
|
|
|
|
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"""
|
|
self.refresh_timer = GLib.timeout_add_seconds(5, self.periodic_network_refresh)
|
|
|
|
def stop_network_monitoring(self):
|
|
"""Stop periodic monitoring"""
|
|
if self.refresh_timer:
|
|
GLib.source_remove(self.refresh_timer)
|
|
self.refresh_timer = None
|
|
|
|
def periodic_network_refresh(self):
|
|
"""Periodically refresh network list to catch external connections"""
|
|
if self._destroyed:
|
|
self.refresh_timer = None
|
|
return False
|
|
|
|
if (
|
|
self._update_in_progress
|
|
or not self.wifi_service
|
|
or not self.wifi_service.enabled
|
|
):
|
|
return True # Continue monitoring
|
|
|
|
try:
|
|
self.update_networks()
|
|
except Exception as e:
|
|
logger.error(f"[WiFi] Error during periodic network refresh: {e}")
|
|
|
|
return True # Continue monitoring
|
|
|
|
def force_network_refresh(self):
|
|
"""Force an immediate refresh of the network list"""
|
|
if self._update_in_progress or self._destroyed:
|
|
return
|
|
|
|
try:
|
|
self.update_networks()
|
|
except Exception as e:
|
|
logger.error(f"[WiFi] Error during forced network refresh: {e}")
|
|
|
|
def setup_pull_to_refresh(self):
|
|
"""Setup pull-to-refresh gesture for the scrolled window"""
|
|
self.vadjustment = self.other_networks_scrolled.get_vadjustment()
|
|
|
|
# Track gesture state
|
|
self.pull_start_y = 0
|
|
self.is_pulling = False
|
|
self.pull_threshold = 50 # pixels to trigger refresh
|
|
|
|
# Connect to scroll events
|
|
self.other_networks_scrolled.connect("scroll-event", self.on_scroll_event)
|
|
self.other_networks_scrolled.connect("button-press-event", self.on_button_press)
|
|
self.other_networks_scrolled.connect(
|
|
"button-release-event", self.on_button_release
|
|
)
|
|
self.other_networks_scrolled.connect(
|
|
"motion-notify-event", self.on_motion_notify
|
|
)
|
|
|
|
# Enable events
|
|
self.other_networks_scrolled.set_events(
|
|
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):
|
|
"""Handle scroll events for pull-to-refresh"""
|
|
if self.vadjustment.get_value() <= 0:
|
|
if event.direction == Gdk.ScrollDirection.UP:
|
|
if self.wifi_service:
|
|
self.wifi_service.scan()
|
|
self.force_network_refresh()
|
|
return True
|
|
return False
|
|
|
|
def on_button_press(self, widget, event):
|
|
"""Handle button press for touch/drag gestures"""
|
|
if self.vadjustment.get_value() <= 0:
|
|
self.pull_start_y = event.y
|
|
self.is_pulling = True
|
|
return False
|
|
|
|
def on_button_release(self, widget, event):
|
|
"""Handle button release for touch/drag gestures"""
|
|
if self.is_pulling:
|
|
pull_distance = event.y - self.pull_start_y
|
|
if pull_distance > self.pull_threshold:
|
|
if self.wifi_service:
|
|
self.wifi_service.scan()
|
|
self.force_network_refresh()
|
|
self.refresh_indicator.set_visible(False)
|
|
self.refresh_indicator.remove_style_class("ready-to-refresh")
|
|
self.is_pulling = False
|
|
return False
|
|
|
|
def on_motion_notify(self, widget, event):
|
|
"""Handle motion events for visual feedback during pull"""
|
|
if self.is_pulling and self.vadjustment.get_value() <= 0:
|
|
pull_distance = event.y - self.pull_start_y
|
|
if pull_distance > 0:
|
|
self.refresh_indicator.set_visible(True)
|
|
if pull_distance >= self.pull_threshold:
|
|
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")
|
|
self.refresh_indicator.remove_style_class("ready-to-refresh")
|
|
else:
|
|
self.refresh_indicator.set_visible(False)
|
|
return False
|
|
|
|
def on_destroy(self, widget):
|
|
"""Cleanup when widget is destroyed"""
|
|
self._destroyed = True
|
|
self.stop_network_monitoring()
|
|
for obj, sig_id in self._signal_ids:
|
|
try:
|
|
obj.disconnect(sig_id)
|
|
except Exception as e:
|
|
logger.error(f"[WiFi] Failed to disconnect signal {sig_id}: {e}")
|
|
self._signal_ids.clear()
|
|
self.wifi_service = None
|
|
self.network_service = None
|
|
try:
|
|
self.other_networks_revealer.child_revealed = False
|
|
except Exception as e:
|
|
logger.error(f"[WiFi] Failed to hide other networks revealer: {e}")
|
|
|
|
def close_wifi(self):
|
|
"""Called when WiFi panel is being closed"""
|
|
self.other_networks_revealer.child_revealed = False
|