diff --git a/modules/controlcenter/bluetooth.py b/modules/controlcenter/bluetooth.py index 2ee38a7..5dad8a2 100644 --- a/modules/controlcenter/bluetooth.py +++ b/modules/controlcenter/bluetooth.py @@ -1,19 +1,22 @@ -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') -from gi.repository import Gdk, GLib, Gtk - -from fabric.bluetooth import BluetoothClient, BluetoothDevice -from fabric.utils import get_relative_path -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 fabric.widgets.svg import Svg - +from loguru import logger from services.battery import Battery +from fabric.widgets.revealer import Revealer +from fabric.widgets.separator import Separator +from fabric.widgets.svg import Svg +from fabric.widgets.scrolledwindow import ScrolledWindow +from fabric.widgets.label import Label +from fabric.widgets.image import Image +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.button import Button +from fabric.widgets.box import Box +from fabric.utils import get_relative_path +from fabric.bluetooth import BluetoothClient, BluetoothDevice +import subprocess +from gi.repository import Gdk, GLib, Gtk +import gi + +gi.require_version("Gtk", "3.0") +gi.require_version("Gdk", "3.0") class BluetoothDeviceSlot(CenterBox): @@ -46,28 +49,27 @@ class BluetoothDeviceSlot(CenterBox): ] # Add battery info if available - if hasattr(device, 'battery_percentage') and device.battery_percentage > 0: + if hasattr(device, "battery_percentage") and device.battery_percentage > 0: battery_box = Box(orientation="h", spacing=4) - + # Create battery icon battery_icon = Svg( size=16, svg_file=get_relative_path( Battery.get_battery_icon_file( - device.battery_percentage, + device.battery_percentage, False, # Not charging for bluetooth devices - "../../config/assets/icons/" + "../../config/assets/icons/", ) ), - name="battery-icon" + name="battery-icon", ) - + # Create battery percentage label battery_label = Label( - label=f"{device.battery_percentage:.0f}%", - name="battery-label" + label=f"{device.battery_percentage:.0f}%", name="battery-label" ) - + battery_box.children = [battery_icon, battery_label] self.end_children = [battery_box] @@ -83,53 +85,57 @@ class BluetoothDeviceSlot(CenterBox): "paired" if self.device.paired else "", ] self.dimage.set_property("style-classes", " ".join(self.styles)) - + # Update battery info if available - if hasattr(self.device, 'battery_percentage') and self.device.battery_percentage > 0: + if ( + hasattr(self.device, "battery_percentage") + and self.device.battery_percentage > 0 + ): if not self.end_children: # Add battery display if not already present battery_box = Box(orientation="h", spacing=4) - + # Create battery icon battery_icon = Svg( size=16, svg_file=get_relative_path( Battery.get_battery_icon_file( - self.device.battery_percentage, + self.device.battery_percentage, False, # Not charging for bluetooth devices - "../../config/assets/icons/" + "../../config/assets/icons/", ) ), - name="battery-icon" + name="battery-icon", ) - + # Create battery percentage label battery_label = Label( - label=f"{self.device.battery_percentage:.0f}%", - name="battery-label" + label=f"{self.device.battery_percentage:.0f}%", name="battery-label" ) - + battery_box.children = [battery_icon, battery_label] self.end_children = [battery_box] else: # Update existing battery display battery_box = self.end_children[0] - if hasattr(battery_box, 'children') and len(battery_box.children) >= 2: + if hasattr(battery_box, "children") and len(battery_box.children) >= 2: battery_icon = battery_box.children[0] battery_label = battery_box.children[1] - + # Update battery icon - battery_icon.set_from_file(get_relative_path( - Battery.get_battery_icon_file( - self.device.battery_percentage, - False, # Not charging for bluetooth devices - "../../config/assets/icons/" + battery_icon.set_from_file( + get_relative_path( + Battery.get_battery_icon_file( + self.device.battery_percentage, + False, # Not charging for bluetooth devices + "../../config/assets/icons/", + ) ) - )) - + ) + # Update battery percentage battery_label.set_label(f"{self.device.battery_percentage:.0f}%") elif self.end_children: # Remove battery display if no longer available self.end_children = [] - + return @@ -138,17 +144,28 @@ class BluetoothConnections(Box): self, parent, show_hidden_devices: bool = False, show_back_button=True, **kwargs ): super().__init__( - spacing=4, + spacing=8, orientation="vertical", style="margin: 8px", + name="bluetooth-connections", **kwargs, ) self.parent = parent self.show_hidden_devices = show_hidden_devices + self.is_scanning = False # Track scanning state self.client = BluetoothClient(on_device_added=self.on_device_added) + # Create pull-to-refresh indicator + self.refresh_indicator = Label( + name="bluetooth-refresh-indicator", + label="↓ Pull to scan for devices", + h_align="center", + visible=False, + style="color: #fff; font-size: 12px; padding: 5px;", + ) + # Create title with optional back button title_children = [] if show_back_button: @@ -158,7 +175,7 @@ class BluetoothConnections(Box): on_clicked=lambda *_: self.parent.close_bluetooth(), ) ) - title_children.append(Label("Bluetooth")) + title_children.append(Label("Bluetooth", name="bluetooth-title")) self.title = Box( orientation="h", @@ -179,125 +196,111 @@ class BluetoothConnections(Box): "notify::enabled", lambda *_: self.toggle_button.set_active(self.client.enabled), ) - # Connect scanning state changes to update scan button self.client.connect("notify::scanning", lambda *_: self.update_scan_label()) - self.not_paired = Box(spacing=2, orientation="vertical") - self.paired = Box(spacing=2, orientation="vertical") + # Connect to device changes + self.client.connect("device-added", self.update_devices) + self.client.connect("device-removed", self.update_devices) - # Create pull-to-refresh indicator - self.refresh_indicator = Label( - name="bluetooth-refresh-indicator", - label="↓ Pull to scan for devices", + # Create Devices section + self.paired_devices_label = Label( + label="Devices", h_align="start", name="networks-title" + ) + self.paired_devices = Box( + spacing=4, orientation="vertical", name="known-networks" + ) + + # Create "No devices available" message + self.no_devices_label = Label( + label="No devices available", h_align="center", + name="no-networks-label", visible=False, ) - self.device_box = Box( - spacing=2, - orientation="vertical", - children=[self.refresh_indicator, self.paired, self.not_paired], + # Create Other Devices section with clickable title + self.other_devices_button = Button( + child=Label("Other Devices", h_align="start"), + name="wifi-other-button", + on_clicked=self.toggle_other_devices, ) + self.other_devices = Box(spacing=4, orientation="vertical") - # Create scrolled window with pull-to-refresh - self.scrolled_window = ScrolledWindow( - min_content_size=(303, 400), - max_content_size=(303, 800), - child=self.device_box, + # Create scrolled window for other devices + self.other_devices_scrolled = ScrolledWindow( + min_content_size=(303, 150), + child=self.other_devices, overlay_scroll=True, ) - # Add pull-to-refresh functionality + # Add pull-to-refresh functionality to scrolled window self.setup_pull_to_refresh() + # Create revealer for Other Devices section + self.other_devices_revealer = Revealer( + child=self.other_devices_scrolled, + transition_type="slide-down", + transition_duration=100, + child_revealed=False, + ) + + # Create More Settings button (same style as Other Devices button) + self.more_settings_button = Button( + child=Label("More Settings", h_align="start"), + name="wifi-other-button", + on_clicked=self.open_bluetooth_settings, + ) + self.children = [ CenterBox( start_children=self.title, end_children=self.toggle_button, name="bluetooth-widget-top", ), - Label("Devices", h_align="start", name="devices-title"), - self.scrolled_window, + self.refresh_indicator, + Separator(orientation="h", name="separator"), + self.paired_devices_label, + self.paired_devices, + self.no_devices_label, + Separator(orientation="h", name="separator"), + self.other_devices_button, + self.other_devices_revealer, + Separator(orientation="h", name="separator"), + self.more_settings_button, ] + + # Connect cleanup on destroy + self.connect("destroy", self.on_destroy) + self.client.notify("scanning") self.client.notify("enabled") - def setup_pull_to_refresh(self): - """Setup pull-to-refresh gesture for the scrolled window""" - # Get the scrolled window's vertical adjustment - self.vadjustment = self.scrolled_window.get_vadjustment() + # Initial device update + self.update_devices() - # Track gesture state - self.pull_start_y = 0 - self.is_pulling = False - self.pull_threshold = 50 # pixels to trigger refresh + def toggle_other_devices(self, *_): + """Toggle the visibility of other devices section""" + current_state = self.other_devices_revealer.child_revealed + self.other_devices_revealer.child_revealed = not current_state - # Connect to scroll events - self.scrolled_window.connect("scroll-event", self.on_scroll_event) - self.scrolled_window.connect("button-press-event", self.on_button_press) - self.scrolled_window.connect("button-release-event", self.on_button_release) - self.scrolled_window.connect("motion-notify-event", self.on_motion_notify) + # Update button text based on state + if self.other_devices_revealer.child_revealed: + # Trigger a scan when revealing other devices + if self.client: + self.client.start_scan() - # Enable events - self.scrolled_window.set_events( - Gdk.EventMask.SCROLL_MASK - | Gdk.EventMask.BUTTON_PRESS_MASK - | Gdk.EventMask.BUTTON_RELEASE_MASK - | Gdk.EventMask.POINTER_MOTION_MASK - ) - - def on_scroll_event(self, widget, event): - """Handle scroll events for pull-to-refresh""" - # 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 - toggle scan (start or stop) - self.client.toggle_scan() - return True # Consume the event - return False # Let normal scrolling continue - - def on_button_press(self, widget, event): - """Handle button press for touch/drag gestures""" - if self.vadjustment.get_value() <= 0: - self.pull_start_y = event.y - self.is_pulling = True - return False - - def on_button_release(self, widget, event): - """Handle button release for touch/drag gestures""" - if self.is_pulling: - pull_distance = event.y - self.pull_start_y - if pull_distance > self.pull_threshold: - # Toggle scan (start or stop) - self.client.toggle_scan() - # Hide refresh indicator - self.refresh_indicator.set_visible(False) - self.refresh_indicator.remove_style_class("ready-to-refresh") - self.is_pulling = False - return False - - def on_motion_notify(self, widget, event): - """Handle motion events for visual feedback during pull""" - if self.is_pulling and self.vadjustment.get_value() <= 0: - pull_distance = event.y - self.pull_start_y - if pull_distance > 0: - # Show refresh indicator when pulling down - self.refresh_indicator.set_visible(True) - if pull_distance >= self.pull_threshold: - if self.client.scanning: - self.refresh_indicator.set_label("↑ Release to stop scanning") - else: - self.refresh_indicator.set_label("↑ Release to scan") - self.refresh_indicator.add_style_class("ready-to-refresh") - else: - if self.client.scanning: - self.refresh_indicator.set_label("↓ Pull to stop scanning") - else: - self.refresh_indicator.set_label("↓ Pull to scan for devices") - self.refresh_indicator.remove_style_class("ready-to-refresh") - else: - self.refresh_indicator.set_visible(False) - return False + def open_bluetooth_settings(self, *_): + """Open Blueman bluetooth manager""" + try: + subprocess.Popen(["blueman-manager"], start_new_session=True) + if self.parent and hasattr(self.parent, "hide_controlcenter"): + self.parent.hide_controlcenter() + except FileNotFoundError: + logger.error( + "[Bluetooth] blueman-manager not found. Please install blueman package." + ) + except Exception as e: + logger.error(f"[Bluetooth] Failed to open bluetooth settings: {e}") def update_scan_label(self): """Update scanning state appearance""" @@ -308,19 +311,70 @@ class BluetoothConnections(Box): self.refresh_indicator.add_style_class("scanning") else: # Hide scanning feedback - if not self.is_pulling: - self.refresh_indicator.set_visible(False) + self.refresh_indicator.set_visible(False) self.refresh_indicator.remove_style_class("scanning") + def update_devices(self, *_): + """Update the list of available devices""" + if not self.client: + return + + # Clear existing devices + for child in self.paired_devices.get_children(): + child.destroy() + for child in self.other_devices.get_children(): + child.destroy() + + # Get current devices + devices = self.client.devices + paired_devices = [] + other_devices = [] + + for device in devices: + if device.name and device.name != "Unknown": + # Categorize devices: paired devices go to "Devices (Paired)" + # All others go to "Other Devices" + if device.paired: + paired_devices.append(device) + else: + other_devices.append(device) + + # Add paired devices + for device in paired_devices: + device_slot = BluetoothDeviceSlot(device) + self.paired_devices.add(device_slot) + + # Add other devices + for device in other_devices: + device_slot = BluetoothDeviceSlot(device) + self.other_devices.add(device_slot) + + # Show/hide sections based on available devices + has_paired_devices = len(paired_devices) > 0 + has_other_devices = len(other_devices) > 0 + has_any_devices = has_paired_devices or has_other_devices + + # Show paired devices section only if there are paired devices + self.paired_devices_label.set_visible(has_paired_devices) + self.paired_devices.set_visible(has_paired_devices) + + # Show "No devices available" message if no devices at all + self.no_devices_label.set_visible(not has_any_devices) + + # Always show the other devices button, regardless of available devices + self.other_devices_button.set_visible(True) # Always visible + + def on_destroy(self, widget): + """Cleanup when widget is destroyed""" + # Make sure other devices revealer is collapsed when closing + self.other_devices_revealer.child_revealed = False + + def close_bluetooth(self): + """Called when Bluetooth panel is being closed""" + # Collapse the other devices section when closing + self.other_devices_revealer.child_revealed = False + def on_device_added(self, client: BluetoothClient, address: str): - if not (device := client.get_device(address)): - return - - if (device.name in ["", None]) and not self.show_hidden_devices: - return - - slot = BluetoothDeviceSlot(device, paired=device.paired) - - if device.paired: - return self.paired.add(slot) - return self.not_paired.add(slot) + """Handle when a new device is added""" + # Update the device list when devices are added + self.update_devices() diff --git a/modules/controlcenter/wifi.py b/modules/controlcenter/wifi.py index 40ba3ff..bda0b85 100644 --- a/modules/controlcenter/wifi.py +++ b/modules/controlcenter/wifi.py @@ -1,22 +1,25 @@ -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Gdk', '3.0') -from gi.repository import Gdk, GLib, Gtk -from loguru import logger - -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 services.network import NetworkClient from widgets.wifi_password_dialog import WiFiPasswordDialog +from services.network import NetworkClient +from fabric.widgets.scrolledwindow import ScrolledWindow +from fabric.widgets.revealer import Revealer +from fabric.widgets.label import Label +from fabric.widgets.image import Image +from fabric.widgets.centerbox import CenterBox +from fabric.widgets.button import Button +from fabric.widgets.box import Box +from loguru import logger +from gi.repository import Gdk, GLib, Gtk +from fabric.widgets.separator import Separator +import gi +import subprocess + +gi.require_version("Gtk", "3.0") +gi.require_version("Gdk", "3.0") class WifiNetworkSlot(CenterBox): def __init__(self, access_point, wifi_service, parent=None, **kwargs): - super().__init__(**kwargs) + super().__init__(name="wifi-network-slot", **kwargs) self.access_point = access_point self.wifi_service = wifi_service self.parent = parent # Reference to control center @@ -38,27 +41,52 @@ class WifiNetworkSlot(CenterBox): # Create connection status indicator using symbolic WiFi icon self.dimage = Image( icon_name="network-wireless-symbolic", - size=5, + size=16, name="device-icon", style_classes=" ".join(self.styles), ) + self.network_label = Label( + label=self.ssid, name="wifi-network-name", h_align="start", h_expand=True + ) + + # Create lock icon for secured networks + self.lock_icon = None + if self.access_point.requires_password: + self.lock_icon = Image( + icon_name="changes-prevent-symbolic", + size=12, + name="wifi-lock-icon", + ) + # Initialize password dialog self.password_dialog = None + # Create the start section with WiFi icon and network name + start_box = Box( + orientation="h", spacing=8, children=[self.dimage, self.network_label] + ) + + # Create end section with lock icon if needed + end_children = [] + if self.lock_icon: + end_children.append(self.lock_icon) + self.start_children = [ Button( - image=self.dimage, + child=start_box, + name="wifi-network-button", on_clicked=lambda *_: self.toggle_connecting(), - ), - Label(label=self.ssid), + ) ] + if end_children: + self.end_children = end_children + # Emit initial change to update display self.on_changed() def toggle_connecting(self): - """Toggle WiFi network connection""" # Check if this network is currently connected is_currently_connected = self.access_point.is_active @@ -71,8 +99,6 @@ class WifiNetworkSlot(CenterBox): 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: # Try to connect - check if password is required @@ -91,8 +117,6 @@ class WifiNetworkSlot(CenterBox): 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 @@ -128,7 +152,6 @@ class WifiNetworkSlot(CenterBox): return False # Remove timeout def on_changed(self, *_): - """Update display when connection state changes""" # 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 = [ @@ -215,9 +238,10 @@ class WifiNetworkSlot(CenterBox): class WifiConnections(Box): def __init__(self, parent, show_back_button=True, **kwargs): super().__init__( - spacing=4, + spacing=8, orientation="vertical", style="margin: 8px", + name="wifi-connections", **kwargs, ) @@ -247,7 +271,7 @@ class WifiConnections(Box): on_clicked=lambda *_: self.parent.close_wifi(), ) ) - title_children.append(Label("WiFi")) + title_children.append(Label("Wi-Fi", name="wifi-title")) self.title = Box( orientation="h", @@ -256,24 +280,51 @@ class WifiConnections(Box): self.toggle_button = Gtk.Switch(visible=True, name="toggle-button") - self.available_networks = Box(spacing=2, orientation="vertical") - - self.device_box = Box( - spacing=2, - orientation="vertical", - children=[self.refresh_indicator, self.available_networks], + # Create Known Network section + self.known_networks_label = Label( + label="Known Network", h_align="start", name="networks-title" + ) + self.known_networks = Box( + spacing=4, orientation="vertical", name="known-networks" ) - # Create scrolled window with pull-to-refresh - self.scrolled_window = ScrolledWindow( - min_content_size=(303, 400), - max_content_size=(303, 800), - child=self.device_box, + # Create "No networks available" message + self.no_networks_label = Label( + label="No networks available", + h_align="center", + name="no-networks-label", + visible=False, + ) + + # Create Other Networks section with clickable title + self.other_networks_button = Button( + child=Label("Other Networks", h_align="start"), + name="wifi-other-button", + on_clicked=self.toggle_other_networks, + ) + self.other_networks = Box(spacing=4, orientation="vertical") + + # Create scrolled window for other networks + self.other_networks_scrolled = ScrolledWindow( + min_content_size=(303, 150), + child=self.other_networks, overlay_scroll=True, ) - # Add pull-to-refresh functionality - self.setup_pull_to_refresh() + # Create revealer for Other Networks section + self.other_networks_revealer = Revealer( + child=self.other_networks_scrolled, + transition_type="slide-down", + transition_duration=100, + child_revealed=False, + ) + + # Create More Settings button (same style as Other Networks button) + self.more_settings_button = Button( + child=Label("More Settings", h_align="start"), + name="wifi-other-button", + on_clicked=self.open_network_settings, + ) self.children = [ CenterBox( @@ -281,13 +332,32 @@ class WifiConnections(Box): end_children=self.toggle_button, name="wifi-widget-top", ), - Label("Networks", h_align="start", name="networks-title"), - self.scrolled_window, + self.refresh_indicator, + Separator(orientation="h", name="separator"), + self.known_networks_label, + self.known_networks, + self.no_networks_label, + Separator(orientation="h", name="separator"), + self.other_networks_button, + self.other_networks_revealer, + Separator(orientation="h", name="separator"), + self.more_settings_button, ] # Connect cleanup on destroy self.connect("destroy", self.on_destroy) + def toggle_other_networks(self, *_): + """Toggle the visibility of other networks section""" + current_state = self.other_networks_revealer.child_revealed + self.other_networks_revealer.child_revealed = not current_state + + # Update button text based on state + if self.other_networks_revealer.child_revealed: + # Trigger a scan when revealing other networks + if self.wifi_service: + self.wifi_service.scan() + def on_network_ready(self, *_): """Called when network service is ready""" self.wifi_service = self.network_service.wifi_device @@ -319,220 +389,101 @@ class WifiConnections(Box): if self.wifi_service: self.toggle_button.set_active(self.wifi_service.wireless_enabled) + def open_network_settings(self, *_): + """Open NetworkManager connection editor""" + try: + subprocess.Popen(["nm-connection-editor"], start_new_session=True) + if self.parent and hasattr(self.parent, "hide_controlcenter"): + self.parent.hide_controlcenter() + except FileNotFoundError: + logger.error( + "[WiFi] nm-connection-editor not found. Please install network-manager-gnome package." + ) + except Exception as e: + logger.error(f"[WiFi] Failed to open network settings: {e}") + def update_networks(self, *_): """Update the list of available networks""" if not self.wifi_service: return # Clear existing networks - for child in self.available_networks.get_children(): + for child in self.known_networks.get_children(): + child.destroy() + for child in self.other_networks.get_children(): child.destroy() - # Add current networks + # Get current networks access_points = self.wifi_service.access_points + known_networks = [] + other_networks = [] + for access_point in access_points: if access_point.ssid and access_point.ssid != "Unknown": - network_slot = WifiNetworkSlot( - access_point, self.wifi_service, parent=self.parent - ) - self.available_networks.add(network_slot) + # Categorize networks: connected or saved networks go to "Known Network" + # All others go to "Other Networks" + if access_point.is_active or self._is_saved_network(access_point): + known_networks.append(access_point) + else: + other_networks.append(access_point) + + # Add known networks + for access_point in known_networks: + network_slot = WifiNetworkSlot( + access_point, self.wifi_service, parent=self.parent + ) + self.known_networks.add(network_slot) + + # Add other networks + for access_point in other_networks: + network_slot = WifiNetworkSlot( + access_point, self.wifi_service, parent=self.parent + ) + self.other_networks.add(network_slot) + + # Show/hide sections based on available networks + has_known_networks = len(known_networks) > 0 + has_other_networks = len(other_networks) > 0 + has_any_networks = has_known_networks or has_other_networks + + # Show known networks section only if there are known networks + self.known_networks_label.set_visible(has_known_networks) + self.known_networks.set_visible(has_known_networks) + + # Show "No networks available" message if no networks at all + self.no_networks_label.set_visible(not has_any_networks) + + # Always show the other networks button, regardless of available networks + self.other_networks_button.set_visible(True) # Always visible # Update all network connection states self.refresh_network_states() + def _is_saved_network(self, access_point): + """Check if a network is saved/known (placeholder implementation)""" + # TODO: Implement proper saved network detection + # This would typically check against NetworkManager's saved connections + # For now, we'll use a simple heuristic + return False + def refresh_network_states(self, *_): """Refresh connection states for all network slots""" - for child in self.available_networks.get_children(): + # Refresh known networks + for child in self.known_networks.get_children(): if hasattr(child, "on_changed"): child.on_changed() - def setup_pull_to_refresh(self): - """Setup pull-to-refresh gesture for the scrolled window""" - # Get the scrolled window's vertical adjustment - self.vadjustment = self.scrolled_window.get_vadjustment() - - # Track gesture state - self.pull_start_y = 0 - self.is_pulling = False - self.pull_threshold = 50 # pixels to trigger refresh - - # Animation state - self.bounce_timeout_id = None - self.bounce_frame = 0 - self.bounce_duration = 60 # Total frames for animation - # Maximum bounce height in pixels (increased for visibility) - self.bounce_amplitude = 30 - - # Connect to scroll events - self.scrolled_window.connect("scroll-event", self.on_scroll_event) - self.scrolled_window.connect("button-press-event", self.on_button_press) - self.scrolled_window.connect("button-release-event", self.on_button_release) - self.scrolled_window.connect("motion-notify-event", self.on_motion_notify) - - # Enable events - self.scrolled_window.set_events( - Gdk.EventMask.SCROLL_MASK - | Gdk.EventMask.BUTTON_PRESS_MASK - | Gdk.EventMask.BUTTON_RELEASE_MASK - | Gdk.EventMask.POINTER_MOTION_MASK - ) - - def on_scroll_event(self, widget, event): - """Handle scroll events for pull-to-refresh""" - # 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 - if self.wifi_service: - self.wifi_service.scan() - # Trigger smooth bounce animation for scroll - self.start_bounce_animation() - return True # Consume the event - return False # Let normal scrolling continue - - def on_button_press(self, widget, event): - """Handle button press for touch/drag gestures""" - if self.vadjustment.get_value() <= 0: - self.pull_start_y = event.y - self.is_pulling = True - return False - - def on_button_release(self, widget, event): - """Handle button release for touch/drag gestures""" - if self.is_pulling: - pull_distance = event.y - self.pull_start_y - if pull_distance > self.pull_threshold: - # Start scan - if self.wifi_service: - self.wifi_service.scan() - # Trigger elastic animation for pull gesture (most satisfying) - self.start_elastic_animation() - else: - # Stop any ongoing animation - self.stop_bounce_animation() - # Hide refresh indicator - self.refresh_indicator.set_visible(False) - self.refresh_indicator.remove_style_class("ready-to-refresh") - self.is_pulling = False - return False - - def on_motion_notify(self, widget, event): - """Handle motion events for visual feedback during pull""" - if self.is_pulling and self.vadjustment.get_value() <= 0: - pull_distance = event.y - self.pull_start_y - if pull_distance > 0: - # Show refresh indicator when pulling down - self.refresh_indicator.set_visible(True) - if pull_distance >= self.pull_threshold: - self.refresh_indicator.set_label("↑ Release to scan") - self.refresh_indicator.add_style_class("ready-to-refresh") - else: - 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 ease_out_bounce(self, t): - """Smooth bounce easing function (0 to 1)""" - if t < 1 / 2.75: - return 7.5625 * t * t - elif t < 2 / 2.75: - t -= 1.5 / 2.75 - return 7.5625 * t * t + 0.75 - elif t < 2.5 / 2.75: - t -= 2.25 / 2.75 - return 7.5625 * t * t + 0.9375 - else: - t -= 2.625 / 2.75 - return 7.5625 * t * t + 0.984375 - - def ease_out_elastic(self, t): - """Elastic easing function for smoother bounce""" - import math - - if t == 0 or t == 1: - return t - - p = 0.3 - s = p / 4 - return math.pow(2, -10 * t) * math.sin((t - s) * (2 * math.pi) / p) + 1 - - def start_bounce_animation(self): - """Start smooth bounce animation for the refresh indicator""" - if self.bounce_timeout_id: - GLib.source_remove(self.bounce_timeout_id) - - self.bounce_frame = 0 - # Higher frame rate for smoother animation (16ms = ~60fps) - self.bounce_timeout_id = GLib.timeout_add(16, self.animate_bounce) - - def animate_bounce(self): - """Animate smooth bounce effect with easing""" - if self.bounce_frame >= self.bounce_duration: - # Animation finished - self.refresh_indicator.set_margin_top(0) - self.bounce_timeout_id = None - # Hide indicator after animation if not pulling - if not self.is_pulling: - self.refresh_indicator.set_visible(False) - return False - - # Calculate progress (0 to 1) - progress = self.bounce_frame / self.bounce_duration - - # Apply easing function for smooth bounce - eased_progress = self.ease_out_bounce(progress) - - # Calculate bounce offset (starts high, bounces down to 0) - bounce_offset = int(self.bounce_amplitude * (1 - eased_progress)) - self.refresh_indicator.set_margin_top(max(0, bounce_offset)) - - self.bounce_frame += 1 - return True - - def start_elastic_animation(self): - """Start elastic animation for more dramatic effect""" - if self.bounce_timeout_id: - GLib.source_remove(self.bounce_timeout_id) - - self.bounce_frame = 0 - self.bounce_duration = 80 # Longer for elastic effect - # Higher amplitude (increased for visibility) - self.bounce_amplitude = 40 - self.bounce_timeout_id = GLib.timeout_add(16, self.animate_elastic) - - def animate_elastic(self): - """Animate elastic bounce effect""" - if self.bounce_frame >= self.bounce_duration: - # Animation finished - self.refresh_indicator.set_margin_top(0) - self.bounce_timeout_id = None - # Hide indicator after animation if not pulling - if not self.is_pulling: - self.refresh_indicator.set_visible(False) - return False - - # Calculate progress (0 to 1) - progress = self.bounce_frame / self.bounce_duration - - # Apply elastic easing - eased_progress = self.ease_out_elastic(progress) - - # Calculate bounce offset - bounce_offset = int(self.bounce_amplitude * (1 - eased_progress)) - self.refresh_indicator.set_margin_top(max(0, bounce_offset)) - - self.bounce_frame += 1 - return True - - def stop_bounce_animation(self): - """Stop any ongoing animations""" - if self.bounce_timeout_id: - GLib.source_remove(self.bounce_timeout_id) - self.bounce_timeout_id = None - self.refresh_indicator.set_margin_top(0) + # Refresh other networks + for child in self.other_networks.get_children(): + if hasattr(child, "on_changed"): + child.on_changed() def on_destroy(self, widget): - """Cleanup animations when widget is destroyed""" - self.stop_bounce_animation() + """Cleanup when widget is destroyed""" + # Make sure other networks revealer is collapsed when closing + self.other_networks_revealer.child_revealed = False + + def close_wifi(self): + """Called when WiFi panel is being closed""" + # Collapse the other networks section when closing + self.other_networks_revealer.child_revealed = False diff --git a/styles/controlcenter.css b/styles/controlcenter.css index a49b9b5..e56dfc7 100644 --- a/styles/controlcenter.css +++ b/styles/controlcenter.css @@ -5,6 +5,13 @@ margin: 0; } +#separator { + min-height: 2px; + margin: 2px 0; + background-color: var(--outline); + border-radius: 8px; +} + #bluetooth-control-window, #wifi-control-window, #battery-control-window { @@ -188,10 +195,10 @@ color: #fff; } -#bluetooth-widget-top { - padding-bottom: 10px; - border-bottom: 1px solid alpha(#aaa, 0.3); -} +/* #bluetooth-widget-top { */ +/* padding-bottom: 10px; */ +/* border-bottom: 1px solid alpha(#aaa, 0.3); */ +/* } */ #device-icon { border-radius: 50%; @@ -824,3 +831,62 @@ margin: -5px; box-shadow: 0 2px 6px alpha(#000, 0.4); } + +/* macOS-style WiFi interface */ +#wifi-connections, +#bluetooth-connections { + background-color: rgba(255, 255, 255, 0.08); + border-radius: 12px; + padding: 12px; +} + +/* WiFi network slots - macOS style */ +#wifi-network-slot button { + border-radius: 8px; + padding: 4px 12px; + /* min-width: 280px; */ + margin: 2px 0; +} + +#wifi-network-slot button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +/* WiFi-specific device icon styles */ +#wifi-connections #device-icon { + background-color: rgba(255, 255, 255, 0.1); +} + +#wifi-connections #device-icon.connected { + background-color: #007aff; + color: #ffffff; +} + +#wifi-network-name { + color: #ffffff; + font-size: 14px; + font-weight: 500; +} + +#wifi-lock-icon { + color: rgba(255, 255, 255, 0.6); + margin-left: 8px; +} + +/* Other Networks and Settings buttons */ +#wifi-other-button { + background: transparent; + border: none; + padding: 8px 12px; + border-radius: 8px; + transition: background-color 0.2s ease; +} + +#wifi-other-button:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +#wifi-other-button label { + font-size: 14px; + font-weight: 500; +}