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::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
575 cairo_t* cr = ctx->cobj();
576 // center slider line
577 float yc = get_height() / 2;
579 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
580 cairo_set_line_width (cr, 3);
581 cairo_move_to (cr, lw, yc);
582 cairo_line_to (cr, get_width () - lw, yc);
583 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
584 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
585 cairo_stroke_preserve (cr);
586 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
591 float acutal_speed = 0.0;
595 speed = _session->transport_speed ();
596 acutal_speed = speed;
597 if (shuttle_grabbed) {
598 speed = requested_speed;
603 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
604 float marker_size = round (get_height() * 0.66);
605 float avail_width = get_width() - marker_size;
606 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
608 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
609 cairo_set_source_rgba (cr, 0, 0, 0, 1);
611 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
613 uint32_t col = UIConfiguration::instance().color ("shuttle");
614 ArdourCanvas::set_source_rgba (cr, col);
616 cairo_set_source (cr, pattern);
618 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
619 cairo_fill_preserve (cr);
620 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
625 if (acutal_speed != 0) {
626 if (Config->get_shuttle_units() == Percentage) {
627 if (acutal_speed == 1.0) {
628 snprintf (buf, sizeof (buf), "%s", _("Play"));
630 if (acutal_speed < 0.0) {
631 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
633 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
638 int semi = speed_as_semitones (acutal_speed, reversed);
640 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
642 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
646 snprintf (buf, sizeof (buf), "%s", _("Stop"));
649 last_speed_displayed = acutal_speed;
651 _info_button.set_text (buf);
654 if (UIConfiguration::instance().get_widget_prelight()) {
656 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
657 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
665 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
667 Config->set_shuttle_behaviour (s);
671 ShuttleControl::set_shuttle_units (ShuttleUnits s)
673 Config->set_shuttle_units (s);
676 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
677 : PBD::Controllable (X_("Shuttle"))
683 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
685 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
689 ShuttleControl::ShuttleControllable::get_value () const
691 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
695 ShuttleControl::parameter_changed (std::string p)
697 if (p == "shuttle-behaviour") {
698 switch (Config->get_shuttle_behaviour ()) {
700 /* back to Sprung - reset to speed = 1.0 if playing
703 if (_session->transport_rolling()) {
704 if (_session->transport_speed() == 1.0) {
707 /* reset current speed and
708 revert to 1.0 as the default
710 _session->request_transport_speed (1.0);
711 /* redraw when speed changes */
724 } else if (p == "shuttle-max-speed") {
726 } else if (p == "shuttle-units") {
733 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
737 if (UIConfiguration::instance().get_widget_prelight()) {
741 return CairoWidget::on_enter_notify_event (ev);
745 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
749 if (UIConfiguration::instance().get_widget_prelight()) {
753 return CairoWidget::on_leave_notify_event (ev);