From 7648eb80867b0e99f08854214c6227351abf3b0e Mon Sep 17 00:00:00 2001 From: ElKowar <5300871+elkowar@users.noreply.github.com> Date: Tue, 19 Apr 2022 13:00:50 +0200 Subject: [PATCH] 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 --- CHANGELOG.md | 9 ++ crates/eww/src/widgets/mod.rs | 23 ++-- crates/eww/src/widgets/widget_definitions.rs | 104 +++++++++++++++---- 3 files changed, 112 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01a11af..625e6b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to eww will be listed here, starting at changes since versio ## [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 - Add `eww inspector` command - 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 `scroll` widget (By: viandoxdev) - Add `notification` window type +- Add drag and drop functionality to eventbox ### Notable Internal changes - Rework state management completely, now making local state and dynamic widget hierarchy changes possible. diff --git a/crates/eww/src/widgets/mod.rs b/crates/eww/src/widgets/mod.rs index 98b27b1..a930332 100644 --- a/crates/eww/src/widgets/mod.rs +++ b/crates/eww/src/widgets/mod.rs @@ -6,15 +6,26 @@ pub mod def_widget_macro; pub mod graph; pub mod widget_definitions; -const CMD_STRING_PLACEHODLER: &str = "{}"; - -/// Run a command that was provided as an attribute. This command may use a -/// placeholder ('{}') which will be replaced by the value provided as `arg` -pub(self) fn run_command(timeout: std::time::Duration, cmd: &str, arg: T) { +/// 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. +/// This can either be the placeholder `{}`, which will be replaced by the first argument, +/// Or a placeholder like `{0}`, `{1}`, etc, which will refer to the respective argument. +pub(self) fn run_command(timeout: std::time::Duration, cmd: &str, args: &[T]) +where + T: 'static + std::fmt::Display + Send + Sync + Clone, +{ use wait_timeout::ChildExt; + let args = args.to_vec(); let cmd = cmd.to_string(); 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); let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn(); match child { diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index a7fd0cf..540a39b 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -10,8 +10,8 @@ use crate::{ use anyhow::{anyhow, Context, Result}; use codespan_reporting::diagnostic::Severity; use eww_shared_util::Spanned; -use gdk::NotifyType; -use gtk::{self, glib, prelude::*}; +use gdk::{ModifierType, NotifyType}; +use gtk::{self, glib, prelude::*, DestDefaults, TargetEntry, TargetList}; use itertools::Itertools; 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, /// the previously connected singal handler first gets disconnected. macro_rules! connect_single_handler { - ($widget:ident, $connect_expr:expr) => {{ + ($widget:ident, if $cond:expr, $connect_expr:expr) => {{ static ID: Lazy>> = 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 { $widget.disconnect(old); } }}; + ($widget:ident, $connect_expr:expr) => {{ + connect_single_handler!($widget, if true, $connect_expr) + }}; } // TODO figure out how to @@ -200,7 +207,7 @@ pub(super) fn resolve_range_attrs(bargs: &mut BuilderArgs, gtk_widget: >k::Ran gtk_widget.set_sensitive(true); gtk_widget.add_events(gdk::EventMask::PROPERTY_CHANGE_MASK); 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 Result { // @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 = "") { 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 { // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { 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 Result { // @prop timeout - timeout of the command prop(timeout: as_duration = Duration::from_millis(200), onchange: as_string) { 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_widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); connect_single_handler!(gtk_widget, gtk_widget.connect_button_press_event(move |_, evt| { match evt.button() { - 1 => run_command(timeout, &onclick, ""), - 2 => run_command(timeout, &onmiddleclick, ""), - 3 => run_command(timeout, &onrightclick, ""), + 1 => run_command(timeout, &onclick, &[""]), + 2 => run_command(timeout, &onmiddleclick, &[""]), + 3 => run_command(timeout, &onrightclick, &[""]), _ => {}, } gtk::Inhibit(false) @@ -552,7 +559,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { connect_single_handler!(gtk_widget, gtk_widget.connect_scroll_event(move |_, evt| { let delta = evt.delta().1; 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) })); @@ -563,7 +570,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk_widget.add_events(gdk::EventMask::ENTER_NOTIFY_MASK); connect_single_handler!(gtk_widget, gtk_widget.connect_enter_notify_event(move |_, evt| { 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) })); @@ -574,7 +581,7 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { gtk_widget.add_events(gdk::EventMask::LEAVE_NOTIFY_MASK); connect_single_handler!(gtk_widget, gtk_widget.connect_leave_notify_event(move |_, evt| { 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) })); @@ -603,6 +610,53 @@ fn build_gtk_event_box(bargs: &mut BuilderArgs) -> Result { } 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) @@ -707,14 +761,15 @@ fn build_gtk_calendar(bargs: &mut BuilderArgs) -> Result { 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: 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: as_duration = Duration::from_millis(200), onclick: as_string) { 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( timeout, &onclick, - format!("{}.{}.{}", w.day(), w.month(), w.year()) + &[w.day(), w.month(), w.year()] ) })); } @@ -780,6 +835,19 @@ fn parse_orientation(o: &str) -> Result { } } +enum DragEntryType { + File, + Text, +} + +/// @var dragtype - "file", "text" +fn parse_dragtype(o: &str) -> Result { + enum_parse! { "dragtype", o, + "file" => DragEntryType::File, + "text" => DragEntryType::Text, + } +} + /// @var transition - "slideright", "slideleft", "slideup", "slidedown", "crossfade", "none" fn parse_transition(t: &str) -> Result { enum_parse! { "transition", t,