Skip to content

Commit 4678e95

Browse files
authored
Merge pull request #33 from pop-os/iced-status-area_jammy
Iced port of status area applet
2 parents 29a2dea + 6a64486 commit 4678e95

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)