Add drag and drop functionality to eventbox (Implements #409) (#432)

* Add drag and drop functionality to eventbox (fixes #409)

* Don't allow dragging empty string values

* Support dragging text

* Provide the type of drag element to the command, separate out day month and year in onclick event of calendar
This commit is contained in:
ElKowar 2022-04-19 13:00:50 +02:00 committed by GitHub
parent b79f7b8fb7
commit 7648eb8086
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 112 additions and 24 deletions

View file

@ -5,6 +5,14 @@ All notable changes to eww will be listed here, starting at changes since versio
## [Unreleased] ## [Unreleased]
### BREAKING CHANGES
- Change the onclick command API to support multiple placeholders.
This changes. the behaviour of the calendar widget's onclick as well as the onhover and onhoverlost
events. Instead of providing the entire date (or, respecively, the x and y mouse coordinates) in
a single value (`day.month.year`, `x y`), the values are now provided as separate placeholders.
The day can now be accessed with `{0}`, the month with `{1}`, and the year with `{2}`, and
similarly x and y are accessed with `{0}` and `{1}`.
### Features ### Features
- Add `eww inspector` command - Add `eww inspector` command
- Add `--no-daemonize` flag - Add `--no-daemonize` flag
@ -18,6 +26,7 @@ All notable changes to eww will be listed here, starting at changes since versio
- Add `desktop` window type (By: Alvaro Lopez) - Add `desktop` window type (By: Alvaro Lopez)
- Add `scroll` widget (By: viandoxdev) - Add `scroll` widget (By: viandoxdev)
- Add `notification` window type - Add `notification` window type
- Add drag and drop functionality to eventbox
### Notable Internal changes ### Notable Internal changes
- Rework state management completely, now making local state and dynamic widget hierarchy changes possible. - Rework state management completely, now making local state and dynamic widget hierarchy changes possible.

View file

@ -6,15 +6,26 @@ pub mod def_widget_macro;
pub mod graph; pub mod graph;
pub mod widget_definitions; pub mod widget_definitions;
const CMD_STRING_PLACEHODLER: &str = "{}"; /// Run a command that was provided as an attribute.
/// This command may use placeholders which will be replaced by the values of the arguments given.
/// Run a command that was provided as an attribute. This command may use a /// This can either be the placeholder `{}`, which will be replaced by the first argument,
/// placeholder ('{}') which will be replaced by the value provided as `arg` /// Or a placeholder like `{0}`, `{1}`, etc, which will refer to the respective argument.
pub(self) fn run_command<T: 'static + std::fmt::Display + Send + Sync>(timeout: std::time::Duration, cmd: &str, arg: T) { pub(self) fn run_command<T>(timeout: std::time::Duration, cmd: &str, args: &[T])
where
T: 'static + std::fmt::Display + Send + Sync + Clone,
{
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
let args = args.to_vec();
let cmd = cmd.to_string(); let cmd = cmd.to_string();
std::thread::spawn(move || { std::thread::spawn(move || {
let cmd = cmd.replace(CMD_STRING_PLACEHODLER, &format!("{}", arg)); let cmd = if !args.is_empty() {
args.iter()
.enumerate()
.fold(cmd.to_string(), |acc, (i, arg)| acc.replace(&format!("{{{}}}", i), &format!("{}", arg)))
.replace("{{}}", &format!("{}", args[0]))
} else {
cmd
};
log::debug!("Running command from widget: {}", cmd); log::debug!("Running command from widget: {}", cmd);
let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn(); let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn();
match child { match child {

View file

@ -10,8 +10,8 @@ use crate::{
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use codespan_reporting::diagnostic::Severity; use codespan_reporting::diagnostic::Severity;
use eww_shared_util::Spanned; use eww_shared_util::Spanned;
use gdk::NotifyType; use gdk::{ModifierType, NotifyType};
use gtk::{self, glib, prelude::*}; use gtk::{self, glib, prelude::*, DestDefaults, TargetEntry, TargetList};
use itertools::Itertools; use itertools::Itertools;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -32,13 +32,20 @@ use yuck::{
/// Connect a gtk signal handler inside of this macro to ensure that when the same code gets run multiple times, /// Connect a gtk signal handler inside of this macro to ensure that when the same code gets run multiple times,
/// the previously connected singal handler first gets disconnected. /// the previously connected singal handler first gets disconnected.
macro_rules! connect_single_handler { macro_rules! connect_single_handler {
($widget:ident, $connect_expr:expr) => {{ ($widget:ident, if $cond:expr, $connect_expr:expr) => {{
static ID: Lazy<std::sync::Mutex<Option<gtk::glib::SignalHandlerId>>> = Lazy::new(|| std::sync::Mutex::new(None)); static ID: Lazy<std::sync::Mutex<Option<gtk::glib::SignalHandlerId>>> = Lazy::new(|| std::sync::Mutex::new(None));
let old = ID.lock().unwrap().replace($connect_expr); let old = if $cond {
ID.lock().unwrap().replace($connect_expr)
} else {
ID.lock().unwrap().take()
};
if let Some(old) = old { if let Some(old) = old {
$widget.disconnect(old); $widget.disconnect(old);
} }
}}; }};
($widget:ident, $connect_expr:expr) => {{
connect_single_handler!($widget, if true, $connect_expr)
}};
} }
// TODO figure out how to // TODO figure out how to
@ -200,7 +207,7 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: &gtk::Ran
gtk_widget.set_sensitive(true); gtk_widget.set_sensitive(true);
gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK); gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK);
connect_single_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| { connect_single_handler!(gtk_widget, gtk_widget.connect_value_changed(move |gtk_widget| {
run_command(timeout, &onchange, gtk_widget.value()); run_command(timeout, &onchange, &[gtk_widget.value()]);
})); }));
} }
}); });
@ -234,7 +241,7 @@ fn build_gtk_combo_box_text(bargs: &mut BuilderArgs) -> Result<gtk::ComboBoxText
// @prop onchange - runs the code when a item was selected, replacing {} with the item as a string // @prop onchange - runs the code when a item was selected, replacing {} with the item as a string
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| {
run_command(timeout, &onchange, gtk_widget.active_text().unwrap_or_else(|| "".into())); run_command(timeout, &onchange, &[gtk_widget.active_text().unwrap_or_else(|| "".into())]);
})); }));
}, },
}); });
@ -278,7 +285,7 @@ fn build_gtk_checkbox(bargs: &mut BuilderArgs) -> Result<gtk::CheckButton> {
// @prop onunchecked - similar to onchecked but when the widget is unchecked // @prop onunchecked - similar to onchecked but when the widget is unchecked
prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_string = "", onunchecked: as_string = "") { prop(timeout: as_duration = Duration::from_millis(200), onchecked: as_string = "", onunchecked: as_string = "") {
connect_single_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| { connect_single_handler!(gtk_widget, gtk_widget.connect_toggled(move |gtk_widget| {
run_command(timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, ""); run_command(timeout, if gtk_widget.is_active() { &onchecked } else { &onunchecked }, &[""]);
})); }));
} }
}); });
@ -298,7 +305,7 @@ fn build_gtk_color_button(bargs: &mut BuilderArgs) -> Result<gtk::ColorButton> {
// @prop timeout - timeout of the command // @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
connect_single_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| { connect_single_handler!(gtk_widget, gtk_widget.connect_color_set(move |gtk_widget| {
run_command(timeout, &onchange, gtk_widget.rgba()); run_command(timeout, &onchange, &[gtk_widget.rgba()]);
})); }));
} }
}); });
@ -318,7 +325,7 @@ fn build_gtk_color_chooser(bargs: &mut BuilderArgs) -> Result<gtk::ColorChooserW
// @prop timeout - timeout of the command // @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
connect_single_handler!(gtk_widget, gtk_widget.connect_color_activated(move |_a, color| { connect_single_handler!(gtk_widget, gtk_widget.connect_color_activated(move |_a, color| {
run_command(timeout, &onchange, *color); run_command(timeout, &onchange, &[*color]);
})); }));
} }
}); });
@ -380,7 +387,7 @@ fn build_gtk_input(bargs: &mut BuilderArgs) -> Result<gtk::Entry> {
// @prop timeout - timeout of the command // @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) {
connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| { connect_single_handler!(gtk_widget, gtk_widget.connect_changed(move |gtk_widget| {
run_command(timeout, &onchange, gtk_widget.text().to_string()); run_command(timeout, &onchange, &[gtk_widget.text().to_string()]);
})); }));
} }
}); });
@ -406,9 +413,9 @@ fn build_gtk_button(bargs: &mut BuilderArgs) -> Result<gtk::Button> {
gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); gtk_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK);
connect_single_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { connect_single_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| {
match evt.button() { match evt.button() {
1 => run_command(timeout, &onclick, ""), 1 => run_command(timeout, &onclick, &[""]),
2 => run_command(timeout, &onmiddleclick, ""), 2 => run_command(timeout, &onmiddleclick, &[""]),
3 => run_command(timeout, &onrightclick, ""), 3 => run_command(timeout, &onrightclick, &[""]),
_ => {}, _ => {},
} }
gtk::Inhibit(false) gtk::Inhibit(false)
@ -552,7 +559,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
connect_single_handler!(gtk_widget, gtk_widget.connect_scroll_event(move |_, evt| { connect_single_handler!(gtk_widget, gtk_widget.connect_scroll_event(move |_, evt| {
let delta = evt.delta().1; let delta = evt.delta().1;
if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959 if delta != 0f64 { // Ignore the first event https://bugzilla.gnome.org/show_bug.cgi?id=675959
run_command(timeout, &onscroll, if delta < 0f64 { "up" } else { "down" }); run_command(timeout, &onscroll, &[if delta < 0f64 { "up" } else { "down" }]);
} }
gtk::Inhibit(false) gtk::Inhibit(false)
})); }));
@ -563,7 +570,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK);
connect_single_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| { connect_single_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| {
if evt.detail() != NotifyType::Inferior { if evt.detail() != NotifyType::Inferior {
run_command(timeout, &onhover, format!("{} {}", evt.position().0, evt.position().1)); run_command(timeout, &onhover, &[evt.position().0, evt.position().1]);
} }
gtk::Inhibit(false) gtk::Inhibit(false)
})); }));
@ -574,7 +581,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK);
connect_single_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| { connect_single_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| {
if evt.detail() != NotifyType::Inferior { if evt.detail() != NotifyType::Inferior {
run_command(timeout, &onhoverlost, format!("{} {}", evt.position().0, evt.position().1)); run_command(timeout, &onhoverlost, &[evt.position().0, evt.position().1]);
} }
gtk::Inhibit(false) gtk::Inhibit(false)
})); }));
@ -603,6 +610,53 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result<gtk::EventBox> {
} }
gtk::Inhibit(false) gtk::Inhibit(false)
})); }));
},
// @prop timeout - timeout of the command
// @prop on_dropped - Command to execute when something is dropped on top of this element. The placeholder `{}` used in the command will be replaced with the uri to the dropped thing.
prop(timeout: as_duration = Duration::from_millis(200), ondropped: as_string) {
gtk_widget.drag_dest_set(
DestDefaults::ALL,
&[
TargetEntry::new("text/uri-list", gtk::TargetFlags::OTHER_APP | gtk::TargetFlags::OTHER_WIDGET, 0),
TargetEntry::new("text/plain", gtk::TargetFlags::OTHER_APP | gtk::TargetFlags::OTHER_WIDGET, 0)
],
gdk::DragAction::COPY,
);
connect_single_handler!(gtk_widget, gtk_widget.connect_drag_data_received(move |_, _, _x, _y, selection_data, _target_type, _timestamp| {
if let Some(data) = selection_data.uris().first(){
run_command(timeout, &ondropped, &[data.to_string(), "file".to_string()]);
} else if let Some(data) = selection_data.text(){
run_command(timeout, &ondropped, &[data.to_string(), "text".to_string()]);
}
}));
},
// @prop dragvalue - URI that will be provided when dragging from this widget
// @prop dragtype - Type of value that should be dragged from this widget. Possible values: $dragtype
prop(dragvalue: as_string, dragtype: as_string = "file") {
let dragtype = parse_dragtype(&dragtype)?;
if dragvalue.is_empty() {
gtk_widget.drag_source_unset();
} else {
let target_entry = match dragtype {
DragEntryType::File => TargetEntry::new("text/uri-list", gtk::TargetFlags::OTHER_APP | gtk::TargetFlags::OTHER_WIDGET, 0),
DragEntryType::Text => TargetEntry::new("text/plain", gtk::TargetFlags::OTHER_APP | gtk::TargetFlags::OTHER_WIDGET, 0),
};
gtk_widget.drag_source_set(
ModifierType::BUTTON1_MASK,
&[target_entry.clone()],
gdk::DragAction::COPY | gdk::DragAction::MOVE,
);
gtk_widget.drag_source_set_target_list(Some(&TargetList::new(&[target_entry])));
}
connect_single_handler!(gtk_widget, if !dragvalue.is_empty(), gtk_widget.connect_drag_data_get(move |_, _, data, _, _| {
match dragtype {
DragEntryType::File => data.set_uris(&[&dragvalue]),
DragEntryType::Text => data.set_text(&dragvalue),
};
}));
} }
}); });
Ok(gtk_widget) Ok(gtk_widget)
@ -707,14 +761,15 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result<gtk::Calendar> {
prop(show_day_names: as_bool) { gtk_widget.set_show_day_names(show_day_names) }, prop(show_day_names: as_bool) { gtk_widget.set_show_day_names(show_day_names) },
// @prop show-week-numbers - show week numbers // @prop show-week-numbers - show week numbers
prop(show_week_numbers: as_bool) { gtk_widget.set_show_week_numbers(show_week_numbers) }, prop(show_week_numbers: as_bool) { gtk_widget.set_show_week_numbers(show_week_numbers) },
// @prop onclick - command to run when the user selects a date. The `{}` placeholder will be replaced by the selected date. // @prop onclick - command to run when the user selects a date. The `{0}` placeholder will be replaced by the selected day, `{1}` will be replaced by the month, and `{2}` by the year.
// @prop timeout - timeout of the command // @prop timeout - timeout of the command
prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) { prop(timeout: as_duration = Duration::from_millis(200), onclick: as_string) {
connect_single_handler!(gtk_widget, gtk_widget.connect_day_selected(move |w| { connect_single_handler!(gtk_widget, gtk_widget.connect_day_selected(move |w| {
log::warn!("BREAKING CHANGE: The date is now provided via three values, set by the placeholders {{0}}, {{1}} and {{2}}. If you're currently using the onclick date, you will need to change this.");
run_command( run_command(
timeout, timeout,
&onclick, &onclick,
format!("{}.{}.{}", w.day(), w.month(), w.year()) &[w.day(), w.month(), w.year()]
) )
})); }));
} }
@ -780,6 +835,19 @@ fn parse_orientation(o: &str) -> Result<gtk::Orientation> {
} }
} }
enum DragEntryType {
File,
Text,
}
/// @var dragtype - "file", "text"
fn parse_dragtype(o: &str) -> Result<DragEntryType> {
enum_parse! { "dragtype", o,
"file" => DragEntryType::File,
"text" => DragEntryType::Text,
}
}
/// @var transition - "slideright", "slideleft", "slideup", "slidedown", "crossfade", "none" /// @var transition - "slideright", "slideleft", "slideup", "slidedown", "crossfade", "none"
fn parse_transition(t: &str) -> Result<gtk::RevealerTransitionType> { fn parse_transition(t: &str) -> Result<gtk::RevealerTransitionType> {
enum_parse! { "transition", t, enum_parse! { "transition", t,