import subprocess 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 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="Wi-Fi", h_align="start" ) wifi_section.add(wifi_title) # WiFi subtitle self.wifi_subtitle = Label( markup="Find and connect to Wi-Fi networks", 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("No WiFi device available") 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( "Find and connect to Wi-Fi networks" ) self.refresh_button.set_sensitive(True) else: self.wifi_switch.set_active(False) self.wifi_subtitle.set_markup("Wi-Fi is turned off") 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"{message}", h_align="center", style="color: var(--error);", ) elif message_type == "scanning": icon_markup = icons.loader message_label = Label( markup=f"{message}", h_align="center", style="color: var(--blue);", ) else: # info icon_markup = icons.wifi_off message_label = Label( markup=f"{message}", 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"{network_name}\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"{ap.ssid}\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