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, bool zero_ok)
433 use_shuttle_fract (false, zero_ok);
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, bool zero_ok)
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;
507 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
509 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
514 ShuttleControl::render (cairo_t* cr)
516 cairo_text_extents_t extents;
519 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
520 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
521 // cairo_fill_preserve (cr);
522 // cairo_stroke (cr);
528 speed = _session->transport_speed ();
532 float visual_fraction = std::min (1.0f, speed/shuttle_max_speed);
533 float marker_size = get_height()-4;
534 float avail_width = get_width() - marker_size;
535 float x = get_width()*0.5 + visual_fraction * avail_width*0.5;
536 float offset = x - marker_size*0.5;
537 // cairo_set_source_rgb (cr, 0, 1, 0.0);
538 cairo_set_source (cr, pattern);
540 cairo_move_to( cr, offset-4, 2);
541 cairo_line_to( cr, offset+4, 2+marker_size*0.5);
542 cairo_line_to( cr, offset-4, 2+marker_size);
543 cairo_line_to( cr, offset-4, 2);
544 } else if ( speed ==0.0 )
545 rounded_rectangle (cr, offset, 4, marker_size-2, marker_size-2, 1);
547 cairo_arc (cr, offset + marker_size*0.5, 2 + marker_size*0.5, marker_size*0.5, 0, 360);
548 cairo_set_line_width (cr, 2);
557 if (Config->get_shuttle_units() == Percentage) {
560 snprintf (buf, sizeof (buf), "%s", _("Playing"));
563 snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
565 snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
572 int semi = speed_as_semitones (speed, reversed);
575 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
577 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
582 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
585 last_speed_displayed = speed;
587 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
588 cairo_text_extents (cr, buf, &extents);
589 cairo_move_to (cr, 10, extents.height + 4);
590 cairo_set_font_size (cr, 13.0);
591 cairo_show_text (cr, buf);
596 switch (Config->get_shuttle_behaviour()) {
598 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
601 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
605 cairo_text_extents (cr, buf, &extents);
606 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 4);
607 cairo_show_text (cr, buf);
609 float _corner_radius = 4.0;
612 float rheight = 10.0;
613 Gtkmm2ext::rounded_rectangle (cr, 2, 1, get_width()-4, rheight, _corner_radius);
614 cairo_set_source (cr, shine_pattern);
617 if (ARDOUR::Config->get_widget_prelight()) {
619 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, _corner_radius);
620 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
627 ShuttleControl::shuttle_unit_clicked ()
629 if (shuttle_unit_menu == 0) {
630 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
632 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
636 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
638 Config->set_shuttle_behaviour (s);
642 ShuttleControl::set_shuttle_units (ShuttleUnits s)
644 Config->set_shuttle_units (s);
648 ShuttleControl::update_speed_display ()
650 if (_session->transport_speed() != last_speed_displayed) {
655 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
656 : PBD::Controllable (X_("Shuttle"))
662 ShuttleControl::ShuttleControllable::set_value (double val)
664 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
668 ShuttleControl::ShuttleControllable::get_value () const
670 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
674 ShuttleControl::parameter_changed (std::string p)
676 if (p == "shuttle-behaviour") {
677 switch (Config->get_shuttle_behaviour ()) {
679 /* back to Sprung - reset to speed = 1.0 if playing
682 if (_session->transport_rolling()) {
683 if (_session->transport_speed() == 1.0) {
686 /* reset current speed and
687 revert to 1.0 as the default
689 _session->request_transport_speed (1.0);
690 /* redraw when speed changes */
703 } else if (p == "shuttle-units") {
710 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
714 if (ARDOUR::Config->get_widget_prelight()) {
718 return CairoWidget::on_enter_notify_event (ev);
722 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
726 if (ARDOUR::Config->get_widget_prelight()) {
730 return CairoWidget::on_leave_notify_event (ev);