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);
276 ShuttleControl::on_button_release_event (GdkEventButton* ev)
282 switch (ev->button) {
284 if (shuttle_grabbed) {
285 shuttle_grabbed = false;
286 remove_modal_grab ();
288 if (Config->get_shuttle_behaviour() == Sprung) {
289 _session->request_transport_speed (shuttle_speed_on_grab);
291 mouse_shuttle (ev->x, true);
297 if (_session->transport_rolling()) {
298 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
312 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
318 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
320 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
324 bool semis = (Config->get_shuttle_units() == Semitones);
326 switch (ev->direction) {
328 case GDK_SCROLL_RIGHT:
330 if (shuttle_fract == 0) {
331 shuttle_fract = semitones_as_fract (1, false);
334 int st = fract_as_semitones (shuttle_fract, rev);
335 shuttle_fract = semitones_as_fract (st + 1, rev);
338 shuttle_fract += 0.00125;
341 case GDK_SCROLL_DOWN:
342 case GDK_SCROLL_LEFT:
344 if (shuttle_fract == 0) {
345 shuttle_fract = semitones_as_fract (1, true);
348 int st = fract_as_semitones (shuttle_fract, rev);
349 shuttle_fract = semitones_as_fract (st - 1, rev);
352 shuttle_fract -= 0.00125;
361 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
362 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
364 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
365 to the far side of it.
368 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
369 switch (ev->direction) {
371 case GDK_SCROLL_RIGHT:
372 shuttle_fract = upper_side_of_dead_zone;
374 case GDK_SCROLL_DOWN:
375 case GDK_SCROLL_LEFT:
376 shuttle_fract = lower_side_of_dead_zone;
379 /* impossible, checked above */
385 use_shuttle_fract (true);
391 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
393 if (!_session || !shuttle_grabbed) {
397 return mouse_shuttle (ev->x, false);
401 ShuttleControl::mouse_shuttle (double x, bool force)
403 double const center = get_width() / 2.0;
404 double distance_from_center = x - center;
406 if (distance_from_center > 0) {
407 distance_from_center = min (distance_from_center, center);
409 distance_from_center = max (distance_from_center, -center);
412 /* compute shuttle fract as expressing how far between the center
413 and the edge we are. positive values indicate we are right of
414 center, negative values indicate left of center
417 shuttle_fract = distance_from_center / center; // center == half the width
418 use_shuttle_fract (force);
423 ShuttleControl::set_shuttle_fract (double f)
426 use_shuttle_fract (false);
430 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
432 assert (speed != 0.0);
436 return (int) round (12.0 * fast_log2 (-speed));
439 return (int) round (12.0 * fast_log2 (speed));
444 ShuttleControl::semitones_as_speed (int semi, bool reverse)
447 return -pow (2.0, (semi / 12.0));
449 return pow (2.0, (semi / 12.0));
454 ShuttleControl::semitones_as_fract (int semi, bool reverse)
456 float speed = semitones_as_speed (semi, reverse);
457 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
461 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
463 assert (fract != 0.0);
464 return speed_as_semitones (fract * 4.0, reverse);
468 ShuttleControl::use_shuttle_fract (bool force)
470 microseconds_t now = get_microseconds();
472 shuttle_fract = max (-1.0f, shuttle_fract);
473 shuttle_fract = min (1.0f, shuttle_fract);
475 /* do not attempt to submit a motion-driven transport speed request
476 more than once per process cycle.
479 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
483 last_shuttle_request = now;
487 if (Config->get_shuttle_units() == Semitones) {
488 if (shuttle_fract != 0.0) {
490 int semi = fract_as_semitones (shuttle_fract, reverse);
491 speed = semitones_as_speed (semi, reverse);
496 speed = shuttle_max_speed * shuttle_fract;
499 _session->request_transport_speed_nonzero (speed, true);
503 ShuttleControl::render (cairo_t* cr)
505 cairo_text_extents_t extents;
508 cairo_set_source_rgb (cr, 0, 0.0, 0.0);
509 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
510 // cairo_fill_preserve (cr);
511 // cairo_stroke (cr);
517 speed = _session->transport_speed ();
521 float visual_fraction = std::min (1.0f, speed/shuttle_max_speed);
522 float marker_size = get_height()-4;
523 float avail_width = get_width() - marker_size;
524 float x = get_width()*0.5 + visual_fraction * avail_width*0.5;
525 float offset = x - marker_size*0.5;
526 // cairo_set_source_rgb (cr, 0, 1, 0.0);
527 cairo_set_source (cr, pattern);
529 cairo_move_to( cr, offset-4, 2);
530 cairo_line_to( cr, offset+4, 2+marker_size*0.5);
531 cairo_line_to( cr, offset-4, 2+marker_size);
532 cairo_line_to( cr, offset-4, 2);
533 } else if ( speed ==0.0 )
534 rounded_rectangle (cr, offset, 4, marker_size-2, marker_size-2, 1);
536 cairo_arc (cr, offset + marker_size*0.5, 2 + marker_size*0.5, marker_size*0.5, 0, 360);
537 cairo_set_line_width (cr, 2);
546 if (Config->get_shuttle_units() == Percentage) {
549 snprintf (buf, sizeof (buf), "%s", _("Playing"));
552 snprintf (buf, sizeof (buf), "<<< %d%%", (int) round (-speed * 100));
554 snprintf (buf, sizeof (buf), ">>> %d%%", (int) round (speed * 100));
561 int semi = speed_as_semitones (speed, reversed);
564 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
566 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
571 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
574 last_speed_displayed = speed;
576 cairo_set_source_rgb (cr, 0.6, 0.6, 0.6);
577 cairo_text_extents (cr, buf, &extents);
578 cairo_move_to (cr, 10, extents.height + 4);
579 cairo_set_font_size (cr, 13.0);
580 cairo_show_text (cr, buf);
585 switch (Config->get_shuttle_behaviour()) {
587 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
590 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
594 cairo_text_extents (cr, buf, &extents);
595 cairo_move_to (cr, get_width() - (fabs(extents.x_advance) + 5), extents.height + 4);
596 cairo_show_text (cr, buf);
598 float _corner_radius = 4.0;
601 float rheight = 10.0;
602 Gtkmm2ext::rounded_rectangle (cr, 2, 1, get_width()-4, rheight, _corner_radius);
603 cairo_set_source (cr, shine_pattern);
606 if (ARDOUR::Config->get_widget_prelight()) {
608 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, _corner_radius);
609 cairo_set_source_rgba (cr, 1, 1, 1, 0.2);
616 ShuttleControl::shuttle_unit_clicked ()
618 if (shuttle_unit_menu == 0) {
619 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
621 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
625 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
627 Config->set_shuttle_behaviour (s);
631 ShuttleControl::set_shuttle_units (ShuttleUnits s)
633 Config->set_shuttle_units (s);
637 ShuttleControl::update_speed_display ()
639 if (_session->transport_speed() != last_speed_displayed) {
644 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
645 : PBD::Controllable (X_("Shuttle"))
651 ShuttleControl::ShuttleControllable::set_value (double val)
659 fract = -((0.5 - val)/0.5);
661 fract = ((val - 0.5)/0.5);
665 sc.set_shuttle_fract (fract);
669 ShuttleControl::ShuttleControllable::get_value () const
671 return sc.get_shuttle_fract ();
675 ShuttleControl::parameter_changed (std::string p)
677 if (p == "shuttle-behaviour") {
678 switch (Config->get_shuttle_behaviour ()) {
680 /* back to Sprung - reset to speed = 1.0 if playing
683 if (_session->transport_rolling()) {
684 if (_session->transport_speed() == 1.0) {
687 /* reset current speed and
688 revert to 1.0 as the default
690 _session->request_transport_speed (1.0);
691 /* redraw when speed changes */
704 } else if (p == "shuttle-units") {
711 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
715 if (ARDOUR::Config->get_widget_prelight()) {
719 return CairoWidget::on_enter_notify_event (ev);
723 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
727 if (ARDOUR::Config->get_widget_prelight()) {
731 return CairoWidget::on_leave_notify_event (ev);