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::reset_speed ()
261 if (_session->transport_rolling()) {
262 _session->request_transport_speed (1.0, true);
264 _session->request_transport_speed (0.0, true);
269 ShuttleControl::set_shuttle_max_speed (float speed)
271 Config->set_shuttle_max_speed (speed);
272 shuttle_max_speed = speed;
273 last_speed_displayed = -99999999;
277 ShuttleControl::on_button_press_event (GdkEventButton* ev)
283 if (binding_proxy.button_press_handler (ev)) {
287 if (Keyboard::is_context_menu_event (ev)) {
288 if (shuttle_context_menu == 0) {
289 build_shuttle_context_menu ();
291 shuttle_context_menu->popup (ev->button, ev->time);
295 switch (ev->button) {
297 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
298 if (_session->transport_rolling()) {
299 _session->request_transport_speed (1.0);
303 shuttle_grabbed = true;
304 shuttle_speed_on_grab = _session->transport_speed ();
305 requested_speed = shuttle_speed_on_grab;
306 mouse_shuttle (ev->x, true);
307 gdk_pointer_grab(ev->window,false,
308 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
323 ShuttleControl::on_button_release_event (GdkEventButton* ev)
329 switch (ev->button) {
331 if (shuttle_grabbed) {
332 shuttle_grabbed = false;
333 remove_modal_grab ();
334 gdk_pointer_ungrab (GDK_CURRENT_TIME);
336 if (Config->get_shuttle_behaviour() == Sprung) {
337 if (shuttle_speed_on_grab == 0 ) {
338 _session->request_stop ();
340 _session->request_transport_speed (shuttle_speed_on_grab);
343 mouse_shuttle (ev->x, true);
349 if (_session->transport_rolling()) {
350 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
364 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
370 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
372 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
376 bool semis = (Config->get_shuttle_units() == Semitones);
378 switch (ev->direction) {
380 case GDK_SCROLL_RIGHT:
382 if (shuttle_fract == 0) {
383 shuttle_fract = semitones_as_fract (1, false);
386 int st = fract_as_semitones (shuttle_fract, rev);
387 shuttle_fract = semitones_as_fract (st + 1, rev);
390 shuttle_fract += 0.00125;
393 case GDK_SCROLL_DOWN:
394 case GDK_SCROLL_LEFT:
396 if (shuttle_fract == 0) {
397 shuttle_fract = semitones_as_fract (1, true);
400 int st = fract_as_semitones (shuttle_fract, rev);
401 shuttle_fract = semitones_as_fract (st - 1, rev);
404 shuttle_fract -= 0.00125;
413 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
414 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
416 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
417 to the far side of it.
420 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
421 switch (ev->direction) {
423 case GDK_SCROLL_RIGHT:
424 shuttle_fract = upper_side_of_dead_zone;
426 case GDK_SCROLL_DOWN:
427 case GDK_SCROLL_LEFT:
428 shuttle_fract = lower_side_of_dead_zone;
431 /* impossible, checked above */
437 use_shuttle_fract (true);
443 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
445 if (!_session || !shuttle_grabbed) {
449 return mouse_shuttle (ev->x, false);
453 ShuttleControl::mouse_shuttle (double x, bool force)
455 double const center = get_width() / 2.0;
456 double distance_from_center = x - center;
458 if (distance_from_center > 0) {
459 distance_from_center = min (distance_from_center, center);
461 distance_from_center = max (distance_from_center, -center);
464 /* compute shuttle fract as expressing how far between the center
465 and the edge we are. positive values indicate we are right of
466 center, negative values indicate left of center
469 shuttle_fract = distance_from_center / center; // center == half the width
470 use_shuttle_fract (force);
475 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
478 use_shuttle_fract (false, zero_ok);
482 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
484 assert (speed != 0.0);
488 return (int) round (12.0 * fast_log2 (-speed));
491 return (int) round (12.0 * fast_log2 (speed));
496 ShuttleControl::semitones_as_speed (int semi, bool reverse)
499 return -pow (2.0, (semi / 12.0));
501 return pow (2.0, (semi / 12.0));
506 ShuttleControl::semitones_as_fract (int semi, bool reverse)
508 float speed = semitones_as_speed (semi, reverse);
509 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
513 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
515 assert (fract != 0.0);
516 return speed_as_semitones (fract * 4.0, reverse);
520 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
522 microseconds_t now = get_microseconds();
524 shuttle_fract = max (-1.0f, shuttle_fract);
525 shuttle_fract = min (1.0f, shuttle_fract);
527 /* do not attempt to submit a motion-driven transport speed request
528 more than once per process cycle.
531 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
535 last_shuttle_request = now;
539 if (Config->get_shuttle_units() == Semitones) {
540 if (shuttle_fract != 0.0) {
542 int semi = fract_as_semitones (shuttle_fract, reverse);
543 speed = semitones_as_speed (semi, reverse);
548 speed = shuttle_max_speed * shuttle_fract;
551 requested_speed = speed;
553 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
555 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
560 ShuttleControl::set_colors ()
564 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
566 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
573 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
575 // center slider line
576 float yc = get_height() / 2;
578 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
579 cairo_set_line_width (cr, 3);
580 cairo_move_to (cr, lw, yc);
581 cairo_line_to (cr, get_width () - lw, yc);
582 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
583 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
584 cairo_stroke_preserve (cr);
585 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
590 float acutal_speed = 0.0;
594 speed = _session->transport_speed ();
595 acutal_speed = speed;
596 if (shuttle_grabbed) {
597 speed = requested_speed;
602 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
603 float marker_size = round (get_height() * 0.66);
604 float avail_width = get_width() - marker_size;
605 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
607 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
608 cairo_set_source_rgba (cr, 0, 0, 0, 1);
610 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
612 uint32_t col = UIConfiguration::instance().color ("shuttle");
613 ArdourCanvas::set_source_rgba (cr, col);
615 cairo_set_source (cr, pattern);
617 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
618 cairo_fill_preserve (cr);
619 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
624 if (acutal_speed != 0) {
625 if (Config->get_shuttle_units() == Percentage) {
626 if (acutal_speed == 1.0) {
627 snprintf (buf, sizeof (buf), "%s", _("Play"));
629 if (acutal_speed < 0.0) {
630 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
632 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
637 int semi = speed_as_semitones (acutal_speed, reversed);
639 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
641 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
645 snprintf (buf, sizeof (buf), "%s", _("Stop"));
648 last_speed_displayed = acutal_speed;
650 _info_button.set_text (buf);
653 if (UIConfiguration::instance().get_widget_prelight()) {
655 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
656 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
664 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
666 Config->set_shuttle_behaviour (s);
670 ShuttleControl::set_shuttle_units (ShuttleUnits s)
672 Config->set_shuttle_units (s);
675 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
676 : PBD::Controllable (X_("Shuttle"))
682 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
684 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
688 ShuttleControl::ShuttleControllable::get_value () const
690 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
694 ShuttleControl::parameter_changed (std::string p)
696 if (p == "shuttle-behaviour") {
697 switch (Config->get_shuttle_behaviour ()) {
699 /* back to Sprung - reset to speed = 1.0 if playing
702 if (_session->transport_rolling()) {
703 if (_session->transport_speed() == 1.0) {
706 /* reset current speed and
707 revert to 1.0 as the default
709 _session->request_transport_speed (1.0);
710 /* redraw when speed changes */
723 } else if (p == "shuttle-max-speed") {
725 } else if (p == "shuttle-units") {
732 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
736 if (UIConfiguration::instance().get_widget_prelight()) {
740 return CairoWidget::on_enter_notify_event (ev);
744 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
748 if (UIConfiguration::instance().get_widget_prelight()) {
752 return CairoWidget::on_leave_notify_event (ev);