Huge updates: ControlCenter, DropDown Menu
This commit is contained in:
34 files changed
+3261
-1440
No files matched your search
@@ -0,0 +1,363 @@
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
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 fabric.widgets.eventbox import EventBox
|
||||
|
||||
|
||||
class BluetoothDeviceSlot(CenterBox):
|
||||
def __init__(self, device: BluetoothDevice, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.device = device
|
||||
self.device.connect("changed", self.on_changed)
|
||||
self.device.connect(
|
||||
"notify::closed", lambda *_: self.device.closed and self.destroy()
|
||||
)
|
||||
|
||||
self.styles = [
|
||||
"connected" if self.device.connected else "",
|
||||
"paired" if self.device.paired else "",
|
||||
]
|
||||
|
||||
self.dimage = Image(
|
||||
icon_name=device.icon_name + "-symbolic", # type: ignore
|
||||
size=5,
|
||||
name="device-icon",
|
||||
style_classes=" ".join(self.styles),
|
||||
)
|
||||
|
||||
self.start_children = [
|
||||
Button(
|
||||
image=self.dimage,
|
||||
on_clicked=lambda *_: self.toggle_connecting(),
|
||||
),
|
||||
Label(label=device.name), # type: ignore
|
||||
]
|
||||
|
||||
self.device.emit("changed") # to update display status
|
||||
|
||||
def toggle_connecting(self):
|
||||
self.device.emit("changed")
|
||||
self.device.set_connecting(not self.device.connected)
|
||||
|
||||
def on_changed(self, *_):
|
||||
self.styles = [
|
||||
"connected" if self.device.connected else "",
|
||||
"paired" if self.device.paired else "",
|
||||
]
|
||||
self.dimage.set_property("style-classes", " ".join(self.styles))
|
||||
return
|
||||
|
||||
|
||||
class BluetoothConnections(Box):
|
||||
def __init__(self, parent, show_hidden_devices: bool = False, **kwargs):
|
||||
super().__init__(
|
||||
spacing=4,
|
||||
orientation="vertical",
|
||||
style="margin: 8px",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.parent = parent
|
||||
self.show_hidden_devices = show_hidden_devices
|
||||
|
||||
self.client = BluetoothClient(on_device_added=self.on_device_added)
|
||||
|
||||
|
||||
|
||||
self.title = Box(
|
||||
orientation="h",
|
||||
children=[
|
||||
Button(
|
||||
image=Image(icon_name="back", size=10),
|
||||
on_clicked=lambda *_: self.parent.close_bluetooth(),
|
||||
),
|
||||
Label("Bluetooth"),
|
||||
],
|
||||
)
|
||||
|
||||
self.toggle_button = Gtk.Switch(visible=True, name="toggle-button")
|
||||
|
||||
self.toggle_button.set_active(self.client.enabled)
|
||||
self.toggle_button.connect(
|
||||
"notify::active",
|
||||
lambda *_: self.client.set_enabled(self.toggle_button.get_active()),
|
||||
)
|
||||
|
||||
self.client.connect(
|
||||
"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")
|
||||
|
||||
# Create pull-to-refresh indicator
|
||||
self.refresh_indicator = Label(
|
||||
name="bluetooth-refresh-indicator",
|
||||
label="↓ Pull to scan for devices",
|
||||
h_align="center",
|
||||
visible=False
|
||||
)
|
||||
|
||||
self.device_box = Box(
|
||||
spacing=2,
|
||||
orientation="vertical",
|
||||
children=[self.refresh_indicator, self.paired, self.not_paired]
|
||||
)
|
||||
|
||||
# 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,
|
||||
overlay_scroll=True,
|
||||
)
|
||||
|
||||
# Add pull-to-refresh functionality
|
||||
self.setup_pull_to_refresh()
|
||||
|
||||
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.client.notify("scanning")
|
||||
self.client.notify("enabled")
|
||||
|
||||
# Connect cleanup on destroy
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Cleanup animations when widget is destroyed"""
|
||||
self.stop_bounce_animation()
|
||||
|
||||
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
|
||||
self.bounce_amplitude = 15 # Maximum bounce height in pixels
|
||||
|
||||
|
||||
|
||||
# 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 - toggle scan (start or stop)
|
||||
self.client.toggle_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:
|
||||
# Toggle scan (start or stop)
|
||||
self.client.toggle_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:
|
||||
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 ease_out_bounce(self, t):
|
||||
"""Smooth bounce easing function (0 to 1)"""
|
||||
import math
|
||||
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
|
||||
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
|
||||
self.bounce_amplitude = 20 # Higher amplitude
|
||||
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
|
||||
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)
|
||||
|
||||
def update_scan_label(self):
|
||||
"""Update scanning state appearance"""
|
||||
if self.client.scanning:
|
||||
# Show scanning feedback in refresh indicator
|
||||
self.refresh_indicator.set_label("Scanning for devices...")
|
||||
self.refresh_indicator.set_visible(True)
|
||||
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.remove_style_class("scanning")
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,9 @@
|
||||
<esh name="ControlCenter">
|
||||
<BoxV h_expand="true" name="control-center-widgets">
|
||||
<BoxH name="top-widget" h_expand="true">
|
||||
<BoxV name="wb-widget" style_classes="menu" spacing="5">
|
||||
<Ref>self.bluetooth_man</Ref>
|
||||
</BoxV>
|
||||
</BoxH>
|
||||
</BoxV>
|
||||
</esh>
|
||||
@@ -0,0 +1,27 @@
|
||||
<esh name="ControlCenter">
|
||||
<BoxV h_expand="true" name="control-center-widgets">
|
||||
<BoxH name="top-widget" h_expand="true">
|
||||
<BoxV name="wb-widget" style_classes="menu" spacing="5">
|
||||
<Ref>self.wlan_widget</Ref>
|
||||
<Ref>self.bluetooth_widget</Ref>
|
||||
</BoxV>
|
||||
<BoxH name="dnd-widget" style_classes="menu" h_expand="true">
|
||||
<Ref>self.focus_widget</Ref>
|
||||
<Svg path="config/assets/icons/dnd.svg" name="dnd-menu-icon"/>
|
||||
</BoxH>
|
||||
</BoxH>
|
||||
<BoxV name="brightness-widget" style_classes="menu" h_expand="true">
|
||||
<Label label="Display" style_classes="title" h_align="start"/>
|
||||
<Ref>self.brightness_scale</Ref>
|
||||
<Label label=" " name="brightness-widget-icon" h_align="start"/>
|
||||
</BoxV>
|
||||
<BoxV name="volume-widget" style_classes="menu" h_expand="true">
|
||||
<Label label="Sound" style_classes="title" h_align="start"/>
|
||||
<Ref>self.volume_scale</Ref>
|
||||
<Label label=" " name="brightness-widget-icon" h_align="start"/>
|
||||
</BoxV>
|
||||
<BoxV>
|
||||
<Ref>self.music_widget</Ref>
|
||||
</BoxV>
|
||||
</BoxV>
|
||||
</esh>
|
||||
@@ -0,0 +1,372 @@
|
||||
from fabric.utils import idle_add
|
||||
from fabric.utils.helpers 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.label import Label
|
||||
from fabric.widgets.scale import Scale
|
||||
from fabric.widgets.svg import Svg
|
||||
from gi.repository import Gdk, GLib
|
||||
|
||||
from modules.controlcenter.bluetooth import BluetoothConnections
|
||||
from modules.controlcenter.wifi import WifiConnections
|
||||
from services.brightness import Brightness
|
||||
from utils.exml import exml
|
||||
from utils.roam import audio_service, modus_service
|
||||
from widgets.wayland import WaylandWindow as Window
|
||||
|
||||
brightness_service = Brightness.get_initial()
|
||||
|
||||
# FIX: Icon not showing up in control center
|
||||
# TODO: Add Player
|
||||
|
||||
|
||||
class ModusControlCenter(Window):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(
|
||||
layer="top",
|
||||
title="modus",
|
||||
anchor="top right",
|
||||
margin="2px 10px 0px 0px",
|
||||
exclusivity="auto",
|
||||
keyboard_mode="on-demand",
|
||||
name="control-center-menu",
|
||||
visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
self.focus_mode = False
|
||||
self._updating_brightness = False
|
||||
self._updating_volume = False
|
||||
|
||||
self.add_keybinding("Escape", self.hide_controlcenter)
|
||||
|
||||
volume = 100
|
||||
wlan = modus_service.sc("wlan-changed", self.wlan_changed)
|
||||
bluetooth = modus_service.sc("bluetooth-changed", self.bluetooth_changed)
|
||||
music = modus_service.sc("music-changed", self.audio_changed)
|
||||
|
||||
audio_service.connect("changed", self.audio_changed)
|
||||
audio_service.connect("changed", self.volume_changed)
|
||||
|
||||
self.wlan_label = Label(wlan, name="wifi-widget-label", h_align="start")
|
||||
self.bluetooth_label = Label(
|
||||
bluetooth, name="bluetooth-widget-label", h_align="start"
|
||||
)
|
||||
self.volume_scale = Scale(
|
||||
value=volume,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(5, 5),
|
||||
name="volume-widget-slider",
|
||||
size=30,
|
||||
h_expand=True,
|
||||
)
|
||||
self.volume_scale.connect("change-value", self.set_volume)
|
||||
self.volume_scale.connect("scroll-event", self.on_volume_scroll)
|
||||
|
||||
current_brightness = brightness_service.screen_brightness
|
||||
brightness_percentage = (
|
||||
int((current_brightness / brightness_service.max_screen) * 100)
|
||||
if brightness_service.max_screen > 0
|
||||
else 50
|
||||
)
|
||||
|
||||
self.brightness_scale = Scale(
|
||||
value=brightness_percentage,
|
||||
min_value=0,
|
||||
max_value=100,
|
||||
increments=(5, 5),
|
||||
name="brightness-widget-slider",
|
||||
size=30,
|
||||
h_expand=True,
|
||||
)
|
||||
|
||||
# Only connect brightness controls if brightness service is available
|
||||
if brightness_service.max_screen > 0:
|
||||
self.brightness_scale.connect("change-value", self.set_brightness)
|
||||
self.brightness_scale.connect("scroll-event", self.on_brightness_scroll)
|
||||
brightness_service.connect("screen", self.brightness_changed)
|
||||
else:
|
||||
# Disable brightness scale if no backlight device available
|
||||
self.brightness_scale.set_sensitive(False)
|
||||
|
||||
self.music_widget = Box(
|
||||
name="music-widget",
|
||||
# children=[Player()],
|
||||
)
|
||||
|
||||
self.wifi_man = WifiConnections(self)
|
||||
self.bluetooth_man = BluetoothConnections(self)
|
||||
|
||||
self.has_bluetooth_open = False
|
||||
self.has_wifi_open = False
|
||||
|
||||
self.bluetooth_svg = Svg(
|
||||
svg_file=get_relative_path(
|
||||
"../../config/assets/icons/bluetooth.svg"
|
||||
if bluetooth == "On"
|
||||
else "../../config/assets/icons/bluetooth-off.svg"
|
||||
),
|
||||
style_classes="icon",
|
||||
)
|
||||
self.wifi_svg = (
|
||||
Svg(
|
||||
svg_file=get_relative_path("../../config/assets/icons/wifi.svg"),
|
||||
style_classes="icon",
|
||||
)
|
||||
if wlan != "No Connection"
|
||||
else Svg(
|
||||
svg_file=get_relative_path("../../config/assets/icons/wifi-off.svg"),
|
||||
style_classes="icon",
|
||||
)
|
||||
)
|
||||
|
||||
self.bluetooth_widget = Button(
|
||||
name="bluetooth-widget",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
children=[
|
||||
self.bluetooth_svg,
|
||||
Box(
|
||||
name="bluetooth-widget-info",
|
||||
orientation="vertical",
|
||||
children=[
|
||||
Label(
|
||||
label="Bluetooth",
|
||||
style_classes="title ct",
|
||||
h_align="start",
|
||||
),
|
||||
self.bluetooth_label,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
on_clicked=self.open_bluetooth,
|
||||
)
|
||||
|
||||
self.wlan_widget = Button(
|
||||
name="wifi-widget",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
children=[
|
||||
self.wifi_svg,
|
||||
Box(
|
||||
name="wifi-widget-info",
|
||||
orientation="vertical",
|
||||
children=[
|
||||
Label(
|
||||
label="Wi-Fi", style_classes="title ct", h_align="start"
|
||||
),
|
||||
self.wlan_label,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
on_clicked=self.open_wifi,
|
||||
)
|
||||
|
||||
self.focus_icon = Svg(
|
||||
svg_file=get_relative_path("../../config/assets/icons/dnd-off.svg"),
|
||||
style_classes="icon",
|
||||
)
|
||||
|
||||
self.focus_widget = Button(
|
||||
name="focus-widget",
|
||||
child=Box(
|
||||
orientation="h",
|
||||
children=[
|
||||
self.focus_icon,
|
||||
Label(label="Focus", style_classes="title ct", h_align="start"),
|
||||
],
|
||||
),
|
||||
on_clicked=self.set_dont_disturb,
|
||||
)
|
||||
|
||||
self.widgets = exml(
|
||||
file=get_relative_path("controlcenter.xml"),
|
||||
root=Box,
|
||||
tags={
|
||||
"Box": Box,
|
||||
"Button": Button,
|
||||
"Label": Label,
|
||||
"Scale": Scale,
|
||||
"Svg": Svg,
|
||||
},
|
||||
refs={
|
||||
"self.wlan_label": self.wlan_label,
|
||||
"self.bluetooth_label": self.bluetooth_label,
|
||||
"self.volume_scale": self.volume_scale,
|
||||
"self.brightness_scale": self.brightness_scale,
|
||||
"self.music_widget": self.music_widget,
|
||||
"self.bluetooth_widget": self.bluetooth_widget,
|
||||
"self.wlan_widget": self.wlan_widget,
|
||||
"self.focus_widget": self.focus_widget,
|
||||
},
|
||||
)
|
||||
|
||||
self.bluetooth_widgets = exml(
|
||||
file=get_relative_path("bluetooth.xml"),
|
||||
root=Box,
|
||||
tags={
|
||||
"Box": Box,
|
||||
"Button": Button,
|
||||
"Label": Label,
|
||||
"Scale": Scale,
|
||||
"Svg": Svg,
|
||||
},
|
||||
refs={"self.bluetooth_man": self.bluetooth_man},
|
||||
)
|
||||
|
||||
self.wifi_widgets = exml(
|
||||
file=get_relative_path("wifi.xml"),
|
||||
root=Box,
|
||||
tags={
|
||||
"Box": Box,
|
||||
"Button": Button,
|
||||
"Label": Label,
|
||||
"Scale": Scale,
|
||||
"Svg": Svg,
|
||||
},
|
||||
refs={"self.wifi_man": self.wifi_man},
|
||||
)
|
||||
|
||||
self.center_box = CenterBox(start_children=[self.widgets])
|
||||
|
||||
self.bluetooth_center_box = CenterBox(start_children=[self.bluetooth_widgets])
|
||||
|
||||
self.wifi_center_box = CenterBox(start_children=[self.wifi_widgets])
|
||||
|
||||
self.widgets.set_size_request(300, -1)
|
||||
self.bluetooth_center_box.set_size_request(300, -1)
|
||||
self.wifi_center_box.set_size_request(300, -1)
|
||||
|
||||
self.children = self.center_box
|
||||
|
||||
def set_dont_disturb(self, *_):
|
||||
self.focus_mode = not self.focus_mode
|
||||
modus_service.dont_disturb = self.focus_mode
|
||||
self.focus_icon.set_from_file(
|
||||
get_relative_path(
|
||||
"../../config/assets/icons/dnd.svg"
|
||||
if self.focus_mode
|
||||
else "../../config/assets/icons/dnd-off.svg"
|
||||
)
|
||||
)
|
||||
|
||||
def set_volume(self, _, __, volume):
|
||||
self._updating_volume = True
|
||||
audio_service.speaker.volume = round(volume)
|
||||
self._updating_volume = False
|
||||
|
||||
def set_brightness(self, _, __, brightness):
|
||||
self._updating_brightness = True
|
||||
brightness_value = int((brightness / 100) * brightness_service.max_screen)
|
||||
brightness_service.screen_brightness = brightness_value
|
||||
self._updating_brightness = False
|
||||
|
||||
def brightness_changed(self, _, brightness_value):
|
||||
if self._updating_brightness:
|
||||
return
|
||||
|
||||
if brightness_service.max_screen > 0:
|
||||
brightness_percentage = int(
|
||||
(brightness_value / brightness_service.max_screen) * 100
|
||||
)
|
||||
|
||||
GLib.idle_add(
|
||||
lambda: self.brightness_scale.set_value(brightness_percentage)
|
||||
)
|
||||
|
||||
def on_volume_scroll(self, widget, event):
|
||||
current_value = self.volume_scale.get_value()
|
||||
scroll_step = 5
|
||||
if event.direction == Gdk.ScrollDirection.UP:
|
||||
new_value = min(100, current_value + scroll_step)
|
||||
elif event.direction == Gdk.ScrollDirection.DOWN:
|
||||
new_value = max(0, current_value - scroll_step)
|
||||
else:
|
||||
return False
|
||||
|
||||
self.volume_scale.set_value(new_value)
|
||||
return True
|
||||
|
||||
def on_brightness_scroll(self, widget, event):
|
||||
current_value = self.brightness_scale.get_value()
|
||||
scroll_step = 5
|
||||
if event.direction == Gdk.ScrollDirection.UP:
|
||||
new_value = min(100, current_value + scroll_step)
|
||||
elif event.direction == Gdk.ScrollDirection.DOWN:
|
||||
new_value = max(0, current_value - scroll_step)
|
||||
else:
|
||||
return False
|
||||
|
||||
self.brightness_scale.set_value(new_value)
|
||||
return True
|
||||
|
||||
def set_children(self, children):
|
||||
self.children = children
|
||||
|
||||
def open_bluetooth(self, *_):
|
||||
idle_add(lambda *_: self.set_children(self.bluetooth_center_box))
|
||||
self.has_bluetooth_open = True
|
||||
|
||||
def open_wifi(self, *_):
|
||||
idle_add(lambda *_: self.set_children(self.wifi_center_box))
|
||||
self.has_wifi_open = True
|
||||
|
||||
def close_bluetooth(self, *_):
|
||||
idle_add(lambda *_: self.set_children(self.center_box))
|
||||
self.has_bluetooth_open = False
|
||||
|
||||
def close_wifi(self, *_):
|
||||
idle_add(lambda *_: self.set_children(self.center_box))
|
||||
self.has_wifi_open = False
|
||||
|
||||
def _set_mousecapture(self, visible: bool):
|
||||
self.set_visible(visible)
|
||||
if not visible:
|
||||
self.close_bluetooth()
|
||||
self.close_wifi()
|
||||
|
||||
def volume_changed(
|
||||
self,
|
||||
_,
|
||||
):
|
||||
if self._updating_volume:
|
||||
return
|
||||
|
||||
GLib.idle_add(
|
||||
lambda: self.volume_scale.set_value(int(audio_service.speaker.volume))
|
||||
)
|
||||
|
||||
def wlan_changed(self, _, wlan):
|
||||
self.wifi_svg.set_from_file(
|
||||
get_relative_path(
|
||||
"../../config/assets/icons/wifi.svg"
|
||||
if wlan != "No Connection"
|
||||
else "../../config/assets/icons/wifi-off.svg"
|
||||
)
|
||||
)
|
||||
GLib.idle_add(lambda: self.wlan_label.set_property("label", wlan))
|
||||
|
||||
def bluetooth_changed(self, _, bluetooth):
|
||||
self.bluetooth_svg.set_from_file(
|
||||
get_relative_path(
|
||||
"../../config/assets/icons/bluetooth.svg"
|
||||
if bluetooth == "On"
|
||||
else "../../config/assets/icons/bluetooth-off.svg"
|
||||
)
|
||||
)
|
||||
GLib.idle_add(lambda: self.bluetooth_label.set_property("label", bluetooth))
|
||||
|
||||
def audio_changed(self, *_):
|
||||
pass
|
||||
|
||||
def _init_mousecapture(self, mousecapture):
|
||||
self._mousecapture_parent = mousecapture
|
||||
|
||||
def hide_controlcenter(self, *_):
|
||||
self._mousecapture_parent.toggle_mousecapture()
|
||||
self.set_visible(False)
|
||||
Whitespace-only changes.
@@ -0,0 +1,430 @@
|
||||
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 Gdk, GLib, Gtk
|
||||
|
||||
from services.network import NetworkService
|
||||
|
||||
|
||||
# FIX: Make sure the network only scan when the pull to refresh started
|
||||
# TODO: Show visual Feeadback and animation like bluetooth
|
||||
|
||||
|
||||
class WifiNetworkSlot(CenterBox):
|
||||
def __init__(self, network_data: dict, wifi_service, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.network_data = network_data
|
||||
self.wifi_service = wifi_service
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
# Initialize styles based on connection state
|
||||
self.styles = [
|
||||
"connected" if self.is_connected else "",
|
||||
]
|
||||
|
||||
# Create connection status indicator using symbolic WiFi icon
|
||||
self.dimage = Image(
|
||||
icon_name="network-wireless-symbolic",
|
||||
size=5,
|
||||
name="device-icon",
|
||||
style_classes=" ".join(self.styles),
|
||||
)
|
||||
|
||||
self.start_children = [
|
||||
Button(
|
||||
image=self.dimage,
|
||||
on_clicked=lambda *_: self.toggle_connecting(),
|
||||
),
|
||||
Label(label=self.ssid),
|
||||
]
|
||||
|
||||
# Emit initial change to update display
|
||||
self.on_changed()
|
||||
|
||||
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
|
||||
|
||||
if is_currently_connected:
|
||||
# Disconnect from network
|
||||
success = self.wifi_service.disconnect_network(self.ssid)
|
||||
if success:
|
||||
self.is_connected = False
|
||||
else:
|
||||
# Connect to network (try without password first, could be enhanced for password input)
|
||||
success = self.wifi_service.connect_network(self.ssid)
|
||||
if success:
|
||||
self.is_connected = True
|
||||
|
||||
# Update display after connection attempt
|
||||
self.on_changed()
|
||||
|
||||
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:
|
||||
active_bssid = self.wifi_service._ap.get_bssid()
|
||||
is_currently_connected = active_bssid == self.bssid
|
||||
|
||||
self.is_connected = is_currently_connected
|
||||
self.styles = [
|
||||
"connected" if self.is_connected else "",
|
||||
]
|
||||
self.dimage.set_property("style-classes", " ".join(self.styles))
|
||||
return
|
||||
|
||||
|
||||
class WifiConnections(Box):
|
||||
def __init__(self, parent, **kwargs):
|
||||
super().__init__(
|
||||
spacing=4,
|
||||
orientation="vertical",
|
||||
style="margin: 8px",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.parent = parent
|
||||
self.network_service = NetworkService()
|
||||
self.wifi_service = None
|
||||
|
||||
# Wait for network service to be ready
|
||||
self.network_service.connect("device-ready", self.on_network_ready)
|
||||
|
||||
# Create pull-to-refresh indicator
|
||||
self.refresh_indicator = Label(
|
||||
name="wifi-refresh-indicator",
|
||||
label="↓ Pull to scan for networks",
|
||||
h_align="center",
|
||||
visible=False,
|
||||
)
|
||||
|
||||
self.title = Box(
|
||||
orientation="h",
|
||||
children=[
|
||||
Button(
|
||||
image=Image(icon_name="back", size=10),
|
||||
on_clicked=lambda *_: self.parent.close_wifi(),
|
||||
),
|
||||
Label("WiFi"),
|
||||
],
|
||||
)
|
||||
|
||||
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 scrolled window with pull-to-refresh
|
||||
self.scrolled_window = ScrolledWindow(
|
||||
min_content_size=(303, 400),
|
||||
max_content_size=(303, 800),
|
||||
child=self.device_box,
|
||||
overlay_scroll=True,
|
||||
)
|
||||
|
||||
# Add pull-to-refresh functionality
|
||||
self.setup_pull_to_refresh()
|
||||
|
||||
self.children = [
|
||||
CenterBox(
|
||||
start_children=self.title,
|
||||
end_children=self.toggle_button,
|
||||
name="wifi-widget-top",
|
||||
),
|
||||
Label("Networks", h_align="start", name="networks-title"),
|
||||
self.scrolled_window,
|
||||
]
|
||||
|
||||
# Connect cleanup on destroy
|
||||
self.connect("destroy", self.on_destroy)
|
||||
|
||||
def on_network_ready(self, *_):
|
||||
"""Called when network service is ready"""
|
||||
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.connect(
|
||||
"notify::active",
|
||||
lambda *_: setattr(
|
||||
self.wifi_service, "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("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
|
||||
)
|
||||
|
||||
# Initial network update
|
||||
self.update_networks()
|
||||
|
||||
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, service, is_scanning):
|
||||
"""Handle scanning state changes"""
|
||||
print(f"WiFi scanning state changed: {is_scanning}") # Debug output
|
||||
self.update_scan_indicator(is_scanning)
|
||||
|
||||
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():
|
||||
child.destroy()
|
||||
|
||||
# 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":
|
||||
network_slot = WifiNetworkSlot(ap_data, self.wifi_service)
|
||||
self.available_networks.add(network_slot)
|
||||
|
||||
# Update all network connection states
|
||||
self.refresh_network_states()
|
||||
|
||||
def refresh_network_states(self, *_):
|
||||
"""Refresh connection states for all network slots"""
|
||||
for child in self.available_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
|
||||
self.bounce_amplitude = 15 # Maximum bounce height in pixels
|
||||
|
||||
# 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
|
||||
print("WiFi scroll scan triggered") # Debug output
|
||||
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:
|
||||
# Trigger scan
|
||||
print("WiFi pull-to-refresh scan triggered") # Debug output
|
||||
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 update_scan_indicator(self, is_scanning):
|
||||
"""Update scanning state appearance"""
|
||||
print(f"WiFi update_scan_indicator called: {is_scanning}") # Debug output
|
||||
if is_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")
|
||||
print("WiFi scanning indicator shown") # Debug output
|
||||
else:
|
||||
# Hide scanning feedback
|
||||
if not self.is_pulling:
|
||||
self.refresh_indicator.set_visible(False)
|
||||
self.refresh_indicator.remove_style_class("scanning")
|
||||
print("WiFi scanning indicator hidden") # Debug output
|
||||
|
||||
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"""
|
||||
print("WiFi bounce animation started") # Debug output
|
||||
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
|
||||
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"""
|
||||
print("WiFi elastic animation started") # Debug output
|
||||
if self.bounce_timeout_id:
|
||||
GLib.source_remove(self.bounce_timeout_id)
|
||||
|
||||
self.bounce_frame = 0
|
||||
self.bounce_duration = 80 # Longer for elastic effect
|
||||
self.bounce_amplitude = 20 # Higher amplitude
|
||||
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
|
||||
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)
|
||||
|
||||
def on_destroy(self, widget):
|
||||
"""Cleanup animations when widget is destroyed"""
|
||||
self.stop_bounce_animation()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<esh name="ControlCenter">
|
||||
<BoxV h_expand="true" name="control-center-widgets">
|
||||
<BoxH name="top-widget" h_expand="true">
|
||||
<BoxV name="wb-widget" style_classes="menu" spacing="5">
|
||||
<Ref>self.wifi_man</Ref>
|
||||
</BoxV>
|
||||
</BoxH>
|
||||
</BoxV>
|
||||
</esh>
|
||||
Reference in new issue
Block a user