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
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])
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue