1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-07-01 18:51:04 +02:00

feat: new clipboard manager module

This commit is contained in:
Jake Stanger 2023-02-25 14:30:45 +00:00
parent 5bbe64bb86
commit 575d6cc30f
No known key found for this signature in database
GPG key ID: C51FC8F9CB0BEA61
26 changed files with 1809 additions and 149 deletions

315
src/modules/clipboard.rs Normal file
View file

@ -0,0 +1,315 @@
use crate::clients::clipboard::{self, ClipboardEvent};
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
use crate::config::{CommonConfig, TruncateMode};
use crate::image::new_icon_button;
use crate::modules::{Module, ModuleInfo, ModuleUpdateEvent, ModuleWidget, WidgetContext};
use crate::popup::Popup;
use crate::try_send;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::{Cancellable, MemoryInputStream};
use gtk::prelude::*;
use gtk::{Button, EventBox, Image, Label, Orientation, RadioButton, Widget};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::spawn;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{debug, error};
#[derive(Debug, Deserialize, Clone)]
pub struct ClipboardModule {
#[serde(default = "default_icon")]
icon: String,
#[serde(default = "default_max_items")]
max_items: usize,
// -- Common --
truncate: Option<TruncateMode>,
#[serde(flatten)]
pub common: Option<CommonConfig>,
}
fn default_icon() -> String {
String::from("󰨸")
}
const fn default_max_items() -> usize {
10
}
#[derive(Debug, Clone)]
pub enum ControllerEvent {
Add(usize, Arc<ClipboardItem>),
Remove(usize),
Activate(usize),
Deactivate,
}
#[derive(Debug, Clone)]
pub enum UIEvent {
Copy(usize),
Remove(usize),
}
impl Module<Button> for ClipboardModule {
type SendMessage = ControllerEvent;
type ReceiveMessage = UIEvent;
fn name() -> &'static str {
"clipboard"
}
fn spawn_controller(
&self,
_info: &ModuleInfo,
tx: Sender<ModuleUpdateEvent<Self::SendMessage>>,
mut rx: Receiver<Self::ReceiveMessage>,
) -> color_eyre::Result<()> {
let max_items = self.max_items;
// listen to clipboard events
spawn(async move {
let mut rx = {
let client = clipboard::get_client();
client.subscribe(max_items).await
};
while let Some(event) = rx.recv().await {
match event {
ClipboardEvent::Add(item) => {
let msg = match &item.value {
ClipboardValue::Other => {
ModuleUpdateEvent::Update(ControllerEvent::Deactivate)
}
_ => ModuleUpdateEvent::Update(ControllerEvent::Add(item.id, item)),
};
try_send!(tx, msg);
}
ClipboardEvent::Remove(id) => {
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Remove(id)));
}
ClipboardEvent::Activate(id) => {
try_send!(tx, ModuleUpdateEvent::Update(ControllerEvent::Activate(id)));
}
}
}
error!("Clipboard client unexpectedly closed");
});
// listen to ui events
spawn(async move {
while let Some(event) = rx.recv().await {
let client = clipboard::get_client();
match event {
UIEvent::Copy(id) => client.copy(id).await,
UIEvent::Remove(id) => client.remove(id),
}
}
});
Ok(())
}
fn into_widget(
self,
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
info: &ModuleInfo,
) -> color_eyre::Result<ModuleWidget<Button>> {
let position = info.bar_position;
let button = new_icon_button(&self.icon, info.icon_theme, 32);
button.style_context().add_class("btn");
button.connect_clicked(move |button| {
let pos = Popup::button_pos(button, position.get_orientation());
try_send!(context.tx, ModuleUpdateEvent::TogglePopup(pos));
});
// we need to bind to the receiver as the channel does not open
// until the popup is first opened.
context.widget_rx.attach(None, |_| Continue(true));
Ok(ModuleWidget {
widget: button,
popup: self.into_popup(context.controller_tx, context.popup_rx, info),
})
}
fn into_popup(
self,
tx: Sender<Self::ReceiveMessage>,
rx: glib::Receiver<Self::SendMessage>,
_info: &ModuleInfo,
) -> Option<gtk::Box>
where
Self: Sized,
{
let container = gtk::Box::builder()
.orientation(Orientation::Vertical)
.spacing(10)
.name("popup-clipboard")
.build();
let entries = gtk::Box::new(Orientation::Vertical, 5);
container.add(&entries);
let hidden_option = RadioButton::new();
entries.add(&hidden_option);
let mut items = HashMap::new();
{
let hidden_option = hidden_option.clone();
rx.attach(None, move |event| {
match event {
ControllerEvent::Add(id, item) => {
debug!("Adding new value with ID {}", id);
let row = gtk::Box::new(Orientation::Horizontal, 0);
row.style_context().add_class("item");
let button = match &item.value {
ClipboardValue::Text(value) => {
let button = RadioButton::from_widget(&hidden_option);
let label = Label::new(Some(value));
button.add(&label);
if let Some(truncate) = self.truncate {
truncate.truncate_label(&label);
}
button.style_context().add_class("text");
button
}
ClipboardValue::Image(bytes) => {
let stream = MemoryInputStream::from_bytes(bytes);
let pixbuf = Pixbuf::from_stream_at_scale(
&stream,
128,
64,
true,
Some(&Cancellable::new()),
)
.expect("Failed to read Pixbuf from stream");
let image = Image::from_pixbuf(Some(&pixbuf));
let button = RadioButton::from_widget(&hidden_option);
button.set_image(Some(&image));
button.set_always_show_image(true);
button.style_context().add_class("image");
button
}
ClipboardValue::Other => unreachable!(),
};
button.style_context().add_class("btn");
button.set_active(true); // if just added, should be on clipboard
let button_wrapper = EventBox::new();
button_wrapper.add(&button);
button_wrapper.set_widget_name(&format!("copy-{id}"));
button_wrapper.set_above_child(true);
{
let tx = tx.clone();
button_wrapper.connect_button_press_event(
move |button_wrapper, event| {
// left click
if event.button() == 1 {
let id = get_button_id(button_wrapper)
.expect("Failed to get id from button name");
debug!("Copying item with id: {id}");
try_send!(tx, UIEvent::Copy(id));
}
Inhibit(true)
},
);
}
let remove_button = Button::with_label("x");
remove_button.set_widget_name(&format!("remove-{id}"));
remove_button.style_context().add_class("btn-remove");
{
let tx = tx.clone();
let entries = entries.clone();
let row = row.clone();
remove_button.connect_clicked(move |button| {
let id = get_button_id(button)
.expect("Failed to get id from button name");
debug!("Removing item with id: {id}");
try_send!(tx, UIEvent::Remove(id));
entries.remove(&row);
});
}
row.add(&button_wrapper);
row.pack_end(&remove_button, false, false, 0);
entries.add(&row);
entries.reorder_child(&row, 0);
row.show_all();
items.insert(id, (row, button));
}
ControllerEvent::Remove(id) => {
debug!("Removing option with ID {id}");
let row = items.remove(&id);
if let Some((row, button)) = row {
if button.is_active() {
hidden_option.set_active(true);
}
entries.remove(&row);
}
}
ControllerEvent::Activate(id) => {
debug!("Activating option with ID {id}");
hidden_option.set_active(false);
let row = items.get(&id);
if let Some((_, button)) = row {
button.set_active(true);
}
}
ControllerEvent::Deactivate => {
debug!("Deactivating current option");
hidden_option.set_active(true);
}
}
Continue(true)
});
}
container.show_all();
hidden_option.hide();
Some(container)
}
}
/// Gets the ID from a widget's name.
///
/// This expects the button name to be
/// in the format `<purpose>-<id>`.
fn get_button_id<W>(button_wrapper: &W) -> Option<usize>
where
W: IsA<Widget>,
{
button_wrapper
.widget_name()
.split_once('-')
.and_then(|(_, id)| id.parse().ok())
}

View file

@ -1,3 +1,5 @@
#[cfg(feature = "clipboard")]
pub mod clipboard;
/// Displays the current date and time.
///
/// A custom date/time format string can be set in the config.