Merge pull request #33 from pop-os/iced-status-area_jammy
Iced port of status area applet
This commit is contained in:
commit
4678e95e9e
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