From 358b04bddc8f9011d88657ae44775350432b4807 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 27 Jul 2025 04:47:41 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(dashboard):=20str?= =?UTF-8?q?eamline=20network=20tile=20with=20NetworkClient=20and=20UI=20up?= =?UTF-8?q?dates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/settings/bluetooth_tab.py | 450 ------------------------ config/settings/gui.py | 12 - config/settings/wifi_tab.py | 568 ------------------------------- main.css | 54 --- modules/dashboard/network.py | 396 +++++++++++---------- utils/icons.py | 1 - 6 files changed, 216 insertions(+), 1265 deletions(-) delete mode 100644 config/settings/bluetooth_tab.py delete mode 100644 config/settings/wifi_tab.py diff --git a/config/settings/bluetooth_tab.py b/config/settings/bluetooth_tab.py deleted file mode 100644 index b93b98f..0000000 --- a/config/settings/bluetooth_tab.py +++ /dev/null @@ -1,450 +0,0 @@ -import subprocess - -from fabric.bluetooth import BluetoothClient, BluetoothDevice -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 GLib, Gtk - -import utils.icons as icons - - -class BluetoothDeviceSlot(CenterBox): - def __init__(self, device: BluetoothDevice, **kwargs): - super().__init__(name="bluetooth-device", **kwargs) - self.device = device - self.device.connect("changed", self.on_changed) - self.device.connect( - "notify::closed", lambda *_: self.device.closed and self.destroy() - ) - - self.connection_label = Label( - markup=icons.bluetooth_disconnected, style="font-size: 22px;" - ) - self.connect_button = Button( - label="Connect", - on_clicked=lambda *_: self._handle_device_action(), - ) - - # Create device name label that we can update - self.device_name_label = Label( - label=device.alias or device.name, - h_expand=True, - h_align="start", - ellipsization="end", - ) - - self.start_children = [ - Box( - spacing=10, - h_expand=True, - h_align="fill", - children=[ - Image(icon_name=device.icon_name + "-symbolic", size=16), - self.device_name_label, - self.connection_label, - ], - ) - ] - self.end_children = self.connect_button - - self.device.emit("changed") - - def _handle_device_action(self): - """Handle connect/disconnect button click""" - if self.device.connected: - # Disconnect - self.device.connected = False - elif self.device.paired: - # Connect to already paired device - self.device.connected = True - else: - # Pair new device using bluetoothctl - self._pair_device_with_bluetoothctl() - - def _pair_device_with_bluetoothctl(self): - """Pair device using bluetoothctl command""" - try: - device_name = self.device.alias or self.device.name - device_address = self.device.address - - # Show pairing notification - subprocess.run( - [ - "notify-send", - "Bluetooth Pairing", - f"Attempting to pair with {device_name}...", - "--icon=bluetooth", - "--urgency=normal", - "--app-name=Bluetooth", - ], - check=False, - ) - - # Use bluetoothctl to pair the device - # This will trigger the system's pairing agent which should show the pairing dialog - pairing_process = subprocess.Popen( - ["bluetoothctl", "pair", device_address], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - - # Monitor the pairing process in a separate thread - GLib.timeout_add( - 100, self._monitor_pairing_process, pairing_process, device_name - ) - - except Exception as e: - print(f"Failed to start pairing process: {e}") - self._show_pairing_failed_notification() - - def _monitor_pairing_process(self, process, device_name): - """Monitor the bluetoothctl pairing process""" - if process.poll() is None: - # Process still running - return True - - # Process finished, check result - stdout, stderr = process.communicate() - return_code = process.returncode - - if return_code == 0 and "Pairing successful" in stdout: - self._show_pairing_success_notification() - # Auto-trust and connect the device after successful pairing - self._trust_and_connect_device() - else: - self._show_pairing_failed_notification() - - return False # Stop monitoring - - def _trust_and_connect_device(self): - """Trust and connect to the device after successful pairing""" - try: - device_address = self.device.address - - # Trust the device - subprocess.run(["bluetoothctl", "trust", device_address], check=False) - - # Connect to the device - GLib.timeout_add(1000, lambda: setattr(self.device, "connected", True)) - - except Exception as e: - print(f"Failed to trust/connect device: {e}") - - def _show_pairing_success_notification(self): - """Show notification when pairing succeeds""" - try: - device_name = self.device.alias or self.device.name - subprocess.run( - [ - "notify-send", - "Bluetooth Paired", - f"Successfully paired with {device_name}", - "--icon=bluetooth", - "--urgency=normal", - "--app-name=Bluetooth", - ], - check=False, - ) - except Exception as e: - print(f"Failed to send pairing success notification: {e}") - - def _show_pairing_failed_notification(self): - """Show notification when pairing fails""" - try: - device_name = self.device.alias or self.device.name - subprocess.run( - [ - "notify-send", - "Bluetooth Pairing Failed", - f"Failed to pair with {device_name}", - "--icon=bluetooth-disabled", - "--urgency=normal", - "--app-name=Bluetooth", - ], - check=False, - ) - except Exception as e: - print(f"Failed to send pairing failed notification: {e}") - - def on_changed(self, *_): - # Update connection icon - self.connection_label.set_markup( - icons.bluetooth_connected - if self.device.connected - else icons.bluetooth_disconnected - ) - - # Check for pairing state changes and show notifications - base_name = self.device.alias or self.device.name - - # Track pairing success/failure - if hasattr(self, "_was_pairing") and self._was_pairing: - if self.device.paired and not self.device.connecting: - # Pairing succeeded - self._show_pairing_success_notification() - self._was_pairing = False - elif not self.device.connecting and not self.device.paired: - # Pairing failed - self._show_pairing_failed_notification() - self._was_pairing = False - - # Update device name with status - if self.device.connecting: - if self.device.paired: - status_text = f"{base_name}\nConnecting..." - self.connect_button.set_label("Connecting...") - else: - status_text = f"{base_name}\nPairing..." - self.connect_button.set_label("Pairing...") - self._was_pairing = True # Track that we're pairing - elif self.device.connected: - battery_info = "" - if self.device.battery_percentage > 0: - battery_info = f" • {self.device.battery_percentage:.0f}%" - status_text = f"{base_name}\nConnected{battery_info}" - self.connect_button.set_label("Disconnect") - else: - status_text = base_name - if self.device.paired: - self.connect_button.set_label("Connect") - else: - self.connect_button.set_label("Pair") - - self.device_name_label.set_label(status_text) - - -class BluetoothTab: - """Bluetooth device management tab for settings""" - - def __init__(self): - self.bluetooth_client = BluetoothClient(on_device_added=self.on_device_added) - self.device_widgets = {} - self.window_size_enforcer = None - - # Connect to bluetooth client signals - self.bluetooth_client.connect("notify::enabled", self._on_bluetooth_changed) - self.bluetooth_client.connect("notify::scanning", self._update_scan_button) - self.bluetooth_client.connect("device-removed", self.on_device_removed) - - 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 create_bluetooth_tab(self): - """Create the Bluetooth 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) - - # Create widgets first - self.bluetooth_subtitle = Label( - markup="Find and connect to Bluetooth devices", h_align="start" - ) - self.bluetooth_switch = Gtk.Switch() - self.scan_icon = Label( - name="bluetooth-tab-icon", markup=icons.radar, style="font-size: 18px;" - ) - self.scan_button = Button( - name="bluetooth-scan", - child=self.scan_icon, - tooltip_text="Scan for devices", - on_clicked=lambda *_: self.bluetooth_client.toggle_scan(), - style="margin:10px;", - ) - - # Header section with title and controls - header_box = CenterBox( - name="bluetooth-header", - style="margin-bottom: 16px;", - start_children=[ - Box( - orientation="v", - spacing=4, - children=[ - Label( - markup="Bluetooth", - h_align="start", - ), - self.bluetooth_subtitle, - ], - ) - ], - end_children=[ - Box( - orientation="h", - spacing=8, - children=[ - self.bluetooth_switch, - self.scan_button, - ], - ) - ], - ) - - # Set up switch - self.bluetooth_switch.set_valign(Gtk.Align.CENTER) - self.bluetooth_switch.connect( - "notify::active", self._on_bluetooth_switch_toggled - ) - - main_vbox.add(header_box) - - # Device containers with size constraints - self.paired_box = Box(spacing=20, orientation="v") - self.paired_box.set_size_request(560, -1) - self.available_box = Box(spacing=20, orientation="v") - self.available_box.set_size_request(560, -1) - - content_box = Box( - spacing=8, - orientation="v", - children=[ - Label( - name="bluetooth-section", - markup="Paired Devices", - h_align="start", - ), - self.paired_box, - Label( - name="bluetooth-section", - markup="Available Devices", - h_align="start", - ), - self.available_box, - ], - ) - # Set size constraints for content box - content_box.set_size_request(560, -1) - - # Devices list in scrolled window - devices_scrolled = ScrolledWindow( - name="bluetooth-devices", - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=False, - v_expand=False, - propagate_width=False, - propagate_height=False, - child=content_box, - ) - - # Set fixed size to prevent dynamic resizing - devices_scrolled.set_size_request(580, 500) - main_vbox.add(devices_scrolled) - - # Initialize bluetooth status and populate existing devices - self._update_bluetooth_status() - self._populate_existing_devices() - self.bluetooth_client.notify("enabled") - self.bluetooth_client.notify("scanning") - - return main_vbox - - def _update_bluetooth_status(self): - """Update Bluetooth status display""" - if self.bluetooth_client.enabled: - self.bluetooth_switch.set_active(True) - self.bluetooth_subtitle.set_markup( - "Find and connect to Bluetooth devices" - ) - self.scan_button.set_sensitive(True) - else: - self.bluetooth_switch.set_active(False) - self.bluetooth_subtitle.set_markup("Bluetooth is turned off") - self.scan_button.set_sensitive(False) - - def _on_bluetooth_changed(self, *args): - """Handle bluetooth state changes""" - GLib.idle_add(self._update_bluetooth_status) - GLib.idle_add(self._refresh_all_devices) - - def _update_scan_button(self, *args): - """Update scan button appearance""" - if self.bluetooth_client.scanning: - self.scan_icon.set_markup(icons.loader) - self.scan_icon.add_style_class("scanning") - self.scan_button.add_style_class("scanning") - self.scan_button.set_tooltip_text("Stop scanning for Bluetooth devices") - else: - self.scan_icon.set_markup(icons.radar) - self.scan_icon.remove_style_class("scanning") - self.scan_button.remove_style_class("scanning") - self.scan_button.set_tooltip_text("Scan for Bluetooth devices") - - def _populate_existing_devices(self): - """Populate existing devices when tab is created""" - if not self.bluetooth_client or not self.bluetooth_client.enabled: - return - - devices = self.bluetooth_client.devices - - for device in devices: - self.on_device_added(self.bluetooth_client, device.address) - - def on_device_added(self, client: BluetoothClient, address: str): - """Handle device added""" - # Check if device already exists to prevent duplicates - if address in self.device_widgets: - return - - if not (device := client.get_device(address)): - return - - slot = BluetoothDeviceSlot(device) - - # Store the slot in device_widgets to track it - self.device_widgets[address] = slot - - if device.paired or device.trusted: - self.paired_box.add(slot) - else: - self.available_box.add(slot) - - # Enforce window size after adding device - if self.window_size_enforcer: - GLib.idle_add(self.window_size_enforcer) - - def on_device_removed(self, client: BluetoothClient, address: str): - """Handle device removed""" - if address in self.device_widgets: - slot = self.device_widgets[address] - # Remove from UI - parent = slot.get_parent() - if parent: - parent.remove(slot) - # Remove from tracking - del self.device_widgets[address] - - # Enforce window size after removing device - if self.window_size_enforcer: - GLib.idle_add(self.window_size_enforcer) - - def _on_bluetooth_switch_toggled(self, switch, gparam): - """Handle Bluetooth switch toggle""" - is_active = switch.get_active() - self.bluetooth_client.enabled = is_active - - # Refresh devices when bluetooth is toggled - if is_active: - GLib.timeout_add(1000, self._populate_existing_devices) - else: - self._clear_all_devices() - - def _clear_all_devices(self): - """Clear all device widgets""" - for child in self.paired_box.get_children(): - self.paired_box.remove(child) - for child in self.available_box.get_children(): - self.available_box.remove(child) - self.device_widgets.clear() - - def _refresh_all_devices(self): - """Clear and repopulate all devices""" - self._clear_all_devices() - self._populate_existing_devices() diff --git a/config/settings/gui.py b/config/settings/gui.py index dbf3a56..900eda7 100644 --- a/config/settings/gui.py +++ b/config/settings/gui.py @@ -15,11 +15,9 @@ from PIL import Image from config.data import APP_NAME, APP_NAME_CAP from config.settings.about_tab import AboutTab from config.settings.appearance_tab import AppearanceTab -from config.settings.bluetooth_tab import BluetoothTab from config.settings.key_bindings_tab import KeyBindingsTab from config.settings.system_tab import SystemTab from config.settings.utils import backup_and_replace, start_config -from config.settings.wifi_tab import WiFiTab gi.require_version("Gtk", "3.0") @@ -53,8 +51,6 @@ class HyprConfGUI(Window): self.show_idle_checkbox = show_idle_checkbox # Initialize all tabs - self.wifi_tab = WiFiTab() - self.bluetooth_tab = BluetoothTab() self.key_bindings_tab = KeyBindingsTab() self.appearance_tab = AppearanceTab(self.themes, self) self.system_tab = SystemTab( @@ -62,10 +58,6 @@ class HyprConfGUI(Window): ) self.about_tab = AboutTab() - # Pass window size enforcement method to tabs - self.wifi_tab.set_window_size_enforcer(self.enforce_window_size) - self.bluetooth_tab.set_window_size_enforcer(self.enforce_window_size) - root_box = Box(orientation="v", spacing=10, style="margin: 10px;") # Set fixed size for root container root_box.set_size_request(620, 620) @@ -87,15 +79,11 @@ class HyprConfGUI(Window): # Set fixed size for tab stack self.tab_stack.set_size_request(580, 580) - self.wifi_tab_content = self.wifi_tab.create_wifi_tab() - self.bluetooth_tab_content = self.bluetooth_tab.create_bluetooth_tab() self.key_bindings_tab_content = self.key_bindings_tab.create_key_bindings_tab() self.appearance_tab_content = self.appearance_tab.create_appearance_tab() self.system_tab_content = self.system_tab.create_system_tab() self.about_tab_content = self.about_tab.create_about_tab() - self.tab_stack.add_titled(self.wifi_tab_content, "Wifi", "Wifi") - self.tab_stack.add_titled(self.bluetooth_tab_content, "Bluetooth", "Bluetooth") self.tab_stack.add_titled( self.key_bindings_tab_content, "key_bindings", "Key Bindings" ) diff --git a/config/settings/wifi_tab.py b/config/settings/wifi_tab.py deleted file mode 100644 index 5fd4f2f..0000000 --- a/config/settings/wifi_tab.py +++ /dev/null @@ -1,568 +0,0 @@ -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 diff --git a/main.css b/main.css index 45ab6a5..31a01a9 100644 --- a/main.css +++ b/main.css @@ -143,60 +143,6 @@ tooltip > * { transition: margin 0.25s cubic-bezier(0.5, 0.25, 0, 1); } -.wavy-circle { - color: var(--primary); - background-color: var(--surface); - border-color: var(--primary); -} - -#matugen-switcher, -#bluetooth-switch, -#wifi-switch, -#dnd-switch { - min-width: 40px; - min-height: 20px; - background-color: var(--surface); - border-radius: 15px; - padding: 2px; - transition: background-color 0.3s ease; -} - -/* Style the switch's slider */ -#matugen-switcher slider, -#bluetooth-switch slider, -#wifi-switch slider, -#dnd-switch slider { - background-color: var(--primary); - border-radius: 16px; - min-width: 16px; - min-height: 8px; - transition: - background-color 0.1s cubic-bezier(0.5, 0.25, 0, 1.25), - transform 0.25s cubic-bezier(0.5, 0.25, 0, 1.25); -} - -/* When the switch is active (checked) */ -#matugen-switcher:checked, -#bluetooth-switch:checked, -#wifi-switch:checked, -#dnd-switch:checked { - background-color: var(--primary); -} - -/* Optional: additional styling for the slider when active */ -#matugen-switcher:checked slider, -#bluetooth-switch:checked slider, -#wifi-switch:checked slider, -#dnd-switch:checked slider { - background-color: var(--shadow); -} - -#matugen-switcher:checked image, -#bluetooth-switch:checked image, -#wifi-switch:checked image, -#dnd-switch:checked image { - color: var(--shadow); -} #bluetooth-header, diff --git a/modules/dashboard/network.py b/modules/dashboard/network.py index a54ad2d..508e25c 100644 --- a/modules/dashboard/network.py +++ b/modules/dashboard/network.py @@ -1,17 +1,68 @@ -from fabric.widgets.label import Label +from fabric.utils import bulk_connect 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 Gtk from modules.dashboard.tile import Tile from services.network import NetworkClient - import utils.icons as icons +class WifiAccessPointSlot(CenterBox): + """A widget representing a single WiFi access point in the dashboard.""" + + def __init__(self, access_point, network_service: NetworkClient, wifi_service, **kwargs): + super().__init__(name="wifi-ap-slot", **kwargs) + self.access_point = access_point + self.network_service = network_service + self.wifi_service = wifi_service + + ssid = access_point.ssid + icon_name = access_point.icon + + self.is_active = access_point.is_active + + self.ap_icon = Image(icon_name=icon_name, size=24) + self.ap_label = Label( + label=ssid, + h_expand=True, + h_align="start", + ellipsization="end" + ) + + self.connect_button = Button( + name="wifi-connect-button", + label="Connected" if self.is_active else "Connect", + sensitive=not self.is_active, + on_clicked=self._on_connect_clicked, + style_classes=["connected"] if self.is_active else None, + ) + + self.set_start_children([ + Box(spacing=8, h_expand=True, h_align="fill", children=[ + self.ap_icon, + self.ap_label, + ]) + ]) + self.set_end_children([self.connect_button]) + + def _on_connect_clicked(self, *_): + """Handle connect button click.""" + if not self.is_active and self.access_point.bssid: + self.connect_button.set_label("Connecting...") + self.connect_button.set_sensitive(False) + self.network_service.connect_wifi_bssid(self.access_point.bssid) + + class Network(Tile): + """Network tile for the dashboard that shows WiFi status and networks.""" + def __init__(self, **kwargs): + # Create status label self.label = Label( style_classes=["desc-label", "off"], label="Disconnected", @@ -26,19 +77,36 @@ class Network(Tile): menu=True, **kwargs, ) - self.nm = NetworkClient() - self.nm.connect("changed", self.handle_connection_change) - self.nm.connect("ready", self.handle_connection_change) + + # Initialize network client + self.network_client = NetworkClient() + self.network_client.connect("changed", self.handle_connection_change) + self.network_client.connect("ready", self.handle_connection_change) + self.network_client.connect("wifi_device_added", self._on_wifi_device_added) + + # Initialize state + self._update_tile_state() + + # Track if content has been created to avoid showing status during init + self._content_created = False + + # Check if WiFi device is already available + if self.network_client.wifi_device: + self._setup_wifi_device() def handle_connection_change(self, *_): - # Get current network state from NetworkClient properties + """Handle network connection state changes.""" + self._update_tile_state() + + def _update_tile_state(self): + """Update tile visual state based on WiFi status.""" connected = False con_label = "Disconnected" - if self.nm.wifi_device and self.nm.wifi_device.active_access_point: + if self.network_client.wifi_device and self.network_client.wifi_device.active_access_point: connected = True - con_label = self.nm.wifi_device.active_access_point.ssid - elif not self.nm.wireless_enabled: + con_label = self.network_client.wifi_device.active_access_point.ssid + elif self.network_client.wifi_device and not self.network_client.wifi_device.wireless_enabled: con_label = "Off" if self.state != connected: @@ -49,14 +117,66 @@ class Network(Tile): else: self.remove_style_class("on") self.add_style_class("off") - print("State change:", connected) + print("WiFi state change:", connected) self.label.set_label(con_label) def create_content(self): - """Create the detailed network content for the dashboard""" - content_box = Box( + """Create the detailed network content for the dashboard.""" + # Create status label + self.status_label = Label( + label="Initializing ", + h_expand=True, + visible=False, + h_align="center" + ) + + # Create refresh button with icon + self.refresh_button_icon = Label( + name="network-refresh-label", + markup=icons.reload + ) + self.refresh_button = Button( + name="network-refresh", + child=self.refresh_button_icon, + tooltip_text="Scan for Wi-Fi networks", + on_clicked=self._refresh_access_points + ) + + # Create back button with chevron left icon + self.back_button_icon = Label( + name="network-back-label", + markup=icons.chevron_left + ) + self.back_button = Button( + name="network-back", + child=self.back_button_icon, + tooltip_text="Back to notifications", + on_clicked=self._on_back_clicked + ) + + # Create header + header_box = CenterBox( + name="network-header", + start_children=[self.back_button], + center_children=[Label(name="network-title", label="Wi-Fi Networks")], + end_children=[self.refresh_button] + ) + + # Create access points list + self.ap_list_box = Box(orientation="v", spacing=4) + scrolled_window = ScrolledWindow( + name="network-ap-scrolled-window", + child=self.ap_list_box, + h_expand=True, + v_expand=True, + propagate_width=False, + propagate_height=False, + ) + + # Main container + main_container = Box( name="network-content", orientation="v", spacing=8, @@ -64,192 +184,108 @@ class Network(Tile): v_expand=True, h_align="fill", style_classes=["tile-content"], + children=[ + header_box, + self.status_label, + scrolled_window, + ] ) - # Header with WiFi toggle - header_box = Box( - orientation="h", - spacing=12, - h_expand=True, - h_align="fill", - ) + # Initialize button states + self.refresh_button.set_sensitive(False) - wifi_label = Label( - markup="Wi-Fi", - h_align="start", - h_expand=True, - ) + # Mark content as created + self._content_created = True - self.wifi_switch = Gtk.Switch( - name="wifi-switch", - active=self.nm.wireless_enabled if self.nm.wifi_device else False, - ) - self.wifi_switch.connect("notify::active", self._on_wifi_toggle) + # Now update UI state since content is ready + if self.network_client.wifi_device: + self._update_wifi_status_ui() - header_box.children = [wifi_label, self.wifi_switch] - content_box.add(header_box) + return main_container - # Networks list - self.networks_container = Box( - orientation="v", - spacing=4, - h_expand=True, - ) + def _on_wifi_device_added(self, *_): + """Handle when WiFi device is added.""" + self._setup_wifi_device() - # Scrolled window for networks - networks_scroll = ScrolledWindow( - h_scrollbar_policy="never", - v_scrollbar_policy="automatic", - h_expand=True, - v_expand=True, - child=self.networks_container, - ) - networks_scroll.set_size_request(-1, 200) + def _setup_wifi_device(self): + """Setup WiFi device connections and initial state.""" + if self.network_client.wifi_device: + self.network_client.wifi_device.connect("changed", self._load_access_points) + self.network_client.wifi_device.connect("notify::wireless-enabled", self._update_wifi_status_ui) - content_box.add(networks_scroll) + # Only update UI if content has been created + if self._content_created and hasattr(self, 'refresh_button'): + self._update_wifi_status_ui() + else: + # Only show error if content has been created + if self._content_created and hasattr(self, 'status_label'): + self.status_label.set_label("Wi-Fi device not available.") + self.status_label.set_visible(True) + if hasattr(self, 'refresh_button'): + self.refresh_button.set_sensitive(False) - # Scan button - scan_button = Button( - label="Scan for Networks", - h_align="center", - on_clicked=self._on_scan_clicked, - ) - content_box.add(scan_button) + def _update_wifi_status_ui(self, *_): + """Update WiFi status UI elements.""" + if not hasattr(self, 'refresh_button') or not self._content_created: + return # Content not created yet - # Populate networks initially - self._refresh_networks() + if self.network_client.wifi_device: + enabled = self.network_client.wifi_device.wireless_enabled + self.refresh_button.set_sensitive(enabled) - return content_box + if not enabled: + self.status_label.set_label("Wi-Fi disabled.") + self.status_label.set_visible(True) + self._clear_ap_list() + else: + self.refresh_button.set_sensitive(False) - def _on_wifi_toggle(self, *_): - """Handle WiFi toggle""" - if self.nm.wifi_device: - self.nm.wifi_device.toggle_wifi() - self._refresh_networks() + def _on_back_clicked(self, *_): + """Handle back button click - return to default (notifications) view.""" + if self.dashboard_instance: + self.dashboard_instance.reset_to_default() - def _on_scan_clicked(self, *_): - """Handle scan button click""" - if self.nm.wifi_device: - self.nm.wifi_device.scan() - self._refresh_networks() + def _refresh_access_points(self, *_): + """Refresh access points list.""" + if self.network_client.wifi_device and self.network_client.wifi_device.wireless_enabled: + self.status_label.set_label("Scanning for Wi-Fi networks...") + self.status_label.set_visible(True) + self._clear_ap_list() + self.network_client.wifi_device.scan() + return False - def _refresh_networks(self): - """Refresh the networks list""" - # Clear existing networks - for child in self.networks_container.get_children(): - self.networks_container.remove(child) + def _clear_ap_list(self): + """Clear the access points list.""" + for child in self.ap_list_box.get_children(): + child.destroy() - if not self.nm.wifi_device or not self.nm.wifi_device.wireless_enabled: - no_wifi_label = Label( - label="WiFi is disabled", - h_align="center", - style_classes=["dim-label"], - ) - self.networks_container.add(no_wifi_label) + def _load_access_points(self, *_): + """Load and display access points.""" + if not hasattr(self, 'ap_list_box'): + return # Content not created yet + + if not self.network_client.wifi_device or not self.network_client.wifi_device.wireless_enabled: + self._clear_ap_list() + self.status_label.set_label("Wi-Fi disabled.") + self.status_label.set_visible(True) return - access_points = self.nm.wifi_device.access_points + self._clear_ap_list() + + access_points = self.network_client.wifi_device.access_points + if not access_points: - no_networks_label = Label( - label="No networks found\nClick 'Scan for Networks' to refresh", - h_align="center", - style_classes=["dim-label"], - ) - self.networks_container.add(no_networks_label) - 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_item(ap) - - self.networks_container.show_all() - - def _create_network_item(self, ap): - """Create a network item widget""" - network_box = Box( - orientation="h", - spacing=8, - h_expand=True, - style_classes=["network-item"], - ) - - # Signal strength icon - signal_strength = min(100, max(0, ap.strength)) - if signal_strength >= 75: - signal_icon = icons.wifi_3 - elif signal_strength >= 50: - signal_icon = icons.wifi_2 - elif signal_strength >= 25: - signal_icon = icons.wifi_1 + self.status_label.set_label("No Wi-Fi networks found.") + self.status_label.set_visible(True) else: - signal_icon = icons.wifi_0 + self.status_label.set_visible(False) + # Sort access points by connection status (connected first) then by signal strength + sorted_aps = sorted(access_points, key=lambda x: (not x.is_active, -x.strength)) + for access_point in sorted_aps: + slot = WifiAccessPointSlot( + access_point, self.network_client, self.network_client.wifi_device + ) + self.ap_list_box.add(slot) + self.ap_list_box.show_all() - signal_label = Label( - markup=signal_icon, - style_classes=["network-signal"], - ) - network_box.add(signal_label) - # Network name and status - info_box = Box( - orientation="v", - h_expand=True, - h_align="start", - ) - - if ap.is_active: - name_markup = f"{ap.ssid}" - status_label = Label( - label="Connected", - h_align="start", - style_classes=["network-status", "connected"], - ) - info_box.add(Label(markup=name_markup, h_align="start")) - info_box.add(status_label) - else: - name_label = Label( - label=ap.ssid, - h_align="start", - style_classes=["network-name"], - ) - info_box.add(name_label) - - network_box.add(info_box) - - # Lock icon for secured networks - if ap.requires_password: - lock_label = Label( - markup=icons.lock, - style_classes=["network-lock"], - ) - network_box.add(lock_label) - - # Make the whole item clickable - network_button = Button( - child=network_box, - style_classes=["network-button"], - on_clicked=lambda *_: self._on_network_clicked(ap), - ) - - self.networks_container.add(network_button) - - def _on_network_clicked(self, ap): - """Handle network item click""" - if ap.is_active: - # Disconnect from current network - if self.nm.wifi_device: - self.nm.wifi_device.disconnect_wifi() - else: - # Connect to network (simplified - would need password handling for secured networks) - if self.nm.wifi_device: - try: - self.nm.wifi_device.connect_to_access_point(ap) - except Exception as e: - print(f"Failed to connect to {ap.ssid}: {e}") - - # Refresh networks after action - self._refresh_networks() diff --git a/utils/icons.py b/utils/icons.py index ed187ea..074570d 100644 --- a/utils/icons.py +++ b/utils/icons.py @@ -89,7 +89,6 @@ coffee: str = "" notifications: str = "" notifications_dots: str = "" -blur: str = chr(0xF00B5) arrow_head: str = "" wifi_off: str = "" bluetooth_off: str = ""