Files
Modus/config/settings/wifi_tab.py
T

536 lines
20 KiB
Python

import gi
from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.entry import Entry
from fabric.widgets.label import Label
from fabric.widgets.scrolledwindow import ScrolledWindow
from gi.repository import GLib, Gtk
from services.network import NetworkClient
from utils import icons
import subprocess
gi.require_version("Gtk", "3.0")
class WiFiTab:
"""WiFi network management tab for settings"""
def __init__(self):
self.network_client = NetworkClient()
self.access_point_widgets = {}
self.password_entries = {}
self.connecting_to = None
self.window_size_enforcer = None
# Connect to network client signals
self.network_client.connect("ready", self._on_network_ready)
self.network_client.connect("changed", self._on_network_changed)
if self.network_client.is_ready:
self._setup_network_signals()
def set_window_size_enforcer(self, enforcer_func):
"""Set the window size enforcer function from the main GUI"""
self.window_size_enforcer = enforcer_func
def _on_network_ready(self, *args):
"""Called when network client is ready"""
self._setup_network_signals()
self._refresh_networks()
def _setup_network_signals(self):
"""Setup signals for WiFi device"""
if self.network_client.wifi_device:
self.network_client.wifi_device.connect("changed", self._on_wifi_changed)
self.network_client.wifi_device.connect("ap-added", self._on_ap_added)
self.network_client.wifi_device.connect("ap-removed", self._on_ap_removed)
def _on_network_changed(self, *args):
"""Called when network state changes"""
self._refresh_networks()
def _on_wifi_changed(self, *args):
"""Called when WiFi state changes"""
self._refresh_networks()
def _on_ap_added(self, device, ap):
"""Called when access point is added"""
self._refresh_networks()
def _on_ap_removed(self, device, ap):
"""Called when access point is removed"""
self._refresh_networks()
def create_wifi_tab(self):
"""Create the WiFi tab content"""
main_vbox = Box(
orientation="v",
spacing=0,
style="padding: 0; margin: 15px;"
)
# Set fixed size for the main container to match tab stack dimensions
main_vbox.set_size_request(580, 580)
# Header section with title and controls
header_box = Box(
orientation="h",
spacing=12,
)
# WiFi section
wifi_section = Box(orientation="v", spacing=4)
# WiFi title
wifi_title = Label(
markup="<span size='large'><b>Wi-Fi</b></span>",
h_align="start"
)
wifi_section.add(wifi_title)
# WiFi subtitle
self.wifi_subtitle = Label(
markup="<span>Find and connect to Wi-Fi networks</span>",
h_align="start"
)
wifi_section.add(self.wifi_subtitle)
header_box.add(wifi_section)
# Spacer to push controls to the right
spacer = Box()
spacer.set_hexpand(True)
header_box.add(spacer)
# Controls section
controls_box = Box(orientation="h", spacing=12)
# WiFi toggle switch
self.wifi_switch = Gtk.Switch()
self.wifi_switch.set_valign(Gtk.Align.CENTER)
self.wifi_switch.connect("notify::active", self._on_wifi_switch_toggled)
controls_box.add(self.wifi_switch)
# Refresh button
self.refresh_button = Button()
self.refresh_icon = Label(name="wifi-tab-icon", markup=icons.reload, style="font-size: 18px;")
self.refresh_button.add(self.refresh_icon)
self.refresh_button.set_size_request(40, 40)
self.refresh_button.set_tooltip_text("Refresh networks")
self.refresh_button.set_style("margin:10px;")
self.refresh_button.connect("clicked", self._on_refresh_clicked)
controls_box.add(self.refresh_button)
header_box.add(controls_box)
main_vbox.add(header_box)
# Networks list in scrolled window
self.networks_scrolled = ScrolledWindow(
h_scrollbar_policy="never",
v_scrollbar_policy="automatic",
h_expand=False,
v_expand=False,
propagate_width=False,
propagate_height=False,
)
# Set fixed size to prevent dynamic resizing (580 - header space ≈ 500)
self.networks_scrolled.set_size_request(580, 500)
self.networks_container = Box(
orientation="v",
spacing=0,
)
# Set size constraints for networks container
self.networks_container.set_size_request(560, -1)
self.networks_scrolled.add(self.networks_container)
main_vbox.add(self.networks_scrolled)
# Initialize the display
GLib.timeout_add(100, self._refresh_networks)
return main_vbox
def _refresh_networks(self):
"""Refresh the networks display"""
# Clear existing widgets
for child in self.networks_container.get_children():
self.networks_container.remove(child)
self.access_point_widgets.clear()
self.password_entries.clear()
# Update WiFi status
self._update_wifi_status()
# Show available networks if WiFi is enabled
if (self.network_client.wifi_device and
self.network_client.wifi_device.wireless_enabled):
self._populate_networks()
self.networks_container.show_all()
# Enforce window size after content changes
if self.window_size_enforcer:
GLib.idle_add(self.window_size_enforcer)
return False
def _update_wifi_status(self):
"""Update WiFi status display"""
if not self.network_client.wifi_device:
self.wifi_subtitle.set_markup(
"<span>No WiFi device available</span>"
)
self.wifi_switch.set_sensitive(False)
return
if self.network_client.wifi_device.wireless_enabled:
self.wifi_switch.set_active(True)
self.wifi_subtitle.set_markup(
"<span>Find and connect to Wi-Fi networks</span>"
)
self.refresh_button.set_sensitive(True)
else:
self.wifi_switch.set_active(False)
self.wifi_subtitle.set_markup(
"<span>Wi-Fi is turned off</span>"
)
self.refresh_button.set_sensitive(False)
def _populate_networks(self):
"""Populate the networks list"""
if not self.network_client.wifi_device:
self._show_status_message("No WiFi device available", "error")
return
access_points = self.network_client.wifi_device.access_points
if not access_points:
self._show_status_message(
"No networks found\nClick 'Scan for Networks' to refresh",
"info"
)
return
# Sort access points by signal strength (connected first, then by strength)
sorted_aps = sorted(
access_points,
key=lambda ap: (not ap.is_active, -ap.strength)
)
for ap in sorted_aps:
self._create_network_widget(ap)
def _show_status_message(self, message, message_type="info"):
"""Show a status message in the networks container"""
status_box = Box(
orientation="v",
spacing=16,
style="padding: 60px;"
)
status_box.set_halign(Gtk.Align.CENTER)
status_box.set_valign(Gtk.Align.CENTER)
# Icon based on message type
if message_type == "error":
icon_markup = icons.cloud_off
message_label = Label(
markup=f"<span size='medium'>{message}</span>",
h_align="center",
style="color: var(--error);"
)
elif message_type == "scanning":
icon_markup = icons.loader
message_label = Label(
markup=f"<span size='medium'>{message}</span>",
h_align="center",
style="color: var(--blue);"
)
else: # info
icon_markup = icons.wifi_off
message_label = Label(
markup=f"<span size='medium'>{message}</span>",
h_align="center",
style="color: var(--outline);"
)
icon_label = Label(name="wifi-tab-icon", markup=icon_markup, style="font-size: 18px;")
icon_label.set_halign(Gtk.Align.CENTER)
status_box.add(icon_label)
message_label.set_line_wrap(True)
message_label.set_justify(Gtk.Justification.CENTER)
message_label.set_halign(Gtk.Align.CENTER)
status_box.add(message_label)
self.networks_container.add(status_box)
def _create_network_widget(self, ap):
"""Create widget for a single network"""
# Main container for this network (Box to allow adding password entry)
network_container = Box(orientation="v", spacing=0)
# Clickable network button
network_button = Button(
style="background-color: transparent; border: none; padding: 0; margin: 0;"
)
network_button.connect("clicked", lambda btn, access_point=ap: self._on_network_clicked(access_point))
# Network info row
info_box = Box(orientation="h", spacing=12)
info_box.set_margin_left(16)
info_box.set_margin_right(16)
info_box.set_margin_top(12)
info_box.set_margin_bottom(12)
# WiFi signal icon
strength = ap.strength
if strength >= 75:
icon_markup = icons.wifi_3
elif strength >= 50:
icon_markup = icons.wifi_2
elif strength >= 25:
icon_markup = icons.wifi_1
else:
icon_markup = icons.wifi_0
signal_label = Label(name="wifi-tab-icon", markup=icon_markup, style="font-size: 24px;")
signal_label.set_size_request(30, -1)
info_box.add(signal_label)
# Network name
network_name = ap.ssid
if ap.is_active:
network_markup = f"<b>{network_name}</b>\nConnected"
network_label = Label(markup=network_markup, h_align="start", style="color: var(--foreground);")
else:
network_markup = f"{network_name}"
network_label = Label(markup=network_markup, h_align="start", style="color: var(--foreground);")
network_label.set_line_wrap(True)
info_box.add(network_label)
# Spacer to push lock icon to the right
spacer = Box()
spacer.set_hexpand(True)
info_box.add(spacer)
# Lock icon for secured networks
if ap.requires_password:
lock_label = Label(name="wifi-tab-icon", markup=icons.lock, style="font-size: 22px;")
info_box.add(lock_label)
network_button.add(info_box)
network_container.add(network_button)
# Store reference
self.access_point_widgets[ap.ssid] = {
'container': network_container,
'button': network_button,
'info_box': info_box,
'network_label': network_label,
'ap': ap
}
self.networks_container.add(network_container)
def _on_network_clicked(self, ap):
"""Handle network item click"""
if ap.is_active:
# If connected, disconnect from this network
self._disconnect_network(ap)
else:
# If not connected, check if password is needed
if ap.requires_password and ap.ssid not in self.password_entries:
# Show password entry interface
self._show_password_entry(ap)
else:
# Connect directly (either no password needed or password already entered)
self._connect_network(ap)
def _on_wifi_switch_toggled(self, switch, gparam):
"""Handle WiFi switch toggle"""
if self.network_client.wifi_device:
is_active = switch.get_active()
self.network_client.wifi_device.wireless_enabled = is_active
GLib.timeout_add(500, self._refresh_networks)
def _on_refresh_clicked(self, button):
"""Handle refresh button click"""
if self.network_client.wifi_device and self.network_client.wifi_device.wireless_enabled:
self.refresh_icon.set_markup(icons.loader)
button.set_sensitive(False)
# Clear current networks and show scanning message
for child in self.networks_container.get_children():
self.networks_container.remove(child)
self._show_status_message("Scanning for networks...", "scanning")
self.networks_container.show_all()
self.network_client.wifi_device.scan()
# Re-enable button after scan
GLib.timeout_add(3000, lambda: [
self.refresh_icon.set_markup(icons.reload),
button.set_sensitive(True),
self._refresh_networks()
])
def _connect_network(self, ap):
"""Connect to a network"""
# Get password if available (either from entry or stored)
password = ""
if ap.requires_password:
if ap.ssid in self.password_entries:
# Get password from stored entry widget
password_widget = self.password_entries[ap.ssid]
if hasattr(password_widget, 'get_text'):
password = password_widget.get_text()
else:
password = str(password_widget) # If it's stored as string
else:
# No password available, this shouldn't happen if called correctly
print(f"Warning: No password available for {ap.ssid}")
return
self.connecting_to = ap.ssid
# Hide password entry if shown
if ap.ssid in self.password_entries:
self._hide_password_entry(ap)
# Update network widget to show connecting state
if ap.ssid in self.access_point_widgets:
network_label = self.access_point_widgets[ap.ssid]['network_label']
network_label.set_markup(f"{ap.ssid}\nConnecting...")
network_label.set_style("color: var(--blue);")
try:
# Connect to network
result = self.network_client.wifi_device.connect_to_wifi(ap, password)
if result is False:
# Connection failed immediately
self._show_connection_error(ap.ssid, "Failed to connect")
self._reset_connection_state(ap.ssid)
else:
# Reset button after timeout if still connecting
GLib.timeout_add(15000, lambda: self._reset_connection_state(ap.ssid))
except Exception as e:
self._show_connection_error(ap.ssid, f"Connection error: {str(e)}")
self._reset_connection_state(ap.ssid)
def _show_connection_error(self, ssid, error_message):
try:
subprocess.run([
"notify-send",
"WiFi Connection Failed",
f"Failed to connect to {ssid}\n{error_message}",
"--icon=network-wireless-offline",
"--urgency=normal",
"--app-name=WiFi"
], check=False)
except Exception as e:
print(f"Failed to send connection error notification: {e}")
def _show_password_entry(self, ap):
"""Show password entry for a network"""
if ap.ssid in self.access_point_widgets:
container = self.access_point_widgets[ap.ssid]['container']
# Check if password entry is already shown
if len(container.get_children()) > 1:
return # Already showing password entry
# Password entry row with theme styling
password_box = Box(
orientation="h",
spacing=8,
style="padding: 8px; margin: 4px 16px 8px 16px; border-radius: 8px;"
)
password_entry = Entry(
placeholder_text="Enter network password",
style="background-color: var(--surface-bright); border: 1px solid var(--outline); border-radius: 8px;"
)
password_entry.set_visibility(False) # Hide password characters
password_entry.set_hexpand(True)
password_entry.connect("activate", lambda entry, access_point=ap: self._on_password_entered(access_point, entry))
password_box.add(password_entry)
# Show/Hide password toggle
show_password_btn = Button(
style="border-radius: 8px;"
)
self.show_password_icon = Label(name="wifi-tab-icon", markup=icons.spy,style="font-size: 18px;")
show_password_btn.add(self.show_password_icon)
show_password_btn.set_tooltip_text("Show/Hide password")
show_password_btn.connect("clicked", lambda btn, entry=password_entry: self._toggle_password_visibility(entry, btn))
password_box.add(show_password_btn)
connect_btn = Button(
style="border-radius: 8px;"
)
connect_icon = Label(name="wifi-tab-icon", markup=icons.connect,style="font-size: 18px;")
connect_btn.add(connect_icon)
connect_btn.connect("clicked", lambda btn, access_point=ap, entry=password_entry: self._on_password_entered(access_point, entry))
password_box.add(connect_btn)
cancel_btn = Button(
style="border-radius: 8px;"
)
cancel_icon = Label(name="wifi-tab-icon", markup=icons.cancel,style="font-size: 18px;")
cancel_btn.add(cancel_icon)
cancel_btn.connect("clicked", lambda btn, access_point=ap: self._hide_password_entry(access_point))
password_box.add(cancel_btn)
container.add(password_box)
container.show_all()
# Store reference and focus
self.password_entries[ap.ssid] = password_entry
GLib.timeout_add(100, lambda: password_entry.grab_focus())
def _toggle_password_visibility(self, entry, button):
"""Toggle password visibility"""
current_visibility = entry.get_visibility()
entry.set_visibility(not current_visibility)
# Use spy icon for hidden, key icon for visible
self.show_password_icon.set_markup(icons.key if not current_visibility else icons.spy)
def _on_password_entered(self, ap, entry):
"""Handle password entry"""
password = entry.get_text()
self.password_entries[ap.ssid] = password
self._hide_password_entry(ap)
self._connect_network(ap)
def _hide_password_entry(self, ap):
"""Hide password entry for a network"""
if ap.ssid in self.access_point_widgets:
container = self.access_point_widgets[ap.ssid]['container']
children = container.get_children()
if len(children) > 1: # Remove password entry box
container.remove(children[-1])
def _disconnect_network(self, ap):
"""Disconnect from a network"""
if self.network_client.wifi_device:
self.network_client.wifi_device.disconnect_wifi()
GLib.timeout_add(1000, self._refresh_networks)
def _reset_connection_state(self, ssid):
"""Reset connection state for a network"""
if ssid in self.access_point_widgets:
# Reset the network label to normal state
ap = self.access_point_widgets[ssid]['ap']
network_label = self.access_point_widgets[ssid]['network_label']
if ap.is_active:
network_markup = f"<b>{ap.ssid}</b>\nConnected"
network_label.set_style("color: var(--foreground);")
else:
network_markup = f"{ap.ssid}"
network_label.set_style("color: var(--foreground);")
network_label.set_markup(network_markup)
self.connecting_to = None
self._refresh_networks()
return False