Skip to content

Commit 6a64486

Browse files
committed
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.
1 parent 29a2dea commit 6a64486

File tree

16 files changed

+1313
-1
lines changed

16 files changed

+1313
-1
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ members = [
99
"cosmic-applet-network",
1010
"cosmic-applet-notifications",
1111
"cosmic-applet-power",
12+
"cosmic-applet-status-area",
1213
"cosmic-applet-time",
1314
"cosmic-applet-workspaces",
1415
"cosmic-panel-button",
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
use cascade::cascade;
2+
use futures::StreamExt;
3+
use gtk4::{
4+
gdk_pixbuf,
5+
glib::{self, clone},
6+
prelude::*,
7+
subclass::prelude::*,
8+
};
9+
use std::{cell::RefCell, collections::HashMap, io};
10+
use zbus::dbus_proxy;
11+
use zvariant::OwnedValue;
12+
13+
use crate::deref_cell::DerefCell;
14+
15+
struct Menu {
16+
box_: gtk4::Box,
17+
children: Vec<i32>,
18+
}
19+
20+
#[derive(Default)]
21+
pub struct StatusMenuInner {
22+
menu_button: DerefCell<libcosmic_applet::AppletButton>,
23+
vbox: DerefCell<gtk4::Box>,
24+
item: DerefCell<StatusNotifierItemProxy<'static>>,
25+
dbus_menu: DerefCell<DBusMenuProxy<'static>>,
26+
menus: RefCell<HashMap<i32, Menu>>,
27+
}
28+
29+
#[glib::object_subclass]
30+
impl ObjectSubclass for StatusMenuInner {
31+
const NAME: &'static str = "S76StatusMenu";
32+
type ParentType = gtk4::Widget;
33+
type Type = StatusMenu;
34+
35+
fn class_init(klass: &mut Self::Class) {
36+
klass.set_layout_manager_type::<gtk4::BinLayout>();
37+
}
38+
}
39+
40+
impl ObjectImpl for StatusMenuInner {
41+
fn constructed(&self, obj: &StatusMenu) {
42+
let vbox = cascade! {
43+
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
44+
};
45+
46+
let menu_button = cascade! {
47+
libcosmic_applet::AppletButton::new();
48+
..set_parent(obj);
49+
..set_popover_child(Some(&vbox));
50+
};
51+
52+
self.menu_button.set(menu_button);
53+
self.vbox.set(vbox);
54+
}
55+
56+
fn dispose(&self, _obj: &StatusMenu) {
57+
self.menu_button.unparent();
58+
}
59+
}
60+
61+
impl WidgetImpl for StatusMenuInner {}
62+
63+
glib::wrapper! {
64+
pub struct StatusMenu(ObjectSubclass<StatusMenuInner>)
65+
@extends gtk4::Widget;
66+
}
67+
68+
impl StatusMenu {
69+
pub async fn new(name: &str) -> zbus::Result<Self> {
70+
let (dest, path) = if let Some(idx) = name.find('/') {
71+
(&name[..idx], &name[idx..])
72+
} else {
73+
(name, "/StatusNotifierItem")
74+
};
75+
76+
let connection = zbus::Connection::session().await?;
77+
let item = StatusNotifierItemProxy::builder(&connection)
78+
.destination(dest.to_string())?
79+
.path(path.to_string())?
80+
.build()
81+
.await?;
82+
let obj = glib::Object::new::<Self>(&[]).unwrap();
83+
let icon_name = item.icon_name().await?;
84+
obj.inner().menu_button.set_button_icon_name(&icon_name);
85+
86+
let menu = item.menu().await?;
87+
let menu = DBusMenuProxy::builder(&connection)
88+
.destination(dest.to_string())?
89+
.path(menu)?
90+
.build()
91+
.await?;
92+
let layout = menu.get_layout(0, -1, &[]).await?.1;
93+
94+
let mut layout_updated_stream = menu.receive_layout_updated().await?;
95+
glib::MainContext::default().spawn_local(clone!(@strong obj => async move {
96+
while let Some(evt) = layout_updated_stream.next().await {
97+
let args = match evt.args() {
98+
Ok(args) => args,
99+
Err(_) => { continue; },
100+
};
101+
obj.layout_updated(args.revision, args.parent);
102+
}
103+
}));
104+
105+
obj.inner().item.set(item);
106+
obj.inner().dbus_menu.set(menu);
107+
108+
println!("{:#?}", layout);
109+
obj.populate_menu(&obj.inner().vbox, &layout);
110+
111+
Ok(obj)
112+
}
113+
114+
fn inner(&self) -> &StatusMenuInner {
115+
StatusMenuInner::from_instance(self)
116+
}
117+
118+
fn layout_updated(&self, _revision: u32, parent: i32) {
119+
let mut menus = self.inner().menus.borrow_mut();
120+
121+
if let Some(Menu { box_, children }) = menus.remove(&parent) {
122+
let mut next_child = box_.first_child();
123+
while let Some(child) = next_child {
124+
next_child = child.next_sibling();
125+
box_.remove(&child);
126+
}
127+
128+
fn remove_child_menus(menus: &mut HashMap<i32, Menu>, children: Vec<i32>) {
129+
for i in children {
130+
if let Some(menu) = menus.remove(&i) {
131+
remove_child_menus(menus, menu.children);
132+
}
133+
}
134+
}
135+
remove_child_menus(&mut menus, children);
136+
137+
glib::MainContext::default().spawn_local(clone!(@weak self as self_ => async move {
138+
match self_.inner().dbus_menu.get_layout(parent, -1, &[]).await {
139+
Ok((_, layout)) => self_.populate_menu(&box_, &layout),
140+
Err(err) => eprintln!("Failed to call 'GetLayout': {}", err),
141+
}
142+
}));
143+
}
144+
}
145+
146+
fn populate_menu(&self, box_: &gtk4::Box, layout: &Layout) {
147+
let mut children = Vec::new();
148+
149+
for i in layout.children() {
150+
children.push(i.id());
151+
152+
if i.type_().as_deref() == Some("separator") {
153+
let separator = cascade! {
154+
gtk4::Separator::new(gtk4::Orientation::Horizontal);
155+
..set_visible(i.visible());
156+
};
157+
box_.append(&separator);
158+
} else if let Some(label) = i.label() {
159+
let mut label = label.to_string();
160+
if let Some(toggle_state) = i.toggle_state() {
161+
if toggle_state != 0 {
162+
label = format!("✓ {}", label);
163+
}
164+
}
165+
166+
let label_widget = cascade! {
167+
gtk4::Label::new(Some(&label));
168+
..set_halign(gtk4::Align::Start);
169+
..set_hexpand(true);
170+
..set_use_underline(true);
171+
};
172+
173+
let hbox = cascade! {
174+
gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
175+
..append(&label_widget);
176+
};
177+
178+
if let Some(icon_data) = i.icon_data() {
179+
let icon_data = io::Cursor::new(icon_data.to_vec());
180+
let pixbuf = gdk_pixbuf::Pixbuf::from_read(icon_data).unwrap(); // XXX unwrap
181+
let image = cascade! {
182+
gtk4::Image::from_pixbuf(Some(&pixbuf));
183+
..set_halign(gtk4::Align::End);
184+
};
185+
hbox.append(&image);
186+
}
187+
188+
let id = i.id();
189+
let close_on_click = i.children_display().as_deref() != Some("submenu");
190+
let button = cascade! {
191+
gtk4::Button::new();
192+
..set_child(Some(&hbox));
193+
..style_context().add_class("flat");
194+
..set_visible(i.visible());
195+
..set_sensitive(i.enabled());
196+
..connect_clicked(clone!(@weak self as self_ => move |_| {
197+
// XXX data, timestamp
198+
if close_on_click {
199+
self_.inner().menu_button.popdown();
200+
}
201+
glib::MainContext::default().spawn_local(clone!(@strong self_ => async move {
202+
let _ = self_.inner().dbus_menu.event(id, "clicked", &0.into(), 0).await;
203+
}));
204+
}));
205+
};
206+
box_.append(&button);
207+
208+
if i.children_display().as_deref() == Some("submenu") {
209+
let vbox = cascade! {
210+
gtk4::Box::new(gtk4::Orientation::Vertical, 0);
211+
};
212+
213+
let revealer = cascade! {
214+
gtk4::Revealer::new();
215+
..set_child(Some(&vbox));
216+
};
217+
218+
self.populate_menu(&vbox, &i);
219+
220+
box_.append(&revealer);
221+
222+
button.connect_clicked(move |_| {
223+
revealer.set_reveal_child(!revealer.reveals_child());
224+
});
225+
}
226+
}
227+
}
228+
229+
self.inner().menus.borrow_mut().insert(
230+
layout.id(),
231+
Menu {
232+
box_: box_.clone(),
233+
children,
234+
},
235+
);
236+
}
237+
}
238+
239+
#[dbus_proxy(interface = "org.kde.StatusNotifierItem")]
240+
trait StatusNotifierItem {
241+
#[dbus_proxy(property)]
242+
fn icon_name(&self) -> zbus::Result<String>;
243+
244+
#[dbus_proxy(property)]
245+
fn menu(&self) -> zbus::Result<zvariant::OwnedObjectPath>;
246+
}
247+
248+
#[derive(Debug)]
249+
pub struct Layout(i32, LayoutProps, Vec<Layout>);
250+
251+
impl<'a> serde::Deserialize<'a> for Layout {
252+
fn deserialize<D: serde::Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
253+
let (id, props, children) =
254+
<(i32, LayoutProps, Vec<(zvariant::Signature<'_>, Self)>)>::deserialize(deserializer)?;
255+
Ok(Self(id, props, children.into_iter().map(|x| x.1).collect()))
256+
}
257+
}
258+
259+
impl zvariant::Type for Layout {
260+
fn signature() -> zvariant::Signature<'static> {
261+
zvariant::Signature::try_from("(ia{sv}av)").unwrap()
262+
}
263+
}
264+
265+
#[derive(Debug, zvariant::DeserializeDict, zvariant::Type)]
266+
pub struct LayoutProps {
267+
#[zvariant(rename = "accessible-desc")]
268+
accessible_desc: Option<String>,
269+
#[zvariant(rename = "children-display")]
270+
children_display: Option<String>,
271+
label: Option<String>,
272+
enabled: Option<bool>,
273+
visible: Option<bool>,
274+
#[zvariant(rename = "type")]
275+
type_: Option<String>,
276+
#[zvariant(rename = "toggle-type")]
277+
toggle_type: Option<String>,
278+
#[zvariant(rename = "toggle-state")]
279+
toggle_state: Option<i32>,
280+
#[zvariant(rename = "icon-data")]
281+
icon_data: Option<Vec<u8>>,
282+
}
283+
284+
#[allow(dead_code)]
285+
impl Layout {
286+
fn id(&self) -> i32 {
287+
self.0
288+
}
289+
290+
fn children(&self) -> &[Self] {
291+
&self.2
292+
}
293+
294+
fn accessible_desc(&self) -> Option<&str> {
295+
self.1.accessible_desc.as_deref()
296+
}
297+
298+
fn children_display(&self) -> Option<&str> {
299+
self.1.children_display.as_deref()
300+
}
301+
302+
fn label(&self) -> Option<&str> {
303+
self.1.label.as_deref()
304+
}
305+
306+
fn enabled(&self) -> bool {
307+
self.1.enabled.unwrap_or(true)
308+
}
309+
310+
fn visible(&self) -> bool {
311+
self.1.visible.unwrap_or(true)
312+
}
313+
314+
fn type_(&self) -> Option<&str> {
315+
self.1.type_.as_deref()
316+
}
317+
318+
fn toggle_type(&self) -> Option<&str> {
319+
self.1.toggle_type.as_deref()
320+
}
321+
322+
fn toggle_state(&self) -> Option<i32> {
323+
self.1.toggle_state
324+
}
325+
326+
fn icon_data(&self) -> Option<&[u8]> {
327+
self.1.icon_data.as_deref()
328+
}
329+
}
330+
331+
#[dbus_proxy(interface = "com.canonical.dbusmenu")]
332+
trait DBusMenu {
333+
fn get_layout(
334+
&self,
335+
parent_id: i32,
336+
recursion_depth: i32,
337+
property_names: &[&str],
338+
) -> zbus::Result<(u32, Layout)>;
339+
340+
fn event(&self, id: i32, event_id: &str, data: &OwnedValue, timestamp: u32)
341+
-> zbus::Result<()>;
342+
343+
#[dbus_proxy(signal)]
344+
fn layout_updated(&self, revision: u32, parent: i32) -> zbus::Result<()>;
345+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "cosmic-applet-status-area"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "GPL-3.0-or-later"
6+
7+
[dependencies]
8+
futures = "0.3"
9+
libcosmic.workspace = true
10+
serde = "1"
11+
tokio = { version = "1.23.0" }
12+
zbus = { version = "3", default-features = false, features = ["tokio"] }
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[Desktop Entry]
2+
Name=Cosmic Applet Status Area
3+
Comment=Applet for Cosmic Panel
4+
Type=Application
5+
Exec=cosmic-applet-status-area
6+
Terminal=false
7+
Categories=GNOME;GTK;
8+
Keywords=Gnome;GTK;
9+
# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
10+
Icon=com.system76.CosmicAppletStatusArea
11+
StartupNotify=true
12+
NoDisplay=true
13+
X-CosmicApplet=true

0 commit comments

Comments
 (0)