2 * Copyright (C) 2011-2016 Paul Davis <paul@linuxaudiosystems.com>
3 * Copyright (C) 2011 Carl Hetherington <carl@carlh.net>
4 * Copyright (C) 2011 David Robillard <d@drobilla.net>
5 * Copyright (C) 2012-2019 Robin Gareus <robin@gareus.org>
6 * Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 #define BASELINESTRETCH (1.25)
29 #include "ardour/ardour.h"
30 #include "ardour/audioengine.h"
31 #include "ardour/rc_configuration.h"
32 #include "ardour/session.h"
34 #include "gtkmm2ext/colors.h"
35 #include "gtkmm2ext/keyboard.h"
36 #include "gtkmm2ext/gui_thread.h"
37 #include "gtkmm2ext/utils.h"
38 #include "gtkmm2ext/rgb_macros.h"
40 #include "widgets/tooltips.h"
43 #include "rgb_macros.h"
44 #include "shuttle_control.h"
49 using namespace Gtkmm2ext;
50 using namespace ARDOUR;
51 using namespace ArdourWidgets;
55 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
60 ShuttleControl::ShuttleControl ()
61 : _controllable (new ShuttleControllable (*this))
62 , binding_proxy (_controllable)
64 _info_button.set_layout_font (UIConfiguration::instance().get_NormalFont());
65 _info_button.set_sizing_text (S_("LogestShuttle|< +00 st"));
66 _info_button.set_name ("shuttle text");
67 _info_button.set_sensitive (false);
68 _info_button.set_visual_state (Gtkmm2ext::NoVisualState);
69 _info_button.set_elements (ArdourButton::Text);
71 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
75 last_shuttle_request = 0;
76 last_speed_displayed = -99999999;
77 shuttle_grabbed = false;
78 shuttle_speed_on_grab = 0;
80 shuttle_max_speed = Config->get_max_transport_speed();
81 shuttle_context_menu = 0;
84 set_flags (CAN_FOCUS);
85 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);
86 set_name (X_("ShuttleControl"));
90 shuttle_max_speed = Config->get_shuttle_max_speed();
92 if (shuttle_max_speed >= Config->get_max_transport_speed()) { shuttle_max_speed = Config->get_max_transport_speed(); }
93 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
94 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
95 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
96 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
97 else { shuttle_max_speed = 1.5f; }
99 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
100 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
104 /* gtkmm 2.4: the C++ wrapper doesn't work */
105 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
106 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
109 ShuttleControl::~ShuttleControl ()
111 cairo_pattern_destroy (pattern);
112 cairo_pattern_destroy (shine_pattern);
113 delete shuttle_context_menu;
117 ShuttleControl::set_session (Session *s)
119 SessionHandlePtr::set_session (s);
122 set_sensitive (true);
123 _session->add_controllable (_controllable);
125 set_sensitive (false);
130 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
133 cairo_pattern_destroy (pattern);
135 cairo_pattern_destroy (shine_pattern);
139 CairoWidget::on_size_allocate ( alloc);
142 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
143 uint32_t col = UIConfiguration::instance().color ("shuttle");
145 UINT_TO_RGBA(col, &r, &g, &b, &a);
146 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
147 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
148 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
151 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
152 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
153 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
154 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
158 ShuttleControl::map_transport_state ()
160 float speed = _session->actual_speed ();
162 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
163 && !( speed == 1.f && last_speed_displayed != 1.f)
164 && !( speed == 0.f && last_speed_displayed != 0.f)
167 return; // nothing to see here, move along.
170 // Q: is there a good reason why we re-calculate this every time?
171 if (fabs(speed) <= (2*DBL_EPSILON)) {
174 if (Config->get_shuttle_units() == Semitones) {
176 int semi = speed_as_semitones (speed, reverse);
177 shuttle_fract = semitones_as_fract (semi, reverse);
179 shuttle_fract = speed/shuttle_max_speed;
187 ShuttleControl::build_shuttle_context_menu ()
189 using namespace Menu_Helpers;
191 shuttle_context_menu = new Menu();
192 MenuList& items = shuttle_context_menu->items();
194 Menu* speed_menu = manage (new Menu());
195 MenuList& speed_items = speed_menu->items();
197 Menu* units_menu = manage (new Menu);
198 MenuList& units_items = units_menu->items();
199 RadioMenuItem::Group units_group;
201 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
202 if (Config->get_shuttle_units() == Percentage) {
203 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
205 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
206 if (Config->get_shuttle_units() == Semitones) {
207 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
209 items.push_back (MenuElem (_("Units"), *units_menu));
211 Menu* style_menu = manage (new Menu);
212 MenuList& style_items = style_menu->items();
213 RadioMenuItem::Group style_group;
215 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
216 if (Config->get_shuttle_behaviour() == Sprung) {
217 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
219 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
220 if (Config->get_shuttle_behaviour() == Wheel) {
221 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
224 items.push_back (MenuElem (_("Mode"), *style_menu));
226 RadioMenuItem::Group speed_group;
228 /* XXX this code assumes that Config->get_max_transport_speed() returns 8 */
230 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
231 if (shuttle_max_speed == 8.0) {
232 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
234 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
235 if (shuttle_max_speed == 6.0) {
236 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
238 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
239 if (shuttle_max_speed == 4.0) {
240 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
242 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
243 if (shuttle_max_speed == 3.0) {
244 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
246 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
247 if (shuttle_max_speed == 2.0) {
248 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
250 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
251 if (shuttle_max_speed == 1.5) {
252 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
255 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
257 items.push_back (SeparatorElem ());
258 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
262 ShuttleControl::reset_speed ()
264 if (_session->transport_rolling()) {
265 _session->request_transport_speed (1.0, true);
267 _session->request_transport_speed (0.0, true);
272 ShuttleControl::set_shuttle_max_speed (float speed)
274 Config->set_shuttle_max_speed (speed);
275 shuttle_max_speed = speed;
276 last_speed_displayed = -99999999;
280 ShuttleControl::on_button_press_event (GdkEventButton* ev)
286 if (binding_proxy.button_press_handler (ev)) {
290 if (Keyboard::is_context_menu_event (ev)) {
291 if (shuttle_context_menu == 0) {
292 build_shuttle_context_menu ();
294 shuttle_context_menu->popup (ev->button, ev->time);
298 switch (ev->button) {
300 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
301 if (_session->transport_rolling()) {
302 _session->request_transport_speed (1.0);
306 shuttle_grabbed = true;
307 shuttle_speed_on_grab = _session->actual_speed ();
308 requested_speed = shuttle_speed_on_grab;
309 mouse_shuttle (ev->x, true);
310 gdk_pointer_grab(ev->window,false,
311 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
326 ShuttleControl::on_button_release_event (GdkEventButton* ev)
332 switch (ev->button) {
334 if (shuttle_grabbed) {
335 shuttle_grabbed = false;
336 remove_modal_grab ();
337 gdk_pointer_ungrab (GDK_CURRENT_TIME);
339 if (Config->get_shuttle_behaviour() == Sprung) {
340 if (shuttle_speed_on_grab == 0 ) {
341 _session->request_stop ();
343 _session->request_transport_speed (shuttle_speed_on_grab);
346 mouse_shuttle (ev->x, true);
352 if (_session->transport_rolling()) {
353 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
367 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
373 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
375 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
379 bool semis = (Config->get_shuttle_units() == Semitones);
381 switch (ev->direction) {
383 case GDK_SCROLL_RIGHT:
385 if (shuttle_fract == 0) {
386 shuttle_fract = semitones_as_fract (1, false);
389 int st = fract_as_semitones (shuttle_fract, rev);
390 shuttle_fract = semitones_as_fract (st + 1, rev);
393 shuttle_fract += 0.00125;
396 case GDK_SCROLL_DOWN:
397 case GDK_SCROLL_LEFT:
399 if (shuttle_fract == 0) {
400 shuttle_fract = semitones_as_fract (1, true);
403 int st = fract_as_semitones (shuttle_fract, rev);
404 shuttle_fract = semitones_as_fract (st - 1, rev);
407 shuttle_fract -= 0.00125;
416 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
417 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
419 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
420 to the far side of it.
423 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
424 switch (ev->direction) {
426 case GDK_SCROLL_RIGHT:
427 shuttle_fract = upper_side_of_dead_zone;
429 case GDK_SCROLL_DOWN:
430 case GDK_SCROLL_LEFT:
431 shuttle_fract = lower_side_of_dead_zone;
434 /* impossible, checked above */
440 use_shuttle_fract (true);
446 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
448 if (!_session || !shuttle_grabbed) {
452 return mouse_shuttle (ev->x, false);
456 ShuttleControl::mouse_shuttle (double x, bool force)
458 double const center = get_width() / 2.0;
459 double distance_from_center = x - center;
461 if (distance_from_center > 0) {
462 distance_from_center = min (distance_from_center, center);
464 distance_from_center = max (distance_from_center, -center);
467 /* compute shuttle fract as expressing how far between the center
468 and the edge we are. positive values indicate we are right of
469 center, negative values indicate left of center
472 shuttle_fract = distance_from_center / center; // center == half the width
473 use_shuttle_fract (force);
478 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
481 use_shuttle_fract (false, zero_ok);
485 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
487 assert (speed != 0.0);
491 return (int) round (12.0 * fast_log2 (-speed));
494 return (int) round (12.0 * fast_log2 (speed));
499 ShuttleControl::semitones_as_speed (int semi, bool reverse)
502 return -pow (2.0, (semi / 12.0));
504 return pow (2.0, (semi / 12.0));
509 ShuttleControl::semitones_as_fract (int semi, bool reverse)
511 float speed = semitones_as_speed (semi, reverse);
512 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
516 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
518 assert (fract != 0.0);
519 return speed_as_semitones (fract * 4.0, reverse);
523 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
525 microseconds_t now = get_microseconds();
527 shuttle_fract = max (-1.0f, shuttle_fract);
528 shuttle_fract = min (1.0f, shuttle_fract);
530 /* do not attempt to submit a motion-driven transport speed request
531 more than once per process cycle.
534 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
538 last_shuttle_request = now;
542 if (Config->get_shuttle_units() == Semitones) {
543 if (shuttle_fract != 0.0) {
545 int semi = fract_as_semitones (shuttle_fract, reverse);
546 speed = semitones_as_speed (semi, reverse);
551 speed = shuttle_max_speed * shuttle_fract;
554 requested_speed = speed;
556 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
558 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
563 ShuttleControl::set_colors ()
567 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
569 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
576 ShuttleControl::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
578 cairo_t* cr = ctx->cobj();
579 // center slider line
580 float yc = get_height() / 2;
582 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
583 cairo_set_line_width (cr, 3);
584 cairo_move_to (cr, lw, yc);
585 cairo_line_to (cr, get_width () - lw, yc);
586 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
587 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
588 cairo_stroke_preserve (cr);
589 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
594 float acutal_speed = 0.0;
598 speed = _session->actual_speed ();
599 acutal_speed = speed;
600 if (shuttle_grabbed) {
601 speed = requested_speed;
606 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
607 float marker_size = round (get_height() * 0.66);
608 float avail_width = get_width() - marker_size;
609 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
611 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
612 cairo_set_source_rgba (cr, 0, 0, 0, 1);
614 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
616 uint32_t col = UIConfiguration::instance().color ("shuttle");
617 Gtkmm2ext::set_source_rgba (cr, col);
619 cairo_set_source (cr, pattern);
621 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
622 cairo_fill_preserve (cr);
623 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
628 if (acutal_speed != 0) {
629 if (Config->get_shuttle_units() == Percentage) {
630 if (acutal_speed == 1.0) {
631 snprintf (buf, sizeof (buf), "%s", _("Play"));
633 if (acutal_speed < 0.0) {
634 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
636 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
641 int semi = speed_as_semitones (acutal_speed, reversed);
643 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
645 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
649 snprintf (buf, sizeof (buf), "%s", _("Stop"));
652 last_speed_displayed = acutal_speed;
654 _info_button.set_text (buf);
657 if (UIConfiguration::instance().get_widget_prelight()) {
659 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
660 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
668 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
670 Config->set_shuttle_behaviour (s);
674 ShuttleControl::set_shuttle_units (ShuttleUnits s)
676 Config->set_shuttle_units (s);
679 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
680 : PBD::Controllable (X_("Shuttle"))
686 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
688 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
692 ShuttleControl::ShuttleControllable::get_value () const
694 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
698 ShuttleControl::parameter_changed (std::string p)
700 if (p == "shuttle-behaviour") {
701 switch (Config->get_shuttle_behaviour ()) {
703 /* back to Sprung - reset to speed = 1.0 if playing
706 if (_session->transport_rolling()) {
707 if (_session->actual_speed() == 1.0) {
710 /* reset current speed and
711 revert to 1.0 as the default
713 _session->request_transport_speed (1.0);
714 /* redraw when speed changes */
727 } else if (p == "shuttle-max-speed") {
729 } else if (p == "shuttle-units") {
736 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
740 if (UIConfiguration::instance().get_widget_prelight()) {
744 return CairoWidget::on_enter_notify_event (ev);
748 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
752 if (UIConfiguration::instance().get_widget_prelight()) {
756 return CairoWidget::on_leave_notify_event (ev);