2024-02-01 21:43:20 +00:00
|
|
|
mod diff;
|
|
|
|
mod icon;
|
|
|
|
mod interface;
|
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
use crate::clients::tray;
|
2024-02-01 21:43:20 +00:00
|
|
|
use crate::config::CommonConfig;
|
|
|
|
use crate::modules::tray::diff::get_diffs;
|
|
|
|
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
2024-03-29 00:23:44 +00:00
|
|
|
use crate::{glib_recv, lock, send_async, spawn};
|
|
|
|
use color_eyre::{Report, Result};
|
2024-02-12 23:57:27 +01:00
|
|
|
use gtk::{prelude::*, PackDirection};
|
2024-02-01 21:43:20 +00:00
|
|
|
use gtk::{IconTheme, MenuBar};
|
|
|
|
use interface::TrayMenu;
|
|
|
|
use serde::Deserialize;
|
|
|
|
use std::collections::HashMap;
|
2024-03-29 00:23:44 +00:00
|
|
|
use system_tray::client::Event;
|
|
|
|
use system_tray::client::{ActivateRequest, UpdateEvent};
|
2024-02-01 21:43:20 +00:00
|
|
|
use tokio::sync::mpsc;
|
2024-03-29 00:23:44 +00:00
|
|
|
use tracing::{debug, error, warn};
|
2024-02-01 21:43:20 +00:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
pub struct TrayModule {
|
2024-02-25 17:13:28 +00:00
|
|
|
#[serde(default = "default_icon_size")]
|
|
|
|
icon_size: u32,
|
|
|
|
|
2024-02-12 23:57:27 +01:00
|
|
|
#[serde(default, deserialize_with = "deserialize_orientation")]
|
2024-02-25 17:13:28 +00:00
|
|
|
direction: Option<PackDirection>,
|
|
|
|
|
2024-02-01 21:43:20 +00:00
|
|
|
#[serde(flatten)]
|
|
|
|
pub common: Option<CommonConfig>,
|
|
|
|
}
|
|
|
|
|
2024-02-25 17:13:28 +00:00
|
|
|
const fn default_icon_size() -> u32 {
|
|
|
|
16
|
|
|
|
}
|
|
|
|
|
2024-02-12 23:57:27 +01:00
|
|
|
fn deserialize_orientation<'de, D>(deserializer: D) -> Result<Option<PackDirection>, D::Error>
|
|
|
|
where
|
|
|
|
D: serde::Deserializer<'de>,
|
|
|
|
{
|
|
|
|
let value = Option::<String>::deserialize(deserializer)?;
|
|
|
|
value
|
|
|
|
.map(|v| match v.as_str() {
|
|
|
|
"left_to_right" => Ok(PackDirection::Ltr),
|
|
|
|
"right_to_left" => Ok(PackDirection::Rtl),
|
|
|
|
"top_to_bottom" => Ok(PackDirection::Ttb),
|
|
|
|
"bottom_to_top" => Ok(PackDirection::Btt),
|
|
|
|
_ => Err(serde::de::Error::custom("invalid value for orientation")),
|
|
|
|
})
|
|
|
|
.transpose()
|
|
|
|
}
|
|
|
|
|
2024-02-01 21:43:20 +00:00
|
|
|
impl Module<MenuBar> for TrayModule {
|
2024-03-29 00:23:44 +00:00
|
|
|
type SendMessage = Event;
|
|
|
|
type ReceiveMessage = ActivateRequest;
|
2024-02-01 21:43:20 +00:00
|
|
|
|
|
|
|
fn name() -> &'static str {
|
|
|
|
"tray"
|
|
|
|
}
|
|
|
|
|
|
|
|
fn spawn_controller(
|
|
|
|
&self,
|
|
|
|
_info: &ModuleInfo,
|
|
|
|
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
|
|
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
|
|
|
) -> Result<()> {
|
|
|
|
let tx = context.tx.clone();
|
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
let client = context.client::<tray::Client>();
|
|
|
|
let mut tray_rx = client.subscribe();
|
2024-02-01 21:43:20 +00:00
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
let initial_items = lock!(client.items()).clone();
|
2024-02-01 21:43:20 +00:00
|
|
|
|
|
|
|
// listen to tray updates
|
|
|
|
spawn(async move {
|
2024-03-29 00:23:44 +00:00
|
|
|
for (key, (item, menu)) in initial_items.into_iter() {
|
|
|
|
send_async!(
|
|
|
|
tx,
|
|
|
|
ModuleUpdateEvent::Update(Event::Add(key.clone(), item.into()))
|
|
|
|
);
|
|
|
|
|
|
|
|
if let Some(menu) = menu.clone() {
|
|
|
|
send_async!(
|
|
|
|
tx,
|
|
|
|
ModuleUpdateEvent::Update(Event::Update(key, UpdateEvent::Menu(menu)))
|
|
|
|
);
|
|
|
|
}
|
2024-02-01 21:43:20 +00:00
|
|
|
}
|
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
while let Ok(message) = tray_rx.recv().await {
|
|
|
|
send_async!(tx, ModuleUpdateEvent::Update(message))
|
|
|
|
}
|
2024-02-01 21:43:20 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// send tray commands
|
|
|
|
spawn(async move {
|
|
|
|
while let Some(cmd) = rx.recv().await {
|
2024-03-29 00:23:44 +00:00
|
|
|
client.activate(cmd).await?;
|
2024-02-01 21:43:20 +00:00
|
|
|
}
|
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
Ok::<_, Report>(())
|
2024-02-01 21:43:20 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn into_widget(
|
|
|
|
self,
|
|
|
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
|
|
info: &ModuleInfo,
|
|
|
|
) -> Result<ModuleParts<MenuBar>> {
|
|
|
|
let container = MenuBar::new();
|
|
|
|
|
2024-02-12 23:57:27 +01:00
|
|
|
let direction = self.direction.unwrap_or(
|
2024-02-18 14:54:17 +00:00
|
|
|
if info.bar_position.orientation() == gtk::Orientation::Vertical {
|
2024-02-12 23:57:27 +01:00
|
|
|
PackDirection::Ttb
|
|
|
|
} else {
|
|
|
|
PackDirection::Ltr
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
container.set_pack_direction(direction);
|
|
|
|
container.set_child_pack_direction(direction);
|
|
|
|
|
2024-02-01 21:43:20 +00:00
|
|
|
{
|
|
|
|
let container = container.clone();
|
|
|
|
let mut menus = HashMap::new();
|
|
|
|
let icon_theme = info.icon_theme.clone();
|
|
|
|
|
|
|
|
// listen for UI updates
|
|
|
|
glib_recv!(context.subscribe(), update =>
|
2024-03-29 00:23:44 +00:00
|
|
|
on_update(update, &container, &mut menus, &icon_theme, self.icon_size, self.prefer_theme_icons, &context.controller_tx)
|
2024-02-01 21:43:20 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(ModuleParts {
|
|
|
|
widget: container,
|
|
|
|
popup: None,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Handles UI updates as callback,
|
|
|
|
/// getting the diff since the previous update and applying it to the menu.
|
|
|
|
fn on_update(
|
2024-03-29 00:23:44 +00:00
|
|
|
update: Event,
|
2024-02-01 21:43:20 +00:00
|
|
|
container: &MenuBar,
|
|
|
|
menus: &mut HashMap<Box<str>, TrayMenu>,
|
|
|
|
icon_theme: &IconTheme,
|
2024-02-25 17:13:28 +00:00
|
|
|
icon_size: u32,
|
2024-03-29 00:23:44 +00:00
|
|
|
prefer_icons: bool,
|
|
|
|
tx: &mpsc::Sender<ActivateRequest>,
|
2024-02-01 21:43:20 +00:00
|
|
|
) {
|
|
|
|
match update {
|
2024-03-29 00:23:44 +00:00
|
|
|
Event::Add(address, item) => {
|
|
|
|
debug!("Received new tray item at '{address}': {item:?}");
|
|
|
|
|
|
|
|
let mut menu_item = TrayMenu::new(tx.clone(), address.clone(), *item);
|
|
|
|
container.add(&menu_item.widget);
|
2024-02-01 21:43:20 +00:00
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
match icon::get_image(&menu_item, icon_theme, icon_size, prefer_icons) {
|
|
|
|
Ok(image) => menu_item.set_image(&image),
|
|
|
|
Err(_) => {
|
|
|
|
let label = menu_item.title.clone().unwrap_or(address.clone());
|
|
|
|
menu_item.set_label(&label)
|
2024-02-01 21:43:20 +00:00
|
|
|
}
|
2024-03-29 00:23:44 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
menu_item.widget.show();
|
|
|
|
menus.insert(address.into(), menu_item);
|
|
|
|
}
|
|
|
|
Event::Update(address, update) => {
|
|
|
|
debug!("Received tray update for '{address}': {update:?}");
|
2024-02-01 21:43:20 +00:00
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
let Some(menu_item) = menus.get_mut(address.as_str()) else {
|
|
|
|
error!("Attempted to update menu at '{address}' but could not find it");
|
|
|
|
return;
|
|
|
|
};
|
2024-02-01 21:43:20 +00:00
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
match update {
|
|
|
|
UpdateEvent::AttentionIcon(_icon) => {
|
|
|
|
warn!("received unimplemented NewAttentionIcon event");
|
|
|
|
}
|
|
|
|
UpdateEvent::Icon(icon) => {
|
|
|
|
if icon.as_ref() != menu_item.icon_name() {
|
|
|
|
match icon::get_image(menu_item, icon_theme, icon_size, prefer_icons) {
|
|
|
|
Ok(image) => menu_item.set_image(&image),
|
|
|
|
Err(_) => menu_item.show_label(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
menu_item.set_icon_name(icon);
|
|
|
|
}
|
|
|
|
UpdateEvent::OverlayIcon(_icon) => {
|
|
|
|
warn!("received unimplemented NewOverlayIcon event");
|
|
|
|
}
|
|
|
|
UpdateEvent::Status(_status) => {
|
|
|
|
warn!("received unimplemented NewStatus event");
|
|
|
|
}
|
|
|
|
UpdateEvent::Title(title) => {
|
|
|
|
if let Some(label_widget) = menu_item.label_widget() {
|
|
|
|
label_widget.set_label(&title.unwrap_or_default());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// UpdateEvent::Tooltip(_tooltip) => {
|
|
|
|
// warn!("received unimplemented NewAttentionIcon event");
|
|
|
|
// }
|
|
|
|
UpdateEvent::Menu(menu) => {
|
|
|
|
debug!("received new menu for '{}'", address);
|
|
|
|
|
|
|
|
let diffs = get_diffs(menu_item.state(), &menu.submenus);
|
2024-02-01 21:43:20 +00:00
|
|
|
|
2024-03-29 00:23:44 +00:00
|
|
|
menu_item.apply_diffs(diffs);
|
|
|
|
menu_item.set_state(menu.submenus);
|
|
|
|
}
|
2024-02-01 21:43:20 +00:00
|
|
|
}
|
|
|
|
}
|
2024-03-29 00:23:44 +00:00
|
|
|
Event::Remove(address) => {
|
|
|
|
debug!("Removing tray item at '{address}'");
|
|
|
|
|
2024-02-01 21:43:20 +00:00
|
|
|
if let Some(menu) = menus.get(address.as_str()) {
|
|
|
|
container.remove(&menu.widget);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|