mirror of
https://github.com/Zedfrigg/ironbar.git
synced 2025-07-01 18:51:04 +02:00
commit
ef41462ef0
9 changed files with 666 additions and 282 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -3002,9 +3002,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-tray"
|
name = "system-tray"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c8f7f19237d2149a8e4b56409c579b26a98748e3cfadba93762c4746c4c7ae2"
|
checksum = "a456e3e6cbd396f1a3a91f8f74d1fdcf2bde85c97afe174442c367f4749fc09b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
|
|
|
@ -125,7 +125,7 @@ mpris = { version = "2.0.1", optional = true }
|
||||||
sysinfo = { version = "0.29.11", optional = true }
|
sysinfo = { version = "0.29.11", optional = true }
|
||||||
|
|
||||||
# tray
|
# tray
|
||||||
system-tray = { version = "0.1.4", optional = true }
|
system-tray = { version = "0.1.5", optional = true }
|
||||||
|
|
||||||
# upower
|
# upower
|
||||||
upower_dbus = { version = "0.3.2", optional = true }
|
upower_dbus = { version = "0.3.2", optional = true }
|
||||||
|
|
|
@ -25,7 +25,7 @@ impl TrayEventReceiver {
|
||||||
let id = format!("ironbar-{}", Ironbar::unique_id());
|
let id = format!("ironbar-{}", Ironbar::unique_id());
|
||||||
|
|
||||||
let (tx, rx) = mpsc::channel(16);
|
let (tx, rx) = mpsc::channel(16);
|
||||||
let (b_tx, b_rx) = broadcast::channel(16);
|
let (b_tx, b_rx) = broadcast::channel(64);
|
||||||
|
|
||||||
let tray = StatusNotifierWatcher::new(rx).await?;
|
let tray = StatusNotifierWatcher::new(rx).await?;
|
||||||
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
let mut host = Box::pin(tray.create_notifier_host(&id)).await?;
|
||||||
|
@ -48,7 +48,7 @@ impl TrayEventReceiver {
|
||||||
item,
|
item,
|
||||||
menu,
|
menu,
|
||||||
} => {
|
} => {
|
||||||
debug!("Adding item with address '{address}'");
|
debug!("Adding/updating item with address '{address}'");
|
||||||
tray.insert(address, (item, menu));
|
tray.insert(address, (item, menu));
|
||||||
}
|
}
|
||||||
NotifierItemMessage::Remove { address } => {
|
NotifierItemMessage::Remove { address } => {
|
||||||
|
|
|
@ -62,8 +62,17 @@ macro_rules! glib_recv {
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
// re-delcare in case ie `context.subscribe()` is passed directly
|
// re-delcare in case ie `context.subscribe()` is passed directly
|
||||||
let mut rx = $rx;
|
let mut rx = $rx;
|
||||||
while let Ok($val) = rx.recv().await {
|
loop {
|
||||||
$expr
|
match rx.recv().await {
|
||||||
|
Ok($val) => $expr,
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
|
||||||
|
tracing::warn!("Channel lagged behind by {count}, this may result in unexpected or broken behaviour");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("{err:?}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}};
|
}};
|
||||||
|
|
|
@ -1,275 +0,0 @@
|
||||||
use crate::clients::system_tray::TrayEventReceiver;
|
|
||||||
use crate::config::CommonConfig;
|
|
||||||
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
|
||||||
use crate::{glib_recv, spawn, try_send};
|
|
||||||
use color_eyre::Result;
|
|
||||||
use glib::ffi::g_strfreev;
|
|
||||||
use glib::translate::ToGlibPtr;
|
|
||||||
use gtk::ffi::gtk_icon_theme_get_search_path;
|
|
||||||
use gtk::gdk_pixbuf::{Colorspace, InterpType};
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use gtk::{
|
|
||||||
gdk_pixbuf, IconLookupFlags, IconTheme, Image, Label, Menu, MenuBar, MenuItem,
|
|
||||||
SeparatorMenuItem,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use std::ffi::CStr;
|
|
||||||
use std::os::raw::{c_char, c_int};
|
|
||||||
use std::ptr;
|
|
||||||
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType};
|
|
||||||
use system_tray::message::tray::StatusNotifierItem;
|
|
||||||
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio::sync::mpsc::{Receiver, Sender};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
|
||||||
pub struct TrayModule {
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub common: Option<CommonConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the GTK icon theme search paths by calling the FFI function.
|
|
||||||
/// Conveniently returns the result as a `HashSet`.
|
|
||||||
fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
|
|
||||||
let mut gtk_paths: *mut *mut c_char = ptr::null_mut();
|
|
||||||
let mut n_elements: c_int = 0;
|
|
||||||
let mut paths = HashSet::new();
|
|
||||||
unsafe {
|
|
||||||
gtk_icon_theme_get_search_path(
|
|
||||||
icon_theme.to_glib_none().0,
|
|
||||||
&mut gtk_paths,
|
|
||||||
&mut n_elements,
|
|
||||||
);
|
|
||||||
// n_elements is never negative (that would be weird)
|
|
||||||
for i in 0..n_elements as usize {
|
|
||||||
let c_str = CStr::from_ptr(*gtk_paths.add(i));
|
|
||||||
if let Ok(str) = c_str.to_str() {
|
|
||||||
paths.insert(str.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g_strfreev(gtk_paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
paths
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to get a GTK `Image` component
|
|
||||||
/// for the status notifier item's icon.
|
|
||||||
fn get_image_from_icon_name(item: &StatusNotifierItem, icon_theme: &IconTheme) -> Option<Image> {
|
|
||||||
if let Some(path) = item.icon_theme_path.as_ref() {
|
|
||||||
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
|
|
||||||
icon_theme.append_search_path(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item.icon_name.as_ref().and_then(|icon_name| {
|
|
||||||
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
|
|
||||||
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recursively gets GTK `MenuItem` components
|
|
||||||
/// for the provided submenu array.
|
|
||||||
fn get_menu_items(
|
|
||||||
menu: &[MenuItemInfo],
|
|
||||||
tx: &Sender<NotifierItemCommand>,
|
|
||||||
id: &str,
|
|
||||||
path: &str,
|
|
||||||
) -> 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();
|
|
||||||
get_menu_items(&item_info.submenu, &tx.clone(), id, path)
|
|
||||||
.iter()
|
|
||||||
.for_each(|item| menu.add(item));
|
|
||||||
|
|
||||||
builder = builder.submenu(&menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
let item = builder.build();
|
|
||||||
|
|
||||||
let info = item_info.clone();
|
|
||||||
let id = id.to_string();
|
|
||||||
let path = path.to_string();
|
|
||||||
|
|
||||||
{
|
|
||||||
let tx = tx.clone();
|
|
||||||
item.connect_activate(move |_item| {
|
|
||||||
try_send!(
|
|
||||||
tx,
|
|
||||||
NotifierItemCommand::MenuItemClicked {
|
|
||||||
submenu_id: info.id,
|
|
||||||
menu_path: path.clone(),
|
|
||||||
notifier_address: id.clone(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Box::new(item)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
(*item).as_ref().clone()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module<MenuBar> for TrayModule {
|
|
||||||
type SendMessage = NotifierItemMessage;
|
|
||||||
type ReceiveMessage = NotifierItemCommand;
|
|
||||||
|
|
||||||
fn name() -> &'static str {
|
|
||||||
"tray"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_controller(
|
|
||||||
&self,
|
|
||||||
_info: &ModuleInfo,
|
|
||||||
context: &WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
||||||
mut rx: Receiver<Self::ReceiveMessage>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tx = context.tx.clone();
|
|
||||||
|
|
||||||
let client = context.client::<TrayEventReceiver>();
|
|
||||||
|
|
||||||
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?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
// send tray commands
|
|
||||||
spawn(async move {
|
|
||||||
while let Some(cmd) = rx.recv().await {
|
|
||||||
tray_tx.send(cmd).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_widget(
|
|
||||||
self,
|
|
||||||
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
||||||
info: &ModuleInfo,
|
|
||||||
) -> Result<ModuleParts<MenuBar>> {
|
|
||||||
let container = MenuBar::new();
|
|
||||||
|
|
||||||
{
|
|
||||||
let container = container.clone();
|
|
||||||
let mut widgets = HashMap::new();
|
|
||||||
let icon_theme = info.icon_theme.clone();
|
|
||||||
|
|
||||||
// listen for UI updates
|
|
||||||
glib_recv!(context.subscribe(), update => {
|
|
||||||
match update {
|
|
||||||
NotifierItemMessage::Update {
|
|
||||||
item,
|
|
||||||
address,
|
|
||||||
menu,
|
|
||||||
} => {
|
|
||||||
let addr = &address;
|
|
||||||
let menu_item = widgets.remove(address.as_str()).unwrap_or_else(|| {
|
|
||||||
let menu_item = MenuItem::new();
|
|
||||||
menu_item.style_context().add_class("item");
|
|
||||||
|
|
||||||
get_image_from_icon_name(&item, &icon_theme)
|
|
||||||
.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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
container.add(&menu_item);
|
|
||||||
menu_item.show_all();
|
|
||||||
menu_item
|
|
||||||
});
|
|
||||||
if let (Some(menu_opts), Some(menu_path)) = (menu, item.menu) {
|
|
||||||
let submenus = menu_opts.submenus;
|
|
||||||
if !submenus.is_empty() {
|
|
||||||
let menu = Menu::new();
|
|
||||||
get_menu_items(
|
|
||||||
&submenus,
|
|
||||||
&context.controller_tx.clone(),
|
|
||||||
&address,
|
|
||||||
&menu_path,
|
|
||||||
)
|
|
||||||
.iter()
|
|
||||||
.for_each(|item| menu.add(item));
|
|
||||||
menu_item.set_submenu(Some(&menu));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
widgets.insert(address, menu_item);
|
|
||||||
}
|
|
||||||
NotifierItemMessage::Remove { address } => {
|
|
||||||
if let Some(widget) = widgets.get(&address) {
|
|
||||||
container.remove(widget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(ModuleParts {
|
|
||||||
widget: container,
|
|
||||||
popup: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
104
src/modules/tray/diff.rs
Normal file
104
src/modules/tray/diff.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use system_tray::message::menu::{MenuItem as MenuItemInfo, ToggleState};
|
||||||
|
|
||||||
|
/// Diff change type and associated info.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Diff {
|
||||||
|
Add(MenuItemInfo),
|
||||||
|
Update(i32, MenuItemDiff),
|
||||||
|
Remove(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diff info to be applied to an existing menu item as an update.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MenuItemDiff {
|
||||||
|
/// Text of the item,
|
||||||
|
pub label: Option<String>,
|
||||||
|
/// Whether the item can be activated or not.
|
||||||
|
pub enabled: Option<bool>,
|
||||||
|
/// True if the item is visible in the menu.
|
||||||
|
pub visible: Option<bool>,
|
||||||
|
/// Icon name of the item, following the freedesktop.org icon spec.
|
||||||
|
// pub icon_name: Option<Option<String>>,
|
||||||
|
/// Describe the current state of a "togglable" item. Can be one of:
|
||||||
|
/// - Some(true): on
|
||||||
|
/// - Some(false): off
|
||||||
|
/// - None: indeterminate
|
||||||
|
pub toggle_state: Option<ToggleState>,
|
||||||
|
/// A submenu for this item, typically this would ve revealed to the user by hovering the current item
|
||||||
|
pub submenu: Vec<Diff>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuItemDiff {
|
||||||
|
fn new(old: &MenuItemInfo, new: &MenuItemInfo) -> Self {
|
||||||
|
macro_rules! diff {
|
||||||
|
($field:ident) => {
|
||||||
|
if old.$field == new.$field {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(new.$field)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(&$field:ident) => {
|
||||||
|
if &old.$field == &new.$field {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(new.$field.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
label: diff!(&label),
|
||||||
|
enabled: diff!(enabled),
|
||||||
|
visible: diff!(visible),
|
||||||
|
// icon_name: diff!(&icon_name),
|
||||||
|
toggle_state: diff!(toggle_state),
|
||||||
|
submenu: get_diffs(&old.submenu, &new.submenu),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this diff contains any changes
|
||||||
|
fn has_diff(&self) -> bool {
|
||||||
|
self.label.is_some()
|
||||||
|
|| self.enabled.is_some()
|
||||||
|
|| self.visible.is_some()
|
||||||
|
// || self.icon_name.is_some()
|
||||||
|
|| self.toggle_state.is_some()
|
||||||
|
|| !self.submenu.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a diff set between old and new state.
|
||||||
|
pub fn get_diffs(old: &[MenuItemInfo], new: &[MenuItemInfo]) -> Vec<Diff> {
|
||||||
|
let mut diffs = vec![];
|
||||||
|
|
||||||
|
for new_item in new {
|
||||||
|
let old_item = old.iter().find(|&item| item.id == new_item.id);
|
||||||
|
|
||||||
|
let diff = match old_item {
|
||||||
|
Some(old_item) => {
|
||||||
|
let item_diff = MenuItemDiff::new(old_item, new_item);
|
||||||
|
if item_diff.has_diff() {
|
||||||
|
Some(Diff::Update(old_item.id, item_diff))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Some(Diff::Add(new_item.clone())),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(diff) = diff {
|
||||||
|
diffs.push(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for old_item in old {
|
||||||
|
let new_item = new.iter().find(|&item| item.id == old_item.id);
|
||||||
|
if new_item.is_none() {
|
||||||
|
diffs.push(Diff::Remove(old_item.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
diffs
|
||||||
|
}
|
86
src/modules/tray/icon.rs
Normal file
86
src/modules/tray/icon.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use glib::ffi::g_strfreev;
|
||||||
|
use glib::translate::ToGlibPtr;
|
||||||
|
use gtk::ffi::gtk_icon_theme_get_search_path;
|
||||||
|
use gtk::gdk_pixbuf::{Colorspace, InterpType};
|
||||||
|
use gtk::prelude::IconThemeExt;
|
||||||
|
use gtk::{gdk_pixbuf, IconLookupFlags, IconTheme, Image};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::os::raw::{c_char, c_int};
|
||||||
|
use std::ptr;
|
||||||
|
use system_tray::message::tray::StatusNotifierItem;
|
||||||
|
|
||||||
|
/// Gets the GTK icon theme search paths by calling the FFI function.
|
||||||
|
/// Conveniently returns the result as a `HashSet`.
|
||||||
|
fn get_icon_theme_search_paths(icon_theme: &IconTheme) -> HashSet<String> {
|
||||||
|
let mut gtk_paths: *mut *mut c_char = ptr::null_mut();
|
||||||
|
let mut n_elements: c_int = 0;
|
||||||
|
let mut paths = HashSet::new();
|
||||||
|
unsafe {
|
||||||
|
gtk_icon_theme_get_search_path(
|
||||||
|
icon_theme.to_glib_none().0,
|
||||||
|
&mut gtk_paths,
|
||||||
|
&mut n_elements,
|
||||||
|
);
|
||||||
|
// n_elements is never negative (that would be weird)
|
||||||
|
for i in 0..n_elements as usize {
|
||||||
|
let c_str = CStr::from_ptr(*gtk_paths.add(i));
|
||||||
|
if let Ok(str) = c_str.to_str() {
|
||||||
|
paths.insert(str.to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_strfreev(gtk_paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to get a GTK `Image` component
|
||||||
|
/// for the status notifier item's icon.
|
||||||
|
pub(crate) fn get_image_from_icon_name(
|
||||||
|
item: &StatusNotifierItem,
|
||||||
|
icon_theme: &IconTheme,
|
||||||
|
) -> Option<Image> {
|
||||||
|
if let Some(path) = item.icon_theme_path.as_ref() {
|
||||||
|
if !path.is_empty() && !get_icon_theme_search_paths(icon_theme).contains(path) {
|
||||||
|
icon_theme.append_search_path(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item.icon_name.as_ref().and_then(|icon_name| {
|
||||||
|
let icon_info = icon_theme.lookup_icon(icon_name, 16, IconLookupFlags::empty());
|
||||||
|
icon_info.map(|icon_info| Image::from_pixbuf(icon_info.load_icon().ok().as_ref()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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`.
|
||||||
|
pub(crate) 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)))
|
||||||
|
}
|
314
src/modules/tray/interface.rs
Normal file
314
src/modules/tray/interface.rs
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
use crate::modules::tray::diff::{Diff, MenuItemDiff};
|
||||||
|
use crate::{spawn, try_send};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{CheckMenuItem, Image, Label, Menu, MenuItem, SeparatorMenuItem};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use system_tray::message::menu::{MenuItem as MenuItemInfo, MenuType, ToggleState, ToggleType};
|
||||||
|
use system_tray::message::NotifierItemCommand;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
/// Calls a method on the underlying widget,
|
||||||
|
/// passing in a single argument.
|
||||||
|
///
|
||||||
|
/// This is useful to avoid matching on
|
||||||
|
/// `TrayMenuWidget` constantly.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// call!(container, add, my_widget)
|
||||||
|
/// ```
|
||||||
|
/// is the same as:
|
||||||
|
/// ```
|
||||||
|
/// match &my_widget {
|
||||||
|
/// TrayMenuWidget::Separator(w) => {
|
||||||
|
/// container.add(w);
|
||||||
|
/// }
|
||||||
|
/// TrayMenuWidget::Standard(w) => {
|
||||||
|
/// container.add(w);
|
||||||
|
/// }
|
||||||
|
/// TrayMenuWidget::Checkbox(w) => {
|
||||||
|
/// container.add(w);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
macro_rules! call {
|
||||||
|
($parent:expr, $method:ident, $child:expr) => {
|
||||||
|
match &$child {
|
||||||
|
TrayMenuWidget::Separator(w) => {
|
||||||
|
$parent.$method(w);
|
||||||
|
}
|
||||||
|
TrayMenuWidget::Standard(w) => {
|
||||||
|
$parent.$method(w);
|
||||||
|
}
|
||||||
|
TrayMenuWidget::Checkbox(w) => {
|
||||||
|
$parent.$method(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main tray icon to show on the bar
|
||||||
|
pub(crate) struct TrayMenu {
|
||||||
|
pub(crate) widget: MenuItem,
|
||||||
|
menu_widget: Menu,
|
||||||
|
image_widget: Option<Image>,
|
||||||
|
label_widget: Option<Label>,
|
||||||
|
|
||||||
|
menu: HashMap<i32, TrayMenuItem>,
|
||||||
|
state: Vec<MenuItemInfo>,
|
||||||
|
icon_name: Option<String>,
|
||||||
|
|
||||||
|
tx: mpsc::Sender<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrayMenu {
|
||||||
|
pub fn new(tx: mpsc::Sender<NotifierItemCommand>, address: String, path: String) -> Self {
|
||||||
|
let widget = MenuItem::new();
|
||||||
|
widget.style_context().add_class("item");
|
||||||
|
|
||||||
|
let (item_tx, mut item_rx) = mpsc::channel(8);
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(id) = item_rx.recv().await {
|
||||||
|
try_send!(
|
||||||
|
tx,
|
||||||
|
NotifierItemCommand::MenuItemClicked {
|
||||||
|
submenu_id: id,
|
||||||
|
menu_path: path.clone(),
|
||||||
|
notifier_address: address.clone(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu = Menu::new();
|
||||||
|
widget.set_submenu(Some(&menu));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
widget,
|
||||||
|
menu_widget: menu,
|
||||||
|
image_widget: None,
|
||||||
|
label_widget: None,
|
||||||
|
state: vec![],
|
||||||
|
icon_name: None,
|
||||||
|
menu: HashMap::new(),
|
||||||
|
tx: item_tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the label text, and shows it in favour of the image.
|
||||||
|
pub fn set_label(&mut self, text: &str) {
|
||||||
|
if let Some(image) = &self.image_widget {
|
||||||
|
image.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.label_widget
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
let label = Label::new(None);
|
||||||
|
self.widget.add(&label);
|
||||||
|
label.show();
|
||||||
|
label
|
||||||
|
})
|
||||||
|
.set_label(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the image, and shows it in favour of the label.
|
||||||
|
pub fn set_image(&mut self, image: &Image) {
|
||||||
|
if let Some(label) = &self.label_widget {
|
||||||
|
label.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(old) = self.image_widget.replace(image.clone()) {
|
||||||
|
self.widget.remove(&old);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.widget.add(image);
|
||||||
|
image.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a diff set to the submenu.
|
||||||
|
pub fn apply_diffs(&mut self, diffs: Vec<Diff>) {
|
||||||
|
for diff in diffs {
|
||||||
|
match diff {
|
||||||
|
Diff::Add(info) => {
|
||||||
|
let item = TrayMenuItem::new(&info, self.tx.clone());
|
||||||
|
call!(self.menu_widget, add, item.widget);
|
||||||
|
self.menu.insert(item.id, item);
|
||||||
|
}
|
||||||
|
Diff::Update(id, info) => {
|
||||||
|
if let Some(item) = self.menu.get_mut(&id) {
|
||||||
|
item.apply_diff(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Diff::Remove(id) => {
|
||||||
|
if let Some(item) = self.menu.remove(&id) {
|
||||||
|
call!(self.menu_widget, remove, item.widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn label_widget(&self) -> Option<&Label> {
|
||||||
|
self.label_widget.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> &[MenuItemInfo] {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_state(&mut self, state: Vec<MenuItemInfo>) {
|
||||||
|
self.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn icon_name(&self) -> Option<&String> {
|
||||||
|
self.icon_name.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_icon_name(&mut self, icon_name: Option<String>) {
|
||||||
|
self.icon_name = icon_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct TrayMenuItem {
|
||||||
|
id: i32,
|
||||||
|
widget: TrayMenuWidget,
|
||||||
|
menu_widget: Menu,
|
||||||
|
submenu: HashMap<i32, TrayMenuItem>,
|
||||||
|
tx: mpsc::Sender<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum TrayMenuWidget {
|
||||||
|
Separator(SeparatorMenuItem),
|
||||||
|
Standard(MenuItem),
|
||||||
|
Checkbox(CheckMenuItem),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrayMenuItem {
|
||||||
|
fn new(info: &MenuItemInfo, tx: mpsc::Sender<i32>) -> Self {
|
||||||
|
let menu = Menu::new();
|
||||||
|
|
||||||
|
let widget = match (info.menu_type, info.toggle_type) {
|
||||||
|
(MenuType::Separator, _) => TrayMenuWidget::Separator(SeparatorMenuItem::new()),
|
||||||
|
(MenuType::Standard, ToggleType::Checkmark) => {
|
||||||
|
let widget = CheckMenuItem::builder()
|
||||||
|
.label(info.label.as_str())
|
||||||
|
.visible(info.visible)
|
||||||
|
.sensitive(info.enabled)
|
||||||
|
.active(info.toggle_state == ToggleState::On)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
let id = info.id;
|
||||||
|
|
||||||
|
widget.connect_activate(move |_item| {
|
||||||
|
try_send!(tx, id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TrayMenuWidget::Checkbox(widget)
|
||||||
|
}
|
||||||
|
(MenuType::Standard, _) => {
|
||||||
|
let builder = MenuItem::builder()
|
||||||
|
.label(&info.label)
|
||||||
|
.visible(info.visible)
|
||||||
|
.sensitive(info.enabled);
|
||||||
|
|
||||||
|
let widget = builder.build();
|
||||||
|
|
||||||
|
{
|
||||||
|
let tx = tx.clone();
|
||||||
|
let id = info.id;
|
||||||
|
|
||||||
|
widget.connect_activate(move |_item| {
|
||||||
|
try_send!(tx, id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TrayMenuWidget::Standard(widget)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id: info.id,
|
||||||
|
widget,
|
||||||
|
menu_widget: menu,
|
||||||
|
submenu: HashMap::new(),
|
||||||
|
tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies a diff to this submenu item.
|
||||||
|
///
|
||||||
|
/// This is called recursively,
|
||||||
|
/// applying the submenu diffs to any further submenu items.
|
||||||
|
fn apply_diff(&mut self, diff: MenuItemDiff) {
|
||||||
|
if let Some(label) = diff.label {
|
||||||
|
match &self.widget {
|
||||||
|
TrayMenuWidget::Separator(widget) => widget.set_label(&label),
|
||||||
|
TrayMenuWidget::Standard(widget) => widget.set_label(&label),
|
||||||
|
TrayMenuWidget::Checkbox(widget) => widget.set_label(&label),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Image support
|
||||||
|
// if let Some(icon_name) = diff.icon_name {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
if let Some(enabled) = diff.enabled {
|
||||||
|
match &self.widget {
|
||||||
|
TrayMenuWidget::Separator(widget) => widget.set_sensitive(enabled),
|
||||||
|
TrayMenuWidget::Standard(widget) => widget.set_sensitive(enabled),
|
||||||
|
TrayMenuWidget::Checkbox(widget) => widget.set_sensitive(enabled),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(visible) = diff.visible {
|
||||||
|
match &self.widget {
|
||||||
|
TrayMenuWidget::Separator(widget) => widget.set_visible(visible),
|
||||||
|
TrayMenuWidget::Standard(widget) => widget.set_visible(visible),
|
||||||
|
TrayMenuWidget::Checkbox(widget) => widget.set_visible(visible),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(toggle_state) = diff.toggle_state {
|
||||||
|
if let TrayMenuWidget::Checkbox(widget) = &self.widget {
|
||||||
|
widget.set_active(toggle_state == ToggleState::On);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for sub_diff in diff.submenu {
|
||||||
|
match sub_diff {
|
||||||
|
Diff::Add(info) => {
|
||||||
|
let menu_item = TrayMenuItem::new(&info, self.tx.clone());
|
||||||
|
call!(self.menu_widget, add, menu_item.widget);
|
||||||
|
|
||||||
|
if let TrayMenuWidget::Standard(widget) = &self.widget {
|
||||||
|
widget.set_submenu(Some(&self.menu_widget));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.submenu.insert(menu_item.id, menu_item);
|
||||||
|
}
|
||||||
|
Diff::Update(id, diff) => {
|
||||||
|
if let Some(sub) = self.submenu.get_mut(&id) {
|
||||||
|
sub.apply_diff(diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Diff::Remove(id) => {
|
||||||
|
if let Some(sub) = self.submenu.remove(&id) {
|
||||||
|
call!(self.menu_widget, remove, sub.widget);
|
||||||
|
}
|
||||||
|
if let TrayMenuWidget::Standard(widget) = &self.widget {
|
||||||
|
if self.submenu.is_empty() {
|
||||||
|
widget.set_submenu(None::<&Menu>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
146
src/modules/tray/mod.rs
Normal file
146
src/modules/tray/mod.rs
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
mod diff;
|
||||||
|
mod icon;
|
||||||
|
mod interface;
|
||||||
|
|
||||||
|
use crate::clients::system_tray::TrayEventReceiver;
|
||||||
|
use crate::config::CommonConfig;
|
||||||
|
use crate::modules::tray::diff::get_diffs;
|
||||||
|
use crate::modules::{Module, ModuleInfo, ModuleParts, ModuleUpdateEvent, WidgetContext};
|
||||||
|
use crate::{glib_recv, spawn};
|
||||||
|
use color_eyre::Result;
|
||||||
|
use gtk::prelude::*;
|
||||||
|
use gtk::{IconTheme, MenuBar};
|
||||||
|
use interface::TrayMenu;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use system_tray::message::{NotifierItemCommand, NotifierItemMessage};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
pub struct TrayModule {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub common: Option<CommonConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module<MenuBar> for TrayModule {
|
||||||
|
type SendMessage = NotifierItemMessage;
|
||||||
|
type ReceiveMessage = NotifierItemCommand;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let client = context.client::<TrayEventReceiver>();
|
||||||
|
|
||||||
|
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?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), mpsc::error::SendError<ModuleUpdateEvent<Self::SendMessage>>>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
// send tray commands
|
||||||
|
spawn(async move {
|
||||||
|
while let Some(cmd) = rx.recv().await {
|
||||||
|
tray_tx.send(cmd).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), mpsc::error::SendError<NotifierItemCommand>>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_widget(
|
||||||
|
self,
|
||||||
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
||||||
|
info: &ModuleInfo,
|
||||||
|
) -> Result<ModuleParts<MenuBar>> {
|
||||||
|
let container = MenuBar::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
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 =>
|
||||||
|
on_update(update, &container, &mut menus, &icon_theme, &context.controller_tx)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
update: NotifierItemMessage,
|
||||||
|
container: &MenuBar,
|
||||||
|
menus: &mut HashMap<Box<str>, TrayMenu>,
|
||||||
|
icon_theme: &IconTheme,
|
||||||
|
tx: &mpsc::Sender<NotifierItemCommand>,
|
||||||
|
) {
|
||||||
|
match update {
|
||||||
|
NotifierItemMessage::Update {
|
||||||
|
item,
|
||||||
|
address,
|
||||||
|
menu,
|
||||||
|
} => {
|
||||||
|
if let (Some(menu_opts), Some(menu_path)) = (menu, &item.menu) {
|
||||||
|
let submenus = menu_opts.submenus;
|
||||||
|
|
||||||
|
let mut menu_item = menus.remove(address.as_str()).unwrap_or_else(|| {
|
||||||
|
let item = TrayMenu::new(tx.clone(), address.clone(), menu_path.to_string());
|
||||||
|
container.add(&item.widget);
|
||||||
|
|
||||||
|
item
|
||||||
|
});
|
||||||
|
|
||||||
|
let label = item.title.as_ref().unwrap_or(&address);
|
||||||
|
if let Some(label_widget) = menu_item.label_widget() {
|
||||||
|
label_widget.set_label(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.icon_name.as_ref() != menu_item.icon_name() {
|
||||||
|
match icon::get_image_from_icon_name(&item, icon_theme)
|
||||||
|
.or_else(|| icon::get_image_from_pixmap(&item))
|
||||||
|
{
|
||||||
|
Some(image) => menu_item.set_image(&image),
|
||||||
|
None => menu_item.set_label(label),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffs = get_diffs(menu_item.state(), &submenus);
|
||||||
|
menu_item.apply_diffs(diffs);
|
||||||
|
menu_item.widget.show();
|
||||||
|
|
||||||
|
menu_item.set_state(submenus);
|
||||||
|
menu_item.set_icon_name(item.icon_name);
|
||||||
|
|
||||||
|
menus.insert(address.into(), menu_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotifierItemMessage::Remove { address } => {
|
||||||
|
if let Some(menu) = menus.get(address.as_str()) {
|
||||||
|
container.remove(&menu.widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue