Skip to content
This repository was archived by the owner on Feb 2, 2019. It is now read-only.

Commit a3c455b

Browse files
committed
{ui, backend}: action DnD, warnings makeover, fix timings
- You can now drag one action onto another to set the target of the dropped-onto action! - This includes nice little UI hints like the cursor not indicating droppability where applicable. - We no longer have that crappy warnings dialog; warnings now appear as banners at the top of the window, like how Spotify does it. - We also have a "clear all warnings" button now...
1 parent a02f66a commit a3c455b

File tree

9 files changed

+512
-345
lines changed

9 files changed

+512
-345
lines changed

Cargo.lock

Lines changed: 120 additions & 118 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sqa-backend/src/actions/audio.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,10 @@ impl EditableAction for Controller {
200200
impl ActionController for Controller {
201201
fn desc(&self, _: &Context) -> String {
202202
if let Some(Ok(ref url)) = self.url {
203-
format!("Play audio at {}", url.file_name().unwrap().to_string_lossy())
203+
format!("{}", url.file_name().unwrap().to_string_lossy())
204204
}
205205
else {
206-
format!("Play audio [invalid]")
206+
format!("[invalid audio cue]")
207207
}
208208
}
209209
fn verify_params(&self, ctx: &Context) -> Vec<ParameterError> {

sqa-backend/src/actions/fade.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ impl ActionController for Controller {
9898
}
9999
fn poll(&mut self, mut ctx: ControllerParams) -> bool {
100100
let _ = self.timeout.poll();
101-
trace!("poll fired");
102101
if self.timeout.is_complete() {
103102
trace!("changing state");
104103
ctx.change_state(PlaybackState::Inactive);

sqa-backend/src/actions/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,16 +320,21 @@ impl Action {
320320
let _ = self.timeout.poll();
321321
let mut continue_polling = false;
322322
if self.timeout.is_complete() {
323+
trace!("poll fired; timeout complete");
323324
if let PlaybackState::Active(_) = self.state {
324325
let st = action!(self.ctl).duration_info();
325-
let delta_millis;
326+
let mut delta_millis;
326327
if let Some(dur) = st {
327328
let (elapsed, pos) = dur.elapsed(false);
328329
let elapsed = elapsed.subsec_nanos();
329330
let delta_nanos = if pos {
330331
1_000_000_000 - elapsed
331332
} else { elapsed };
332333
delta_millis = delta_nanos / 1_000_000;
334+
if delta_millis < 500 {
335+
delta_millis = 1000 + delta_millis;
336+
}
337+
trace!("elapsed: {:?}; waiting {}ms", elapsed, delta_millis);
333338
}
334339
else {
335340
delta_millis = 1000;
@@ -345,6 +350,7 @@ impl Action {
345350
}
346351
}
347352
else if self.timeout.is_waiting() {
353+
trace!("poll fired; timeout still waiting");
348354
let _ = self.timeout.poll();
349355
continue_polling = true;
350356
}

sqa-ui/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ sqa-backend = { path = "../sqa-backend" }
88
rosc = { path = "../rosc" }
99
tokio-core = "0.1"
1010
futures = "0.1"
11-
gtk = { git = "https://github.com/gtk-rs/gtk", features = ["v3_12"] }
12-
gdk = { git = "https://github.com/gtk-rs/gdk", features = ["v3_12"] }
11+
gtk = { git = "https://github.com/gtk-rs/gtk", features = ["v3_16"] }
12+
gdk = { git = "https://github.com/gtk-rs/gdk", features = ["v3_16"] }
1313
glib = { git = "https://github.com/gtk-rs/glib" }
1414
error-chain = "0.10"
1515
time = "0.1"

sqa-ui/src/actions/fade.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,5 @@ impl ActionUI for FadeUI {
179179
fn change_cur_page(&mut self, cp: Option<u32>) {
180180
self.temp.change_cur_page(cp)
181181
}
182+
fn is_dnd_target(&self) -> bool { true }
182183
}

sqa-ui/src/actions/mod.rs

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use gtk::prelude::*;
2-
use gtk::{self, Widget, Menu, TreeView, ListStore, Builder, MenuItem, TreeSelection, TargetEntry, TargetFlags, Stack};
2+
use gtk::{self, Widget, Menu, TreeView, ListStore, SelectionMode, Builder, MenuItem, TreeSelection, TreeIter, TargetEntry, TargetFlags, Stack};
3+
use gtk::Box as GBox;
34
use gdk::WindowExt;
45
use gdk;
6+
use gtk::DragContextExtManual;
57
use uuid::Uuid;
68
use std::collections::HashMap;
79
use std::mem;
10+
use std::cell::Cell;
11+
use std::rc::Rc;
812
use sync::UISender;
913
use sqa_backend::codec::{Command, Reply};
1014
use sqa_backend::mixer::MixerConf;
@@ -40,6 +44,7 @@ pub enum ActionMessageInner {
4044
EditAction,
4145
ChangeName(Option<String>),
4246
ChangePrewait(Duration),
47+
Retarget(Uuid),
4348
CloseButton,
4449
}
4550
impl DurationEntryMessage for ActionMessageInner {
@@ -73,6 +78,7 @@ pub struct ActionController {
7378
mexec: MenuItem,
7479
mcreate_audio: MenuItem,
7580
mcreate_fade: MenuItem,
81+
drag_notif: GBox,
7682
cur_page: Option<u32>,
7783
sel_handler: u64
7884
}
@@ -86,6 +92,7 @@ pub trait ActionUI {
8692
fn on_action_list(&mut self, _l: &HashMap<Uuid, OpaqueAction>) {}
8793
fn on_selection_finished(&mut self, _sel: Uuid) {}
8894
fn on_selection_cancelled(&mut self) {}
95+
fn is_dnd_target(&self) -> bool { false }
8996
fn change_cur_page(&mut self, Option<u32>);
9097
}
9198

@@ -95,13 +102,23 @@ struct TreeSelectGetter {
95102
ts: ListStore
96103
}
97104
impl TreeSelectGetter {
105+
pub fn iter_to_value(&self, ti: TreeIter) -> Option<Uuid> {
106+
if let Some(v) = self.ts.get_value(&ti, 0).get::<String>() {
107+
if let Ok(uu) = Uuid::parse_str(&v) {
108+
return Some(uu);
109+
}
110+
}
111+
None
112+
}
113+
pub fn iter_is_dnd_target(&self, ti: TreeIter) -> bool {
114+
if let Some(v) = self.ts.get_value(&ti, 7).get::<bool>() {
115+
return v;
116+
}
117+
false
118+
}
98119
pub fn get(&self) -> Option<Uuid> {
99120
if let Some((_, ti)) = self.sel.get_selected() {
100-
if let Some(v) = self.ts.get_value(&ti, 0).get::<String>() {
101-
if let Ok(uu) = Uuid::parse_str(&v) {
102-
return Some(uu);
103-
}
104-
}
121+
return self.iter_to_value(ti)
105122
}
106123
None
107124
}
@@ -129,11 +146,12 @@ impl ActionController {
129146
let sel_handler = 0;
130147
build!(ActionController using b
131148
with ctls, opas, tx, mixer, cur_widget, cur_sel, cur_page, sel_handler
132-
get view, store, menu, medit, mload, mexec, mdelete, mcreate_audio, mcreate_fade, sidebar)
149+
get view, store, menu, medit, mload, mexec, mdelete, mcreate_audio, mcreate_fade, sidebar, drag_notif)
133150
}
134151
pub fn bind(&mut self, tx: &UISender) {
135152
use self::ActionMessageInner::*;
136153
self.tx = Some(tx.clone());
154+
self.view.get_selection().set_mode(SelectionMode::Single);
137155
let tsg = TreeSelectGetter { ts: self.store.clone(), sel: self.view.get_selection() };
138156
bind_action_menu_items! {
139157
self, tx, tsg,
@@ -160,13 +178,105 @@ impl ActionController {
160178
self.mcreate_fade.connect_activate(clone!(tx; |_| {
161179
tx.send_internal(ActionInternalMessage::Create("fade"));
162180
}));
163-
let dnd_targets = vec![TargetEntry::new("text/uri-list", TargetFlags::empty(), 0)];
181+
let row_dnd_target = TargetEntry::new("STRING", gtk::TARGET_SAME_WIDGET, 0);
182+
let dnd_targets = vec![
183+
TargetEntry::new("text/uri-list", TargetFlags::empty(), 0),
184+
row_dnd_target.clone()
185+
];
186+
self.view.drag_source_set(gdk::BUTTON1_MASK, &vec![row_dnd_target], gdk::ACTION_COPY | gdk::ACTION_MOVE);
164187
self.view.drag_dest_set(gtk::DEST_DEFAULT_ALL, &dnd_targets, gdk::ACTION_COPY | gdk::ACTION_MOVE);
188+
let dn = self.drag_notif.clone();
189+
let press_coords = Rc::new(Cell::new((0.0, 0.0)));
190+
let drag_uu: Rc<Cell<Option<Uuid>>> = Rc::new(Cell::new(None));
191+
self.view.connect_button_press_event(clone!(press_coords; |_, eb| {
192+
let coords = eb.get_position();
193+
debug!("dnd: set position to {:?}", coords);
194+
press_coords.set(coords);
195+
Inhibit(false)
196+
}));
197+
self.view.connect_drag_begin(clone!(dn, tsg, press_coords, drag_uu; |slf, dctx| {
198+
let (x, y) = press_coords.get();
199+
let (x, y) = (x.trunc() as _, y.trunc() as _);
200+
if let Some((Some(path), _, _, _)) = slf.get_path_at_pos(x, y) {
201+
debug!("dnd: drag begun");
202+
dn.show_all();
165203

166-
self.view.connect_drag_data_received(clone!(tx; |_, _, _, _, data, _, _| {
204+
if let Some(ti) = tsg.ts.get_iter(&path) {
205+
if let Some(uu) = tsg.iter_to_value(ti) {
206+
debug!("dnd: dragging uu {}", uu);
207+
drag_uu.set(Some(uu));
208+
}
209+
}
210+
if let Some(surf) = slf.create_row_drag_icon(&path) {
211+
dctx.drag_set_icon_surface(&surf);
212+
}
213+
}
214+
else {
215+
debug!("dnd: cancelling failed drag");
216+
}
217+
}));
218+
self.view.connect_drag_motion(clone!(drag_uu, tsg; |slf, dctx, x, y, _| {
219+
use gdk::DragContextExtManual;
220+
if drag_uu.get().is_some() {
221+
let (x, y) = slf.convert_widget_to_bin_window_coords(x, y);
222+
if let Some((Some(path), _, _, _)) = slf.get_path_at_pos(x, y) {
223+
if let Some(ti) = tsg.ts.get_iter(&path) {
224+
if !tsg.iter_is_dnd_target(ti) {
225+
dctx.drag_status(gdk::DragAction::empty(), 0);
226+
}
227+
else {
228+
dctx.drag_status(gdk::ACTION_MOVE, 0);
229+
}
230+
return Inhibit(false);
231+
}
232+
}
233+
dctx.drag_status(gdk::DragAction::empty(), 0);
234+
}
235+
else {
236+
dctx.drag_status(gdk::ACTION_COPY, 0);
237+
}
238+
Inhibit(false)
239+
}));
240+
self.view.connect_drag_end(clone!(drag_uu; |_, _| {
241+
debug!("dnd: drag ended");
242+
drag_uu.set(None);
243+
dn.hide();
244+
}));
245+
self.view.connect_drag_data_get(clone!(drag_uu; |_, _, data, _, _| {
246+
if let Some(uu) = drag_uu.get() {
247+
debug!("dnd: drag_data_get called for uu {}", uu);
248+
data.set_text(&format!("{}", uu), -1);
249+
}
250+
else {
251+
debug!("dnd: drag_data_get called, but no uu");
252+
}
253+
}));
254+
self.view.connect_drag_data_received(clone!(tx, tsg; |slf, _, x, y, data, _, _| {
167255
let uris = data.get_uris();
168256
debug!("dnd: got uris {:?}", uris);
169-
if uris.len() == 0 { return; }
257+
if uris.len() == 0 {
258+
if let Some(txt) = data.get_text() {
259+
if let Ok(uu) = txt.parse::<Uuid>() {
260+
debug!("dnd: got UUID dropped {}, widget coords ({}, {})", uu, x, y);
261+
let (x, y) = slf.convert_widget_to_bin_window_coords(x, y);
262+
debug!("dnd: bin window coords ({}, {})", x, y);
263+
if let Some((Some(path), _, _, _)) = slf.get_path_at_pos(x, y) {
264+
if let Some(ti) = tsg.ts.get_iter(&path) {
265+
if let Some(uu2) = tsg.iter_to_value(ti) {
266+
debug!("dnd: was dropped onto {}", uu2);
267+
tx.send_internal((uu2, ActionMessageInner::Retarget(uu)));
268+
}
269+
}
270+
}
271+
}
272+
else {
273+
return;
274+
}
275+
}
276+
else {
277+
return;
278+
}
279+
}
170280
tx.send_internal(ActionInternalMessage::FilesDropped(uris));
171281
}));
172282

@@ -194,26 +304,42 @@ impl ActionController {
194304
Active(_) => "gtk-media-play",
195305
Errored(_) => "gtk-dialog-error"
196306
};
307+
let mut duration_progress = 0;
197308
let duration = match action.state {
198309
Active(Some(ref dur)) => {
199310
let (elapsed, pos) = dur.elapsed(true);
200311
let text = if pos { "T+" } else { "T-" };
312+
if pos && dur.est_duration.is_some() {
313+
let ed = dur.est_duration.unwrap();
314+
let ed = (ed.as_secs() as f32 * 1_000_000_000.0) + ed.subsec_nanos() as f32;
315+
let elapsed = (elapsed.as_secs() as f32 * 1_000_000_000.0) + elapsed.subsec_nanos() as f32;
316+
let mut perc = ((elapsed / ed) * 100.0).trunc();
317+
if perc > 100.0 {
318+
perc = 100.0;
319+
}
320+
duration_progress = perc as u32;
321+
}
201322
format!("{}{}", text, DurationEntry::format(elapsed, false))
202323
},
203324
_ => "".into()
204325
};
326+
let is_dnd_target = self.ctls.get(&uu).unwrap().is_dnd_target();
205327
let iter = self.store.insert_with_values(None, &[
206328
0, // uuid
207329
1, // description
208330
2, // icon-state (playback state icon)
209331
3, // icon-type (action type icon)
210332
4, // duration
333+
6, // duration progress bar (0-100 percent)
334+
7, // is-dnd-target (can we drop other actions onto this one?)
211335
], &[
212336
&uu.to_string(),
213337
&action.display_name(),
214338
&state,
215339
&typ,
216-
&duration
340+
&duration,
341+
&duration_progress,
342+
&is_dnd_target
217343
]);
218344
if let Some(u2) = sel {
219345
if *uu == u2 {
@@ -407,6 +533,7 @@ impl ActionController {
407533
tx.send(Command::UpdateActionMetadata { uuid: msg.0, meta: opa.meta.clone() });
408534
}
409535
},
536+
Retarget(uu) => ctl.on_selection_finished(uu),
410537
CloseButton => ctl.close_window(),
411538
x => ctl.on_message(x)
412539
}

0 commit comments

Comments
 (0)