This commit is contained in:
t
2025-02-05 19:25:48 +01:00
parent cbe4355c3d
commit 810f54cb65
258 changed files with 4730 additions and 3 deletions
@@ -0,0 +1,62 @@
from ignis.widgets import Widget
from ignis.app import IgnisApp
from .volume import volume_control
from .quick_settings import quick_settings
from .user import user
from .media import media
from .notification_center import notification_center
from .brightness import brightness_slider
app = IgnisApp.get_default()
def control_center_widget() -> Widget.Box:
return Widget.Box(
vertical=True,
css_classes=["control-center"],
child=[
Widget.Box(
vertical=True,
css_classes=["control-center-widget"],
child=[
quick_settings(),
volume_control(),
brightness_slider(),
user(),
media(),
],
),
notification_center(),
],
)
def control_center() -> Widget.RevealerWindow:
revealer = Widget.Revealer(
transition_type="slide_left",
child=control_center_widget(),
transition_duration=300,
reveal_child=True,
)
box = Widget.Box(
child=[
Widget.Button(
vexpand=True,
hexpand=True,
css_classes=["unset"],
on_click=lambda x: app.close_window("ignis_CONTROL_CENTER"),
),
revealer,
],
)
return Widget.RevealerWindow(
visible=False,
popup=True,
kb_mode="on_demand",
layer="top",
css_classes=["unset"],
anchor=["top", "right", "bottom", "left"],
namespace="ignis_CONTROL_CENTER",
child=box,
revealer=revealer,
)
@@ -0,0 +1,27 @@
from ignis.services.backlight import BacklightService
from ignis.widgets import Widget
backlight = BacklightService.get_default()
def brightness_slider() -> Widget.Scale:
return Widget.Box(
visible=backlight.bind("available"),
hexpand=True,
style="margin-top: 0.25rem;",
child=[
Widget.Icon(
image="display-brightness-symbolic",
css_classes=["material-slider-icon"],
pixel_size=18,
),
Widget.Scale(
min=0,
max=backlight.max_brightness,
hexpand=True,
value=backlight.bind("brightness"),
css_classes=["material-slider"],
on_change=lambda x: backlight.set_brightness(x.value),
),
],
)
@@ -0,0 +1,14 @@
from services.material import MaterialService
from .qs_button import QSButton
material = MaterialService.get_default()
def dark_mode_button() -> QSButton:
return QSButton(
label="Dark",
icon_name="night-light-symbolic",
on_activate=lambda x: material.set_dark_mode(True),
on_deactivate=lambda x: material.set_dark_mode(False),
active=material.bind("dark-mode"),
)
@@ -0,0 +1,19 @@
from .qs_button import QSButton
from ignis.services.notifications import NotificationService
notifications = NotificationService.get_default()
def dnd_button() -> QSButton:
return QSButton(
label=notifications.bind("dnd", lambda value: "Silent" if value else "Noisy"),
icon_name=notifications.bind(
"dnd",
transform=lambda value: "notification-disabled-symbolic"
if value
else "notification-symbolic",
),
on_activate=lambda x: notifications.set_dnd(not notifications.dnd),
on_deactivate=lambda x: notifications.set_dnd(not notifications.dnd),
active=notifications.bind("dnd"),
)
@@ -0,0 +1,100 @@
from typing import List
from ignis.widgets import Widget
from ignis.utils import Utils
from .qs_button import QSButton
from ignis.services.network import NetworkService, EthernetDevice
network = NetworkService.get_default()
class EthernetConnectionItem(Widget.Button):
def __init__(self, device: EthernetDevice):
super().__init__(
css_classes=["ethernet-connection-item", "unset"],
on_click=lambda x: device.disconnect_from()
if device.is_connected
else device.connect_to(),
child=Widget.Box(
child=[
Widget.Icon(image="network-wired-symbolic"),
Widget.Label(
label=device.name,
ellipsize="end",
max_width_chars=20,
halign="start",
css_classes=["ethernet-connection-label"],
),
Widget.Button(
child=Widget.Label(
label=device.bind(
"is_connected",
lambda value: "Disconnect" if value else "Connect",
)
),
css_classes=["ethernet-connection-item-connect-label", "unset"],
halign="end",
hexpand=True,
),
]
),
)
def ethernet_control() -> List[QSButton]:
networks_list = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=[
Widget.Box(
css_classes=["ethernet-header-box"],
child=[
Widget.Icon(icon_name="network-wired-symbolic", pixel_size=28),
Widget.Label(
label="Wired connections",
css_classes=["ethernet-header-label"],
),
],
),
Widget.Box(
vertical=True,
child=network.ethernet.bind(
"devices",
lambda value: [EthernetConnectionItem(i) for i in value],
),
),
Widget.Separator(css_classes=["ethernet-network-list-separator"]),
Widget.Button(
css_classes=["ethernet-connection-item", "unset"],
style="margin-bottom: 0;",
on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Settings",
halign="start",
css_classes=["ethernet-connection-label"],
),
]
),
),
],
),
)
if len(network.ethernet.devices) > 0:
return [
QSButton(
label="Wired",
icon_name="network-wired-symbolic",
on_activate=lambda x: networks_list.toggle(),
on_deactivate=lambda x: networks_list.toggle(),
content=networks_list,
active=network.ethernet.bind("is_connected"),
)
]
else:
return []
@@ -0,0 +1,202 @@
import os
import ignis
from ignis.widgets import Widget
from ignis.services.mpris import MprisService, MprisPlayer
from ignis.utils import Utils
from services.material import MaterialService
from ignis.app import IgnisApp
from ignis.exceptions import StylePathNotFoundError
mpris = MprisService.get_default()
app = IgnisApp.get_default()
material = MaterialService.get_default()
MEDIA_TEMPLATE = Utils.get_current_dir() + "/../../scss/media.scss"
MEDIA_SCSS_CACHE_DIR = ignis.CACHE_DIR + "/media" # type: ignore
MEDIA_ART_FALLBACK = Utils.get_current_dir() + "/../../misc/media-art-fallback.png"
os.makedirs(MEDIA_SCSS_CACHE_DIR, exist_ok=True)
PLAYER_ICONS = {"spotify": "󰓇", "firefox": "󰈹", "chrome": "󰊯", None: ""}
class Player(Widget.Revealer):
def __init__(self, player: MprisPlayer) -> None:
self._player = player
self._colors_path = f"{MEDIA_SCSS_CACHE_DIR}/{self._player.desktop_entry}.scss"
player.connect("closed", lambda x: self.destroy())
player.connect("notify::art-url", lambda x, y: self.load_colors())
self.load_colors()
super().__init__(
transition_type="slide_down",
reveal_child=False,
css_classes=[self.get_css("media")],
child=Widget.Overlay(
child=Widget.Box(css_classes=[self.get_css("media-image")]),
overlays=[
Widget.Box(
hexpand=True,
vexpand=True,
css_classes=[self.get_css("media-image-gradient")],
),
Widget.Label(
label=self.get_player_icon(),
halign="start",
valign="start",
css_classes=[self.get_css("media-player-icon")],
),
Widget.Box(
vertical=True,
hexpand=True,
css_classes=[self.get_css("media-content")],
child=[
Widget.Box(
vexpand=True,
valign="center",
child=[
Widget.Box(
hexpand=True,
vertical=True,
child=[
Widget.Label(
ellipsize="end",
label=player.bind("title"),
max_width_chars=30,
halign="start",
css_classes=[
self.get_css("media-title")
],
),
Widget.Label(
label=player.bind("artist"),
max_width_chars=30,
ellipsize="end",
halign="start",
css_classes=[
self.get_css("media-artist")
],
),
],
),
Widget.Button(
child=Widget.Icon(
image=player.bind(
"playback_status",
lambda value: "media-playback-pause-symbolic"
if value == "Playing"
else "media-playback-start-symbolic",
),
pixel_size=18,
),
on_click=lambda x: player.play_pause(),
visible=player.bind("can_play"),
css_classes=player.bind(
"playback_status",
lambda value: [
self.get_css("media-playback-button"),
"playing",
]
if value == "Playing"
else [
self.get_css("media-playback-button"),
"paused",
],
),
),
],
),
],
),
Widget.Box(
vexpand=True,
valign="end",
style="padding: 1rem;",
child=[
Widget.Scale(
value=player.bind("position"),
max=player.bind("length"),
hexpand=True,
css_classes=[self.get_css("media-scale")],
on_change=lambda x: player.set_position(x.value),
visible=player.bind(
"position", lambda value: value != -1
),
),
Widget.Button(
child=Widget.Icon(
image="media-skip-backward-symbolic",
pixel_size=20,
),
css_classes=[self.get_css("media-skip-button")],
on_click=lambda x: player.previous(),
visible=player.bind("can_go_previous"),
style="margin-left: 1rem;",
),
Widget.Button(
child=Widget.Icon(
image="media-skip-forward-symbolic",
pixel_size=20,
),
css_classes=[self.get_css("media-skip-button")],
on_click=lambda x: player.next(),
visible=player.bind("can_go_next"),
style="margin-left: 1rem;",
),
],
),
],
),
)
def get_player_icon(self) -> str:
if self._player.desktop_entry == "firefox":
return PLAYER_ICONS["firefox"]
elif self._player.desktop_entry == "spotify":
return PLAYER_ICONS["spotify"]
elif "chromium" in self._player.track_id or "chrome" in self._player.track_id:
return PLAYER_ICONS["chrome"]
else:
return PLAYER_ICONS[None]
def destroy(self) -> None:
self.set_reveal_child(False)
Utils.Timeout(self.transition_duration, super().unparent)
def get_css(self, class_name: str) -> str:
return f"{class_name}-{self._player.desktop_entry}"
def load_colors(self) -> None:
if not self._player.art_url:
art_url = MEDIA_ART_FALLBACK
else:
art_url = self._player.art_url
try:
app.remove_css(self._colors_path)
except StylePathNotFoundError:
pass
colors = material.get_colors_from_img(art_url, True)
colors["art_url"] = art_url
colors["desktop_entry"] = self._player.desktop_entry
material.render_template(
colors, input_file=MEDIA_TEMPLATE, output_file=self._colors_path
)
app.apply_css(self._colors_path)
def media() -> Widget.Box:
def add_player(box: Widget.Box, obj: MprisPlayer) -> None:
player = Player(obj)
box.append(player)
player.set_reveal_child(True)
return Widget.Box(
vertical=True,
setup=lambda self: mpris.connect(
"player_added", lambda x, player: add_player(self, player)
),
css_classes=["rec-unset"],
)
@@ -0,0 +1,242 @@
from ignis.widgets import Widget
from ignis.services.notifications import Notification, NotificationService
from ignis.utils import Utils
from gi.repository import GLib # type: ignore
notifications = NotificationService.get_default()
class ScreenshotLayout(Widget.Box):
def __init__(self, notification: Notification) -> None:
super().__init__(
vertical=True,
hexpand=True,
child=[
Widget.Box(
child=[
Widget.Picture(
image=notification.icon,
content_fit="cover",
width=1920 // 7,
height=1080 // 7,
style="border-radius: 1rem; background-color: black;",
),
Widget.Button(
child=Widget.Icon(
image="window-close-symbolic", pixel_size=20
),
halign="end",
valign="start",
hexpand=True,
css_classes=["notification-close"],
on_click=lambda x: notification.close(),
),
],
),
Widget.Label(
label="Screenshot saved",
css_classes=["notification-screenshot-label"],
),
Widget.Box(
homogeneous=True,
style="margin-top: 0.75rem;",
spacing=10,
child=[
Widget.Button(
child=Widget.Label(label="Open"),
css_classes=["notification-action"],
on_click=lambda x: Utils.exec_sh_async(
f"xdg-open {notification.icon}"
),
),
Widget.Button(
child=Widget.Label(label="Close"),
css_classes=["notification-action"],
on_click=lambda x: notification.close(),
),
],
),
],
)
class NormalLayout(Widget.Box):
def __init__(self, notification: Notification) -> None:
super().__init__(
vertical=True,
hexpand=True,
child=[
Widget.Box(
child=[
Widget.Icon(
image=notification.icon
if notification.icon
else "dialog-information-symbolic",
pixel_size=48,
halign="start",
valign="start",
),
Widget.Box(
vertical=True,
style="margin-left: 0.75rem;",
child=[
Widget.Label(
ellipsize="end",
label=notification.summary,
halign="start",
visible=notification.summary != "",
css_classes=["notification-summary"],
),
Widget.Label(
label=notification.body,
ellipsize="end",
halign="start",
css_classes=["notification-body"],
visible=notification.body != "",
),
],
),
Widget.Button(
child=Widget.Icon(
image="window-close-symbolic", pixel_size=20
),
halign="end",
valign="start",
hexpand=True,
css_classes=["notification-close"],
on_click=lambda x: notification.close(),
),
],
),
Widget.Box(
child=[
Widget.Button(
child=Widget.Label(label=action.label),
on_click=lambda x, action=action: action.invoke(),
css_classes=["notification-action"],
)
for action in notification.actions
],
homogeneous=True,
style="margin-top: 0.75rem;" if notification.actions else "",
spacing=10,
),
],
)
class NotificationWidget(Widget.Box):
def __init__(self, notification: Notification) -> None:
layout: NormalLayout | ScreenshotLayout
if notification.app_name == "grimblast":
layout = ScreenshotLayout(notification)
else:
layout = NormalLayout(notification)
super().__init__(
css_classes=["notification"],
child=[layout],
)
class Popup(Widget.Revealer):
def __init__(self, notification: Notification, **kwargs):
widget = NotificationWidget(notification)
super().__init__(child=widget, transition_type="slide_down", **kwargs)
notification.connect("closed", lambda x: self.destroy())
def destroy(self):
self.reveal_child = False
Utils.Timeout(self.transition_duration, self.unparent)
def loading_notif_label() -> Widget.Label:
return Widget.Label(
label="Loading notifications...",
valign="center",
vexpand=True,
css_classes=["notification-center-info-label"],
)
def no_notifications_label() -> Widget.Label:
return Widget.Label(
label="No notifications",
valign="center",
vexpand=True,
visible=notifications.bind("notifications", lambda value: len(value) == 0),
css_classes=["notification-center-info-label"],
)
def on_notified(box: Widget.Box, notification: Notification) -> None:
notify = Popup(notification)
box.prepend(notify)
notify.reveal_child = True
def load_notifications() -> list[Widget.Label | Popup]:
widgets = []
for i in notifications.notifications:
GLib.idle_add(lambda i=i: widgets.append(Popup(i, reveal_child=True)))
return widgets
def notification_list() -> Widget.Box:
box = Widget.Box(
vertical=True,
child=[loading_notif_label()],
vexpand=True,
css_classes=["rec-unset"],
setup=lambda self: notifications.connect(
"notified",
lambda x, notification: on_notified(self, notification),
),
)
Utils.ThreadTask(
load_notifications,
lambda result: box.set_child(result + [no_notifications_label()]),
).run()
return box
def notification_center() -> Widget.Box:
main_box = Widget.Box(
vertical=True,
vexpand=True,
css_classes=["notification-center"],
child=[
Widget.Box(
css_classes=["notification-center-header", "rec-unset"],
child=[
Widget.Label(
label=notifications.bind(
"notifications", lambda value: str(len(value))
),
css_classes=["notification-count"],
),
Widget.Label(
label="notifications",
css_classes=["notification-header-label"],
),
Widget.Button(
child=Widget.Label(label="Clear all"),
halign="end",
hexpand=True,
on_click=lambda x: notifications.clear_all(),
css_classes=["notification-clear-all"],
),
],
),
Widget.Scroll(
child=notification_list(),
vexpand=True,
),
],
)
return main_box
@@ -0,0 +1,64 @@
from ignis.widgets import Widget
from gi.repository import GObject # type: ignore
from typing import Callable
from ignis.gobject import Binding
class QSButton(Widget.Button):
def __init__(
self,
label: str | Binding,
icon_name: str | Binding,
on_activate: Callable | None = None,
on_deactivate: Callable | None = None,
content: Widget.Revealer | None = None,
**kwargs,
):
self.on_activate = on_activate
self.on_deactivate = on_deactivate
self._active = False
self._content = content
super().__init__(
child=Widget.Box(
child=[
Widget.Icon(image=icon_name),
Widget.Label(label=label, css_classes=["qs-button-label"]),
Widget.Arrow(
halign="end",
hexpand=True,
pixel_size=20,
rotated=content.bind("reveal_child"),
)
if content
else None,
]
),
on_click=self.__callback,
css_classes=["qs-button", "unset"],
hexpand=True,
**kwargs,
)
def __callback(self, *args) -> None:
if self.active:
if self.on_deactivate:
self.on_deactivate(self)
else:
if self.on_activate:
self.on_activate(self)
@GObject.Property
def active(self) -> bool:
return self._active
@active.setter
def active(self, value: bool) -> None:
self._active = value
if value:
self.add_css_class("active")
else:
self.remove_css_class("active")
@GObject.Property
def content(self) -> Widget.Revealer | None:
return self._content
@@ -0,0 +1,64 @@
from ignis.widgets import Widget
from .wifi import wifi_control
from .record import record_control
from .dnd import dnd_button
from .dark_mode import dark_mode_button
from .ethernet import ethernet_control
from .vpn import vpn_control
from .qs_button import QSButton
from ignis.services.network import NetworkService
network = NetworkService.get_default()
def add_button(main_box: Widget.Box, buttons: tuple[QSButton, ...], i: int) -> None:
row = Widget.Box(homogeneous=True)
if len(main_box.child) > 0:
row.style = "margin-top: 0.5rem;"
main_box.append(row)
button1 = buttons[i]
row.append(button1)
if button1.content:
main_box.append(button1.content)
if i + 1 < len(buttons):
button2 = buttons[i + 1]
button2.style = "margin-left: 0.5rem;"
row.append(button2)
if button2.content:
main_box.append(button2.content)
def qs_fabric(main_box: Widget.Box, *buttons: QSButton) -> None:
for i in range(0, len(buttons), 2):
add_button(main_box, buttons, i)
def qs_config(main_box: Widget.Box) -> None:
qs_fabric(
main_box,
*wifi_control(),
*ethernet_control(),
*vpn_control(),
dnd_button(),
dark_mode_button(),
record_control(),
)
def update_box(main_box: Widget.Box):
main_box.child = []
qs_config(main_box)
def quick_settings() -> Widget.Box:
main_box = Widget.Box(vertical=True, css_classes=["qs-main-box"])
network.wifi.connect("notify::devices", lambda x, y: update_box(main_box))
network.ethernet.connect("notify::devices", lambda x, y: update_box(main_box))
network.vpn.connect("notify::connections", lambda x, y: update_box(main_box))
return main_box
@@ -0,0 +1,100 @@
from ignis.widgets import Widget
from .qs_button import QSButton
from ignis.services.recorder import RecorderService
recorder = RecorderService.get_default()
def record_control() -> QSButton:
record_audio_switch = Widget.Switch(halign="end", hexpand=True, valign="center")
dropdown = Widget.DropDown(
items=["Internal audio", "Microphone", "Both sources"],
css_classes=["record-dropdown"],
)
def start_recording(record_menu: Widget.Revealer) -> None:
record_menu.set_reveal_child(False)
microphone = False
internal = False
if record_audio_switch.active:
if dropdown.selected == "Internal audio":
internal = True
elif dropdown.selected == "Microphone":
microphone = True
else:
internal = True
microphone = True
recorder.start_recording(
record_microphone=microphone, record_internal_audio=internal
)
record_menu = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
css_classes=["record-menu"],
vertical=True,
child=[
Widget.Icon(
image="media-record-symbolic",
pixel_size=36,
halign="center",
css_classes=["record-icon"],
),
Widget.Label(
label="Start recording?",
halign="center",
style="font-size: 1.2rem;",
),
Widget.Box(
style="margin-top: 0.5rem;",
child=[
Widget.Icon(
image="microphone-sensitivity-medium-symbolic",
pixel_size=20,
style="margin-right: 0.5rem;",
),
Widget.Box(
vertical=True,
child=[
Widget.Label(
label="Record audio",
style="font-size: 1.1rem;",
halign="start",
),
dropdown,
],
),
record_audio_switch,
],
),
Widget.Box(
style="margin-top: 1rem;",
child=[
Widget.Button(
child=Widget.Label(label="Cancel"),
css_classes=["record-cancel-button", "unset"],
on_click=lambda x: record_menu.set_reveal_child(False), # type: ignore
),
Widget.Button(
child=Widget.Label(label="Start recording"),
halign="end",
hexpand=True,
css_classes=["record-start-button", "unset"],
on_click=lambda x: start_recording(record_menu), # type: ignore
),
],
),
],
),
)
return QSButton(
label="Recording",
icon_name="media-record-symbolic",
on_activate=lambda x: record_menu.toggle(),
on_deactivate=lambda x: recorder.stop_recording(),
active=recorder.bind("active"),
content=record_menu,
)
@@ -0,0 +1,76 @@
import os
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.app import IgnisApp
from ignis.services.fetch import FetchService
from ..settings import settings_window
from options import avatar_opt
fetch = FetchService.get_default()
app = IgnisApp.get_default()
def format_uptime(value):
days, hours, minutes, seconds = value
if days:
return f"up {days:02}:{hours:02}:{minutes:02}"
else:
return f"up {hours:02}:{minutes:02}"
def user_image() -> Widget.Picture:
return Widget.Picture(
image=avatar_opt.bind(
"value",
lambda value: "user-info" if not os.path.exists(value) else value,
),
width=44,
height=44,
content_fit="cover",
style="border-radius: 10rem;",
)
def username() -> Widget.Box:
return Widget.Box(
child=[
Widget.Label(
label=os.getenv("USER"), css_classes=["user-name"], halign="start"
),
Widget.Label(
label=Utils.Poll(
timeout=60 * 1000, callback=lambda x: fetch.uptime
).bind("output", lambda value: format_uptime(value)),
halign="start",
css_classes=["user-name-secondary"],
),
],
vertical=True,
css_classes=["user-name-box"],
)
def settings_button() -> Widget.Button:
return Widget.Button(
child=Widget.Icon(image="emblem-system-symbolic", pixel_size=20),
halign="end",
hexpand=True,
css_classes=["user-settings", "unset"],
on_click=lambda x: settings_window(),
)
def power_button() -> Widget.Button:
return Widget.Button(
child=Widget.Icon(image="system-shutdown-symbolic", pixel_size=20),
halign="end",
css_classes=["user-power", "unset"],
on_click=lambda x: app.toggle_window("ignis_POWERMENU"),
)
def user() -> Widget.Box:
return Widget.Box(
child=[user_image(), username(), settings_button(), power_button()],
css_classes=["user"],
)
@@ -0,0 +1,161 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from ignis.services.audio import AudioService, Stream
audio = AudioService.get_default()
def volume_scale(stream: Stream) -> Widget.Scale:
return Widget.Scale(
css_classes=["material-slider"],
value=stream.bind("volume"),
step=5,
hexpand=True,
on_change=lambda x: stream.set_volume(x.value),
sensitive=stream.bind("is_muted", lambda value: not value),
)
def device_entry(stream: Stream, _type: str) -> Widget.Button:
widget = Widget.Button(
child=Widget.Box(
child=[
Widget.Icon(image="audio-card-symbolic"),
Widget.Label(
label=stream.description,
ellipsize="end",
max_width_chars=30,
halign="start",
css_classes=["volume-entry-label"],
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=stream.bind("is_default"),
),
]
),
css_classes=["volume-entry", "unset"],
hexpand=True,
on_click=lambda x: setattr(audio, _type, stream),
)
stream.connect("removed", lambda x: widget.unparent())
return widget
def device_list(
header_label: str, header_icon: str, _type: str, **kwargs
) -> Widget.Revealer:
box = Widget.Box(
css_classes=["control-center-menu"],
vertical=True,
child=[
Widget.Box(
child=[
Widget.Icon(image=header_icon, pixel_size=24),
Widget.Label(
label=header_label,
halign="start",
css_classes=["volume-entry-list-header-label"],
),
],
css_classes=["volume-entry-list-header-box"],
),
Widget.Box(
vertical=True,
setup=lambda self: audio.connect(
f"{_type}-added",
lambda x, stream: self.append(device_entry(stream, _type)),
),
),
Widget.Separator(css_classes=["volume-entry-list-separator"]),
Widget.Button(
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Sound Settings",
halign="start",
css_classes=["volume-entry-label"],
),
]
),
css_classes=["volume-entry", "unset"],
style="margin-bottom: 0;",
on_click=lambda x: Utils.exec_sh_async("pavucontrol"),
),
],
**kwargs,
)
return Widget.Revealer(
child=box, transition_type="slide_down", transition_duration=300
)
def volume_icon(stream: Stream) -> Widget.Button:
return Widget.Button(
child=Widget.Icon(
image=stream.bind("icon_name"),
pixel_size=18,
),
css_classes=["material-slider-icon", "unset", "hover-surface"],
on_click=lambda x: stream.set_is_muted(not stream.is_muted),
)
def device_list_arrow(device_list: Widget.Revealer) -> Widget.Button:
return Widget.ArrowButton(
arrow=Widget.Arrow(pixel_size=20),
css_classes=["material-slider-arrow", "hover-surface"],
on_click=lambda x: device_list.toggle(),
)
def volume_control():
speakers_list = device_list(
header_label="Sound Output",
header_icon="audio-headphones-symbolic",
_type="speaker",
style="margin-bottom: 1rem;",
)
microphones_list = device_list(
header_label="Sound Input",
header_icon="microphone-sensitivity-high-symbolic",
_type="microphone",
)
speaker_icon = volume_icon(audio.speaker)
microphone_icon = volume_icon(audio.microphone)
speaker_scale = volume_scale(audio.speaker)
microphone_scale = volume_scale(audio.microphone)
speaker_arrow = device_list_arrow(speakers_list)
microphone_arrow = device_list_arrow(microphones_list)
speaker_control = Widget.Box(
vertical=True,
child=[
Widget.Box(child=[speaker_icon, speaker_scale, speaker_arrow]),
speakers_list,
],
style="margin-top: 1rem;",
)
microphone_control = Widget.Box(
vertical=True,
child=[
Widget.Box(child=[microphone_icon, microphone_scale, microphone_arrow]),
microphones_list,
],
style="margin-top: 0.25rem;",
)
return Widget.Box(
vertical=True,
child=[speaker_control, microphone_control],
)
@@ -0,0 +1,120 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from .qs_button import QSButton
from typing import List
from ignis.services.network import NetworkService, VpnConnection
network = NetworkService.get_default()
class VpnNetworkItem(Widget.Button):
def __init__(self, conn: VpnConnection):
super().__init__(
css_classes=["vpn-connection-item", "unset"],
on_click=lambda x: conn.toggle_connection(),
child=Widget.Box(
child=[
Widget.Label(
label=conn.name,
ellipsize="end",
max_width_chars=20,
halign="start",
css_classes=["vpn-connection-label"],
),
Widget.Button(
child=Widget.Label(
label=conn.bind(
"is_connected",
lambda value: "Disconnect" if value else "Connect",
)
),
css_classes=["vpn-connection-item-connect-label", "unset"],
halign="end",
hexpand=True,
),
]
),
)
def vpn_qsbutton() -> QSButton:
networks_list = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=[
Widget.Box(
css_classes=["vpn-header-box"],
child=[
Widget.Icon(
icon_name="network-vpn-symbolic", pixel_size=28),
Widget.Label(
label="VPN connections",
css_classes=["vpn-header-label"],
),
],
),
Widget.Box(
vertical=True,
child=network.vpn.bind(
"connections",
transform=lambda value: [
VpnNetworkItem(i) for i in value],
),
),
Widget.Separator(
css_classes=["vpn-connection-list-separator"]),
Widget.Button(
css_classes=["vpn-connection-item", "unset"],
on_click=lambda x: Utils.exec_sh_async(
"nm-connection-editor"),
style="margin-bottom: 0;",
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Manager",
halign="start",
css_classes=["vpn-connection-label"],
),
]
),
),
],
),
)
def get_label(id: str) -> str:
if id:
return id
else:
return "VPN"
def get_icon(icon_name: str) -> str:
if network.vpn.is_connected:
return icon_name
else:
return "network-vpn-symbolic"
def toggle_list(x) -> None:
networks_list.toggle()
return QSButton(
label=network.vpn.bind("active_vpn_id", get_label),
icon_name=network.vpn.bind("icon-name", get_icon),
on_activate=toggle_list,
on_deactivate=toggle_list,
active=network.vpn.bind("is-connected"),
content=networks_list,
)
def vpn_control() -> List[QSButton]:
if len(network.vpn.connections) > 0:
return [vpn_qsbutton()]
else:
return []
@@ -0,0 +1,112 @@
from ignis.widgets import Widget
from ignis.utils import Utils
from .qs_button import QSButton
from ignis.services.network import NetworkService, WifiAccessPoint, WifiDevice
from typing import List
network = NetworkService.get_default()
class WifiNetworkItem(Widget.Button):
def __init__(self, access_point: WifiAccessPoint):
super().__init__(
css_classes=["wifi-network-item", "unset"],
on_click=lambda x: access_point.connect_to_graphical(),
child=Widget.Box(
child=[
Widget.Icon(
image=access_point.bind(
"strength", transform=lambda value: access_point.icon_name
),
),
Widget.Label(
label=access_point.ssid,
halign="start",
css_classes=["wifi-network-label"],
),
Widget.Icon(
image="object-select-symbolic",
halign="end",
hexpand=True,
visible=access_point.bind("is_connected"),
),
]
),
)
def wifi_qsbutton(device: WifiDevice) -> QSButton:
networks_list = Widget.Revealer(
transition_duration=300,
transition_type="slide_down",
child=Widget.Box(
vertical=True,
css_classes=["control-center-menu"],
child=[
Widget.Box(
child=[
Widget.Label(label="Wi-Fi", css_classes=["wifi-header-label"]),
Widget.Switch(
halign="end",
hexpand=True,
active=network.wifi.enabled,
on_change=lambda x, state: network.wifi.set_enabled(state),
),
],
css_classes=["toggle-box", "wifi-header-box"],
),
Widget.Box(
vertical=True,
child=device.bind(
"access_points",
transform=lambda value: [WifiNetworkItem(i) for i in value],
),
),
Widget.Separator(css_classes=["wifi-network-list-separator"]),
Widget.Button(
css_classes=["wifi-network-item", "unset"],
on_click=lambda x: Utils.exec_sh_async("nm-connection-editor"),
style="margin-bottom: 0;",
child=Widget.Box(
child=[
Widget.Icon(image="preferences-system-symbolic"),
Widget.Label(
label="Network Settings",
halign="start",
css_classes=["wifi-network-label"],
),
]
),
),
],
),
)
def get_label(ssid: str) -> str:
if ssid:
return ssid
else:
return "Wi-Fi"
def get_icon(icon_name: str) -> str:
if device.ap.is_connected:
return icon_name
else:
return "network-wireless-symbolic"
def toggle_list(x) -> None:
device.scan()
networks_list.toggle()
return QSButton(
label=device.ap.bind("ssid", get_label),
icon_name=device.ap.bind("icon-name", get_icon),
on_activate=toggle_list,
on_deactivate=toggle_list,
active=network.wifi.bind("enabled"),
content=networks_list,
)
def wifi_control() -> List[QSButton]:
return [wifi_qsbutton(dev) for dev in network.wifi.devices]