2023-02-25 14:30:45 +00:00
|
|
|
use crate::clients::clipboard::{self, ClipboardEvent};
|
|
|
|
use crate::clients::wayland::{ClipboardItem, ClipboardValue};
|
|
|
|
use crate::config::{CommonConfig, TruncateMode};
|
|
|
|
use crate::image::new_icon_button;
|
2023-07-16 18:57:00 +01:00
|
|
|
use crate::modules::{
|
|
|
|
Module, ModuleInfo, ModuleParts, ModulePopup, ModuleUpdateEvent, PopupButton, WidgetContext,
|
|
|
|
};
|
2023-12-17 23:51:43 +00:00
|
|
|
use crate::{glib_recv, spawn, try_send};
|
|
|
|
use glib::Propagation;
|
2023-02-25 14:30:45 +00:00
|
|
|
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;
|
2023-12-17 23:51:43 +00:00
|
|
|
use tokio::sync::{broadcast, mpsc};
|
2023-02-25 14:30:45 +00:00
|
|
|
use tracing::{debug, error};
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize, Clone)]
|
|
|
|
pub struct ClipboardModule {
|
|
|
|
#[serde(default = "default_icon")]
|
|
|
|
icon: String,
|
|
|
|
|
2023-04-22 22:18:36 +01:00
|
|
|
#[serde(default = "default_icon_size")]
|
|
|
|
icon_size: i32,
|
|
|
|
|
2023-02-25 14:30:45 +00:00
|
|
|
#[serde(default = "default_max_items")]
|
|
|
|
max_items: usize,
|
|
|
|
|
|
|
|
// -- Common --
|
|
|
|
truncate: Option<TruncateMode>,
|
|
|
|
|
|
|
|
#[serde(flatten)]
|
|
|
|
pub common: Option<CommonConfig>,
|
|
|
|
}
|
|
|
|
|
|
|
|
fn default_icon() -> String {
|
|
|
|
String::from("")
|
|
|
|
}
|
|
|
|
|
2023-04-22 22:18:36 +01:00
|
|
|
const fn default_icon_size() -> i32 {
|
|
|
|
32
|
|
|
|
}
|
|
|
|
|
2023-02-25 14:30:45 +00:00
|
|
|
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,
|
2023-12-17 23:51:43 +00:00
|
|
|
tx: mpsc::Sender<ModuleUpdateEvent<Self::SendMessage>>,
|
|
|
|
mut rx: mpsc::Receiver<Self::ReceiveMessage>,
|
2023-02-25 14:30:45 +00:00
|
|
|
) -> color_eyre::Result<()> {
|
|
|
|
let max_items = self.max_items;
|
|
|
|
|
|
|
|
// listen to clipboard events
|
|
|
|
spawn(async move {
|
|
|
|
let mut rx = {
|
|
|
|
let client = clipboard::get_client();
|
2023-04-29 22:08:02 +01:00
|
|
|
client.subscribe(max_items)
|
2023-02-25 14:30:45 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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 {
|
2023-06-29 16:57:47 +01:00
|
|
|
UIEvent::Copy(id) => client.copy(id),
|
2023-02-25 14:30:45 +00:00
|
|
|
UIEvent::Remove(id) => client.remove(id),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn into_widget(
|
|
|
|
self,
|
|
|
|
context: WidgetContext<Self::SendMessage, Self::ReceiveMessage>,
|
|
|
|
info: &ModuleInfo,
|
2023-07-16 18:57:00 +01:00
|
|
|
) -> color_eyre::Result<ModuleParts<Button>> {
|
2023-04-22 22:18:36 +01:00
|
|
|
let button = new_icon_button(&self.icon, info.icon_theme, self.icon_size);
|
2023-02-25 14:30:45 +00:00
|
|
|
button.style_context().add_class("btn");
|
|
|
|
|
2023-12-17 23:51:43 +00:00
|
|
|
let tx = context.tx.clone();
|
2023-02-25 14:30:45 +00:00
|
|
|
button.connect_clicked(move |button| {
|
2023-12-17 23:51:43 +00:00
|
|
|
try_send!(tx, ModuleUpdateEvent::TogglePopup(button.popup_id()));
|
2023-02-25 14:30:45 +00:00
|
|
|
});
|
|
|
|
|
2023-12-17 23:51:43 +00:00
|
|
|
let rx = context.subscribe();
|
2023-07-16 18:57:00 +01:00
|
|
|
let popup = self
|
2023-12-17 23:51:43 +00:00
|
|
|
.into_popup(context.controller_tx, rx, info)
|
2023-07-16 18:57:00 +01:00
|
|
|
.into_popup_parts(vec![&button]);
|
|
|
|
|
|
|
|
Ok(ModuleParts::new(button, popup))
|
2023-02-25 14:30:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn into_popup(
|
|
|
|
self,
|
2023-12-17 23:51:43 +00:00
|
|
|
tx: mpsc::Sender<Self::ReceiveMessage>,
|
2023-12-31 00:50:03 +00:00
|
|
|
rx: broadcast::Receiver<Self::SendMessage>,
|
2023-02-25 14:30:45 +00:00
|
|
|
_info: &ModuleInfo,
|
|
|
|
) -> Option<gtk::Box>
|
|
|
|
where
|
|
|
|
Self: Sized,
|
|
|
|
{
|
2023-05-06 00:40:06 +01:00
|
|
|
let container = gtk::Box::new(Orientation::Vertical, 10);
|
2023-02-25 14:30:45 +00:00
|
|
|
|
|
|
|
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();
|
2023-12-17 23:51:43 +00:00
|
|
|
glib_recv!(rx, event => {
|
2023-02-25 14:30:45 +00:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
|
2023-12-17 23:51:43 +00:00
|
|
|
Propagation::Stop
|
2023-02-25 14:30:45 +00:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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())
|
|
|
|
}
|