Move status area code into an applet

This commit is contained in:
Ian Douglas Scott 2022-06-03 19:36:05 -07:00
parent 8df3019973
commit f50ed504b1
16 changed files with 313 additions and 11 deletions

View file

@ -0,0 +1,16 @@
[package]
name = "cosmic-applet-status-area"
version = "0.1.0"
edition = "2021"
license = "GPL-3.0-or-later"
[dependencies]
cascade = "1"
futures = "0.3"
gtk4 = "0.4.6"
once_cell = "1.12"
serde = "1"
zbus = "2.0.1"
zbus_names = "2"
zvariant = "3"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", features = ["gtk4"]}

View file

@ -0,0 +1,10 @@
[Desktop Entry]
Name=Cosmic Applet Status Area
Type=Application
Exec=cosmic-applet-status-area
Terminal=false
Categories=GNOME;GTK;
Keywords=Gnome;GTK;
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
Icon=com.system76.CosmicAppletStatusArea
NoDisplay=true

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="128px" height="128px" viewBox="0 0 128 128" version="1.1">
<defs>
<filter id="alpha" filterUnits="objectBoundingBox" x="0%" y="0%" width="100%" height="100%">
<feColorMatrix type="matrix" in="SourceGraphic" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
</filter>
<mask id="mask0">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip1">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10632" clip-path="url(#clip1)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 123.503906 236 C 123.503906 268.863281 96.863281 295.503906 64 295.503906 C 31.136719 295.503906 4.496094 268.863281 4.496094 236 C 4.496094 203.136719 31.136719 176.496094 64 176.496094 C 96.863281 176.496094 123.503906 203.136719 123.503906 236 Z M 123.503906 236 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask1">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip2">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10635" clip-path="url(#clip2)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 29.195312 180.496094 L 98.804688 180.496094 C 103.609375 180.496094 107.503906 184.046875 107.503906 188.425781 L 107.503906 283.574219 C 107.503906 287.953125 103.609375 291.503906 98.804688 291.503906 L 29.195312 291.503906 C 24.390625 291.503906 20.496094 287.953125 20.496094 283.574219 L 20.496094 188.425781 C 20.496094 184.046875 24.390625 180.496094 29.195312 180.496094 Z M 29.195312 180.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask2">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip3">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10638" clip-path="url(#clip3)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 20.417969 184.496094 L 107.582031 184.496094 C 111.957031 184.496094 115.503906 188.042969 115.503906 192.417969 L 115.503906 279.582031 C 115.503906 283.957031 111.957031 287.503906 107.582031 287.503906 L 20.417969 287.503906 C 16.042969 287.503906 12.496094 283.957031 12.496094 279.582031 L 12.496094 192.417969 C 12.496094 188.042969 16.042969 184.496094 20.417969 184.496094 Z M 20.417969 184.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
<mask id="mask3">
<g filter="url(#alpha)">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(0%,0%,0%);fill-opacity:0.1;stroke:none;"/>
</g>
</mask>
<clipPath id="clip4">
<rect x="0" y="0" width="192" height="152"/>
</clipPath>
<g id="surface10641" clip-path="url(#clip4)">
<path style="fill:none;stroke-width:0.99;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-dasharray:0.99,0.99;stroke-miterlimit:4;" d="M 16.425781 200.496094 L 111.574219 200.496094 C 115.953125 200.496094 119.503906 204.390625 119.503906 209.195312 L 119.503906 278.804688 C 119.503906 283.609375 115.953125 287.503906 111.574219 287.503906 L 16.425781 287.503906 C 12.046875 287.503906 8.496094 283.609375 8.496094 278.804688 L 8.496094 209.195312 C 8.496094 204.390625 12.046875 200.496094 16.425781 200.496094 Z M 16.425781 200.496094 " transform="matrix(1,0,0,1,8,-156)"/>
</g>
</defs>
<g id="surface10578">
<rect x="0" y="0" width="128" height="128" style="fill:rgb(94.117647%,94.117647%,94.117647%);fill-opacity:1;stroke:none;"/>
<use xlink:href="#surface10632" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask0)"/>
<use xlink:href="#surface10635" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask1)"/>
<use xlink:href="#surface10638" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask2)"/>
<use xlink:href="#surface10641" transform="matrix(1,0,0,1,-8,-16)" mask="url(#mask3)"/>
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(38.431373%,62.7451%,91.764706%);stroke-opacity:1;stroke-miterlimit:4;" d="M 0 289 L 128 289 " transform="matrix(1,0,0,1,0,-172)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -0,0 +1,53 @@
use futures::prelude::*;
use gtk4::glib::{self, clone};
use std::cell::Cell;
use zbus::fdo::{DBusProxy, RequestNameFlags, RequestNameReply};
use zbus_names::WellKnownName;
pub async fn create<
F: Fn(zbus::ConnectionBuilder<'static>) -> zbus::Result<zbus::ConnectionBuilder<'static>>,
>(
well_known_name: &'static str,
serve_cb: F,
) -> zbus::Result<zbus::Connection> {
let well_known_name = WellKnownName::try_from(well_known_name)?;
let connection = serve_cb(zbus::ConnectionBuilder::session()?)?
.build()
.await?;
let dbus_proxy = DBusProxy::new(&connection).await?;
let mut name_owner_changed_stream = dbus_proxy.receive_name_owner_changed().await?;
let flags = RequestNameFlags::AllowReplacement.into();
match dbus_proxy
.request_name(well_known_name.as_ref(), flags)
.await?
{
RequestNameReply::InQueue => {
eprintln!("Bus name '{}' already owned", well_known_name);
}
_ => {}
}
glib::MainContext::default().spawn_local(clone!(@strong connection => async move {
let have_bus_name = Cell::new(false);
let unique_name = connection.unique_name().map(|x| x.as_ref());
while let Some(evt) = name_owner_changed_stream.next().await {
let args = match evt.args() {
Ok(args) => args,
Err(_) => { continue; },
};
if args.name.as_ref() == well_known_name {
if args.new_owner.as_ref() == unique_name.as_ref() {
eprintln!("Acquired bus name: {}", well_known_name);
have_bus_name.set(true);
} else if have_bus_name.get() {
eprintln!("Lost bus name: {}", well_known_name);
have_bus_name.set(false);
}
}
}
}));
Ok(connection)
}

View file

@ -0,0 +1,31 @@
use once_cell::unsync::OnceCell;
/// Wrapper around `OnceCell` implementing `Deref`, and thus also panicking
/// when not set (or set twice).
///
/// To be used in place of `gtk::TemplateChild`, but without xml.
pub struct DerefCell<T>(OnceCell<T>);
impl<T> DerefCell<T> {
#[track_caller]
pub fn set(&self, value: T) {
if self.0.set(value).is_err() {
panic!("Initialized twice");
}
}
}
impl<T> Default for DerefCell<T> {
fn default() -> Self {
Self(OnceCell::default())
}
}
impl<T> std::ops::Deref for DerefCell<T> {
type Target = T;
#[track_caller]
fn deref(&self) -> &T {
self.0.get().unwrap()
}
}

View file

@ -0,0 +1,30 @@
use gtk4::{glib, prelude::*};
mod dbus_service;
mod deref_cell;
mod popover_container;
mod status_area;
mod status_menu;
mod status_notifier_watcher;
use status_area::StatusArea;
fn main() {
gtk4::init().unwrap();
// XXX Implement DBus service somewhere other than applet?
glib::MainContext::default().spawn_local(status_notifier_watcher::start());
let status_area = StatusArea::new();
gtk4::Window::builder()
.decorated(false)
.child(&status_area)
.resizable(false)
.width_request(1)
.height_request(1)
.build()
.show();
let main_loop = glib::MainLoop::new(None, false);
main_loop.run();
}

View file

@ -0,0 +1,89 @@
use cascade::cascade;
use gtk4::{glib, prelude::*, subclass::prelude::*};
use crate::deref_cell::DerefCell;
/// Unlike gtk4's `MenuButton`, this supports a custom child.
#[derive(Default)]
pub struct PopoverContainerInner {
child: DerefCell<gtk4::Widget>,
popover: DerefCell<gtk4::Popover>,
}
#[glib::object_subclass]
impl ObjectSubclass for PopoverContainerInner {
const NAME: &'static str = "S76PopoverContainer";
type ParentType = gtk4::Widget;
type Type = PopoverContainer;
}
impl ObjectImpl for PopoverContainerInner {
fn constructed(&self, obj: &PopoverContainer) {
let popover = cascade! {
gtk4::Popover::new();
..set_parent(obj);
};
self.popover.set(popover);
}
fn dispose(&self, _obj: &PopoverContainer) {
self.child.unparent();
self.popover.unparent();
}
}
impl WidgetImpl for PopoverContainerInner {
fn measure(
&self,
_obj: &PopoverContainer,
orientation: gtk4::Orientation,
for_size: i32,
) -> (i32, i32, i32, i32) {
self.child.measure(orientation, for_size)
}
fn size_allocate(&self, _obj: &PopoverContainer, width: i32, height: i32, baseline: i32) {
self.child
.size_allocate(&gtk4::Allocation::new(0, 0, width, height), baseline);
self.popover.present();
}
fn focus(&self, _obj: &PopoverContainer, direction: gtk4::DirectionType) -> bool {
if self.popover.is_visible() {
self.popover.child_focus(direction)
} else {
self.child.child_focus(direction)
}
}
}
glib::wrapper! {
pub struct PopoverContainer(ObjectSubclass<PopoverContainerInner>)
@extends gtk4::Widget;
}
impl PopoverContainer {
pub fn new<T: IsA<gtk4::Widget>>(child: &T) -> Self {
let obj = glib::Object::new::<Self>(&[]).unwrap();
child.set_parent(&obj);
obj.inner().child.set(child.clone().upcast());
obj
}
fn inner(&self) -> &PopoverContainerInner {
PopoverContainerInner::from_instance(self)
}
pub fn popover(&self) -> &gtk4::Popover {
&self.inner().popover
}
pub fn popup(&self) {
self.popover().popup();
}
pub fn popdown(&self) {
self.popover().popdown();
}
}

View file

@ -0,0 +1,145 @@
use cascade::cascade;
use futures::stream::StreamExt;
use gtk4::{
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use once_cell::unsync::OnceCell;
use std::{cell::RefCell, collections::HashMap};
use zbus::dbus_proxy;
use crate::deref_cell::DerefCell;
use crate::status_menu::StatusMenu;
#[derive(Default)]
pub struct StatusAreaInner {
box_: DerefCell<gtk4::Box>,
watcher: OnceCell<StatusNotifierWatcherProxy<'static>>,
icons: RefCell<HashMap<String, StatusMenu>>,
}
#[glib::object_subclass]
impl ObjectSubclass for StatusAreaInner {
const NAME: &'static str = "S76StatusArea";
type ParentType = gtk4::Widget;
type Type = StatusArea;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for StatusAreaInner {
fn constructed(&self, obj: &StatusArea) {
let box_ = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..set_parent(obj);
};
self.box_.set(box_);
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
async {
let connection = zbus::Connection::session().await?;
let watcher = StatusNotifierWatcherProxy::new(&connection).await?;
let name = connection.unique_name().unwrap().as_str();
if let Err(err) = watcher.register_status_notifier_host(name).await {
eprintln!("Failed to register status notifier host: {}", err);
}
let mut registered_stream = watcher.receive_status_notifier_item_registered().await?;
let mut unregistered_stream = watcher.receive_status_notifier_item_unregistered().await?;
for name in watcher.registered_status_notifier_items().await? {
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
obj.item_registered(&name).await;
}));
}
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
if let Some(evt) = registered_stream.next().await {
if let Ok(args) = evt.args() {
obj.item_registered(&args.name).await;
}
}
}));
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
if let Some(evt) = unregistered_stream.next().await {
if let Ok(args) = evt.args() {
obj.item_unregistered(&args.name);
}
}
}));
let _ = obj.inner().watcher.set(watcher);
Ok::<_, zbus::Error>(())
}.await.unwrap_or_else(|err| {
eprintln!("Failed to connect to 'org.kde.StatusNotifierWatcher': {}", err);
});
}));
}
fn dispose(&self, _obj: &StatusArea) {
self.box_.unparent();
}
}
impl WidgetImpl for StatusAreaInner {}
glib::wrapper! {
pub struct StatusArea(ObjectSubclass<StatusAreaInner>)
@extends gtk4::Widget;
}
impl StatusArea {
pub fn new() -> Self {
glib::Object::new(&[]).unwrap()
}
fn inner(&self) -> &StatusAreaInner {
StatusAreaInner::from_instance(self)
}
async fn item_registered(&self, name: &str) {
match StatusMenu::new(&name).await {
Ok(item) => {
self.inner().box_.append(&item);
self.item_unregistered(name);
self.inner()
.icons
.borrow_mut()
.insert(name.to_owned(), item);
}
Err(err) => eprintln!("Failed to connect to '{}': {}", name, err),
}
}
fn item_unregistered(&self, name: &str) {
if let Some(icon) = self.inner().icons.borrow_mut().remove(name) {
self.inner().box_.remove(&icon);
}
}
}
#[dbus_proxy(
interface = "org.kde.StatusNotifierWatcher",
default_service = "org.kde.StatusNotifierWatcher",
default_path = "/StatusNotifierWatcher"
)]
trait StatusNotifierWatcher {
fn register_status_notifier_host(&self, name: &str) -> zbus::Result<()>;
#[dbus_proxy(property)]
fn registered_status_notifier_items(&self) -> zbus::Result<Vec<String>>;
#[dbus_proxy(signal)]
fn status_notifier_item_registered(&self, name: &str) -> zbus::Result<()>;
#[dbus_proxy(signal)]
fn status_notifier_item_unregistered(&self, name: &str) -> zbus::Result<()>;
}

View file

@ -0,0 +1,354 @@
use cascade::cascade;
use futures::StreamExt;
use gtk4::{
gdk_pixbuf,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use std::{cell::RefCell, collections::HashMap, io};
use zbus::dbus_proxy;
use zvariant::OwnedValue;
use crate::deref_cell::DerefCell;
use crate::popover_container::PopoverContainer;
struct Menu {
box_: gtk4::Box,
children: Vec<i32>,
}
#[derive(Default)]
pub struct StatusMenuInner {
button: DerefCell<gtk4::ToggleButton>,
popover_container: DerefCell<PopoverContainer>,
vbox: DerefCell<gtk4::Box>,
item: DerefCell<StatusNotifierItemProxy<'static>>,
dbus_menu: DerefCell<DBusMenuProxy<'static>>,
menus: RefCell<HashMap<i32, Menu>>,
}
#[glib::object_subclass]
impl ObjectSubclass for StatusMenuInner {
const NAME: &'static str = "S76StatusMenu";
type ParentType = gtk4::Widget;
type Type = StatusMenu;
fn class_init(klass: &mut Self::Class) {
klass.set_layout_manager_type::<gtk4::BinLayout>();
}
}
impl ObjectImpl for StatusMenuInner {
fn constructed(&self, obj: &StatusMenu) {
let vbox = cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
};
let button = cascade! {
gtk4::ToggleButton::new();
..set_has_frame(false);
};
let popover_container = cascade! {
PopoverContainer::new(&button);
..set_parent(obj);
..popover().set_child(Some(&vbox));
..popover().bind_property("visible", &button, "active").flags(glib::BindingFlags::BIDIRECTIONAL).build();
};
self.button.set(button);
self.popover_container.set(popover_container);
self.vbox.set(vbox);
}
fn dispose(&self, _obj: &StatusMenu) {
self.button.unparent();
}
}
impl WidgetImpl for StatusMenuInner {}
glib::wrapper! {
pub struct StatusMenu(ObjectSubclass<StatusMenuInner>)
@extends gtk4::Widget;
}
impl StatusMenu {
pub async fn new(name: &str) -> zbus::Result<Self> {
let (dest, path) = if let Some(idx) = name.find('/') {
(&name[..idx], &name[idx..])
} else {
(name, "/StatusNotifierItem")
};
let connection = zbus::Connection::session().await?;
let item = StatusNotifierItemProxy::builder(&connection)
.destination(dest.to_string())?
.path(path.to_string())?
.build()
.await?;
let obj = glib::Object::new::<Self>(&[]).unwrap();
let icon_name = item.icon_name().await?;
obj.inner().button.set_icon_name(&icon_name);
let menu = item.menu().await?;
let menu = DBusMenuProxy::builder(&connection)
.destination(dest.to_string())?
.path(menu)?
.build()
.await?;
let layout = menu.get_layout(0, -1, &[]).await?.1;
let mut layout_updated_stream = menu.receive_layout_updated().await?;
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
while let Some(evt) = layout_updated_stream.next().await {
let args = match evt.args() {
Ok(args) => args,
Err(_) => { continue; },
};
obj.layout_updated(args.revision, args.parent);
}
}));
obj.inner().item.set(item);
obj.inner().dbus_menu.set(menu);
println!("{:#?}", layout);
obj.populate_menu(&obj.inner().vbox, &layout);
Ok(obj)
}
fn inner(&self) -> &StatusMenuInner {
StatusMenuInner::from_instance(self)
}
fn layout_updated(&self, _revision: u32, parent: i32) {
let mut menus = self.inner().menus.borrow_mut();
if let Some(Menu { box_, children }) = menus.remove(&parent) {
let mut next_child = box_.first_child();
while let Some(child) = next_child {
next_child = child.next_sibling();
box_.remove(&child);
}
fn remove_child_menus(menus: &mut HashMap<i32, Menu>, children: Vec<i32>) {
for i in children {
if let Some(menu) = menus.remove(&i) {
remove_child_menus(menus, menu.children);
}
}
}
remove_child_menus(&mut menus, children);
glib::MainContext::default().spawn_local(clone!(@weak self as self_ => async move {
match self_.inner().dbus_menu.get_layout(parent, -1, &[]).await {
Ok((_, layout)) => self_.populate_menu(&box_, &layout),
Err(err) => eprintln!("Failed to call 'GetLayout': {}", err),
}
}));
}
}
fn populate_menu(&self, box_: &gtk4::Box, layout: &Layout) {
let mut children = Vec::new();
for i in layout.children() {
children.push(i.id());
if i.type_().as_deref() == Some("separator") {
let separator = cascade! {
gtk4::Separator::new(gtk4::Orientation::Horizontal);
..set_visible(i.visible());
};
box_.append(&separator);
} else if let Some(label) = i.label() {
let mut label = label.to_string();
if let Some(toggle_state) = i.toggle_state() {
if toggle_state != 0 {
label = format!("{}", label);
}
}
let label_widget = cascade! {
gtk4::Label::new(Some(&label));
..set_halign(gtk4::Align::Start);
..set_hexpand(true);
..set_use_underline(true);
};
let hbox = cascade! {
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
..append(&label_widget);
};
if let Some(icon_data) = i.icon_data() {
let icon_data = io::Cursor::new(icon_data.to_vec());
let pixbuf = gdk_pixbuf::Pixbuf::from_read(icon_data).unwrap(); // XXX unwrap
let image = cascade! {
gtk4::Image::from_pixbuf(Some(&pixbuf));
..set_halign(gtk4::Align::End);
};
hbox.append(&image);
}
let id = i.id();
let close_on_click = i.children_display().as_deref() != Some("submenu");
let button = cascade! {
gtk4::Button::new();
..set_child(Some(&hbox));
..style_context().add_class("flat");
..set_visible(i.visible());
..set_sensitive(i.enabled());
..connect_clicked(clone!(@weak self as self_ => move |_| {
// XXX data, timestamp
if close_on_click {
self_.inner().popover_container.popdown();
}
glib::MainContext::default().spawn_local(clone!(@strong self_ => async move {
let _ = self_.inner().dbus_menu.event(id, "clicked", &0.into(), 0).await;
}));
}));
};
box_.append(&button);
if i.children_display().as_deref() == Some("submenu") {
let vbox = cascade! {
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
};
let revealer = cascade! {
gtk4::Revealer::new();
..set_child(Some(&vbox));
};
self.populate_menu(&vbox, &i);
box_.append(&revealer);
button.connect_clicked(move |_| {
revealer.set_reveal_child(!revealer.reveals_child());
});
}
}
}
self.inner().menus.borrow_mut().insert(
layout.id(),
Menu {
box_: box_.clone(),
children,
},
);
}
}
#[dbus_proxy(interface = "org.kde.StatusNotifierItem")]
trait StatusNotifierItem {
#[dbus_proxy(property)]
fn icon_name(&self) -> zbus::Result<String>;
#[dbus_proxy(property)]
fn menu(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
}
#[derive(Debug)]
pub struct Layout(i32, LayoutProps, Vec<Layout>);
impl<'a> serde::Deserialize<'a> for Layout {
fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
let (id, props, children) =
<(i32, LayoutProps, Vec<(zvariant::Signature<'_>, Self)>)>::deserialize(deserializer)?;
Ok(Self(id, props, children.into_iter().map(|x| x.1).collect()))
}
}
impl zvariant::Type for Layout {
fn signature() -> zvariant::Signature<'static> {
zvariant::Signature::try_from("(ia{sv}av)").unwrap()
}
}
#[derive(Debug, zvariant::DeserializeDict, zvariant::Type)]
pub struct LayoutProps {
#[zvariant(rename = "accessible-desc")]
accessible_desc: Option<String>,
#[zvariant(rename = "children-display")]
children_display: Option<String>,
label: Option<String>,
enabled: Option<bool>,
visible: Option<bool>,
#[zvariant(rename = "type")]
type_: Option<String>,
#[zvariant(rename = "toggle-type")]
toggle_type: Option<String>,
#[zvariant(rename = "toggle-state")]
toggle_state: Option<i32>,
#[zvariant(rename = "icon-data")]
icon_data: Option<Vec<u8>>,
}
#[allow(dead_code)]
impl Layout {
fn id(&self) -> i32 {
self.0
}
fn children(&self) -> &[Self] {
&self.2
}
fn accessible_desc(&self) -> Option<&str> {
self.1.accessible_desc.as_deref()
}
fn children_display(&self) -> Option<&str> {
self.1.children_display.as_deref()
}
fn label(&self) -> Option<&str> {
self.1.label.as_deref()
}
fn enabled(&self) -> bool {
self.1.enabled.unwrap_or(true)
}
fn visible(&self) -> bool {
self.1.visible.unwrap_or(true)
}
fn type_(&self) -> Option<&str> {
self.1.type_.as_deref()
}
fn toggle_type(&self) -> Option<&str> {
self.1.toggle_type.as_deref()
}
fn toggle_state(&self) -> Option<i32> {
self.1.toggle_state
}
fn icon_data(&self) -> Option<&[u8]> {
self.1.icon_data.as_deref()
}
}
#[dbus_proxy(interface = "com.canonical.dbusmenu")]
trait DBusMenu {
fn get_layout(
&self,
parent_id: i32,
recursion_depth: i32,
property_names: &[&str],
) -> zbus::Result<(u32, Layout)>;
fn event(&self, id: i32, event_id: &str, data: &OwnedValue, timestamp: u32)
-> zbus::Result<()>;
#[dbus_proxy(signal)]
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
}

View file

@ -0,0 +1,70 @@
#![allow(non_snake_case)]
use std::sync::{Arc, Mutex};
use zbus::{dbus_interface, MessageHeader, Result, SignalContext};
use crate::dbus_service;
#[derive(Default)]
struct StatusNotifierWatcher {
items: Arc<Mutex<Vec<String>>>,
}
#[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
impl StatusNotifierWatcher {
async fn RegisterStatusNotifierItem(
&self,
service: &str,
#[zbus(header)] hdr: MessageHeader<'_>,
#[zbus(signal_context)] ctxt: SignalContext<'_>,
) {
let service = format!("{}{}", hdr.sender().unwrap().unwrap(), service);
Self::StatusNotifierItemRegistered(&ctxt, &service)
.await
.unwrap();
// XXX emit unreigstered
self.items.lock().unwrap().push(service);
}
fn RegisterStatusNotifierHost(&self, _service: &str) {
// XXX emit registed/unregistered
}
#[dbus_interface(property)]
fn RegisteredStatusNotifierItems(&self) -> Vec<String> {
self.items.lock().unwrap().clone()
}
#[dbus_interface(property)]
fn IsStatusNotifierHostRegistered(&self) -> bool {
true
}
#[dbus_interface(property)]
fn ProtocolVersion(&self) -> i32 {
0
}
#[dbus_interface(signal)]
async fn StatusNotifierItemRegistered(ctxt: &SignalContext<'_>, service: &str) -> Result<()>;
#[dbus_interface(signal)]
async fn StatusNotifierItemUnregistered(ctxt: &SignalContext<'_>, service: &str) -> Result<()>;
#[dbus_interface(signal)]
async fn StatusNotifierHostRegistered(ctxt: &SignalContext<'_>) -> Result<()>;
#[dbus_interface(signal)]
async fn StatusNotifierHostUnregistered(ctxt: &SignalContext<'_>) -> Result<()>;
}
pub async fn start() {
if let Err(err) = dbus_service::create("org.kde.StatusNotifierWatcher", |builder| {
builder.serve_at("/StatusNotifierWatcher", StatusNotifierWatcher::default())
})
.await
{
eprintln!("Failed to start `StatusNotifierWatcher` service: {}", err);
}
}