✨ feat(controlcenter): integrate network and bluetooth indicators with control center

This commit is contained in:
sankalp committed 2025-08-05 08:57:44 +05:45
1 parent 6530df911e
commit 25d12a838b
6 files changed
+732 -1016

No files matched your search

+12 -7
View File
@@ -54,7 +54,7 @@ class BluetoothDeviceSlot(CenterBox):
class BluetoothConnections(Box):
def __init__(self, parent, show_hidden_devices: bool = False, **kwargs):
def __init__(self, parent, show_hidden_devices: bool = False, show_back_button=True, **kwargs):
super().__init__(
spacing=4,
orientation="vertical",
@@ -67,15 +67,20 @@ class BluetoothConnections(Box):
self.client = BluetoothClient(on_device_added=self.on_device_added)
self.title = Box(
orientation="h",
children=[
# Create title with optional back button
title_children = []
if show_back_button:
title_children.append(
Button(
image=Image(icon_name="back", size=10),
on_clicked=lambda *_: self.parent.close_bluetooth(),
),
Label("Bluetooth"),
],
)
)
title_children.append(Label("Bluetooth"))
self.title = Box(
orientation="h",
children=title_children,
)
self.toggle_button = Gtk.Switch(visible=True, name="toggle-button")
+1
View File
@@ -104,6 +104,7 @@ class ModusControlCenter(Window):
# Disable brightness scale if no backlight device available
self.brightness_scale.set_sensitive(False)
# Create music widget with ultra-lazy player container
self.music_widget = Box(
name="music-widget",
children=[PlayerBoxStack(MprisPlayerManager())],
+76 -129
View File
@@ -6,31 +6,25 @@ 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 services.network import NetworkService
from services.network import NetworkClient
from widgets.wifi_password_dialog import WiFiPasswordDialog
class WifiNetworkSlot(CenterBox):
def __init__(self, network_data: dict, wifi_service, parent=None, **kwargs):
def __init__(self, access_point, wifi_service, parent=None, **kwargs):
super().__init__(**kwargs)
self.network_data = network_data
self.access_point = access_point
self.wifi_service = wifi_service
self.parent = parent # Reference to control center
# Get network info
self.ssid = network_data.get("ssid", "Unknown")
self.bssid = network_data.get("bssid", "")
self.strength = network_data.get("strength", 0)
self.icon_name = network_data.get(
"icon-name", "network-wireless-signal-none-symbolic"
)
# Get network info from AccessPoint object
self.ssid = access_point.ssid
self.bssid = access_point.bssid
self.strength = access_point.strength
self.icon_name = access_point.icon
# The network service puts the same active-ap reference in all networks
# We need to check if THIS specific network's BSSID matches the active AP's BSSID
self.is_connected = False
if self.wifi_service and self.wifi_service._ap:
active_bssid = self.wifi_service._ap.get_bssid()
self.is_connected = active_bssid == self.bssid
# Check if this network is currently connected
self.is_connected = access_point.is_active
# Initialize styles based on connection state
self.styles = [
@@ -61,11 +55,8 @@ class WifiNetworkSlot(CenterBox):
def toggle_connecting(self):
"""Toggle WiFi network connection"""
# Check if this network is currently connected by comparing BSSIDs
is_currently_connected = False
if self.wifi_service and self.wifi_service._ap:
active_bssid = self.wifi_service._ap.get_bssid()
is_currently_connected = active_bssid == self.bssid
# Check if this network is currently connected
is_currently_connected = self.access_point.is_active
if is_currently_connected:
# Show disconnecting state
@@ -73,32 +64,34 @@ class WifiNetworkSlot(CenterBox):
self.dimage.add_style_class("disconnecting")
# Disconnect from network
success = self.wifi_service.disconnect_network(self.ssid)
if success:
self.is_connected = False
# Remove disconnecting state after a short delay to show feedback
from gi.repository import GLib
self.wifi_service.disconnect_wifi()
self.is_connected = False
# Remove disconnecting state after a short delay to show feedback
from gi.repository import GLib
GLib.timeout_add(500, lambda: self._reset_disconnect_state())
else:
# Reset state if disconnect failed
self._reset_disconnect_state()
GLib.timeout_add(500, lambda: self._reset_disconnect_state())
else:
# Try to connect without password first (for open networks or saved connections)
self.dimage.set_property("icon-name", "network-wireless-acquiring-symbolic")
self.dimage.add_style_class("connecting")
success = self.wifi_service.connect_network(self.ssid)
if success:
self.is_connected = True
# Remove connecting state after a short delay
from gi.repository import GLib
GLib.timeout_add(500, lambda: self._reset_connect_state())
else:
# Connection failed, likely needs password - show password dialog
self._reset_connect_state()
# Try to connect - check if password is required
if self.access_point.requires_password:
# Show password dialog immediately
self._show_password_dialog()
else:
# Try to connect without password (for open networks)
self.dimage.set_property(
"icon-name", "network-wireless-acquiring-symbolic"
)
self.dimage.add_style_class("connecting")
success = self.wifi_service.connect_to_wifi(self.access_point)
if success:
self.is_connected = True
# Remove connecting state after a short delay
from gi.repository import GLib
GLib.timeout_add(500, lambda: self._reset_connect_state())
else:
# Connection failed
self._reset_connect_state()
# Update display after connection attempt
self.on_changed()
@@ -119,19 +112,8 @@ class WifiNetworkSlot(CenterBox):
def on_changed(self, *_):
"""Update display when connection state changes"""
# Check if this network is currently connected by comparing BSSIDs
is_currently_connected = False
if self.wifi_service and self.wifi_service._ap:
# Also check the internet connection state to ensure we're actually connected
internet_state = self.wifi_service.internet
if internet_state == "activated":
active_bssid = self.wifi_service._ap.get_bssid()
is_currently_connected = active_bssid == self.bssid
# If deactivating or deactivated, definitely not connected
elif internet_state in ["deactivating", "deactivated", "disconnected"]:
is_currently_connected = False
self.is_connected = is_currently_connected
# Check if this network is currently connected using the access point's is_active property
self.is_connected = self.access_point.is_active
self.styles = [
"connected" if self.is_connected else "",
]
@@ -165,7 +147,7 @@ class WifiNetworkSlot(CenterBox):
# Try to connect with password
try:
success = self.wifi_service.connect_network(ssid, password)
success = self.wifi_service.connect_to_wifi(self.access_point, password)
if success:
self.is_connected = True
# Remove connecting state after a short delay
@@ -201,7 +183,7 @@ class WifiNetworkSlot(CenterBox):
class WifiConnections(Box):
def __init__(self, parent, **kwargs):
def __init__(self, parent, show_back_button=True, **kwargs):
super().__init__(
spacing=4,
orientation="vertical",
@@ -210,12 +192,12 @@ class WifiConnections(Box):
)
self.parent = parent
self.network_service = NetworkService()
self.network_service = NetworkClient()
self.wifi_service = None
self.is_scanning = False # Track scanning state
# Wait for network service to be ready
self.network_service.connect("device-ready", self.on_network_ready)
self.network_service.connect("wifi-device-added", self.on_network_ready)
# Create pull-to-refresh indicator
self.refresh_indicator = Label(
@@ -226,15 +208,20 @@ class WifiConnections(Box):
style="color: #fff; font-size: 12px; padding: 5px;",
)
self.title = Box(
orientation="h",
children=[
# Create title with optional back button
title_children = []
if show_back_button:
title_children.append(
Button(
image=Image(icon_name="back", size=10),
on_clicked=lambda *_: self.parent.close_wifi(),
),
Label("WiFi"),
],
)
)
title_children.append(Label("WiFi"))
self.title = Box(
orientation="h",
children=title_children,
)
self.toggle_button = Gtk.Switch(visible=True, name="toggle-button")
@@ -276,43 +263,31 @@ class WifiConnections(Box):
self.wifi_service = self.network_service.wifi_device
if self.wifi_service:
# Set up WiFi toggle
self.toggle_button.set_active(self.wifi_service.enabled)
self.toggle_button.set_active(self.wifi_service.wireless_enabled)
self.toggle_button.connect(
"notify::active",
lambda *_: setattr(
self.wifi_service, "enabled", self.toggle_button.get_active()
self.wifi_service,
"wireless_enabled",
self.toggle_button.get_active(),
),
)
# Connect to WiFi service signals
self.wifi_service.connect("notify::enabled", self.on_wifi_enabled_changed)
self.wifi_service.connect("notify::scanning", self.on_scanning_changed)
self.wifi_service.connect("changed", self.update_networks)
# Also connect to access point changes to update connection states
self.wifi_service.connect(
"notify::access-points", self.refresh_network_states
"notify::wireless-enabled", self.on_wifi_enabled_changed
)
# Connect to internet state changes to handle disconnections
self.wifi_service.connect("notify::internet", self.refresh_network_states)
self.wifi_service.connect("notify::ssid", self.refresh_network_states)
self.wifi_service.connect("changed", self.update_networks)
self.wifi_service.connect("ap-added", self.update_networks)
self.wifi_service.connect("ap-removed", self.update_networks)
# Initial network update
self.update_networks()
# Initialize scanning state
self.wifi_service.notify("scanning")
def on_wifi_enabled_changed(self, *_):
"""Handle WiFi enabled state changes"""
if self.wifi_service:
self.toggle_button.set_active(self.wifi_service.enabled)
def on_scanning_changed(self, *_):
"""Handle scanning state changes"""
if self.wifi_service:
is_scanning = self.wifi_service.scanning
self.is_scanning = is_scanning # Update our tracking state
self.update_scan_label()
self.toggle_button.set_active(self.wifi_service.wireless_enabled)
def update_networks(self, *_):
"""Update the list of available networks"""
@@ -325,10 +300,10 @@ class WifiConnections(Box):
# Add current networks
access_points = self.wifi_service.access_points
for ap_data in access_points:
if ap_data.get("ssid") and ap_data.get("ssid") != "Unknown":
for access_point in access_points:
if access_point.ssid and access_point.ssid != "Unknown":
network_slot = WifiNetworkSlot(
ap_data, self.wifi_service, parent=self.parent
access_point, self.wifi_service, parent=self.parent
)
self.available_networks.add(network_slot)
@@ -377,8 +352,8 @@ class WifiConnections(Box):
# Only handle pull-to-refresh when at the top
if self.vadjustment.get_value() <= 0:
if event.direction == Gdk.ScrollDirection.UP:
# Scrolling up at the top - start scan (only if not already scanning)
if self.wifi_service and not self.is_scanning:
# Scrolling up at the top - start scan
if self.wifi_service:
self.wifi_service.scan()
# Trigger smooth bounce animation for scroll
self.start_bounce_animation()
@@ -397,8 +372,8 @@ class WifiConnections(Box):
if self.is_pulling:
pull_distance = event.y - self.pull_start_y
if pull_distance > self.pull_threshold:
# Start scan (only if not already scanning)
if self.wifi_service and not self.is_scanning:
# Start scan
if self.wifi_service:
self.wifi_service.scan()
# Trigger elastic animation for pull gesture (most satisfying)
self.start_elastic_animation()
@@ -419,36 +394,15 @@ class WifiConnections(Box):
# Show refresh indicator when pulling down
self.refresh_indicator.set_visible(True)
if pull_distance >= self.pull_threshold:
if self.is_scanning:
self.refresh_indicator.set_label("↑ Already scanning...")
else:
self.refresh_indicator.set_label("↑ Release to scan")
self.refresh_indicator.set_label("↑ Release to scan")
self.refresh_indicator.add_style_class("ready-to-refresh")
else:
if self.is_scanning:
self.refresh_indicator.set_label("↓ Scanning in progress...")
else:
self.refresh_indicator.set_label("↓ Pull to scan for networks")
self.refresh_indicator.set_label("↓ Pull to scan for networks")
self.refresh_indicator.remove_style_class("ready-to-refresh")
else:
self.refresh_indicator.set_visible(False)
return False
def update_scan_label(self):
"""Update scanning state appearance"""
if self.wifi_service and self.wifi_service.scanning:
# Show scanning feedback in refresh indicator
self.refresh_indicator.set_label("Scanning for networks...")
self.refresh_indicator.set_visible(True)
self.refresh_indicator.add_style_class("scanning")
self.refresh_indicator.show_all()
else:
# Hide scanning feedback only if not animating and not pulling
if not self.is_pulling and not self.bounce_timeout_id:
self.refresh_indicator.set_visible(False)
self.refresh_indicator.remove_style_class("scanning")
def ease_out_bounce(self, t):
"""Smooth bounce easing function (0 to 1)"""
if t < 1 / 2.75:
@@ -489,11 +443,8 @@ class WifiConnections(Box):
# Animation finished
self.refresh_indicator.set_margin_top(0)
self.bounce_timeout_id = None
# Hide indicator after animation if not scanning and not pulling
if (
not (self.wifi_service and self.wifi_service.scanning)
and not self.is_pulling
):
# Hide indicator after animation if not pulling
if not self.is_pulling:
self.refresh_indicator.set_visible(False)
return False
@@ -527,11 +478,8 @@ class WifiConnections(Box):
# Animation finished
self.refresh_indicator.set_margin_top(0)
self.bounce_timeout_id = None
# Hide indicator after animation if not scanning and not pulling
if (
not (self.wifi_service and self.wifi_service.scanning)
and not self.is_pulling
):
# Hide indicator after animation if not pulling
if not self.is_pulling:
self.refresh_indicator.set_visible(False)
return False
@@ -558,4 +506,3 @@ class WifiConnections(Box):
def on_destroy(self, widget):
"""Cleanup animations when widget is destroyed"""
self.stop_bounce_animation()
+114 -37
View File
@@ -4,8 +4,12 @@ from fabric.widgets.box import Box
from fabric.widgets.button import Button
from fabric.widgets.svg import Svg
from services.battery import Battery
from services.network import NetworkService
from services.network import NetworkClient
from utils.roam import modus_service
from modules.controlcenter.wifi import WifiConnections
from modules.controlcenter.bluetooth import BluetoothConnections
from widgets.mousecapture import DropDownMouseCapture
from widgets.wayland import WaylandWindow as Window
class BluetoothIndicator(Box):
@@ -21,10 +25,32 @@ class BluetoothIndicator(Box):
),
)
self.bt_button = Button(name="bt-button", child=self.bt_icon)
self.bt_button = Button(
name="bt-button", child=self.bt_icon, on_clicked=self.on_bluetooth_clicked
)
self.add(self.bt_button)
# Create Bluetooth control center widget
self.bluetooth_window = Window(
layer="overlay",
title="modus",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
keyboard_mode="on-demand",
name="bluetooth-control-window",
visible=False,
)
self.bluetooth_widget = BluetoothConnections(self, show_back_button=False)
self.bluetooth_window.children = [self.bluetooth_widget]
# Create mouse capture for Bluetooth widget
self.bluetooth_mousecapture = DropDownMouseCapture(
layer="top", child_window=self.bluetooth_window
)
modus_service.connect("bluetooth-changed", self.on_bluetooth_changed)
self.bluetooth.connect("changed", self.on_bluetooth_direct_changed)
self.bluetooth.connect("device-added", self.on_device_added)
@@ -107,12 +133,24 @@ class BluetoothIndicator(Box):
modus_service.bluetooth = bluetooth_state
def on_bluetooth_clicked(self, *args):
"""Handle Bluetooth indicator click"""
self.bluetooth_mousecapture.toggle_mousecapture()
def close_bluetooth(self, *args):
"""Close Bluetooth control center"""
self.bluetooth_mousecapture.hide_child_window()
def hide_controlcenter(self, *args):
"""Hide Bluetooth control center"""
self.bluetooth_mousecapture.hide_child_window()
class NetworkIndicator(Box):
def __init__(self, **kwargs):
super().__init__(name="network-indicator", orientation="h", **kwargs)
self.network_service = NetworkService()
self.network_service = NetworkClient()
self.network_icon = Svg(
name="network-icon",
@@ -122,11 +160,40 @@ class NetworkIndicator(Box):
),
)
self.network_button = Button(name="network-button", child=self.network_icon)
self.network_button = Button(
name="network-button",
child=self.network_icon,
on_clicked=self.on_wifi_clicked,
)
self.add(self.network_button)
# Create WiFi control center widget
self.wifi_window = Window(
layer="overlay",
title="modus",
anchor="top right",
margin="2px 10px 0px 0px",
exclusivity="auto",
keyboard_mode="on-demand",
name="wifi-control-window",
visible=False,
)
self.wifi_widget = WifiConnections(self, show_back_button=False)
self.wifi_window.children = [self.wifi_widget]
# Create mouse capture for WiFi widget
self.wifi_mousecapture = DropDownMouseCapture(
layer="top", child_window=self.wifi_window
)
modus_service.connect("wlan-changed", self.on_wlan_changed)
self.network_service.connect("device-ready", self.on_device_ready)
self.network_service.connect("wifi-device-added", self.on_wifi_device_added)
self.network_service.connect(
"ethernet-device-added", self.on_ethernet_device_added
)
self.network_service.connect("changed", self.on_network_changed)
self.update_modus_service_wlan_state()
self.update_state()
@@ -134,17 +201,21 @@ class NetworkIndicator(Box):
def on_wlan_changed(self, service, new_wlan_state):
self.update_state()
def on_device_ready(self, *args):
def on_wifi_device_added(self, *args):
"""Called when WiFi device is added"""
if self.network_service.wifi_device:
self.network_service.wifi_device.connect(
"changed", self.on_network_direct_changed
)
self.update_modus_service_wlan_state()
self.update_state()
def on_ethernet_device_added(self, *args):
"""Called when Ethernet device is added"""
if self.network_service.ethernet_device:
self.network_service.ethernet_device.connect(
"changed", self.on_network_direct_changed
)
self.update_modus_service_wlan_state()
self.update_state()
@@ -157,30 +228,28 @@ class NetworkIndicator(Box):
self.update_state()
def update_modus_service_wlan_state(self):
primary_device = self.network_service.primary_device
wlan_state = "disconnected"
if primary_device == "wifi" and self.network_service.wifi_device:
# Check WiFi first (prioritize WiFi over Ethernet)
if self.network_service.wifi_device:
wifi = self.network_service.wifi_device
if not wifi.enabled:
if not wifi.wireless_enabled:
wlan_state = "disabled"
elif wifi.internet == "activated":
wlan_state = f"connected:{wifi.ssid}"
if wifi.strength >= 0:
wlan_state += f":{wifi.strength}%"
elif wifi.internet == "activating":
wlan_state = f"connecting:{wifi.ssid}"
elif wifi.internet in ["deactivating", "deactivated", "disconnected"]:
wlan_state = "No Connection"
elif wifi.active_access_point:
ap = wifi.active_access_point
wlan_state = f"connected:{ap.ssid}"
if ap.strength >= 0:
wlan_state += f":{ap.strength}%"
else:
wlan_state = "enabled"
elif primary_device == "wired" and self.network_service.ethernet_device:
# Check Ethernet if WiFi is not connected
elif self.network_service.ethernet_device:
ethernet = self.network_service.ethernet_device
if ethernet.internet == "activated":
wlan_state = "ethernet:connected"
if hasattr(ethernet, "speed") and ethernet.speed > 0:
wlan_state += f":{ethernet.speed}Mbps"
if hasattr(ethernet, "speed") and ethernet.speed:
wlan_state += f":{ethernet.speed}"
elif ethernet.internet == "activating":
wlan_state = "ethernet:connecting"
else:
@@ -189,37 +258,33 @@ class NetworkIndicator(Box):
modus_service.wlan = wlan_state
def update_state(self):
primary_device = self.network_service.primary_device
tooltip = "No network connection"
icon_file = "wifi-off-clear.svg"
if primary_device == "wifi" and self.network_service.wifi_device:
# Check WiFi first (prioritize WiFi over Ethernet)
if self.network_service.wifi_device:
wifi = self.network_service.wifi_device
if not wifi.enabled:
if not wifi.wireless_enabled:
icon_file = "wifi-off-clear.svg"
tooltip = "WiFi disabled"
elif wifi.internet == "activated":
elif wifi.active_access_point:
ap = wifi.active_access_point
icon_file = "wifi-clear.svg"
tooltip = f"Connected to {wifi.ssid}"
if wifi.strength >= 0:
tooltip += f" ({wifi.strength}%)"
elif wifi.internet == "activating":
icon_file = "wifi-clear.svg"
tooltip = f"Connecting to {wifi.ssid}..."
elif wifi.internet in ["deactivating", "deactivated", "disconnected"]:
icon_file = "wifi-off-clear.svg"
tooltip = "WiFi disconnected"
tooltip = f"Connected to {ap.ssid}"
if ap.strength >= 0:
tooltip += f" ({ap.strength}%)"
else:
icon_file = "wifi-off-clear.svg"
tooltip = "WiFi disconnected"
elif primary_device == "wired" and self.network_service.ethernet_device:
# Check Ethernet if WiFi is not connected
elif self.network_service.ethernet_device:
ethernet = self.network_service.ethernet_device
if ethernet.internet == "activated":
icon_file = "network-wired.svg"
tooltip = "Ethernet connected"
if hasattr(ethernet, "speed") and ethernet.speed > 0:
tooltip += f" ({ethernet.speed} Mbps)"
if hasattr(ethernet, "speed") and ethernet.speed:
tooltip += f" ({ethernet.speed})"
elif ethernet.internet == "activating":
icon_file = "network-wired.svg"
tooltip = "Ethernet connecting..."
@@ -232,6 +297,18 @@ class NetworkIndicator(Box):
)
self.network_button.set_tooltip_text(tooltip)
def on_wifi_clicked(self, *args):
"""Handle WiFi indicator click"""
self.wifi_mousecapture.toggle_mousecapture()
def close_wifi(self, *args):
"""Close WiFi control center"""
self.wifi_mousecapture.hide_child_window()
def hide_controlcenter(self, *args):
"""Hide WiFi control center"""
self.wifi_mousecapture.hide_child_window()
class BatteryIndicator(Box):
def __init__(self, **kwargs):
+520 -843
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -5,6 +5,15 @@
margin: 0;
}
#bluetooth-control-window,
#wifi-control-window {
margin: 6px;
background-color: alpha(#fff, 0.09);
border: 1px solid alpha(#111, 0.3);
box-shadow: inset 0 0 200px 0 alpha(#111, 0.3);
border-radius: 16px;
}
#control-center-widgets {
background-color: alpha(#fff, 0.09);
box-shadow: inset 0 0 0 1px alpha(#aaa, 0.4);