♻️ refactor(dashboard): streamline network tile with NetworkClient and UI updates
This commit is contained in:
6 files changed
+216
-1265
No files matched your search
@@ -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="<span>Find and connect to Bluetooth devices</span>", 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="<span size='large'><b>Bluetooth</b></span>",
|
||||
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="<b>Paired Devices</b>",
|
||||
h_align="start",
|
||||
),
|
||||
self.paired_box,
|
||||
Label(
|
||||
name="bluetooth-section",
|
||||
markup="<b>Available Devices</b>",
|
||||
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(
|
||||
"<span>Find and connect to Bluetooth devices</span>"
|
||||
)
|
||||
self.scan_button.set_sensitive(True)
|
||||
else:
|
||||
self.bluetooth_switch.set_active(False)
|
||||
self.bluetooth_subtitle.set_markup("<span>Bluetooth is turned off</span>")
|
||||
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()
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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="<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
|
||||
@@ -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,
|
||||
|
||||
+216
-180
@@ -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="<b>Wi-Fi</b>",
|
||||
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"<b>{ap.ssid}</b>"
|
||||
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()
|
||||
@@ -89,7 +89,6 @@ coffee: str = ""
|
||||
notifications: str = ""
|
||||
notifications_dots: str = ""
|
||||
|
||||
blur: str = chr(0xF00B5)
|
||||
arrow_head: str = ""
|
||||
wifi_off: str = ""
|
||||
bluetooth_off: str = ""
|
||||
|
||||
Reference in new issue
Block a user