♻️ refactor(dashboard): streamline network tile with NetworkClient and UI updates

This commit is contained in:
sankalp committed 2025-07-27 04:47:41 +05:45
1 parent 377e5d8733
commit 358b04bddc
6 files changed
+216 -1265

No files matched your search

-450
View File
@@ -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()
-12
View File
@@ -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"
)
-568
View File
@@ -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
-54
View File
@@ -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
View File
@@ -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()
-1
View File
@@ -89,7 +89,6 @@ coffee: str = "&#xef0e;"
notifications: str = "&#xea35;"
notifications_dots: str = "&#xede3;"
blur: str = chr(0xF00B5)
arrow_head: str = "&#xea61;"
wifi_off: str = "&#xecfa;"
bluetooth_off: str = "&#xeceb;"