Iced port of status area applet
This is based on the GTK version of the status area applet that was previously in this repository. This exposes app indicators found over dbus. As used in applications like nm-applet and steam.
This commit is contained in:
parent
29a2dea760
commit
6a64486163
16 changed files with 1313 additions and 1 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -841,6 +841,17 @@ dependencies = [
|
|||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-applet-status-area"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"libcosmic",
|
||||
"serde",
|
||||
"tokio",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-applet-time"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ members = [
|
|||
"cosmic-applet-network",
|
||||
"cosmic-applet-notifications",
|
||||
"cosmic-applet-power",
|
||||
"cosmic-applet-status-area",
|
||||
"cosmic-applet-time",
|
||||
"cosmic-applet-workspaces",
|
||||
"cosmic-panel-button",
|
||||
|
|
|
|||
345
applets/cosmic-applet-status-area/src/status_menu.rs
Normal file
345
applets/cosmic-applet-status-area/src/status_menu.rs
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
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;
|
||||
|
||||
struct Menu {
|
||||
box_: gtk4::Box,
|
||||
children: Vec<i32>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StatusMenuInner {
|
||||
menu_button: DerefCell<libcosmic_applet::AppletButton>,
|
||||
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 menu_button = cascade! {
|
||||
libcosmic_applet::AppletButton::new();
|
||||
..set_parent(obj);
|
||||
..set_popover_child(Some(&vbox));
|
||||
};
|
||||
|
||||
self.menu_button.set(menu_button);
|
||||
self.vbox.set(vbox);
|
||||
}
|
||||
|
||||
fn dispose(&self, _obj: &StatusMenu) {
|
||||
self.menu_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().menu_button.set_button_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_: >k4::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().menu_button.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<()>;
|
||||
}
|
||||
12
cosmic-applet-status-area/Cargo.toml
Normal file
12
cosmic-applet-status-area/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "cosmic-applet-status-area"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
libcosmic.workspace = true
|
||||
serde = "1"
|
||||
tokio = { version = "1.23.0" }
|
||||
zbus = { version = "3", default-features = false, features = ["tokio"] }
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
[Desktop Entry]
|
||||
Name=Cosmic Applet Status Area
|
||||
Comment=Applet for Cosmic Panel
|
||||
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
|
||||
StartupNotify=true
|
||||
NoDisplay=true
|
||||
X-CosmicApplet=true
|
||||
|
|
@ -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 |
215
cosmic-applet-status-area/src/components/app.rs
Normal file
215
cosmic-applet-status-area/src/components/app.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
use cosmic::{
|
||||
app::{self, Command},
|
||||
iced::{
|
||||
self,
|
||||
wayland::{
|
||||
popup::{destroy_popup, get_popup},
|
||||
window::resize_window,
|
||||
},
|
||||
window, Subscription,
|
||||
},
|
||||
iced_style::application,
|
||||
Theme,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::{components::status_menu, subscriptions::status_notifier_watcher};
|
||||
|
||||
// XXX copied from libcosmic
|
||||
const APPLET_PADDING: u32 = 8;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Msg {
|
||||
Closed(window::Id),
|
||||
// XXX don't use index (unique window id? or I guess that's created and destroyed)
|
||||
StatusMenu((usize, status_menu::Msg)),
|
||||
StatusNotifier(status_notifier_watcher::Event),
|
||||
TogglePopup(usize),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
core: app::Core,
|
||||
connection: Option<zbus::Connection>,
|
||||
menus: BTreeMap<usize, status_menu::State>,
|
||||
open_menu: Option<usize>,
|
||||
max_menu_id: usize,
|
||||
max_popup_id: u128,
|
||||
popup: Option<window::Id>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn next_menu_id(&mut self) -> usize {
|
||||
self.max_menu_id += 1;
|
||||
self.max_menu_id
|
||||
}
|
||||
|
||||
fn next_popup_id(&mut self) -> window::Id {
|
||||
self.max_popup_id += 1;
|
||||
window::Id(self.max_popup_id)
|
||||
}
|
||||
|
||||
fn resize_window(&self) -> Command<Msg> {
|
||||
let icon_size = self.core.applet_helper.suggested_size().0 as u32 + APPLET_PADDING * 2;
|
||||
let n = self.menus.len() as u32;
|
||||
resize_window(window::Id(0), 1.max(icon_size * n), icon_size)
|
||||
}
|
||||
}
|
||||
|
||||
impl cosmic::Application for App {
|
||||
type Message = Msg;
|
||||
type Executor = iced::executor::Default;
|
||||
type Flags = ();
|
||||
const APP_ID: &'static str = "com.system76.CosmicAppletStatusArea";
|
||||
|
||||
fn init(core: app::Core, _flags: ()) -> (Self, app::Command<Msg>) {
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
..Self::default()
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn core(&self) -> &app::Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut app::Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<<Theme as application::StyleSheet>::Style> {
|
||||
Some(app::applet::style())
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Msg) -> Command<Msg> {
|
||||
match message {
|
||||
Msg::Closed(surface) => {
|
||||
if self.popup == Some(surface) {
|
||||
self.popup = None;
|
||||
self.open_menu = None;
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
Msg::StatusMenu((id, msg)) => match self.menus.get_mut(&id) {
|
||||
Some(state) => state
|
||||
.update(msg)
|
||||
.map(move |msg| app::message::app(Msg::StatusMenu((id, msg)))),
|
||||
None => Command::none(),
|
||||
},
|
||||
Msg::StatusNotifier(event) => match event {
|
||||
status_notifier_watcher::Event::Connected(connection) => {
|
||||
self.connection = Some(connection);
|
||||
Command::none()
|
||||
}
|
||||
status_notifier_watcher::Event::Registered(name) => {
|
||||
let (state, cmd) = status_menu::State::new(name);
|
||||
let id = self.next_menu_id();
|
||||
self.menus.insert(id, state);
|
||||
Command::batch([
|
||||
self.resize_window(),
|
||||
cmd.map(move |msg| app::message::app(Msg::StatusMenu((id, msg)))),
|
||||
])
|
||||
}
|
||||
status_notifier_watcher::Event::Unregistered(name) => {
|
||||
if let Some((id, _)) =
|
||||
self.menus.iter().find(|(_id, menu)| menu.name() == &name)
|
||||
{
|
||||
let id = *id;
|
||||
self.menus.remove(&id);
|
||||
if self.open_menu == Some(id) {
|
||||
self.open_menu = None;
|
||||
if let Some(popup_id) = self.popup {
|
||||
return destroy_popup(popup_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.resize_window()
|
||||
}
|
||||
status_notifier_watcher::Event::Error(err) => {
|
||||
eprintln!("Status notifier error: {}", err);
|
||||
Command::none()
|
||||
}
|
||||
},
|
||||
Msg::TogglePopup(id) => {
|
||||
self.open_menu = if self.open_menu != Some(id) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Reuse popup if a different menu is opened.
|
||||
// Had issue creating new one. Does it make a difference?
|
||||
if self.open_menu.is_some() {
|
||||
if self.popup.is_none() {
|
||||
let id = self.next_popup_id();
|
||||
let popup_settings = self.core.applet_helper.get_popup_settings(
|
||||
window::Id(0),
|
||||
id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
self.popup = Some(id);
|
||||
return get_popup(popup_settings);
|
||||
}
|
||||
} else if let Some(id) = self.popup {
|
||||
return destroy_popup(id);
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Msg> {
|
||||
let mut subscriptions = Vec::new();
|
||||
|
||||
subscriptions.push(status_notifier_watcher::subscription().map(Msg::StatusNotifier));
|
||||
|
||||
for (id, menu) in self.menus.iter() {
|
||||
subscriptions.push(menu.subscription().with(*id).map(Msg::StatusMenu));
|
||||
}
|
||||
|
||||
iced::Subscription::batch(subscriptions)
|
||||
}
|
||||
|
||||
fn view(&self) -> cosmic::Element<'_, Msg> {
|
||||
// XXX connect open event
|
||||
iced::widget::row(
|
||||
self.menus
|
||||
.iter()
|
||||
.map(|(id, menu)| {
|
||||
self.core
|
||||
.applet_helper
|
||||
.icon_button(menu.icon_name())
|
||||
.on_press(Msg::TogglePopup(*id))
|
||||
.into()
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_window(&self, _surface: window::Id) -> cosmic::Element<'_, Msg> {
|
||||
match self.open_menu {
|
||||
Some(id) => match self.menus.get(&id) {
|
||||
Some(menu) => self
|
||||
.core
|
||||
.applet_helper
|
||||
.popup_container(menu.popup_view().map(move |msg| Msg::StatusMenu((id, msg))))
|
||||
.into(),
|
||||
None => unreachable!(),
|
||||
},
|
||||
None => iced::widget::text("").into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close_requested(&self, id: window::Id) -> Option<Msg> {
|
||||
Some(Msg::Closed(id))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
app::applet::run::<App>(true, ())
|
||||
}
|
||||
2
cosmic-applet-status-area/src/components/mod.rs
Normal file
2
cosmic-applet-status-area/src/components/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod app;
|
||||
pub mod status_menu;
|
||||
170
cosmic-applet-status-area/src/components/status_menu.rs
Normal file
170
cosmic-applet-status-area/src/components/status_menu.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use cosmic::{iced, theme};
|
||||
|
||||
use crate::subscriptions::status_notifier_item::{Layout, StatusNotifierItem};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Msg {
|
||||
Layout(Result<Layout, String>),
|
||||
Click(i32, bool),
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
item: StatusNotifierItem,
|
||||
layout: Option<Layout>,
|
||||
expanded: Option<i32>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(item: StatusNotifierItem) -> (Self, iced::Command<Msg>) {
|
||||
(
|
||||
Self {
|
||||
item,
|
||||
layout: None,
|
||||
expanded: None,
|
||||
},
|
||||
iced::Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Msg) -> iced::Command<Msg> {
|
||||
match message {
|
||||
Msg::Layout(layout) => {
|
||||
match layout {
|
||||
Ok(layout) => {
|
||||
self.layout = Some(layout);
|
||||
}
|
||||
Err(err) => eprintln!("Error getting layout from icon: {}", err),
|
||||
}
|
||||
iced::Command::none()
|
||||
}
|
||||
Msg::Click(id, is_submenu) => {
|
||||
let menu_proxy = self.item.menu_proxy().clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = menu_proxy.event(id, "clicked", &0.into(), 0).await;
|
||||
});
|
||||
if is_submenu {
|
||||
self.expanded = if self.expanded != Some(id) {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
} else {
|
||||
// TODO: Close menu?
|
||||
}
|
||||
iced::Command::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self.item.name()
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> &str {
|
||||
self.item.icon_name()
|
||||
}
|
||||
|
||||
pub fn popup_view(&self) -> cosmic::Element<Msg> {
|
||||
if let Some(layout) = self.layout.as_ref() {
|
||||
layout_view(layout, self.expanded)
|
||||
} else {
|
||||
iced::widget::text("").into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> iced::Subscription<Msg> {
|
||||
self.item.layout_subscription().map(Msg::Layout)
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_view(layout: &Layout, expanded: Option<i32>) -> cosmic::Element<Msg> {
|
||||
iced::widget::column(
|
||||
layout
|
||||
.children()
|
||||
.iter()
|
||||
.filter_map(|i| {
|
||||
if !i.visible() {
|
||||
None
|
||||
} else if i.type_().as_deref() == Some("separator") {
|
||||
Some(iced::widget::horizontal_rule(2).into())
|
||||
} else if let Some(label) = i.label() {
|
||||
// Strip _ when not doubled
|
||||
// TODO: interpret as "access key"? And label with underline.
|
||||
let mut is_underscore = false;
|
||||
let label = label
|
||||
.chars()
|
||||
.filter(|c| {
|
||||
let prev_is_underscore = is_underscore;
|
||||
is_underscore = !is_underscore && *c == '_';
|
||||
*c != '_' || prev_is_underscore
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
let is_submenu = i.children_display().as_deref() == Some("submenu");
|
||||
let is_expanded = expanded == Some(i.id());
|
||||
|
||||
let text = iced::widget::text(label).width(iced::Length::Fill);
|
||||
|
||||
let mut children: Vec<cosmic::Element<_>> = vec![text.into()];
|
||||
if is_submenu {
|
||||
let icon = cosmic::widget::icon(
|
||||
if is_expanded {
|
||||
"go-down-symbolic"
|
||||
} else {
|
||||
"go-next-symbolic"
|
||||
},
|
||||
14,
|
||||
)
|
||||
.style(theme::Svg::Symbolic);
|
||||
children.push(icon.into());
|
||||
}
|
||||
if let Some(icon_data) = i.icon_data() {
|
||||
let handle = iced::widget::image::Handle::from_memory(icon_data.to_vec());
|
||||
children.insert(0, iced::widget::Image::new(handle).into());
|
||||
} else if let Some(icon_name) = i.icon_name() {
|
||||
let icon = cosmic::widget::icon(icon_name, 14).style(theme::Svg::Symbolic);
|
||||
children.insert(0, icon.into());
|
||||
}
|
||||
if i.toggle_state() == Some(1) {
|
||||
let icon = cosmic::widget::icon("emblem-ok-symbolic", 14)
|
||||
.style(theme::Svg::Symbolic);
|
||||
children.push(icon.into());
|
||||
}
|
||||
let button = row_button(children).on_press(Msg::Click(i.id(), is_submenu));
|
||||
|
||||
if is_submenu && is_expanded {
|
||||
Some(
|
||||
iced::widget::column![
|
||||
button,
|
||||
// XXX nested
|
||||
iced::widget::container(layout_view(i, None)).padding(
|
||||
iced::Padding {
|
||||
left: 12.,
|
||||
..iced::Padding::ZERO
|
||||
}
|
||||
)
|
||||
]
|
||||
.into(),
|
||||
)
|
||||
} else {
|
||||
Some(button.into())
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn row_button(content: Vec<cosmic::Element<Msg>>) -> iced::widget::Button<Msg, cosmic::Renderer> {
|
||||
cosmic::widget::button(cosmic::app::applet::applet_button_theme())
|
||||
.custom(vec![iced::widget::Row::with_children(content)
|
||||
.spacing(8)
|
||||
.align_items(iced::Alignment::Center)
|
||||
.width(iced::Length::Fill)
|
||||
.into()])
|
||||
.width(iced::Length::Fill)
|
||||
.padding([8, 24])
|
||||
}
|
||||
6
cosmic-applet-status-area/src/main.rs
Normal file
6
cosmic-applet-status-area/src/main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
mod components;
|
||||
mod subscriptions;
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
components::app::main()
|
||||
}
|
||||
2
cosmic-applet-status-area/src/subscriptions/mod.rs
Normal file
2
cosmic-applet-status-area/src/subscriptions/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod status_notifier_item;
|
||||
pub mod status_notifier_watcher;
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
use cosmic::iced;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use zbus::zvariant::{self, OwnedValue};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StatusNotifierItem {
|
||||
name: String,
|
||||
icon_name: String,
|
||||
_item_proxy: StatusNotifierItemProxy<'static>,
|
||||
menu_proxy: DBusMenuProxy<'static>,
|
||||
}
|
||||
|
||||
impl StatusNotifierItem {
|
||||
pub async fn new(connection: &zbus::Connection, name: String) -> zbus::Result<Self> {
|
||||
let (dest, path) = if let Some(idx) = name.find('/') {
|
||||
(&name[..idx], &name[idx..])
|
||||
} else {
|
||||
(name.as_str(), "/StatusNotifierItem")
|
||||
};
|
||||
|
||||
let item_proxy = StatusNotifierItemProxy::builder(&connection)
|
||||
.destination(dest.to_string())?
|
||||
.path(path.to_string())?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let icon_name = item_proxy.icon_name().await?;
|
||||
|
||||
let menu_path = item_proxy.menu().await?;
|
||||
let menu_proxy = DBusMenuProxy::builder(&connection)
|
||||
.destination(dest.to_string())?
|
||||
.path(menu_path)?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
icon_name,
|
||||
_item_proxy: item_proxy,
|
||||
menu_proxy,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> &str {
|
||||
&self.icon_name
|
||||
}
|
||||
|
||||
// TODO: Only fetch changed part of layout, if that's any faster
|
||||
pub fn layout_subscription(&self) -> iced::Subscription<Result<Layout, String>> {
|
||||
let menu_proxy = self.menu_proxy.clone();
|
||||
iced::subscription::run_with_id(
|
||||
format!("status-notifier-item-{}", &self.name),
|
||||
async move {
|
||||
let initial = futures::stream::once(get_layout(menu_proxy.clone()));
|
||||
let layout_updated_stream = menu_proxy.receive_layout_updated().await.unwrap();
|
||||
let updates = layout_updated_stream.then(move |_| get_layout(menu_proxy.clone()));
|
||||
initial.chain(updates)
|
||||
}
|
||||
.flatten_stream(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn menu_proxy(&self) -> &DBusMenuProxy<'static> {
|
||||
&self.menu_proxy
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_layout(menu_proxy: DBusMenuProxy<'static>) -> Result<Layout, String> {
|
||||
match menu_proxy.get_layout(0, -1, &[]).await {
|
||||
Ok((_, layout)) => Ok(layout),
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus::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(Clone, 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(Clone, Debug, zvariant::DeserializeDict)]
|
||||
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>>,
|
||||
#[zvariant(rename = "icon-name")]
|
||||
icon_name: Option<String>,
|
||||
disposition: Option<String>,
|
||||
shortcut: Option<String>,
|
||||
}
|
||||
|
||||
impl zvariant::Type for LayoutProps {
|
||||
fn signature() -> zvariant::Signature<'static> {
|
||||
zvariant::Signature::try_from("a{sv}").unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Layout {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn children(&self) -> &[Self] {
|
||||
&self.2
|
||||
}
|
||||
|
||||
pub fn accessible_desc(&self) -> Option<&str> {
|
||||
self.1.accessible_desc.as_deref()
|
||||
}
|
||||
|
||||
pub fn children_display(&self) -> Option<&str> {
|
||||
self.1.children_display.as_deref()
|
||||
}
|
||||
|
||||
pub fn label(&self) -> Option<&str> {
|
||||
self.1.label.as_deref()
|
||||
}
|
||||
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.1.enabled.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn visible(&self) -> bool {
|
||||
self.1.visible.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub fn type_(&self) -> Option<&str> {
|
||||
self.1.type_.as_deref()
|
||||
}
|
||||
|
||||
pub fn toggle_type(&self) -> Option<&str> {
|
||||
self.1.toggle_type.as_deref()
|
||||
}
|
||||
|
||||
pub fn toggle_state(&self) -> Option<i32> {
|
||||
self.1.toggle_state
|
||||
}
|
||||
|
||||
pub fn icon_data(&self) -> Option<&[u8]> {
|
||||
self.1.icon_data.as_deref()
|
||||
}
|
||||
|
||||
pub fn icon_name(&self) -> Option<&str> {
|
||||
self.1.icon_name.as_deref()
|
||||
}
|
||||
|
||||
pub fn disposition(&self) -> Option<&str> {
|
||||
self.1.disposition.as_deref()
|
||||
}
|
||||
|
||||
pub fn shortcut(&self) -> Option<&str> {
|
||||
self.1.shortcut.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[zbus::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<()>;
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
use futures::{Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
|
||||
use super::Event;
|
||||
use crate::subscriptions::status_notifier_item::StatusNotifierItem;
|
||||
|
||||
// TODO: Don't use trait object
|
||||
pub type EventStream = Pin<Box<dyn Stream<Item = Event> + Send>>;
|
||||
|
||||
#[zbus::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<()>;
|
||||
}
|
||||
|
||||
pub async fn watch(connection: &zbus::Connection) -> zbus::Result<EventStream> {
|
||||
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 connection_clone = connection.clone();
|
||||
let registered_stream = watcher
|
||||
.receive_status_notifier_item_registered()
|
||||
.await?
|
||||
.then(move |evt| Box::pin(item_registered(connection_clone.clone(), evt)));
|
||||
let unregistered_stream = watcher
|
||||
.receive_status_notifier_item_unregistered()
|
||||
.await?
|
||||
.map(|evt| match evt.args() {
|
||||
Ok(args) => Event::Unregistered(args.name.to_string()),
|
||||
Err(err) => Event::Error(err.to_string()),
|
||||
});
|
||||
|
||||
let items = watcher.registered_status_notifier_items().await?;
|
||||
let connection = connection.clone();
|
||||
let items_stream = futures::stream::iter(items.into_iter())
|
||||
.then(move |name| status_notifier_item(connection.clone(), name));
|
||||
|
||||
Ok(Box::pin(items_stream.chain(futures::stream_select!(
|
||||
registered_stream,
|
||||
unregistered_stream
|
||||
))))
|
||||
}
|
||||
|
||||
async fn item_registered(connection: zbus::Connection, evt: StatusNotifierItemRegistered) -> Event {
|
||||
match evt.args() {
|
||||
Ok(args) => status_notifier_item(connection, args.name.to_string()).await,
|
||||
Err(err) => Event::Error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn status_notifier_item(connection: zbus::Connection, name: String) -> Event {
|
||||
match StatusNotifierItem::new(&connection, name).await {
|
||||
Ok(item) => Event::Registered(item),
|
||||
Err(err) => Event::Error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// TODO: Both this and server proxy could emit same events, have way to generate stream from either?
|
||||
|
||||
use cosmic::iced;
|
||||
use futures::StreamExt;
|
||||
|
||||
use crate::subscriptions::status_notifier_item::StatusNotifierItem;
|
||||
|
||||
mod client;
|
||||
mod server;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
Connected(zbus::Connection),
|
||||
Registered(StatusNotifierItem),
|
||||
Unregistered(String),
|
||||
Error(String), // XXX
|
||||
}
|
||||
|
||||
enum State {
|
||||
NotConnected,
|
||||
Connected(client::EventStream),
|
||||
Failed,
|
||||
}
|
||||
|
||||
pub fn subscription() -> iced::Subscription<Event> {
|
||||
iced::subscription::unfold(
|
||||
"status-notifier-watcher",
|
||||
State::NotConnected,
|
||||
|state| async move {
|
||||
match state {
|
||||
State::NotConnected => match connect().await {
|
||||
Ok((connection, stream)) => {
|
||||
(Event::Connected(connection), State::Connected(stream))
|
||||
}
|
||||
Err(err) => (Event::Error(err.to_string()), State::Failed),
|
||||
},
|
||||
State::Connected(mut stream) => match stream.next().await {
|
||||
Some(event) => (event, State::Connected(stream)),
|
||||
None => iced::futures::future::pending().await,
|
||||
},
|
||||
State::Failed => iced::futures::future::pending().await,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn connect() -> zbus::Result<(zbus::Connection, client::EventStream)> {
|
||||
// Connect to session dbus socket
|
||||
let connection = zbus::Connection::session().await?;
|
||||
|
||||
// Start `StatusNotifierWatcher` service, if there isn't one running already
|
||||
server::create_service(&connection).await?;
|
||||
|
||||
// Connect client and listen for registered/unregistered
|
||||
let stream = client::watch(&connection).await?;
|
||||
|
||||
Ok((connection, stream))
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
// TODO: `g_bus_own_name` like abstraction in zbus
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use futures::prelude::*;
|
||||
use zbus::{
|
||||
dbus_interface,
|
||||
fdo::{DBusProxy, RequestNameFlags, RequestNameReply},
|
||||
names::{BusName, UniqueName, WellKnownName},
|
||||
MessageHeader, Result, SignalContext,
|
||||
};
|
||||
|
||||
const NAME: WellKnownName =
|
||||
WellKnownName::from_static_str_unchecked("org.kde.StatusNotifierWatcher");
|
||||
const OBJECT_PATH: &str = "/StatusNotifierWatcher";
|
||||
|
||||
#[derive(Default)]
|
||||
struct StatusNotifierWatcher {
|
||||
items: Vec<(UniqueName<'static>, String)>,
|
||||
}
|
||||
|
||||
#[dbus_interface(name = "org.kde.StatusNotifierWatcher")]
|
||||
impl StatusNotifierWatcher {
|
||||
async fn register_status_notifier_item(
|
||||
&mut self,
|
||||
service: &str,
|
||||
#[zbus(header)] hdr: MessageHeader<'_>,
|
||||
#[zbus(signal_context)] ctxt: SignalContext<'_>,
|
||||
) {
|
||||
let sender = hdr.sender().unwrap().unwrap();
|
||||
let service = if service.starts_with('/') {
|
||||
format!("{}{}", sender, service)
|
||||
} else {
|
||||
service.to_string()
|
||||
};
|
||||
Self::status_notifier_item_registered(&ctxt, &service)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
self.items.push((sender.to_owned(), service));
|
||||
}
|
||||
|
||||
fn register_status_notifier_host(&self, _service: &str) {
|
||||
// XXX emit registed/unregistered
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
fn registered_status_notifier_items(&self) -> Vec<String> {
|
||||
self.items.iter().map(|(_, x)| x.clone()).collect()
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
fn is_status_notifier_host_registered(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[dbus_interface(property)]
|
||||
fn protocol_version(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_item_registered(ctxt: &SignalContext<'_>, service: &str)
|
||||
-> Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_item_unregistered(
|
||||
ctxt: &SignalContext<'_>,
|
||||
service: &str,
|
||||
) -> Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_host_registered(ctxt: &SignalContext<'_>) -> Result<()>;
|
||||
|
||||
#[dbus_interface(signal)]
|
||||
async fn status_notifier_host_unregistered(ctxt: &SignalContext<'_>) -> Result<()>;
|
||||
}
|
||||
|
||||
pub async fn create_service(connection: &zbus::Connection) -> zbus::Result<()> {
|
||||
connection
|
||||
.object_server()
|
||||
.at(OBJECT_PATH, StatusNotifierWatcher::default())
|
||||
.await?;
|
||||
let interface = connection
|
||||
.object_server()
|
||||
.interface::<_, StatusNotifierWatcher>(OBJECT_PATH)
|
||||
.await
|
||||
.unwrap();
|
||||
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(NAME.as_ref(), flags).await? {
|
||||
RequestNameReply::InQueue => {
|
||||
eprintln!("Bus name '{}' already owned", NAME);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let connection = connection.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut have_bus_name = 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() == NAME {
|
||||
if args.new_owner.as_ref() == unique_name.as_ref() {
|
||||
eprintln!("Acquired bus name: {}", NAME);
|
||||
have_bus_name = true;
|
||||
} else if have_bus_name {
|
||||
eprintln!("Lost bus name: {}", NAME);
|
||||
have_bus_name = false;
|
||||
}
|
||||
} else if let BusName::Unique(name) = &args.name {
|
||||
let mut interface = interface.get_mut().await;
|
||||
if let Some(idx) = interface
|
||||
.items
|
||||
.iter()
|
||||
.position(|(unique_name, _)| unique_name == name)
|
||||
{
|
||||
let ctxt = zbus::SignalContext::new(&connection, OBJECT_PATH).unwrap();
|
||||
let service = interface.items.remove(idx).1;
|
||||
StatusNotifierWatcher::status_notifier_item_unregistered(&ctxt, &service)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
3
justfile
3
justfile
|
|
@ -45,6 +45,7 @@ _install_notifications: (_install 'com.system76.CosmicAppletNotifications' 'cosm
|
|||
_install_power: (_install 'com.system76.CosmicAppletPower' 'cosmic-applet-power')
|
||||
_install_workspace: (_install 'com.system76.CosmicAppletWorkspaces' 'cosmic-applet-workspaces')
|
||||
_install_time: (_install 'com.system76.CosmicAppletTime' 'cosmic-applet-time')
|
||||
_install_status_area: (_install 'com.system76.CosmicAppletStatusArea' 'cosmic-applet-status-area')
|
||||
|
||||
# TODO: Turn this into one configurable applet?
|
||||
_install_panel_button: (_install_bin 'cosmic-panel-button')
|
||||
|
|
@ -53,7 +54,7 @@ _install_app_button: (_install_button 'com.system76.CosmicPanelAppButton' 'cosmi
|
|||
_install_workspaces_button: (_install_button 'com.system76.CosmicPanelWorkspacesButton' 'cosmic-panel-workspaces-button')
|
||||
|
||||
# Installs files into the system
|
||||
install: _install_app_list _install_audio _install_battery _install_bluetooth _install_graphics _install_network _install_notifications _install_power _install_workspace _install_time _install_panel_button _install_app_button _install_workspaces_button
|
||||
install: _install_app_list _install_audio _install_battery _install_bluetooth _install_graphics _install_network _install_notifications _install_power _install_workspace _install_time _install_panel_button _install_app_button _install_workspaces_button _install_status_area
|
||||
|
||||
# Extracts vendored dependencies if vendor=1
|
||||
_extract_vendor:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue