update
This commit is contained in:
5 files changed
+982
-22
No files matched your search
+21
-21
@@ -1,27 +1,27 @@
|
||||
$wallpaper = /home/sankalp/Pictures/wallpapers/example-1.jpg
|
||||
$background = 1a1110
|
||||
$foreground = f1dedd
|
||||
$wallpaper = /home/sankalp/Pictures/wallpapers/2.png
|
||||
$background = 131318
|
||||
$foreground = e4e1e9
|
||||
|
||||
$primary = ffb3ad
|
||||
$secondary = e7bdb9
|
||||
$tertiary = e1c28c
|
||||
$surface = 1a1110
|
||||
$surface_bright = 423736
|
||||
$outline = a08c8a
|
||||
$primary = bcc2ff
|
||||
$secondary = c4c5dd
|
||||
$tertiary = e6bad6
|
||||
$surface = 131318
|
||||
$surface_bright = 39393f
|
||||
$outline = 90909a
|
||||
$error = ff5945
|
||||
|
||||
$shadow = 000000
|
||||
|
||||
$red = ffb4aa
|
||||
$green = b7d085
|
||||
$yellow = dec56e
|
||||
$blue = cebdfe
|
||||
$magenta = fcb0d5
|
||||
$cyan = 84d5c4
|
||||
$red = ffb2b9
|
||||
$green = 95d5a7
|
||||
$yellow = b8cf84
|
||||
$blue = bdc2ff
|
||||
$magenta = e4b7f3
|
||||
$cyan = 82d3e2
|
||||
$white = 82d3e0
|
||||
$red_dim = ff5a44
|
||||
$green_dim = 89ad42
|
||||
$yellow_dim = bb9b2b
|
||||
$blue_dim = 8359fc
|
||||
$magenta_dim = f84ea1
|
||||
$cyan_dim = 3eb59c
|
||||
$red_dim = ff4c5c
|
||||
$green_dim = 4cb86a
|
||||
$yellow_dim = 8baa43
|
||||
$blue_dim = 5764ff
|
||||
$magenta_dim = c360e4
|
||||
$cyan_dim = 30b5ce
|
||||
@@ -0,0 +1,418 @@
|
||||
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 subprocess
|
||||
|
||||
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 = {}
|
||||
|
||||
# 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 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 GUI window
|
||||
main_vbox.set_size_request(620, 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
|
||||
self.paired_box = Box(spacing=20, orientation="v")
|
||||
self.available_box = Box(spacing=20, orientation="v", )
|
||||
|
||||
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,
|
||||
]
|
||||
)
|
||||
|
||||
# Devices list in scrolled window
|
||||
devices_scrolled = ScrolledWindow(
|
||||
name="bluetooth-devices",
|
||||
h_scrollbar_policy="never",
|
||||
v_scrollbar_policy="automatic",
|
||||
h_expand=True,
|
||||
v_expand=True,
|
||||
propagate_width=False,
|
||||
propagate_height=False,
|
||||
child=content_box,
|
||||
)
|
||||
|
||||
devices_scrolled.set_size_request(-1, 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:
|
||||
return self.paired_box.add(slot)
|
||||
else:
|
||||
return self.available_box.add(slot)
|
||||
|
||||
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]
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
|
||||
+14
-1
@@ -18,6 +18,8 @@ from PIL import Image
|
||||
|
||||
from config.data import APP_NAME, APP_NAME_CAP, NOTIF_POS_DEFAULT, NOTIF_POS_KEY
|
||||
from config.settings.utils import backup_and_replace, bind_vars, start_config
|
||||
from config.settings.wifi_tab import WiFiTab
|
||||
from config.settings.bluetooth_tab import BluetoothTab
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
@@ -37,6 +39,10 @@ class HyprConfGUI(Window):
|
||||
self.show_lock_checkbox = show_lock_checkbox
|
||||
self.show_idle_checkbox = show_idle_checkbox
|
||||
|
||||
# Initialize WiFi and Bluetooth tabs
|
||||
self.wifi_tab = WiFiTab()
|
||||
self.bluetooth_tab = BluetoothTab()
|
||||
|
||||
root_box = Box(orientation="v", spacing=10, style="margin: 10px;")
|
||||
self.add(root_box)
|
||||
|
||||
@@ -49,12 +55,19 @@ class HyprConfGUI(Window):
|
||||
v_expand=False,
|
||||
h_expand=False,
|
||||
)
|
||||
|
||||
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.create_key_bindings_tab()
|
||||
self.appearance_tab_content = self.create_appearance_tab()
|
||||
self.system_tab_content = self.create_system_tab()
|
||||
self.about_tab_content = self.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"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,525 @@
|
||||
import gi
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.entry import Entry
|
||||
from fabric.widgets.label import Label
|
||||
from fabric.widgets.scrolledwindow import ScrolledWindow
|
||||
from gi.repository import GLib, Gtk
|
||||
|
||||
from services.network import NetworkClient
|
||||
from utils import icons
|
||||
import subprocess
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
|
||||
|
||||
class WiFiTab:
|
||||
"""WiFi network management tab for settings"""
|
||||
|
||||
def __init__(self):
|
||||
self.network_client = NetworkClient()
|
||||
self.access_point_widgets = {}
|
||||
self.password_entries = {}
|
||||
self.connecting_to = None
|
||||
|
||||
# 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 _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 GUI window
|
||||
main_vbox.set_size_request(620, 580) # Match GUI window content area
|
||||
|
||||
# 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=True,
|
||||
v_expand=True,
|
||||
propagate_width=False,
|
||||
propagate_height=False,
|
||||
)
|
||||
|
||||
# Set size to match available space (580 - header space ≈ 500)
|
||||
self.networks_scrolled.set_size_request(-1, 500)
|
||||
|
||||
self.networks_container = Box(
|
||||
orientation="v",
|
||||
spacing=0,
|
||||
)
|
||||
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()
|
||||
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
|
||||
Reference in new issue
Block a user