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 = 8.0f;
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 >= 8.f) { shuttle_max_speed = 8.0f; }
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 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
229 if (shuttle_max_speed == 8.0) {
230 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
232 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
233 if (shuttle_max_speed == 6.0) {
234 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
236 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
237 if (shuttle_max_speed == 4.0) {
238 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
240 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
241 if (shuttle_max_speed == 3.0) {
242 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
244 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
245 if (shuttle_max_speed == 2.0) {
246 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
248 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
249 if (shuttle_max_speed == 1.5) {
250 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
253 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
255 items.push_back (SeparatorElem ());
256 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
260 ShuttleControl::reset_speed ()
262 if (_session->transport_rolling()) {
263 _session->request_transport_speed (1.0, true);
265 _session->request_transport_speed (0.0, true);
270 ShuttleControl::set_shuttle_max_speed (float speed)
272 Config->set_shuttle_max_speed (speed);
273 shuttle_max_speed = speed;
274 last_speed_displayed = -99999999;
278 ShuttleControl::on_button_press_event (GdkEventButton* ev)
284 if (binding_proxy.button_press_handler (ev)) {
288 if (Keyboard::is_context_menu_event (ev)) {
289 if (shuttle_context_menu == 0) {
290 build_shuttle_context_menu ();
292 shuttle_context_menu->popup (ev->button, ev->time);
296 switch (ev->button) {
298 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
299 if (_session->transport_rolling()) {
300 _session->request_transport_speed (1.0);
304 shuttle_grabbed = true;
305 shuttle_speed_on_grab = _session->actual_speed ();
306 requested_speed = shuttle_speed_on_grab;
307 mouse_shuttle (ev->x, true);
308 gdk_pointer_grab(ev->window,false,
309 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
324 ShuttleControl::on_button_release_event (GdkEventButton* ev)
330 switch (ev->button) {
332 if (shuttle_grabbed) {
333 shuttle_grabbed = false;
334 remove_modal_grab ();
335 gdk_pointer_ungrab (GDK_CURRENT_TIME);
337 if (Config->get_shuttle_behaviour() == Sprung) {
338 if (shuttle_speed_on_grab == 0 ) {
339 _session->request_stop ();
341 _session->request_transport_speed (shuttle_speed_on_grab);
344 mouse_shuttle (ev->x, true);
350 if (_session->transport_rolling()) {
351 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
365 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
371 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
373 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
377 bool semis = (Config->get_shuttle_units() == Semitones);
379 switch (ev->direction) {
381 case GDK_SCROLL_RIGHT:
383 if (shuttle_fract == 0) {
384 shuttle_fract = semitones_as_fract (1, false);
387 int st = fract_as_semitones (shuttle_fract, rev);
388 shuttle_fract = semitones_as_fract (st + 1, rev);
391 shuttle_fract += 0.00125;
394 case GDK_SCROLL_DOWN:
395 case GDK_SCROLL_LEFT:
397 if (shuttle_fract == 0) {
398 shuttle_fract = semitones_as_fract (1, true);
401 int st = fract_as_semitones (shuttle_fract, rev);
402 shuttle_fract = semitones_as_fract (st - 1, rev);
405 shuttle_fract -= 0.00125;
414 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
415 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
417 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
418 to the far side of it.
421 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
422 switch (ev->direction) {
424 case GDK_SCROLL_RIGHT:
425 shuttle_fract = upper_side_of_dead_zone;
427 case GDK_SCROLL_DOWN:
428 case GDK_SCROLL_LEFT:
429 shuttle_fract = lower_side_of_dead_zone;
432 /* impossible, checked above */
438 use_shuttle_fract (true);
444 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
446 if (!_session || !shuttle_grabbed) {
450 return mouse_shuttle (ev->x, false);
454 ShuttleControl::mouse_shuttle (double x, bool force)
456 double const center = get_width() / 2.0;
457 double distance_from_center = x - center;
459 if (distance_from_center > 0) {
460 distance_from_center = min (distance_from_center, center);
462 distance_from_center = max (distance_from_center, -center);
465 /* compute shuttle fract as expressing how far between the center
466 and the edge we are. positive values indicate we are right of
467 center, negative values indicate left of center
470 shuttle_fract = distance_from_center / center; // center == half the width
471 use_shuttle_fract (force);
476 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
479 use_shuttle_fract (false, zero_ok);
483 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
485 assert (speed != 0.0);
489 return (int) round (12.0 * fast_log2 (-speed));
492 return (int) round (12.0 * fast_log2 (speed));
497 ShuttleControl::semitones_as_speed (int semi, bool reverse)
500 return -pow (2.0, (semi / 12.0));
502 return pow (2.0, (semi / 12.0));
507 ShuttleControl::semitones_as_fract (int semi, bool reverse)
509 float speed = semitones_as_speed (semi, reverse);
510 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
514 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
516 assert (fract != 0.0);
517 return speed_as_semitones (fract * 4.0, reverse);
521 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
523 microseconds_t now = get_microseconds();
525 shuttle_fract = max (-1.0f, shuttle_fract);
526 shuttle_fract = min (1.0f, shuttle_fract);
528 /* do not attempt to submit a motion-driven transport speed request
529 more than once per process cycle.
532 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
536 last_shuttle_request = now;
540 if (Config->get_shuttle_units() == Semitones) {
541 if (shuttle_fract != 0.0) {
543 int semi = fract_as_semitones (shuttle_fract, reverse);
544 speed = semitones_as_speed (semi, reverse);
549 speed = shuttle_max_speed * shuttle_fract;
552 requested_speed = speed;
554 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
556 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
561 ShuttleControl::set_colors ()
565 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
567 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
574 ShuttleControl::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
576 cairo_t* cr = ctx->cobj();
577 // center slider line
578 float yc = get_height() / 2;
580 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
581 cairo_set_line_width (cr, 3);
582 cairo_move_to (cr, lw, yc);
583 cairo_line_to (cr, get_width () - lw, yc);
584 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
585 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
586 cairo_stroke_preserve (cr);
587 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
592 float acutal_speed = 0.0;
596 speed = _session->actual_speed ();
597 acutal_speed = speed;
598 if (shuttle_grabbed) {
599 speed = requested_speed;
604 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
605 float marker_size = round (get_height() * 0.66);
606 float avail_width = get_width() - marker_size;
607 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
609 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
610 cairo_set_source_rgba (cr, 0, 0, 0, 1);
612 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
614 uint32_t col = UIConfiguration::instance().color ("shuttle");
615 Gtkmm2ext::set_source_rgba (cr, col);
617 cairo_set_source (cr, pattern);
619 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
620 cairo_fill_preserve (cr);
621 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
626 if (acutal_speed != 0) {
627 if (Config->get_shuttle_units() == Percentage) {
628 if (acutal_speed == 1.0) {
629 snprintf (buf, sizeof (buf), "%s", _("Play"));
631 if (acutal_speed < 0.0) {
632 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
634 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
639 int semi = speed_as_semitones (acutal_speed, reversed);
641 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
643 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
647 snprintf (buf, sizeof (buf), "%s", _("Stop"));
650 last_speed_displayed = acutal_speed;
652 _info_button.set_text (buf);
655 if (UIConfiguration::instance().get_widget_prelight()) {
657 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
658 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
666 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
668 Config->set_shuttle_behaviour (s);
672 ShuttleControl::set_shuttle_units (ShuttleUnits s)
674 Config->set_shuttle_units (s);
677 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
678 : PBD::Controllable (X_("Shuttle"))
684 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
686 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
690 ShuttleControl::ShuttleControllable::get_value () const
692 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
696 ShuttleControl::parameter_changed (std::string p)
698 if (p == "shuttle-behaviour") {
699 switch (Config->get_shuttle_behaviour ()) {
701 /* back to Sprung - reset to speed = 1.0 if playing
704 if (_session->transport_rolling()) {
705 if (_session->actual_speed() == 1.0) {
708 /* reset current speed and
709 revert to 1.0 as the default
711 _session->request_transport_speed (1.0);
712 /* redraw when speed changes */
725 } else if (p == "shuttle-max-speed") {
727 } else if (p == "shuttle-units") {
734 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
738 if (UIConfiguration::instance().get_widget_prelight()) {
742 return CairoWidget::on_enter_notify_event (ev);
746 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
750 if (UIConfiguration::instance().get_widget_prelight()) {
754 return CairoWidget::on_leave_notify_event (ev);