2022-11-06 23:38:51 +00:00
|
|
|
use crate::clients::system_tray::get_tray_event_client;
|
2022-11-28 21:55:08 +00:00
|
|
|
use crate::config::CommonConfig;
|
2022-09-25 22:49:00 +01:00
|
|
|
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
|
2022-12-11 22:45:52 +00:00
|
|
|
use crate::{await_sync, try_send};
|
2022-08-21 23:36:07 +01:00
|
|
|
use color_eyre::Result;
|
2023-04-21 23:02:02 +01:00
|
|
|
use gtk::gdk_pixbuf::{Colorspace, InterpType};
|
2022-08-14 14:30:13 +01:00
|
|
|
use gtk::prelude::*;
|
2023-04-21 23:02:02 +01:00
|
|
|
use gtk::{
|
|
|
|
gdk_pixbuf, IconLookupFlags, IconTheme, Image, Label, Menu, MenuBar, MenuItem,
|
|
|
|
SeparatorMenuItem,
|
|
|
|
};
|
2022-08-14 14:30:13 +01:00
|
|
|
use serde::Deserialize;
|
|
|
|
use std::collections::HashMap;
|
2022-09-25 22:49:00 +01:00
|
|
|
use stray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
2022-08-14 14:30:13 +01:00
|
|
|
use stray::message::tray::StatusNotifierItem;
|
|
|
|
use stray::message::{NotifierItemCommand, NotifierItemMessage};
|
|
|
|
use tokio::spawn;
|
|
|
|
use tokio::sync::mpsc;
|
2022-09-25 22:49:00 +01:00
|
|
|
use tokio::sync::mpsc::{Receiver, Sender};
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
2022-11-28 21:55:08 +00:00
|
|
|
pub struct TrayModule {
|
|
|
|
#[serde(flatten)]
|
2022-12-04 23:23:22 +00:00
|
|
|
pub common: Option<CommonConfig>,
|
2022-11-28 21:55:08 +00:00
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2023-04-21 23:02:02 +01:00
|
|
|
/// Attempts to get a GTK `Image` component
|
2022-08-14 14:30:13 +01:00
|
|
|
/// for the status notifier item's icon.
|
2023-04-21 23:02:02 +01:00
|
|
|
fn get_image_from_icon_name(item: &StatusNotifierItem) -> Option<Image> {
|
2023-02-03 09:40:53 +01:00
|
|
|
let theme = item
|
|
|
|
.icon_theme_path
|
|
|
|
.as_ref()
|
|
|
|
.map(|path| {
|
|
|
|
let theme = IconTheme::new();
|
|
|
|
theme.append_search_path(path);
|
|
|
|
theme
|
2022-08-21 23:36:07 +01:00
|
|
|
})
|
2023-02-03 09:40:53 +01:00
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
item.icon_name.as_ref().and_then(|icon_name| {
|
|
|
|
let icon_info = theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
|
|
|
|
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
|
2022-08-14 14:30:13 +01:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-04-21 23:02:02 +01:00
|
|
|
/// Attempts to get an image from the item pixmap.
|
|
|
|
///
|
|
|
|
/// The pixmap is supplied in ARGB32 format,
|
|
|
|
/// which has 8 bits per sample and a bit stride of `4*width`.
|
|
|
|
fn get_image_from_pixmap(item: &StatusNotifierItem) -> Option<Image> {
|
|
|
|
const BITS_PER_SAMPLE: i32 = 8; //
|
|
|
|
|
|
|
|
let pixmap = item
|
|
|
|
.icon_pixmap
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|pixmap| pixmap.first())?;
|
|
|
|
|
|
|
|
let bytes = glib::Bytes::from(&pixmap.pixels);
|
|
|
|
let row_stride = pixmap.width * 4; //
|
|
|
|
|
|
|
|
let pixbuf = gdk_pixbuf::Pixbuf::from_bytes(
|
|
|
|
&bytes,
|
|
|
|
Colorspace::Rgb,
|
|
|
|
true,
|
|
|
|
BITS_PER_SAMPLE,
|
|
|
|
pixmap.width,
|
|
|
|
pixmap.height,
|
|
|
|
row_stride,
|
|
|
|
);
|
|
|
|
|
|
|
|
let pixbuf = pixbuf
|
|
|
|
.scale_simple(16, 16, InterpType::Bilinear)
|
|
|
|
.unwrap_or(pixbuf);
|
|
|
|
Some(Image::from_pixbuf(Some(&pixbuf)))
|
|
|
|
}
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
/// Recursively gets GTK `MenuItem` components
|
|
|
|
/// for the provided submenu array.
|
|
|
|
fn get_menu_items(
|
|
|
|
menu: &[MenuItemInfo],
|
2022-09-25 22:49:00 +01:00
|
|
|
tx: &Sender<NotifierItemCommand>,
|
2022-08-21 23:36:07 +01:00
|
|
|
id: &str,
|
|
|
|
path: &str,
|
2022-08-14 14:30:13 +01:00
|
|
|
) -> Vec<MenuItem> {
|
|
|
|
menu.iter()
|
|
|
|
.map(|item_info| {
|
|
|
|
let item: Box<dyn AsRef<MenuItem>> = match item_info.menu_type {
|
|
|
|
MenuType::Separator => Box::new(SeparatorMenuItem::new()),
|
|
|
|
MenuType::Standard => {
|
|
|
|
let mut builder = MenuItem::builder()
|
|
|
|
.label(item_info.label.as_str())
|
|
|
|
.visible(item_info.visible)
|
|
|
|
.sensitive(item_info.enabled);
|
|
|
|
|
|
|
|
if !item_info.submenu.is_empty() {
|
|
|
|
let menu = Menu::new();
|
2022-08-21 23:36:07 +01:00
|
|
|
get_menu_items(&item_info.submenu, &tx.clone(), id, path)
|
2022-08-14 14:30:13 +01:00
|
|
|
.iter()
|
|
|
|
.for_each(|item| menu.add(item));
|
|
|
|
|
|
|
|
builder = builder.submenu(&menu);
|
|
|
|
}
|
|
|
|
|
|
|
|
let item = builder.build();
|
|
|
|
|
|
|
|
let info = item_info.clone();
|
2022-08-21 23:36:07 +01:00
|
|
|
let id = id.to_string();
|
|
|
|
let path = path.to_string();
|
2022-08-14 14:30:13 +01:00
|
|
|
|
|
|
|
{
|
|
|
|
let tx = tx.clone();
|
|
|
|
item.connect_activate(move |_item| {
|
2022-12-11 22:45:52 +00:00
|
|
|
try_send!(
|
|
|
|
tx,
|
|
|
|
NotifierItemCommand::MenuItemClicked {
|
|
|
|
submenu_id: info.id,
|
|
|
|
menu_path: path.clone(),
|
|
|
|
notifier_address: id.clone(),
|
|
|
|
}
|
|
|
|
);
|
2022-08-14 14:30:13 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Box::new(item)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
(*item).as_ref().clone()
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Module<MenuBar> for TrayModule {
|
2022-09-25 22:49:00 +01:00
|
|
|
type SendMessage = NotifierItemMessage;
|
|
|
|
type ReceiveMessage = NotifierItemCommand;
|
|
|
|
|
2022-12-04 23:23:22 +00:00
|
|
|
fn name() -> &'static str {
|
|
|
|
"tray"
|
|
|
|
}
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
fn spawn_controller(
|
|
|
|
&self,
|
|
|
|
_info: &ModuleInfo,
|
|
|
|
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
|
|
|
mut rx: Receiver<Self::ReceiveMessage>,
|
|
|
|
) -> Result<()> {
|
|
|
|
let client = await_sync(async { get_tray_event_client().await });
|
|
|
|
let (tray_tx, mut tray_rx) = client.subscribe();
|
|
|
|
|
|
|
|
// listen to tray updates
|
|
|
|
spawn(async move {
|
|
|
|
while let Ok(message) = tray_rx.recv().await {
|
|
|
|
tx.send(ModuleUpdateEvent::Update(message)).await?;
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
|
|
|
});
|
2022-08-14 14:30:13 +01:00
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
// send tray commands
|
2022-08-14 14:30:13 +01:00
|
|
|
spawn(async move {
|
2022-09-25 22:49:00 +01:00
|
|
|
while let Some(cmd) = rx.recv().await {
|
|
|
|
tray_tx.send(cmd).await?;
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
2022-09-25 22:49:00 +01:00
|
|
|
|
|
|
|
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
|
2022-08-14 14:30:13 +01:00
|
|
|
});
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn into_widget(
|
|
|
|
self,
|
|
|
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
|
|
_info: &ModuleInfo,
|
|
|
|
) -> Result<ModuleWidget<MenuBar>> {
|
|
|
|
let container = MenuBar::new();
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
{
|
|
|
|
let container = container.clone();
|
|
|
|
let mut widgets = HashMap::new();
|
|
|
|
|
|
|
|
// listen for UI updates
|
2022-09-25 22:49:00 +01:00
|
|
|
context.widget_rx.attach(None, move |update| {
|
2022-08-14 14:30:13 +01:00
|
|
|
match update {
|
2022-09-25 22:49:00 +01:00
|
|
|
NotifierItemMessage::Update {
|
|
|
|
item,
|
|
|
|
address,
|
|
|
|
menu,
|
|
|
|
} => {
|
2023-04-16 21:37:47 +01:00
|
|
|
let addr = &address;
|
2022-09-25 22:49:00 +01:00
|
|
|
let menu_item = widgets.remove(address.as_str()).unwrap_or_else(|| {
|
2022-08-14 14:30:13 +01:00
|
|
|
let menu_item = MenuItem::new();
|
|
|
|
menu_item.style_context().add_class("item");
|
2023-04-16 21:37:47 +01:00
|
|
|
|
2023-04-21 23:02:02 +01:00
|
|
|
get_image_from_icon_name(&item)
|
|
|
|
.or_else(|| get_image_from_pixmap(&item))
|
|
|
|
.map_or_else(
|
|
|
|
|| {
|
|
|
|
let label =
|
|
|
|
Label::new(Some(item.title.as_ref().unwrap_or(addr)));
|
|
|
|
menu_item.add(&label);
|
|
|
|
},
|
|
|
|
|image| {
|
|
|
|
image.set_widget_name(address.as_str());
|
|
|
|
menu_item.add(&image);
|
|
|
|
},
|
2023-04-16 21:37:47 +01:00
|
|
|
);
|
|
|
|
|
2022-08-14 14:30:13 +01:00
|
|
|
container.add(&menu_item);
|
|
|
|
menu_item.show_all();
|
|
|
|
menu_item
|
|
|
|
});
|
2022-08-21 23:36:07 +01:00
|
|
|
if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
|
2022-08-14 14:30:13 +01:00
|
|
|
let submenus = menu_opts.submenus;
|
|
|
|
if !submenus.is_empty() {
|
|
|
|
let menu = Menu::new();
|
2022-09-25 22:49:00 +01:00
|
|
|
get_menu_items(
|
|
|
|
&submenus,
|
|
|
|
&context.controller_tx.clone(),
|
|
|
|
&address,
|
|
|
|
&menu_path,
|
|
|
|
)
|
|
|
|
.iter()
|
|
|
|
.for_each(|item| menu.add(item));
|
2022-08-14 14:30:13 +01:00
|
|
|
menu_item.set_submenu(Some(&menu));
|
|
|
|
}
|
|
|
|
}
|
2022-09-25 22:49:00 +01:00
|
|
|
widgets.insert(address, menu_item);
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
2022-09-25 22:49:00 +01:00
|
|
|
NotifierItemMessage::Remove { address } => {
|
|
|
|
if let Some(widget) = widgets.get(&address) {
|
2022-08-21 23:36:07 +01:00
|
|
|
container.remove(widget);
|
|
|
|
}
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Continue(true)
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2022-09-25 22:49:00 +01:00
|
|
|
Ok(ModuleWidget {
|
|
|
|
widget: container,
|
|
|
|
popup: None,
|
|
|
|
})
|
2022-08-14 14:30:13 +01:00
|
|
|
}
|
|
|
|
}
|