273 lines
9.7 KiB
Python
273 lines
9.7 KiB
Python
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
|
|
|
|
import utils.icons as icons
|
|
from modules.dashboard.tile import Tile, add_hover_cursor
|
|
|
|
|
|
class BluetoothDeviceSlot(CenterBox):
|
|
"""A widget representing a single Bluetooth device in the dashboard."""
|
|
|
|
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(
|
|
name="bluetooth-connection", markup=icons.bluetooth_disconnected
|
|
)
|
|
self.connect_button = Button(
|
|
name="bluetooth-connect",
|
|
label="Connect",
|
|
on_clicked=lambda *_: self.device.set_connecting(not self.device.connected),
|
|
style_classes=["connected"] if self.device.connected else None,
|
|
)
|
|
|
|
self.start_children = [
|
|
Box(
|
|
spacing=8,
|
|
h_expand=True,
|
|
h_align="fill",
|
|
children=[
|
|
Image(icon_name=device.icon_name + "-symbolic", size=16),
|
|
Label(
|
|
label=device.name[:8] + "..." if len(device.name) > 8 else device.name,
|
|
h_expand=True,
|
|
h_align="start",
|
|
),
|
|
self.connection_label,
|
|
],
|
|
)
|
|
]
|
|
self.end_children = self.connect_button
|
|
|
|
self.device.emit("changed")
|
|
|
|
def on_changed(self, *_):
|
|
"""Handle device state changes."""
|
|
self.connection_label.set_markup(
|
|
icons.bluetooth_connected
|
|
if self.device.connected
|
|
else icons.bluetooth_disconnected
|
|
)
|
|
if self.device.connecting:
|
|
self.connect_button.set_label(
|
|
"Connecting..." if not self.device.connecting else "..."
|
|
)
|
|
else:
|
|
self.connect_button.set_label(
|
|
"Connect" if not self.device.connected else "Disconnect"
|
|
)
|
|
if self.device.connected:
|
|
self.connect_button.add_style_class("connected")
|
|
else:
|
|
self.connect_button.remove_style_class("connected")
|
|
|
|
|
|
class Bluetooth(Tile):
|
|
"""Bluetooth tile for the dashboard that shows Bluetooth status and devices."""
|
|
|
|
def __init__(self, **kwargs):
|
|
# Create status label
|
|
self.label = Label(
|
|
style_classes=["desc-label", "off"],
|
|
label="Off",
|
|
h_align="start",
|
|
)
|
|
self.state = False
|
|
|
|
super().__init__(
|
|
label="Bluetooth",
|
|
props=self.label,
|
|
markup=icons.bluetooth,
|
|
menu=True,
|
|
**kwargs,
|
|
)
|
|
|
|
# Initialize bluetooth client with device handling
|
|
self.bluetooth_client = BluetoothClient(on_device_added=self.on_device_added)
|
|
self.bluetooth_client.connect(
|
|
"notify::enabled", self._on_bluetooth_state_changed
|
|
)
|
|
self.bluetooth_client.connect("notify::scanning", self._update_scan_label)
|
|
self.bluetooth_client.connect(
|
|
"device-added", self._on_device_connection_changed
|
|
)
|
|
self.bluetooth_client.connect(
|
|
"device-removed", self._on_device_connection_changed
|
|
)
|
|
|
|
# Initialize state
|
|
self._update_tile_state()
|
|
|
|
# Override the type_box button behavior to toggle Bluetooth
|
|
self._setup_content_button()
|
|
|
|
def _update_tile_state(self):
|
|
"""Update tile visual state based on Bluetooth status."""
|
|
self.state = self.bluetooth_client.enabled
|
|
if self.state:
|
|
self.remove_style_class("off")
|
|
self.add_style_class("on")
|
|
self.label.set_label("On")
|
|
else:
|
|
self.remove_style_class("on")
|
|
self.add_style_class("off")
|
|
self.label.set_label("Off")
|
|
|
|
# Update tile icon based on Bluetooth state
|
|
self._update_tile_icon()
|
|
|
|
def _setup_content_button(self):
|
|
"""Setup the content button to toggle Bluetooth instead of showing menu."""
|
|
if hasattr(self, "type_box"):
|
|
# Disconnect the existing click handler
|
|
self.type_box.disconnect_by_func(self.handle_click)
|
|
# Connect to Bluetooth toggle handler
|
|
self.type_box.connect("clicked", self._on_bluetooth_toggle)
|
|
|
|
def _on_bluetooth_toggle(self, *_):
|
|
"""Handle Bluetooth toggle when clicking on the main content area."""
|
|
# Toggle Bluetooth state
|
|
self.bluetooth_client.enabled = not self.bluetooth_client.enabled
|
|
|
|
def _update_tile_icon(self):
|
|
"""Update the tile icon based on Bluetooth status and connected devices."""
|
|
if not self.bluetooth_client.enabled:
|
|
# Bluetooth is disabled
|
|
self.icon.set_markup(icons.bluetooth_off)
|
|
return
|
|
|
|
# Bluetooth is enabled - check for connected devices
|
|
connected_devices = self.bluetooth_client.connected_devices
|
|
if connected_devices:
|
|
# Has connected devices
|
|
self.icon.set_markup(icons.bluetooth_connected)
|
|
else:
|
|
# Enabled but no connected devices
|
|
if self.bluetooth_client.scanning:
|
|
# Currently scanning - could add animation here if desired
|
|
self.icon.set_markup(icons.bluetooth)
|
|
else:
|
|
# Idle state
|
|
self.icon.set_markup(icons.bluetooth)
|
|
|
|
def create_content(self):
|
|
"""Create the detailed bluetooth content for the dashboard."""
|
|
# Create back button with chevron left icon
|
|
self.back_button_icon = Label(
|
|
name="bluetooth-back-label", markup=icons.chevron_left
|
|
)
|
|
self.back_button = Button(
|
|
name="bluetooth-back",
|
|
child=self.back_button_icon,
|
|
tooltip_text="Back to notifications",
|
|
on_clicked=self._on_back_clicked,
|
|
)
|
|
add_hover_cursor(self.back_button)
|
|
|
|
# Create scan button with icon
|
|
self.scan_label = Label(name="bluetooth-scan-label", markup=icons.radar)
|
|
self.scan_button = Button(
|
|
name="bluetooth-scan",
|
|
child=self.scan_label,
|
|
tooltip_text="Scan for Bluetooth devices",
|
|
on_clicked=lambda *_: self.bluetooth_client.toggle_scan(),
|
|
)
|
|
add_hover_cursor(self.scan_button)
|
|
|
|
# Create device containers
|
|
self.paired_box = Box(spacing=2, orientation="v")
|
|
self.available_box = Box(spacing=2, orientation="v")
|
|
|
|
# Create content structure
|
|
content_box = Box(spacing=4, orientation="v")
|
|
content_box.add(self.paired_box)
|
|
content_box.add(Label(name="bluetooth-section", label="Available"))
|
|
content_box.add(self.available_box)
|
|
|
|
# Create header
|
|
header_box = CenterBox(
|
|
name="bluetooth-header",
|
|
start_children=[self.back_button],
|
|
center_children=[Label(name="bluetooth-title", label="Bluetooth Devices")],
|
|
end_children=[self.scan_button],
|
|
)
|
|
|
|
# Main container
|
|
main_container = Box(
|
|
name="bluetooth-content",
|
|
orientation="v",
|
|
spacing=8,
|
|
h_expand=True,
|
|
v_expand=True,
|
|
h_align="fill",
|
|
style_classes=["tile-content"],
|
|
children=[
|
|
header_box,
|
|
ScrolledWindow(
|
|
name="bluetooth-devices",
|
|
min_content_size=(-1, -1),
|
|
child=content_box,
|
|
v_expand=True,
|
|
propagate_width=False,
|
|
propagate_height=False,
|
|
),
|
|
],
|
|
)
|
|
|
|
# Initialize notifications
|
|
self.bluetooth_client.notify("scanning")
|
|
self.bluetooth_client.notify("enabled")
|
|
|
|
return main_container
|
|
|
|
def on_device_added(self, client: BluetoothClient, address: str):
|
|
"""Handle new device discovery."""
|
|
if not (device := client.get_device(address)):
|
|
return
|
|
|
|
# Connect to device changes to update tile icon when connection state changes
|
|
device.connect("changed", self._on_device_connection_changed)
|
|
|
|
slot = BluetoothDeviceSlot(device)
|
|
|
|
if device.paired:
|
|
return self.paired_box.add(slot)
|
|
return self.available_box.add(slot)
|
|
|
|
def _update_scan_label(self, *_):
|
|
"""Update scan button appearance based on scanning state."""
|
|
if self.bluetooth_client.scanning:
|
|
self.scan_label.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_label.remove_style_class("scanning")
|
|
self.scan_button.remove_style_class("scanning")
|
|
self.scan_button.set_tooltip_text("Scan for Bluetooth devices")
|
|
|
|
# Update tile icon when scanning state changes
|
|
self._update_tile_icon()
|
|
|
|
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_bluetooth_state_changed(self, *_):
|
|
"""Handle bluetooth state change."""
|
|
self._update_tile_state()
|
|
|
|
def _on_device_connection_changed(self, *_):
|
|
"""Handle device connection/disconnection changes."""
|
|
self._update_tile_icon()
|