2 Copyright (C) 2011 Paul Davis
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.
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.
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.
19 #define BASELINESTRETCH (1.25)
25 #include "ardour/ardour.h"
26 #include "ardour/audioengine.h"
27 #include "ardour/rc_configuration.h"
28 #include "ardour/session.h"
30 #include "gtkmm2ext/keyboard.h"
31 #include "gtkmm2ext/gui_thread.h"
32 #include "gtkmm2ext/cairocell.h"
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/rgb_macros.h"
37 #include "rgb_macros.h"
38 #include "shuttle_control.h"
44 using namespace Gtkmm2ext;
45 using namespace ARDOUR;
46 using namespace ARDOUR_UI_UTILS;
50 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
55 ShuttleControl::ShuttleControl ()
56 : _controllable (new ShuttleControllable (*this))
57 , binding_proxy (_controllable)
59 _info_button.set_layout_font (UIConfiguration::instance().get_NormalFont());
60 _info_button.set_sizing_text (S_("LogestShuttle|< +00 st"));
61 _info_button.set_name ("shuttle text");
62 _info_button.set_sensitive (false);
63 _info_button.set_visual_state (Gtkmm2ext::NoVisualState);
64 _info_button.set_elements (ArdourButton::Text);
66 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
70 last_shuttle_request = 0;
71 last_speed_displayed = -99999999;
72 shuttle_grabbed = false;
73 shuttle_speed_on_grab = 0;
75 shuttle_max_speed = 8.0f;
76 shuttle_style_menu = 0;
77 shuttle_unit_menu = 0;
78 shuttle_context_menu = 0;
81 set_flags (CAN_FOCUS);
82 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);
83 set_name (X_("ShuttleControl"));
87 shuttle_max_speed = Config->get_shuttle_max_speed();
89 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
90 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
91 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
92 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
93 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
94 else { shuttle_max_speed = 1.5f; }
96 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
97 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
101 /* gtkmm 2.4: the C++ wrapper doesn't work */
102 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
103 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
106 ShuttleControl::~ShuttleControl ()
108 cairo_pattern_destroy (pattern);
109 cairo_pattern_destroy (shine_pattern);
113 ShuttleControl::set_session (Session *s)
115 SessionHandlePtr::set_session (s);
118 set_sensitive (true);
119 _session->add_controllable (_controllable);
121 set_sensitive (false);
126 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
129 cairo_pattern_destroy (pattern);
131 cairo_pattern_destroy (shine_pattern);
135 CairoWidget::on_size_allocate ( alloc);
138 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
139 uint32_t col = UIConfiguration::instance().color ("shuttle");
141 UINT_TO_RGBA(col, &r, &g, &b, &a);
142 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
143 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
144 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
147 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
148 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
149 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
150 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
154 ShuttleControl::map_transport_state ()
156 float speed = _session->transport_speed ();
158 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
159 && !( speed == 1.f && last_speed_displayed != 1.f)
160 && !( speed == 0.f && last_speed_displayed != 0.f)
163 return; // nothing to see here, move along.
166 // Q: is there a good reason why we re-calculate this every time?
167 if (fabs(speed) <= (2*DBL_EPSILON)) {
170 if (Config->get_shuttle_units() == Semitones) {
172 int semi = speed_as_semitones (speed, reverse);
173 shuttle_fract = semitones_as_fract (semi, reverse);
175 shuttle_fract = speed/shuttle_max_speed;
183 ShuttleControl::build_shuttle_context_menu ()
185 using namespace Menu_Helpers;
187 shuttle_context_menu = new Menu();
188 MenuList& items = shuttle_context_menu->items();
190 Menu* speed_menu = manage (new Menu());
191 MenuList& speed_items = speed_menu->items();
193 Menu* units_menu = manage (new Menu);
194 MenuList& units_items = units_menu->items();
195 RadioMenuItem::Group units_group;
197 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
198 if (Config->get_shuttle_units() == Percentage) {
199 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
201 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
202 if (Config->get_shuttle_units() == Semitones) {
203 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
205 items.push_back (MenuElem (_("Units"), *units_menu));
207 Menu* style_menu = manage (new Menu);
208 MenuList& style_items = style_menu->items();
209 RadioMenuItem::Group style_group;
211 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
212 if (Config->get_shuttle_behaviour() == Sprung) {
213 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
215 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
216 if (Config->get_shuttle_behaviour() == Wheel) {
217 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
220 items.push_back (MenuElem (_("Mode"), *style_menu));
222 RadioMenuItem::Group speed_group;
224 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
225 if (shuttle_max_speed == 8.0) {
226 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
228 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
229 if (shuttle_max_speed == 6.0) {
230 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
232 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
233 if (shuttle_max_speed == 4.0) {
234 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
236 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
237 if (shuttle_max_speed == 3.0) {
238 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
240 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
241 if (shuttle_max_speed == 2.0) {
242 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
244 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
245 if (shuttle_max_speed == 1.5) {
246 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
249 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
251 items.push_back (SeparatorElem ());
252 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
256 ShuttleControl::show_shuttle_context_menu ()
258 if (shuttle_context_menu == 0) {
259 build_shuttle_context_menu ();
262 shuttle_context_menu->popup (1, gtk_get_current_event_time());
266 ShuttleControl::reset_speed ()
268 if (_session->transport_rolling()) {
269 _session->request_transport_speed (1.0, true);
271 _session->request_transport_speed (0.0, true);
276 ShuttleControl::set_shuttle_max_speed (float speed)
278 Config->set_shuttle_max_speed (speed);
279 shuttle_max_speed = speed;
280 last_speed_displayed = -99999999;
284 ShuttleControl::on_button_press_event (GdkEventButton* ev)
290 if (binding_proxy.button_press_handler (ev)) {
294 if (Keyboard::is_context_menu_event (ev)) {
295 show_shuttle_context_menu ();
299 switch (ev->button) {
301 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
302 if (_session->transport_rolling()) {
303 _session->request_transport_speed (1.0);
307 shuttle_grabbed = true;
308 shuttle_speed_on_grab = _session->transport_speed ();
309 requested_speed = shuttle_speed_on_grab;
310 mouse_shuttle (ev->x, true);
311 gdk_pointer_grab(ev->window,false,
312 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
327 ShuttleControl::on_button_release_event (GdkEventButton* ev)
333 switch (ev->button) {
335 if (shuttle_grabbed) {
336 shuttle_grabbed = false;
337 remove_modal_grab ();
338 gdk_pointer_ungrab (GDK_CURRENT_TIME);
340 if (Config->get_shuttle_behaviour() == Sprung) {
341 if (shuttle_speed_on_grab == 0 ) {
342 _session->request_stop ();
344 _session->request_transport_speed (shuttle_speed_on_grab);
347 mouse_shuttle (ev->x, true);
353 if (_session->transport_rolling()) {
354 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
368 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
374 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
376 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
380 bool semis = (Config->get_shuttle_units() == Semitones);
382 switch (ev->direction) {
384 case GDK_SCROLL_RIGHT:
386 if (shuttle_fract == 0) {
387 shuttle_fract = semitones_as_fract (1, false);
390 int st = fract_as_semitones (shuttle_fract, rev);
391 shuttle_fract = semitones_as_fract (st + 1, rev);
394 shuttle_fract += 0.00125;
397 case GDK_SCROLL_DOWN:
398 case GDK_SCROLL_LEFT:
400 if (shuttle_fract == 0) {
401 shuttle_fract = semitones_as_fract (1, true);
404 int st = fract_as_semitones (shuttle_fract, rev);
405 shuttle_fract = semitones_as_fract (st - 1, rev);
408 shuttle_fract -= 0.00125;
417 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
418 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
420 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
421 to the far side of it.
424 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
425 switch (ev->direction) {
427 case GDK_SCROLL_RIGHT:
428 shuttle_fract = upper_side_of_dead_zone;
430 case GDK_SCROLL_DOWN:
431 case GDK_SCROLL_LEFT:
432 shuttle_fract = lower_side_of_dead_zone;
435 /* impossible, checked above */
441 use_shuttle_fract (true);
447 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
449 if (!_session || !shuttle_grabbed) {
453 return mouse_shuttle (ev->x, false);
457 ShuttleControl::mouse_shuttle (double x, bool force)
459 double const center = get_width() / 2.0;
460 double distance_from_center = x - center;
462 if (distance_from_center > 0) {
463 distance_from_center = min (distance_from_center, center);
465 distance_from_center = max (distance_from_center, -center);
468 /* compute shuttle fract as expressing how far between the center
469 and the edge we are. positive values indicate we are right of
470 center, negative values indicate left of center
473 shuttle_fract = distance_from_center / center; // center == half the width
474 use_shuttle_fract (force);
479 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
482 use_shuttle_fract (false, zero_ok);
486 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
488 assert (speed != 0.0);
492 return (int) round (12.0 * fast_log2 (-speed));
495 return (int) round (12.0 * fast_log2 (speed));
500 ShuttleControl::semitones_as_speed (int semi, bool reverse)
503 return -pow (2.0, (semi / 12.0));
505 return pow (2.0, (semi / 12.0));
510 ShuttleControl::semitones_as_fract (int semi, bool reverse)
512 float speed = semitones_as_speed (semi, reverse);
513 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
517 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
519 assert (fract != 0.0);
520 return speed_as_semitones (fract * 4.0, reverse);
524 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
526 microseconds_t now = get_microseconds();
528 shuttle_fract = max (-1.0f, shuttle_fract);
529 shuttle_fract = min (1.0f, shuttle_fract);
531 /* do not attempt to submit a motion-driven transport speed request
532 more than once per process cycle.
535 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
539 last_shuttle_request = now;
543 if (Config->get_shuttle_units() == Semitones) {
544 if (shuttle_fract != 0.0) {
546 int semi = fract_as_semitones (shuttle_fract, reverse);
547 speed = semitones_as_speed (semi, reverse);
552 speed = shuttle_max_speed * shuttle_fract;
555 requested_speed = speed;
557 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
559 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
564 ShuttleControl::set_colors ()
568 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
570 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
577 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
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->transport_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);
615 cairo_set_source (cr, pattern);
616 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
617 cairo_fill_preserve (cr);
618 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
623 if (acutal_speed != 0) {
624 if (Config->get_shuttle_units() == Percentage) {
625 if (acutal_speed == 1.0) {
626 snprintf (buf, sizeof (buf), "%s", _("Play"));
628 if (acutal_speed < 0.0) {
629 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
631 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
636 int semi = speed_as_semitones (acutal_speed, reversed);
638 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
640 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
644 snprintf (buf, sizeof (buf), "%s", _("Stop"));
647 last_speed_displayed = acutal_speed;
649 _info_button.set_text (buf);
652 if (UIConfiguration::instance().get_widget_prelight()) {
654 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
655 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
663 ShuttleControl::shuttle_unit_clicked ()
665 if (shuttle_unit_menu == 0) {
666 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
668 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
672 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
674 Config->set_shuttle_behaviour (s);
678 ShuttleControl::set_shuttle_units (ShuttleUnits s)
680 Config->set_shuttle_units (s);
683 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
684 : PBD::Controllable (X_("Shuttle"))
690 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
692 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
696 ShuttleControl::ShuttleControllable::get_value () const
698 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
702 ShuttleControl::parameter_changed (std::string p)
704 if (p == "shuttle-behaviour") {
705 switch (Config->get_shuttle_behaviour ()) {
707 /* back to Sprung - reset to speed = 1.0 if playing
710 if (_session->transport_rolling()) {
711 if (_session->transport_speed() == 1.0) {
714 /* reset current speed and
715 revert to 1.0 as the default
717 _session->request_transport_speed (1.0);
718 /* redraw when speed changes */
731 } else if (p == "shuttle-max-speed") {
733 } else if (p == "shuttle-units") {
740 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
744 if (UIConfiguration::instance().get_widget_prelight()) {
748 return CairoWidget::on_enter_notify_event (ev);
752 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
756 if (UIConfiguration::instance().get_widget_prelight()) {
760 return CairoWidget::on_leave_notify_event (ev);