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.
21 #include <cairo/cairo.h>
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"
34 #include "ardour_ui.h"
35 #include "rgb_macros.h"
36 #include "shuttle_control.h"
41 using namespace Gtkmm2ext;
42 using namespace ARDOUR;
46 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
51 ShuttleControl::ShuttleControl ()
52 : _controllable (new ShuttleControllable (*this))
53 , binding_proxy (_controllable)
55 ARDOUR_UI::instance()->set_tip (*this, _("Shuttle speed control (Context-click for options)"));
59 last_shuttle_request = 0;
60 last_speed_displayed = -99999999;
61 shuttle_grabbed = false;
62 shuttle_speed_on_grab = 0;
64 shuttle_max_speed = 8.0f;
65 shuttle_style_menu = 0;
66 shuttle_unit_menu = 0;
67 shuttle_context_menu = 0;
70 set_flags (CAN_FOCUS);
71 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);
72 set_size_request (85, 20);
73 set_name (X_("ShuttleControl"));
75 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
77 /* gtkmm 2.4: the C++ wrapper doesn't work */
78 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
79 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
82 ShuttleControl::~ShuttleControl ()
84 cairo_pattern_destroy (pattern);
85 cairo_pattern_destroy (shine_pattern);
89 ShuttleControl::set_session (Session *s)
91 SessionHandlePtr::set_session (s);
95 _session->add_controllable (_controllable);
97 set_sensitive (false);
102 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
105 cairo_pattern_destroy (pattern);
107 cairo_pattern_destroy (shine_pattern);
111 CairoWidget::on_size_allocate ( alloc);
114 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
115 uint32_t col = ARDOUR_UI::config()->canvasvar_Shuttle.get();
117 UINT_TO_RGBA(col, &r, &g, &b, &a);
118 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
119 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
120 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
123 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
124 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
125 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
126 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
130 ShuttleControl::map_transport_state ()
132 float speed = _session->transport_speed ();
134 if (fabs(speed) <= (2*DBL_EPSILON)) {
137 if (Config->get_shuttle_units() == Semitones) {
139 int semi = speed_as_semitones (speed, reverse);
140 shuttle_fract = semitones_as_fract (semi, reverse);
142 shuttle_fract = speed/shuttle_max_speed;
150 ShuttleControl::build_shuttle_context_menu ()
152 using namespace Menu_Helpers;
154 shuttle_context_menu = new Menu();
155 MenuList& items = shuttle_context_menu->items();
157 Menu* speed_menu = manage (new Menu());
158 MenuList& speed_items = speed_menu->items();
160 Menu* units_menu = manage (new Menu);
161 MenuList& units_items = units_menu->items();
162 RadioMenuItem::Group units_group;
164 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
165 if (Config->get_shuttle_units() == Percentage) {
166 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
168 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
169 if (Config->get_shuttle_units() == Semitones) {
170 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
172 items.push_back (MenuElem (_("Units"), *units_menu));
174 Menu* style_menu = manage (new Menu);
175 MenuList& style_items = style_menu->items();
176 RadioMenuItem::Group style_group;
178 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
179 if (Config->get_shuttle_behaviour() == Sprung) {
180 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
182 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
183 if (Config->get_shuttle_behaviour() == Wheel) {
184 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
187 items.push_back (MenuElem (_("Mode"), *style_menu));
189 RadioMenuItem::Group speed_group;
191 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
192 if (shuttle_max_speed == 8.0) {
193 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
195 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
196 if (shuttle_max_speed == 6.0) {
197 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
199 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
200 if (shuttle_max_speed == 4.0) {
201 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
203 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
204 if (shuttle_max_speed == 3.0) {
205 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
207 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
208 if (shuttle_max_speed == 2.0) {
209 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
211 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
212 if (shuttle_max_speed == 1.5) {
213 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
216 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
221 ShuttleControl::show_shuttle_context_menu ()
223 if (shuttle_context_menu == 0) {
224 build_shuttle_context_menu ();
227 shuttle_context_menu->popup (1, gtk_get_current_event_time());
231 ShuttleControl::set_shuttle_max_speed (float speed)
233 shuttle_max_speed = speed;
237 ShuttleControl::on_button_press_event (GdkEventButton* ev)
243 if (binding_proxy.button_press_handler (ev)) {
247 if (Keyboard::is_context_menu_event (ev)) {
248 show_shuttle_context_menu ();
252 switch (ev->button) {
254 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
255 if (_session->transport_rolling()) {
256 _session->request_transport_speed (1.0);
260 shuttle_grabbed = true;
261 shuttle_speed_on_grab = _session->transport_speed ();
262 mouse_shuttle (ev->x, true);
263 gdk_pointer_grab(ev->window,false,
264 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
279 ShuttleControl::on_button_release_event (GdkEventButton* ev)
285 switch (ev->button) {
287 if (shuttle_grabbed) {
288 shuttle_grabbed = false;
289 remove_modal_grab ();
290 gdk_pointer_ungrab (GDK_CURRENT_TIME);
292 if (Config->get_shuttle_behaviour() == Sprung) {
293 if (shuttle_speed_on_grab == 0 ) {
294 _session->request_transport_speed (1.0);
296 _session->request_transport_speed (shuttle_speed_on_grab);
298 mouse_shuttle (ev->x, true);
304 if (_session->transport_rolling()) {
305 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
319 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
325 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
327 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
331 bool semis = (Config->get_shuttle_units() == Semitones);
333 switch (ev->direction) {
335 case GDK_SCROLL_RIGHT:
337 if (shuttle_fract == 0) {
338 shuttle_fract = semitones_as_fract (1, false);
341 int st = fract_as_semitones (shuttle_fract, rev);
342 shuttle_fract = semitones_as_fract (st + 1, rev);
345 shuttle_fract += 0.00125;
348 case GDK_SCROLL_DOWN:
349 case GDK_SCROLL_LEFT:
351 if (shuttle_fract == 0) {
352 shuttle_fract = semitones_as_fract (1, true);
355 int st = fract_as_semitones (shuttle_fract, rev);
356 shuttle_fract = semitones_as_fract (st - 1, rev);
359 shuttle_fract -= 0.00125;
368 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
369 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
371 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
372 to the far side of it.
375 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
376 switch (ev->direction) {
378 case GDK_SCROLL_RIGHT:
379 shuttle_fract = upper_side_of_dead_zone;
381 case GDK_SCROLL_DOWN:
382 case GDK_SCROLL_LEFT:
383 shuttle_fract = lower_side_of_dead_zone;
386 /* impossible, checked above */
392 use_shuttle_fract (true);
398 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
400 if (!_session || !shuttle_grabbed) {
404 return mouse_shuttle (ev->x, false);
408 ShuttleControl::mouse_shuttle (double x, bool force)
410 double const center = get_width() / 2.0;
411 double distance_from_center = x - center;
413 if (distance_from_center > 0) {
414 distance_from_center = min (distance_from_center, center);
416 distance_from_center = max (distance_from_center, -center);
419 /* compute shuttle fract as expressing how far between the center
420 and the edge we are. positive values indicate we are right of
421 center, negative values indicate left of center
424 shuttle_fract = distance_from_center / center; // center == half the width
425 use_shuttle_fract (force);
430 ShuttleControl::set_shuttle_fract (double f)
433 use_shuttle_fract (false);
437 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
439 assert (speed != 0.0);
443 return (int) round (12.0 * fast_log2 (-speed));
446 return (int) round (12.0 * fast_log2 (speed));
451 ShuttleControl::semitones_as_speed (int semi, bool reverse)
454 return -pow (2.0, (semi / 12.0));
456 return pow (2.0, (semi / 12.0));
461 ShuttleControl::semitones_as_fract (int semi, bool reverse)
463 float speed = semitones_as_speed (semi, reverse);
464 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
468 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
470 assert (fract != 0.0);
471 return speed_as_semitones (fract * 4.0, reverse);
475 ShuttleControl::use_shuttle_fract (bool force)
477 microseconds_t now = get_microseconds();
479 shuttle_fract = max (-1.0f, shuttle_fract);
480 shuttle_fract = min (1.0f, shuttle_fract);
482 /* do not attempt to submit a motion-driven transport speed request
483 more than once per process cycle.
486 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
490 last_shuttle_request = now;
494 if (Config->get_shuttle_units() == Semitones) {
495 if (shuttle_fract != 0.0) {
497 int semi = fract_as_semitones (shuttle_fract, reverse);
498 speed = semitones_as_speed (semi, reverse);
503 speed = shuttle_max_speed * shuttle_fract;
506 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
510 ShuttleControl::render (cairo_t* cr)
512 cairo_text_extents_t extents;
515 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
516 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
517 // cairo_fill_preserve (cr);
518 // cairo_stroke (cr);
524 speed = _session->transport_speed ();
528 float visual_fraction = std::min (1.0f, speed/shuttle_max_speed);
529 float marker_size = get_height()-4;
530 float avail_width = get_width() - marker_size;
531 float x = get_width()*0.5 + visual_fraction * avail_width*0.5;
532 float offset = x - marker_size*0.5;
533 // cairo_set_source_rgb (cr, 0, 1, 0.0);
534 cairo_set_source (cr, pattern);
536 cairo_move_to( cr, offset-4, 2);
537 cairo_line_to( cr, offset+4, 2+marker_size*0.5);
538 cairo_line_to( cr, offset-4, 2+marker_size);
539 cairo_line_to( cr, offset-4, 2);
540 } else if ( speed ==0.0 )
541 rounded_rectangle (cr, offset, 4, marker_size-2, marker_size-2, 1);
543 cairo_arc (cr, offset + marker_size*0.5, 2 + marker_size*0.5, marker_size*0.5, 0, 360);
544 cairo_set_line_width (cr, 2);
553 if (Config->get_shuttle_units() == Percentage) {
556 snprintf (buf, sizeof (buf), "%s", _("Playing"));
559 snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
561 snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
568 int semi = speed_as_semitones (speed, reversed);
571 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
573 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
578 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
581 last_speed_displayed = speed;
583 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
584 cairo_text_extents (cr, buf, &extents);
585 cairo_move_to (cr, 10, extents.height + 4);
586 cairo_set_font_size (cr, 13.0);
587 cairo_show_text (cr, buf);
592 switch (Config->get_shuttle_behaviour()) {
594 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
597 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
601 cairo_text_extents (cr, buf, &extents);
602 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 4);
603 cairo_show_text (cr, buf);
605 float _corner_radius = 4.0;
608 float rheight = 10.0;
609 Gtkmm2ext::rounded_rectangle (cr, 2, 1, get_width()-4, rheight, _corner_radius);
610 cairo_set_source (cr, shine_pattern);
613 if (ARDOUR::Config->get_widget_prelight()) {
615 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, _corner_radius);
616 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
623 ShuttleControl::shuttle_unit_clicked ()
625 if (shuttle_unit_menu == 0) {
626 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
628 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
632 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
634 Config->set_shuttle_behaviour (s);
638 ShuttleControl::set_shuttle_units (ShuttleUnits s)
640 Config->set_shuttle_units (s);
644 ShuttleControl::update_speed_display ()
646 if (_session->transport_speed() != last_speed_displayed) {
651 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
652 : PBD::Controllable (X_("Shuttle"))
658 ShuttleControl::ShuttleControllable::set_value (double val)
666 fract = -((0.5 - val)/0.5);
668 fract = ((val - 0.5)/0.5);
672 sc.set_shuttle_fract (fract);
676 ShuttleControl::ShuttleControllable::get_value () const
678 return sc.get_shuttle_fract ();
682 ShuttleControl::parameter_changed (std::string p)
684 if (p == "shuttle-behaviour") {
685 switch (Config->get_shuttle_behaviour ()) {
687 /* back to Sprung - reset to speed = 1.0 if playing
690 if (_session->transport_rolling()) {
691 if (_session->transport_speed() == 1.0) {
694 /* reset current speed and
695 revert to 1.0 as the default
697 _session->request_transport_speed (1.0);
698 /* redraw when speed changes */
711 } else if (p == "shuttle-units") {
718 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
722 if (ARDOUR::Config->get_widget_prelight()) {
726 return CairoWidget::on_enter_notify_event (ev);
730 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
734 if (ARDOUR::Config->get_widget_prelight()) {
738 return CairoWidget::on_leave_notify_event (ev);