shift-click on shuttle controller in wheel mode resets it to 1.0 (100%) (#4994)
[ardour.git] / gtk2_ardour / shuttle_control.cc
1 /*
2     Copyright (C) 2011 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 */
18
19 #include <algorithm>
20
21 #include <cairo/cairo.h>
22
23 #include "ardour/ardour.h"
24 #include "ardour/audioengine.h"
25 #include "ardour/rc_configuration.h"
26 #include "ardour/session.h"
27
28 #include "gtkmm2ext/keyboard.h"
29 #include "gtkmm2ext/gui_thread.h"
30
31 #include "ardour_ui.h"
32 #include "rgb_macros.h"
33 #include "shuttle_control.h"
34
35 #include "i18n.h"
36
37 using namespace Gtk;
38 using namespace Gtkmm2ext;
39 using namespace ARDOUR;
40 using std::min;
41 using std::max;
42
43 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
44 {
45         return FALSE;
46 }
47
48 ShuttleControl::ShuttleControl ()
49         : _controllable (new ShuttleControllable (*this))
50         , binding_proxy (_controllable)
51 {
52         ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
53
54         pattern = 0;
55         last_shuttle_request = 0;
56         last_speed_displayed = -99999999;
57         shuttle_grabbed = false;
58         shuttle_speed_on_grab = 0;
59         shuttle_fract = 0.0;
60         shuttle_max_speed = 8.0f;
61         shuttle_style_menu = 0;
62         shuttle_unit_menu = 0;
63         shuttle_context_menu = 0;
64
65         set_flags (CAN_FOCUS);
66         add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|Gdk::BUTTON_RELEASE_MASK|Gdk::BUTTON_PRESS_MASK|Gdk::POINTER_MOTION_MASK|Gdk::SCROLL_MASK);
67         set_size_request (100, 15);
68         set_name (X_("ShuttleControl"));
69
70         Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
71
72         /* gtkmm 2.4: the C++ wrapper doesn't work */
73         g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
74         // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
75 }
76
77 ShuttleControl::~ShuttleControl ()
78 {
79         cairo_pattern_destroy (pattern);
80 }
81
82 void
83 ShuttleControl::set_session (Session *s)
84 {
85         SessionHandlePtr::set_session (s);
86
87         if (_session) {
88                 set_sensitive (true);
89                 _session->add_controllable (_controllable);
90         } else {
91                 set_sensitive (false);
92         }
93 }
94
95 void
96 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
97 {
98         if (pattern) {
99                 cairo_pattern_destroy (pattern);
100                 pattern = 0;
101         }
102
103         pattern = cairo_pattern_create_linear (0, 0, alloc.get_width(), alloc.get_height());
104
105         /* add 3 color stops */
106
107         uint32_t col = ARDOUR_UI::config()->canvasvar_Shuttle.get();
108
109         int r,b,g,a;
110         UINT_TO_RGBA(col, &r, &g, &b, &a);
111
112         cairo_pattern_add_color_stop_rgb (pattern, 0.0, 0, 0, 0);
113         cairo_pattern_add_color_stop_rgb (pattern, 0.5, r/255.0, g/255.0, b/255.0);
114         cairo_pattern_add_color_stop_rgb (pattern, 1.0, 0, 0, 0);
115
116         DrawingArea::on_size_allocate (alloc);
117 }
118
119 void
120 ShuttleControl::map_transport_state ()
121 {
122         float speed = _session->transport_speed ();
123
124         if (fabs(speed) <= (2*DBL_EPSILON)) {
125                 shuttle_fract = 0;
126         } else {
127                 if (Config->get_shuttle_units() == Semitones) {
128                         bool reverse;
129                         int semi = speed_as_semitones (speed, reverse);
130                         shuttle_fract = semitones_as_fract (semi, reverse);
131                 } else {
132                         shuttle_fract = speed/shuttle_max_speed;
133                 }
134         }
135
136         queue_draw ();
137 }
138
139 void
140 ShuttleControl::build_shuttle_context_menu ()
141 {
142         using namespace Menu_Helpers;
143
144         shuttle_context_menu = new Menu();
145         MenuList& items = shuttle_context_menu->items();
146
147         Menu* speed_menu = manage (new Menu());
148         MenuList& speed_items = speed_menu->items();
149
150         Menu* units_menu = manage (new Menu);
151         MenuList& units_items = units_menu->items();
152         RadioMenuItem::Group units_group;
153
154         units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
155         if (Config->get_shuttle_units() == Percentage) {
156                 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
157         }
158         units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
159         if (Config->get_shuttle_units() == Semitones) {
160                 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
161         }
162         items.push_back (MenuElem (_("Units"), *units_menu));
163
164         Menu* style_menu = manage (new Menu);
165         MenuList& style_items = style_menu->items();
166         RadioMenuItem::Group style_group;
167
168         style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
169         if (Config->get_shuttle_behaviour() == Sprung) {
170                 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
171         }
172         style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
173         if (Config->get_shuttle_behaviour() == Wheel) {
174                 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
175         }
176
177         items.push_back (MenuElem (_("Mode"), *style_menu));
178
179         RadioMenuItem::Group speed_group;
180
181         speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
182         if (shuttle_max_speed == 8.0) {
183                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
184         }
185         speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
186         if (shuttle_max_speed == 6.0) {
187                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
188         }
189         speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
190         if (shuttle_max_speed == 4.0) {
191                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
192         }
193         speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
194         if (shuttle_max_speed == 3.0) {
195                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
196         }
197         speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
198         if (shuttle_max_speed == 2.0) {
199                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
200         }
201         speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
202         if (shuttle_max_speed == 1.5) {
203                 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
204         }
205
206         items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
207
208 }
209
210 void
211 ShuttleControl::show_shuttle_context_menu ()
212 {
213         if (shuttle_context_menu == 0) {
214                 build_shuttle_context_menu ();
215         }
216
217         shuttle_context_menu->popup (1, gtk_get_current_event_time());
218 }
219
220 void
221 ShuttleControl::set_shuttle_max_speed (float speed)
222 {
223         shuttle_max_speed = speed;
224 }
225
226 bool
227 ShuttleControl::on_button_press_event (GdkEventButton* ev)
228 {
229         if (!_session) {
230                 return true;
231         }
232
233         if (binding_proxy.button_press_handler (ev)) {
234                 return true;
235         }
236
237         if (Keyboard::is_context_menu_event (ev)) {
238                 show_shuttle_context_menu ();
239                 return true;
240         }
241
242         switch (ev->button) {
243         case 1:
244                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
245                         if (_session->transport_rolling()) {
246                                 _session->request_transport_speed (1.0);
247                         }
248                 } else {
249                         add_modal_grab ();
250                         shuttle_grabbed = true;
251                         shuttle_speed_on_grab = _session->transport_speed ();
252                         mouse_shuttle (ev->x, true);
253                 }
254                 break;
255
256         case 2:
257         case 3:
258                 return true;
259                 break;
260         }
261
262         return true;
263 }
264
265 bool
266 ShuttleControl::on_button_release_event (GdkEventButton* ev)
267 {
268         if (!_session) {
269                 return true;
270         }
271
272         switch (ev->button) {
273         case 1:
274                 if (shuttle_grabbed) {
275                         shuttle_grabbed = false;
276                         remove_modal_grab ();
277                         
278                         if (Config->get_shuttle_behaviour() == Sprung) {
279                                 _session->request_transport_speed (shuttle_speed_on_grab);
280                         } else {
281                                 mouse_shuttle (ev->x, true);
282                         }
283                 }
284                 return true;
285
286         case 2:
287                 if (_session->transport_rolling()) {
288                         _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
289                 }
290                 return true;
291
292         case 3:
293         default:
294                 return true;
295
296         }
297
298         return true;
299 }
300
301 bool
302 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
303 {
304         return false;
305 }
306
307 bool
308 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
309 {
310         if (!_session || Config->get_shuttle_behaviour() != Wheel) {
311                 return true;
312         }
313
314         bool semis = (Config->get_shuttle_units() == Semitones);
315
316         switch (ev->direction) {
317         case GDK_SCROLL_UP:
318         case GDK_SCROLL_RIGHT:
319                 if (semis) {
320                         if (shuttle_fract == 0) {
321                                 shuttle_fract = semitones_as_fract (1, false);
322                         } else {
323                                 bool rev;
324                                 int st = fract_as_semitones (shuttle_fract, rev);
325                                 shuttle_fract = semitones_as_fract (st + 1, rev);
326                         }
327                 } else {
328                         shuttle_fract += 0.00125;
329                 }
330                 break;
331         case GDK_SCROLL_DOWN:
332         case GDK_SCROLL_LEFT:
333                 if (semis) {
334                         if (shuttle_fract == 0) {
335                                 shuttle_fract = semitones_as_fract (1, true);
336                         } else {
337                                 bool rev;
338                                 int st = fract_as_semitones (shuttle_fract, rev);
339                                 shuttle_fract = semitones_as_fract (st - 1, rev);
340                         }
341                 } else {
342                         shuttle_fract -= 0.00125;
343                 }
344                 break;
345         default:
346                 return false;
347         }
348         
349         if (semis) {
350
351                 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
352                 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
353
354                 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
355                    to the far side of it.
356                 */
357
358                 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
359                         switch (ev->direction) {
360                         case GDK_SCROLL_UP:
361                         case GDK_SCROLL_RIGHT:
362                                 shuttle_fract = upper_side_of_dead_zone;
363                                 break;
364                         case GDK_SCROLL_DOWN:
365                         case GDK_SCROLL_LEFT:
366                                 shuttle_fract = lower_side_of_dead_zone;
367                                 break;
368                         default:
369                                 /* impossible, checked above */
370                                 return false;
371                         }
372                 }
373         }
374
375         use_shuttle_fract (true);
376
377         return true;
378 }
379
380 bool
381 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
382 {
383         if (!_session || !shuttle_grabbed) {
384                 return true;
385         }
386
387         return mouse_shuttle (ev->x, false);
388 }
389
390 gint
391 ShuttleControl::mouse_shuttle (double x, bool force)
392 {
393         double const center = get_width() / 2.0;
394         double distance_from_center = x - center;
395
396         if (distance_from_center > 0) {
397                 distance_from_center = min (distance_from_center, center);
398         } else {
399                 distance_from_center = max (distance_from_center, -center);
400         }
401
402         /* compute shuttle fract as expressing how far between the center
403            and the edge we are. positive values indicate we are right of
404            center, negative values indicate left of center
405         */
406
407         shuttle_fract = distance_from_center / center; // center == half the width
408         use_shuttle_fract (force);
409         return true;
410 }
411
412 void
413 ShuttleControl::set_shuttle_fract (double f)
414 {
415         shuttle_fract = f;
416         use_shuttle_fract (false);
417 }
418
419 int
420 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
421 {
422         assert (speed != 0.0);
423
424         if (speed < 0.0) {
425                 reverse = true;
426                 return (int) round (12.0 * fast_log2 (-speed));
427         } else {
428                 reverse = false;
429                 return (int) round (12.0 * fast_log2 (speed));
430         }
431 }
432
433 float
434 ShuttleControl::semitones_as_speed (int semi, bool reverse)
435 {
436         if (reverse) {
437                 return -pow (2.0, (semi / 12.0));
438         } else {
439                 return pow (2.0, (semi / 12.0));
440         }
441 }
442
443 float
444 ShuttleControl::semitones_as_fract (int semi, bool reverse)
445 {
446         float speed = semitones_as_speed (semi, reverse);
447         return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
448 }
449
450 int
451 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
452 {
453         assert (fract != 0.0);
454         return speed_as_semitones (fract * 4.0, reverse);
455 }
456
457 void
458 ShuttleControl::use_shuttle_fract (bool force)
459 {
460         microseconds_t now = get_microseconds();
461
462         shuttle_fract = max (-1.0f, shuttle_fract);
463         shuttle_fract = min (1.0f, shuttle_fract);
464
465         /* do not attempt to submit a motion-driven transport speed request
466            more than once per process cycle.
467         */
468
469         if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
470                 return;
471         }
472
473         last_shuttle_request = now;
474
475         double speed = 0;
476
477         if (Config->get_shuttle_units() == Semitones) {
478                 if (shuttle_fract != 0.0) {
479                         bool reverse;
480                         int semi = fract_as_semitones (shuttle_fract, reverse);
481                         speed = semitones_as_speed (semi, reverse);
482                 } else {
483                         speed = 0.0;
484                 }
485         } else {
486                 speed = shuttle_max_speed * shuttle_fract;
487         }
488
489         _session->request_transport_speed_nonzero (speed, true);
490 }
491
492 bool
493 ShuttleControl::on_expose_event (GdkEventExpose*)
494 {
495         cairo_text_extents_t extents;
496         Glib::RefPtr<Gdk::Window> win (get_window());
497         Glib::RefPtr<Gtk::Style> style (get_style());
498
499         cairo_t* cr = gdk_cairo_create (win->gobj());
500
501         cairo_set_source (cr, pattern);
502         cairo_rectangle (cr, 0.0, 0.0, get_width(), get_height());
503         cairo_fill_preserve (cr);
504
505         cairo_set_source_rgb (cr, 0, 0, 0.0);
506         cairo_stroke (cr);
507
508         float speed = 0.0;
509
510         if (_session) {
511                 speed = _session->transport_speed ();
512         }
513
514         /* Marker */
515
516         double visual_fraction = std::min (1.0f, speed/shuttle_max_speed);
517         double x = (get_width() / 2.0) + (0.5 * (get_width() * visual_fraction));
518         cairo_move_to (cr, x, 1);
519         cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
520         cairo_line_to (cr, x, get_height()-1);
521         cairo_stroke (cr);
522
523         /* speed text */
524
525         char buf[32];
526
527         if (speed != 0) {
528
529                 if (Config->get_shuttle_units() == Percentage) {
530
531                         if (speed == 1.0) {
532                                 snprintf (buf, sizeof (buf), "%s", _("Playing"));
533                         } else {
534                                 if (speed < 0.0) {
535                                         snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
536                                 } else {
537                                         snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
538                                 }
539                         }
540
541                 } else {
542
543                         bool reversed;
544                         int semi = speed_as_semitones (speed, reversed);
545
546                         if (reversed) {
547                                 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
548                         } else {
549                                 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
550                         }
551                 }
552
553         } else {
554                 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
555         }
556
557         last_speed_displayed = speed;
558
559         cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
560         cairo_text_extents (cr, buf, &extents);
561         cairo_move_to (cr, 10, extents.height + 2);
562         cairo_show_text (cr, buf);
563
564         /* style text */
565
566
567         switch (Config->get_shuttle_behaviour()) {
568         case Sprung:
569                 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
570                 break;
571         case Wheel:
572                 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
573                 break;
574         }
575
576         cairo_text_extents (cr, buf, &extents);
577
578         cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 2);
579         cairo_show_text (cr, buf);
580
581         cairo_destroy (cr);
582
583         return true;
584 }
585
586 void
587 ShuttleControl::shuttle_unit_clicked ()
588 {
589         if (shuttle_unit_menu == 0) {
590                 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
591         }
592         shuttle_unit_menu->popup (1, gtk_get_current_event_time());
593 }
594
595 void
596 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
597 {
598         Config->set_shuttle_behaviour (s);
599 }
600
601 void
602 ShuttleControl::set_shuttle_units (ShuttleUnits s)
603 {
604         Config->set_shuttle_units (s);
605 }
606
607 void
608 ShuttleControl::update_speed_display ()
609 {
610         if (_session->transport_speed() != last_speed_displayed) {
611                 queue_draw ();
612         }
613 }
614
615 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
616         : PBD::Controllable (X_("Shuttle"))
617         , sc (s)
618 {
619 }
620
621 void
622 ShuttleControl::ShuttleControllable::set_value (double val)
623 {
624         double fract;
625
626         if (val == 0.5) {
627                 fract = 0.0;
628         } else {
629                 if (val < 0.5) {
630                         fract = -((0.5 - val)/0.5);
631                 } else {
632                         fract = ((val - 0.5)/0.5);
633                 }
634         }
635
636         sc.set_shuttle_fract (fract);
637 }
638
639 double
640 ShuttleControl::ShuttleControllable::get_value () const
641 {
642         return sc.get_shuttle_fract ();
643 }
644
645 void
646 ShuttleControl::parameter_changed (std::string p)
647 {
648         if (p == "shuttle-behaviour") {
649                 switch (Config->get_shuttle_behaviour ()) {
650                 case Sprung:
651                         /* back to Sprung - reset to speed = 1.0 if playing
652                          */
653                         if (_session) {
654                                 if (_session->transport_rolling()) {
655                                         if (_session->transport_speed() == 1.0) {
656                                                 queue_draw ();
657                                         } else {
658                                                 /* reset current speed and
659                                                    revert to 1.0 as the default
660                                                 */
661                                                 _session->request_transport_speed (1.0);
662                                                 /* redraw when speed changes */
663                                         }
664                                 } else {
665                                         queue_draw ();
666                                 }
667                         }
668                         break;
669
670                 case Wheel:
671                         queue_draw ();
672                         break;
673                 }
674
675         } else if (p == "shuttle-units") {
676                 queue_draw ();
677         }
678 }