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.
23 #include "ardour/ardour.h"
24 #include "ardour/audioengine.h"
25 #include "ardour/rc_configuration.h"
26 #include "ardour/session.h"
28 #include "gtkmm2ext/keyboard.h"
29 #include "gtkmm2ext/gui_thread.h"
30 #include "gtkmm2ext/cairocell.h"
31 #include "gtkmm2ext/utils.h"
32 #include "gtkmm2ext/rgb_macros.h"
35 #include "rgb_macros.h"
36 #include "shuttle_control.h"
42 using namespace Gtkmm2ext;
43 using namespace ARDOUR;
44 using namespace ARDOUR_UI_UTILS;
48 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
53 ShuttleControl::ShuttleControl ()
54 : _controllable (new ShuttleControllable (*this))
55 , binding_proxy (_controllable)
57 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
61 last_shuttle_request = 0;
62 last_speed_displayed = -99999999;
63 shuttle_grabbed = false;
64 shuttle_speed_on_grab = 0;
66 shuttle_max_speed = 8.0f;
67 shuttle_style_menu = 0;
68 shuttle_unit_menu = 0;
69 shuttle_context_menu = 0;
72 set_flags (CAN_FOCUS);
73 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);
74 set_size_request (85, 20);
75 set_name (X_("ShuttleControl"));
77 shuttle_max_speed = Config->get_shuttle_max_speed();
79 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
80 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
81 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
82 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
83 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
84 else { shuttle_max_speed = 1.5f; }
86 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
88 /* gtkmm 2.4: the C++ wrapper doesn't work */
89 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
90 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
93 ShuttleControl::~ShuttleControl ()
95 cairo_pattern_destroy (pattern);
96 cairo_pattern_destroy (shine_pattern);
100 ShuttleControl::set_session (Session *s)
102 SessionHandlePtr::set_session (s);
105 set_sensitive (true);
106 _session->add_controllable (_controllable);
108 set_sensitive (false);
113 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
116 cairo_pattern_destroy (pattern);
118 cairo_pattern_destroy (shine_pattern);
122 CairoWidget::on_size_allocate ( alloc);
125 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
126 uint32_t col = UIConfiguration::instance().color ("shuttle");
128 UINT_TO_RGBA(col, &r, &g, &b, &a);
129 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
130 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
131 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
134 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
135 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
136 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
137 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
141 ShuttleControl::map_transport_state ()
143 float speed = _session->transport_speed ();
145 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
146 && !( speed == 1.f && last_speed_displayed != 1.f)
147 && !( speed == 0.f && last_speed_displayed != 0.f)
150 return; // nothing to see here, move along.
153 // Q: is there a good reason why we re-calculate this every time?
154 if (fabs(speed) <= (2*DBL_EPSILON)) {
157 if (Config->get_shuttle_units() == Semitones) {
159 int semi = speed_as_semitones (speed, reverse);
160 shuttle_fract = semitones_as_fract (semi, reverse);
162 shuttle_fract = speed/shuttle_max_speed;
170 ShuttleControl::build_shuttle_context_menu ()
172 using namespace Menu_Helpers;
174 shuttle_context_menu = new Menu();
175 MenuList& items = shuttle_context_menu->items();
177 Menu* speed_menu = manage (new Menu());
178 MenuList& speed_items = speed_menu->items();
180 Menu* units_menu = manage (new Menu);
181 MenuList& units_items = units_menu->items();
182 RadioMenuItem::Group units_group;
184 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
185 if (Config->get_shuttle_units() == Percentage) {
186 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
188 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
189 if (Config->get_shuttle_units() == Semitones) {
190 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
192 items.push_back (MenuElem (_("Units"), *units_menu));
194 Menu* style_menu = manage (new Menu);
195 MenuList& style_items = style_menu->items();
196 RadioMenuItem::Group style_group;
198 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
199 if (Config->get_shuttle_behaviour() == Sprung) {
200 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
202 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
203 if (Config->get_shuttle_behaviour() == Wheel) {
204 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
207 items.push_back (MenuElem (_("Mode"), *style_menu));
209 RadioMenuItem::Group speed_group;
211 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
212 if (shuttle_max_speed == 8.0) {
213 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
215 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
216 if (shuttle_max_speed == 6.0) {
217 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
219 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
220 if (shuttle_max_speed == 4.0) {
221 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
223 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
224 if (shuttle_max_speed == 3.0) {
225 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
227 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
228 if (shuttle_max_speed == 2.0) {
229 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
231 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
232 if (shuttle_max_speed == 1.5) {
233 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
236 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
238 items.push_back (SeparatorElem ());
239 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
243 ShuttleControl::show_shuttle_context_menu ()
245 if (shuttle_context_menu == 0) {
246 build_shuttle_context_menu ();
249 shuttle_context_menu->popup (1, gtk_get_current_event_time());
253 ShuttleControl::reset_speed ()
255 if (_session->transport_rolling()) {
256 _session->request_transport_speed (1.0, true);
258 _session->request_transport_speed (0.0, true);
263 ShuttleControl::set_shuttle_max_speed (float speed)
265 Config->set_shuttle_max_speed (speed);
266 shuttle_max_speed = speed;
267 last_speed_displayed = -99999999;
271 ShuttleControl::on_button_press_event (GdkEventButton* ev)
277 if (binding_proxy.button_press_handler (ev)) {
281 if (Keyboard::is_context_menu_event (ev)) {
282 show_shuttle_context_menu ();
286 switch (ev->button) {
288 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
289 if (_session->transport_rolling()) {
290 _session->request_transport_speed (1.0);
294 shuttle_grabbed = true;
295 shuttle_speed_on_grab = _session->transport_speed ();
296 mouse_shuttle (ev->x, true);
297 gdk_pointer_grab(ev->window,false,
298 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
313 ShuttleControl::on_button_release_event (GdkEventButton* ev)
319 switch (ev->button) {
321 if (shuttle_grabbed) {
322 shuttle_grabbed = false;
323 remove_modal_grab ();
324 gdk_pointer_ungrab (GDK_CURRENT_TIME);
326 if (Config->get_shuttle_behaviour() == Sprung) {
327 if (shuttle_speed_on_grab == 0 ) {
328 _session->request_stop ();
330 _session->request_transport_speed (shuttle_speed_on_grab);
333 mouse_shuttle (ev->x, true);
339 if (_session->transport_rolling()) {
340 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
354 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
360 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
362 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
366 bool semis = (Config->get_shuttle_units() == Semitones);
368 switch (ev->direction) {
370 case GDK_SCROLL_RIGHT:
372 if (shuttle_fract == 0) {
373 shuttle_fract = semitones_as_fract (1, false);
376 int st = fract_as_semitones (shuttle_fract, rev);
377 shuttle_fract = semitones_as_fract (st + 1, rev);
380 shuttle_fract += 0.00125;
383 case GDK_SCROLL_DOWN:
384 case GDK_SCROLL_LEFT:
386 if (shuttle_fract == 0) {
387 shuttle_fract = semitones_as_fract (1, true);
390 int st = fract_as_semitones (shuttle_fract, rev);
391 shuttle_fract = semitones_as_fract (st - 1, rev);
394 shuttle_fract -= 0.00125;
403 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
404 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
406 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
407 to the far side of it.
410 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
411 switch (ev->direction) {
413 case GDK_SCROLL_RIGHT:
414 shuttle_fract = upper_side_of_dead_zone;
416 case GDK_SCROLL_DOWN:
417 case GDK_SCROLL_LEFT:
418 shuttle_fract = lower_side_of_dead_zone;
421 /* impossible, checked above */
427 use_shuttle_fract (true);
433 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
435 if (!_session || !shuttle_grabbed) {
439 return mouse_shuttle (ev->x, false);
443 ShuttleControl::mouse_shuttle (double x, bool force)
445 double const center = get_width() / 2.0;
446 double distance_from_center = x - center;
448 if (distance_from_center > 0) {
449 distance_from_center = min (distance_from_center, center);
451 distance_from_center = max (distance_from_center, -center);
454 /* compute shuttle fract as expressing how far between the center
455 and the edge we are. positive values indicate we are right of
456 center, negative values indicate left of center
459 shuttle_fract = distance_from_center / center; // center == half the width
460 use_shuttle_fract (force);
465 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
468 use_shuttle_fract (false, zero_ok);
472 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
474 assert (speed != 0.0);
478 return (int) round (12.0 * fast_log2 (-speed));
481 return (int) round (12.0 * fast_log2 (speed));
486 ShuttleControl::semitones_as_speed (int semi, bool reverse)
489 return -pow (2.0, (semi / 12.0));
491 return pow (2.0, (semi / 12.0));
496 ShuttleControl::semitones_as_fract (int semi, bool reverse)
498 float speed = semitones_as_speed (semi, reverse);
499 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
503 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
505 assert (fract != 0.0);
506 return speed_as_semitones (fract * 4.0, reverse);
510 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
512 microseconds_t now = get_microseconds();
514 shuttle_fract = max (-1.0f, shuttle_fract);
515 shuttle_fract = min (1.0f, shuttle_fract);
517 /* do not attempt to submit a motion-driven transport speed request
518 more than once per process cycle.
521 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
525 last_shuttle_request = now;
529 if (Config->get_shuttle_units() == Semitones) {
530 if (shuttle_fract != 0.0) {
532 int semi = fract_as_semitones (shuttle_fract, reverse);
533 speed = semitones_as_speed (semi, reverse);
538 speed = shuttle_max_speed * shuttle_fract;
542 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
544 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
549 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
551 cairo_text_extents_t extents;
554 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
555 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
561 speed = _session->transport_speed ();
565 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
566 float marker_size = get_height() - 5.0;
567 float avail_width = get_width() - marker_size - 4;
568 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
569 // cairo_set_source_rgb (cr, 0, 1, 0.0);
570 cairo_set_source (cr, pattern);
572 cairo_move_to( cr, x, 2.5);
573 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
574 cairo_line_to( cr, x, 2.5 + marker_size);
575 cairo_close_path(cr);
576 } else if ( speed ==0.0 )
577 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
579 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
580 cairo_set_line_width (cr, 1.75);
589 if (Config->get_shuttle_units() == Percentage) {
592 snprintf (buf, sizeof (buf), "%s", _("Playing"));
595 snprintf (buf, sizeof (buf), "<<< %.1f%%", -speed * 100.f);
597 snprintf (buf, sizeof (buf), ">>> %.1f%%", speed * 100.f);
604 int semi = speed_as_semitones (speed, reversed);
607 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
609 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
614 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
617 last_speed_displayed = speed;
619 // TODO use a proper pango layout, scale font
620 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
621 cairo_set_font_size (cr, 13.0);
622 cairo_text_extents (cr, "0|", &extents); // note the descender
623 const float text_ypos = (get_height() + extents.height - 1.) * .5;
625 cairo_move_to (cr, 10, text_ypos);
626 cairo_show_text (cr, buf);
631 switch (Config->get_shuttle_behaviour()) {
633 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
636 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
640 cairo_text_extents (cr, buf, &extents);
641 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), text_ypos);
642 cairo_show_text (cr, buf);
644 if (UIConfiguration::instance().get_widget_prelight()) {
646 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
647 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
654 ShuttleControl::shuttle_unit_clicked ()
656 if (shuttle_unit_menu == 0) {
657 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
659 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
663 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
665 Config->set_shuttle_behaviour (s);
669 ShuttleControl::set_shuttle_units (ShuttleUnits s)
671 Config->set_shuttle_units (s);
674 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
675 : PBD::Controllable (X_("Shuttle"))
681 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
683 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
687 ShuttleControl::ShuttleControllable::get_value () const
689 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
693 ShuttleControl::parameter_changed (std::string p)
695 if (p == "shuttle-behaviour") {
696 switch (Config->get_shuttle_behaviour ()) {
698 /* back to Sprung - reset to speed = 1.0 if playing
701 if (_session->transport_rolling()) {
702 if (_session->transport_speed() == 1.0) {
705 /* reset current speed and
706 revert to 1.0 as the default
708 _session->request_transport_speed (1.0);
709 /* redraw when speed changes */
722 } else if (p == "shuttle-units") {
729 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
733 if (UIConfiguration::instance().get_widget_prelight()) {
737 return CairoWidget::on_enter_notify_event (ev);
741 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
745 if (UIConfiguration::instance().get_widget_prelight()) {
749 return CairoWidget::on_leave_notify_event (ev);