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.
19 #define BASELINESTRETCH (1.25)
25 #include "ardour/ardour.h"
26 #include "ardour/audioengine.h"
27 #include "ardour/rc_configuration.h"
28 #include "ardour/session.h"
30 #include "gtkmm2ext/keyboard.h"
31 #include "gtkmm2ext/gui_thread.h"
32 #include "gtkmm2ext/cairocell.h"
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/rgb_macros.h"
36 #include "canvas/utils.h"
37 #include "canvas/colors.h"
40 #include "rgb_macros.h"
41 #include "shuttle_control.h"
47 using namespace Gtkmm2ext;
48 using namespace ARDOUR;
49 using namespace ARDOUR_UI_UTILS;
53 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
58 ShuttleControl::ShuttleControl ()
59 : _controllable (new ShuttleControllable (*this))
60 , binding_proxy (_controllable)
62 _info_button.set_layout_font (UIConfiguration::instance().get_NormalFont());
63 _info_button.set_sizing_text (S_("LogestShuttle|< +00 st"));
64 _info_button.set_name ("shuttle text");
65 _info_button.set_sensitive (false);
66 _info_button.set_visual_state (Gtkmm2ext::NoVisualState);
67 _info_button.set_elements (ArdourButton::Text);
69 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
73 last_shuttle_request = 0;
74 last_speed_displayed = -99999999;
75 shuttle_grabbed = false;
76 shuttle_speed_on_grab = 0;
78 shuttle_max_speed = 8.0f;
79 shuttle_style_menu = 0;
80 shuttle_unit_menu = 0;
81 shuttle_context_menu = 0;
84 set_flags (CAN_FOCUS);
85 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);
86 set_name (X_("ShuttleControl"));
90 shuttle_max_speed = Config->get_shuttle_max_speed();
92 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
93 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
94 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
95 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
96 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
97 else { shuttle_max_speed = 1.5f; }
99 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
100 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
104 /* gtkmm 2.4: the C++ wrapper doesn't work */
105 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
106 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
109 ShuttleControl::~ShuttleControl ()
111 cairo_pattern_destroy (pattern);
112 cairo_pattern_destroy (shine_pattern);
116 ShuttleControl::set_session (Session *s)
118 SessionHandlePtr::set_session (s);
121 set_sensitive (true);
122 _session->add_controllable (_controllable);
124 set_sensitive (false);
129 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
132 cairo_pattern_destroy (pattern);
134 cairo_pattern_destroy (shine_pattern);
138 CairoWidget::on_size_allocate ( alloc);
141 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
142 uint32_t col = UIConfiguration::instance().color ("shuttle");
144 UINT_TO_RGBA(col, &r, &g, &b, &a);
145 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
146 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
147 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
150 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
151 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
152 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
153 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
157 ShuttleControl::map_transport_state ()
159 float speed = _session->transport_speed ();
161 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
162 && !( speed == 1.f && last_speed_displayed != 1.f)
163 && !( speed == 0.f && last_speed_displayed != 0.f)
166 return; // nothing to see here, move along.
169 // Q: is there a good reason why we re-calculate this every time?
170 if (fabs(speed) <= (2*DBL_EPSILON)) {
173 if (Config->get_shuttle_units() == Semitones) {
175 int semi = speed_as_semitones (speed, reverse);
176 shuttle_fract = semitones_as_fract (semi, reverse);
178 shuttle_fract = speed/shuttle_max_speed;
186 ShuttleControl::build_shuttle_context_menu ()
188 using namespace Menu_Helpers;
190 shuttle_context_menu = new Menu();
191 MenuList& items = shuttle_context_menu->items();
193 Menu* speed_menu = manage (new Menu());
194 MenuList& speed_items = speed_menu->items();
196 Menu* units_menu = manage (new Menu);
197 MenuList& units_items = units_menu->items();
198 RadioMenuItem::Group units_group;
200 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
201 if (Config->get_shuttle_units() == Percentage) {
202 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
204 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
205 if (Config->get_shuttle_units() == Semitones) {
206 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
208 items.push_back (MenuElem (_("Units"), *units_menu));
210 Menu* style_menu = manage (new Menu);
211 MenuList& style_items = style_menu->items();
212 RadioMenuItem::Group style_group;
214 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
215 if (Config->get_shuttle_behaviour() == Sprung) {
216 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
218 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
219 if (Config->get_shuttle_behaviour() == Wheel) {
220 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
223 items.push_back (MenuElem (_("Mode"), *style_menu));
225 RadioMenuItem::Group speed_group;
227 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
228 if (shuttle_max_speed == 8.0) {
229 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
231 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
232 if (shuttle_max_speed == 6.0) {
233 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
235 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
236 if (shuttle_max_speed == 4.0) {
237 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
239 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
240 if (shuttle_max_speed == 3.0) {
241 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
243 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
244 if (shuttle_max_speed == 2.0) {
245 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
247 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
248 if (shuttle_max_speed == 1.5) {
249 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
252 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
254 items.push_back (SeparatorElem ());
255 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
259 ShuttleControl::show_shuttle_context_menu ()
261 if (shuttle_context_menu == 0) {
262 build_shuttle_context_menu ();
265 shuttle_context_menu->popup (1, gtk_get_current_event_time());
269 ShuttleControl::reset_speed ()
271 if (_session->transport_rolling()) {
272 _session->request_transport_speed (1.0, true);
274 _session->request_transport_speed (0.0, true);
279 ShuttleControl::set_shuttle_max_speed (float speed)
281 Config->set_shuttle_max_speed (speed);
282 shuttle_max_speed = speed;
283 last_speed_displayed = -99999999;
287 ShuttleControl::on_button_press_event (GdkEventButton* ev)
293 if (binding_proxy.button_press_handler (ev)) {
297 if (Keyboard::is_context_menu_event (ev)) {
298 show_shuttle_context_menu ();
302 switch (ev->button) {
304 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
305 if (_session->transport_rolling()) {
306 _session->request_transport_speed (1.0);
310 shuttle_grabbed = true;
311 shuttle_speed_on_grab = _session->transport_speed ();
312 requested_speed = shuttle_speed_on_grab;
313 mouse_shuttle (ev->x, true);
314 gdk_pointer_grab(ev->window,false,
315 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
330 ShuttleControl::on_button_release_event (GdkEventButton* ev)
336 switch (ev->button) {
338 if (shuttle_grabbed) {
339 shuttle_grabbed = false;
340 remove_modal_grab ();
341 gdk_pointer_ungrab (GDK_CURRENT_TIME);
343 if (Config->get_shuttle_behaviour() == Sprung) {
344 if (shuttle_speed_on_grab == 0 ) {
345 _session->request_stop ();
347 _session->request_transport_speed (shuttle_speed_on_grab);
350 mouse_shuttle (ev->x, true);
356 if (_session->transport_rolling()) {
357 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
371 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
377 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
379 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
383 bool semis = (Config->get_shuttle_units() == Semitones);
385 switch (ev->direction) {
387 case GDK_SCROLL_RIGHT:
389 if (shuttle_fract == 0) {
390 shuttle_fract = semitones_as_fract (1, false);
393 int st = fract_as_semitones (shuttle_fract, rev);
394 shuttle_fract = semitones_as_fract (st + 1, rev);
397 shuttle_fract += 0.00125;
400 case GDK_SCROLL_DOWN:
401 case GDK_SCROLL_LEFT:
403 if (shuttle_fract == 0) {
404 shuttle_fract = semitones_as_fract (1, true);
407 int st = fract_as_semitones (shuttle_fract, rev);
408 shuttle_fract = semitones_as_fract (st - 1, rev);
411 shuttle_fract -= 0.00125;
420 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
421 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
423 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
424 to the far side of it.
427 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
428 switch (ev->direction) {
430 case GDK_SCROLL_RIGHT:
431 shuttle_fract = upper_side_of_dead_zone;
433 case GDK_SCROLL_DOWN:
434 case GDK_SCROLL_LEFT:
435 shuttle_fract = lower_side_of_dead_zone;
438 /* impossible, checked above */
444 use_shuttle_fract (true);
450 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
452 if (!_session || !shuttle_grabbed) {
456 return mouse_shuttle (ev->x, false);
460 ShuttleControl::mouse_shuttle (double x, bool force)
462 double const center = get_width() / 2.0;
463 double distance_from_center = x - center;
465 if (distance_from_center > 0) {
466 distance_from_center = min (distance_from_center, center);
468 distance_from_center = max (distance_from_center, -center);
471 /* compute shuttle fract as expressing how far between the center
472 and the edge we are. positive values indicate we are right of
473 center, negative values indicate left of center
476 shuttle_fract = distance_from_center / center; // center == half the width
477 use_shuttle_fract (force);
482 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
485 use_shuttle_fract (false, zero_ok);
489 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
491 assert (speed != 0.0);
495 return (int) round (12.0 * fast_log2 (-speed));
498 return (int) round (12.0 * fast_log2 (speed));
503 ShuttleControl::semitones_as_speed (int semi, bool reverse)
506 return -pow (2.0, (semi / 12.0));
508 return pow (2.0, (semi / 12.0));
513 ShuttleControl::semitones_as_fract (int semi, bool reverse)
515 float speed = semitones_as_speed (semi, reverse);
516 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
520 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
522 assert (fract != 0.0);
523 return speed_as_semitones (fract * 4.0, reverse);
527 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
529 microseconds_t now = get_microseconds();
531 shuttle_fract = max (-1.0f, shuttle_fract);
532 shuttle_fract = min (1.0f, shuttle_fract);
534 /* do not attempt to submit a motion-driven transport speed request
535 more than once per process cycle.
538 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
542 last_shuttle_request = now;
546 if (Config->get_shuttle_units() == Semitones) {
547 if (shuttle_fract != 0.0) {
549 int semi = fract_as_semitones (shuttle_fract, reverse);
550 speed = semitones_as_speed (semi, reverse);
555 speed = shuttle_max_speed * shuttle_fract;
558 requested_speed = speed;
560 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
562 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
567 ShuttleControl::set_colors ()
571 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
573 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
580 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
582 // center slider line
583 float yc = get_height() / 2;
585 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
586 cairo_set_line_width (cr, 3);
587 cairo_move_to (cr, lw, yc);
588 cairo_line_to (cr, get_width () - lw, yc);
589 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
590 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
591 cairo_stroke_preserve (cr);
592 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
597 float acutal_speed = 0.0;
601 speed = _session->transport_speed ();
602 acutal_speed = speed;
603 if (shuttle_grabbed) {
604 speed = requested_speed;
609 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
610 float marker_size = round (get_height() * 0.66);
611 float avail_width = get_width() - marker_size;
612 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
614 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
615 cairo_set_source_rgba (cr, 0, 0, 0, 1);
617 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
619 uint32_t col = UIConfiguration::instance().color ("shuttle");
620 ArdourCanvas::set_source_rgba (cr, col);
622 cairo_set_source (cr, pattern);
624 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
625 cairo_fill_preserve (cr);
626 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
631 if (acutal_speed != 0) {
632 if (Config->get_shuttle_units() == Percentage) {
633 if (acutal_speed == 1.0) {
634 snprintf (buf, sizeof (buf), "%s", _("Play"));
636 if (acutal_speed < 0.0) {
637 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
639 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
644 int semi = speed_as_semitones (acutal_speed, reversed);
646 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
648 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
652 snprintf (buf, sizeof (buf), "%s", _("Stop"));
655 last_speed_displayed = acutal_speed;
657 _info_button.set_text (buf);
660 if (UIConfiguration::instance().get_widget_prelight()) {
662 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
663 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
671 ShuttleControl::shuttle_unit_clicked ()
673 if (shuttle_unit_menu == 0) {
674 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
676 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
680 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
682 Config->set_shuttle_behaviour (s);
686 ShuttleControl::set_shuttle_units (ShuttleUnits s)
688 Config->set_shuttle_units (s);
691 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
692 : PBD::Controllable (X_("Shuttle"))
698 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
700 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
704 ShuttleControl::ShuttleControllable::get_value () const
706 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
710 ShuttleControl::parameter_changed (std::string p)
712 if (p == "shuttle-behaviour") {
713 switch (Config->get_shuttle_behaviour ()) {
715 /* back to Sprung - reset to speed = 1.0 if playing
718 if (_session->transport_rolling()) {
719 if (_session->transport_speed() == 1.0) {
722 /* reset current speed and
723 revert to 1.0 as the default
725 _session->request_transport_speed (1.0);
726 /* redraw when speed changes */
739 } else if (p == "shuttle-max-speed") {
741 } else if (p == "shuttle-units") {
748 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
752 if (UIConfiguration::instance().get_widget_prelight()) {
756 return CairoWidget::on_enter_notify_event (ev);
760 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
764 if (UIConfiguration::instance().get_widget_prelight()) {
768 return CairoWidget::on_leave_notify_event (ev);