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 "ardour_ui.h"
36 #include "rgb_macros.h"
37 #include "shuttle_control.h"
42 using namespace Gtkmm2ext;
43 using namespace ARDOUR;
47 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
52 ShuttleControl::ShuttleControl ()
53 : _controllable (new ShuttleControllable (*this))
54 , binding_proxy (_controllable)
56 ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
60 last_shuttle_request = 0;
61 last_speed_displayed = -99999999;
62 shuttle_grabbed = false;
63 shuttle_speed_on_grab = 0;
65 shuttle_max_speed = 8.0f;
66 shuttle_style_menu = 0;
67 shuttle_unit_menu = 0;
68 shuttle_context_menu = 0;
71 set_flags (CAN_FOCUS);
72 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);
73 set_size_request (85, 20);
74 set_name (X_("ShuttleControl"));
76 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
78 /* gtkmm 2.4: the C++ wrapper doesn't work */
79 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
80 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
83 ShuttleControl::~ShuttleControl ()
85 cairo_pattern_destroy (pattern);
86 cairo_pattern_destroy (shine_pattern);
90 ShuttleControl::set_session (Session *s)
92 SessionHandlePtr::set_session (s);
96 _session->add_controllable (_controllable);
98 set_sensitive (false);
103 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
106 cairo_pattern_destroy (pattern);
108 cairo_pattern_destroy (shine_pattern);
112 CairoWidget::on_size_allocate ( alloc);
115 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
116 uint32_t col = ARDOUR_UI::config()->color ("shuttle");
118 UINT_TO_RGBA(col, &r, &g, &b, &a);
119 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
120 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
121 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
124 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
125 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
126 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
127 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
131 ShuttleControl::map_transport_state ()
133 float speed = _session->transport_speed ();
135 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
136 && !( speed == 1.f && last_speed_displayed != 1.f)
137 && !( speed == 0.f && last_speed_displayed != 0.f)
140 return; // nothing to see here, move along.
143 // Q: is there a good reason why we re-calculate this every time?
144 if (fabs(speed) <= (2*DBL_EPSILON)) {
147 if (Config->get_shuttle_units() == Semitones) {
149 int semi = speed_as_semitones (speed, reverse);
150 shuttle_fract = semitones_as_fract (semi, reverse);
152 shuttle_fract = speed/shuttle_max_speed;
160 ShuttleControl::build_shuttle_context_menu ()
162 using namespace Menu_Helpers;
164 shuttle_context_menu = new Menu();
165 MenuList& items = shuttle_context_menu->items();
167 Menu* speed_menu = manage (new Menu());
168 MenuList& speed_items = speed_menu->items();
170 Menu* units_menu = manage (new Menu);
171 MenuList& units_items = units_menu->items();
172 RadioMenuItem::Group units_group;
174 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
175 if (Config->get_shuttle_units() == Percentage) {
176 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
178 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
179 if (Config->get_shuttle_units() == Semitones) {
180 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
182 items.push_back (MenuElem (_("Units"), *units_menu));
184 Menu* style_menu = manage (new Menu);
185 MenuList& style_items = style_menu->items();
186 RadioMenuItem::Group style_group;
188 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
189 if (Config->get_shuttle_behaviour() == Sprung) {
190 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
192 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
193 if (Config->get_shuttle_behaviour() == Wheel) {
194 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
197 items.push_back (MenuElem (_("Mode"), *style_menu));
199 RadioMenuItem::Group speed_group;
201 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
202 if (shuttle_max_speed == 8.0) {
203 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
205 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
206 if (shuttle_max_speed == 6.0) {
207 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
209 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
210 if (shuttle_max_speed == 4.0) {
211 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
213 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
214 if (shuttle_max_speed == 3.0) {
215 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
217 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
218 if (shuttle_max_speed == 2.0) {
219 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
221 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
222 if (shuttle_max_speed == 1.5) {
223 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
226 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
231 ShuttleControl::show_shuttle_context_menu ()
233 if (shuttle_context_menu == 0) {
234 build_shuttle_context_menu ();
237 shuttle_context_menu->popup (1, gtk_get_current_event_time());
241 ShuttleControl::set_shuttle_max_speed (float speed)
243 shuttle_max_speed = speed;
244 last_speed_displayed = -99999999;
248 ShuttleControl::on_button_press_event (GdkEventButton* ev)
254 if (binding_proxy.button_press_handler (ev)) {
258 if (Keyboard::is_context_menu_event (ev)) {
259 show_shuttle_context_menu ();
263 switch (ev->button) {
265 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
266 if (_session->transport_rolling()) {
267 _session->request_transport_speed (1.0);
271 shuttle_grabbed = true;
272 shuttle_speed_on_grab = _session->transport_speed ();
273 mouse_shuttle (ev->x, true);
274 gdk_pointer_grab(ev->window,false,
275 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
290 ShuttleControl::on_button_release_event (GdkEventButton* ev)
296 switch (ev->button) {
298 if (shuttle_grabbed) {
299 shuttle_grabbed = false;
300 remove_modal_grab ();
301 gdk_pointer_ungrab (GDK_CURRENT_TIME);
303 if (Config->get_shuttle_behaviour() == Sprung) {
304 if (shuttle_speed_on_grab == 0 ) {
305 _session->request_transport_speed (1.0);
307 _session->request_transport_speed (shuttle_speed_on_grab);
309 mouse_shuttle (ev->x, true);
315 if (_session->transport_rolling()) {
316 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
330 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
336 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
338 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
342 bool semis = (Config->get_shuttle_units() == Semitones);
344 switch (ev->direction) {
346 case GDK_SCROLL_RIGHT:
348 if (shuttle_fract == 0) {
349 shuttle_fract = semitones_as_fract (1, false);
352 int st = fract_as_semitones (shuttle_fract, rev);
353 shuttle_fract = semitones_as_fract (st + 1, rev);
356 shuttle_fract += 0.00125;
359 case GDK_SCROLL_DOWN:
360 case GDK_SCROLL_LEFT:
362 if (shuttle_fract == 0) {
363 shuttle_fract = semitones_as_fract (1, true);
366 int st = fract_as_semitones (shuttle_fract, rev);
367 shuttle_fract = semitones_as_fract (st - 1, rev);
370 shuttle_fract -= 0.00125;
379 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
380 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
382 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
383 to the far side of it.
386 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
387 switch (ev->direction) {
389 case GDK_SCROLL_RIGHT:
390 shuttle_fract = upper_side_of_dead_zone;
392 case GDK_SCROLL_DOWN:
393 case GDK_SCROLL_LEFT:
394 shuttle_fract = lower_side_of_dead_zone;
397 /* impossible, checked above */
403 use_shuttle_fract (true);
409 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
411 if (!_session || !shuttle_grabbed) {
415 return mouse_shuttle (ev->x, false);
419 ShuttleControl::mouse_shuttle (double x, bool force)
421 double const center = get_width() / 2.0;
422 double distance_from_center = x - center;
424 if (distance_from_center > 0) {
425 distance_from_center = min (distance_from_center, center);
427 distance_from_center = max (distance_from_center, -center);
430 /* compute shuttle fract as expressing how far between the center
431 and the edge we are. positive values indicate we are right of
432 center, negative values indicate left of center
435 shuttle_fract = distance_from_center / center; // center == half the width
436 use_shuttle_fract (force);
441 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
444 use_shuttle_fract (false, zero_ok);
448 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
450 assert (speed != 0.0);
454 return (int) round (12.0 * fast_log2 (-speed));
457 return (int) round (12.0 * fast_log2 (speed));
462 ShuttleControl::semitones_as_speed (int semi, bool reverse)
465 return -pow (2.0, (semi / 12.0));
467 return pow (2.0, (semi / 12.0));
472 ShuttleControl::semitones_as_fract (int semi, bool reverse)
474 float speed = semitones_as_speed (semi, reverse);
475 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
479 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
481 assert (fract != 0.0);
482 return speed_as_semitones (fract * 4.0, reverse);
486 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
488 microseconds_t now = get_microseconds();
490 shuttle_fract = max (-1.0f, shuttle_fract);
491 shuttle_fract = min (1.0f, shuttle_fract);
493 /* do not attempt to submit a motion-driven transport speed request
494 more than once per process cycle.
497 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
501 last_shuttle_request = now;
505 if (Config->get_shuttle_units() == Semitones) {
506 if (shuttle_fract != 0.0) {
508 int semi = fract_as_semitones (shuttle_fract, reverse);
509 speed = semitones_as_speed (semi, reverse);
514 speed = shuttle_max_speed * shuttle_fract;
518 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
520 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
525 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
527 cairo_text_extents_t extents;
530 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
531 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
537 speed = _session->transport_speed ();
541 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
542 float marker_size = get_height() - 5.0;
543 float avail_width = get_width() - marker_size - 4;
544 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
545 // cairo_set_source_rgb (cr, 0, 1, 0.0);
546 cairo_set_source (cr, pattern);
548 cairo_move_to( cr, x, 2.5);
549 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
550 cairo_line_to( cr, x, 2.5 + marker_size);
551 cairo_close_path(cr);
552 } else if ( speed ==0.0 )
553 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
555 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
556 cairo_set_line_width (cr, 1.75);
565 if (Config->get_shuttle_units() == Percentage) {
568 snprintf (buf, sizeof (buf), "%s", _("Playing"));
571 snprintf (buf, sizeof (buf), "<<< %.1f%%", -speed * 100.f);
573 snprintf (buf, sizeof (buf), ">>> %.1f%%", speed * 100.f);
580 int semi = speed_as_semitones (speed, reversed);
583 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
585 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
590 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
593 last_speed_displayed = speed;
595 // TODO use a proper pango layout, scale font
596 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
597 cairo_set_font_size (cr, 13.0);
598 cairo_text_extents (cr, "0|", &extents); // note the descender
599 const float text_ypos = (get_height() + extents.height - 1.) * .5;
601 cairo_move_to (cr, 10, text_ypos);
602 cairo_show_text (cr, buf);
607 switch (Config->get_shuttle_behaviour()) {
609 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
612 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
616 cairo_text_extents (cr, buf, &extents);
617 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), text_ypos);
618 cairo_show_text (cr, buf);
620 if (ARDOUR_UI::config()->get_widget_prelight()) {
622 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
623 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
630 ShuttleControl::shuttle_unit_clicked ()
632 if (shuttle_unit_menu == 0) {
633 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
635 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
639 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
641 Config->set_shuttle_behaviour (s);
645 ShuttleControl::set_shuttle_units (ShuttleUnits s)
647 Config->set_shuttle_units (s);
650 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
651 : PBD::Controllable (X_("Shuttle"))
657 ShuttleControl::ShuttleControllable::set_value (double val)
659 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
663 ShuttleControl::ShuttleControllable::get_value () const
665 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
669 ShuttleControl::parameter_changed (std::string p)
671 if (p == "shuttle-behaviour") {
672 switch (Config->get_shuttle_behaviour ()) {
674 /* back to Sprung - reset to speed = 1.0 if playing
677 if (_session->transport_rolling()) {
678 if (_session->transport_speed() == 1.0) {
681 /* reset current speed and
682 revert to 1.0 as the default
684 _session->request_transport_speed (1.0);
685 /* redraw when speed changes */
698 } else if (p == "shuttle-units") {
705 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
709 if (ARDOUR_UI::config()->get_widget_prelight()) {
713 return CairoWidget::on_enter_notify_event (ev);
717 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
721 if (ARDOUR_UI::config()->get_widget_prelight()) {
725 return CairoWidget::on_leave_notify_event (ev);