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/colors.h"
31 #include "gtkmm2ext/keyboard.h"
32 #include "gtkmm2ext/gui_thread.h"
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/rgb_macros.h"
36 #include "widgets/tooltips.h"
39 #include "rgb_macros.h"
40 #include "shuttle_control.h"
45 using namespace Gtkmm2ext;
46 using namespace ARDOUR;
47 using namespace ArdourWidgets;
51 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
56 ShuttleControl::ShuttleControl ()
57 : _controllable (new ShuttleControllable (*this))
58 , binding_proxy (_controllable)
60 _info_button.set_layout_font (UIConfiguration::instance().get_NormalFont());
61 _info_button.set_sizing_text (S_("LogestShuttle|< +00 st"));
62 _info_button.set_name ("shuttle text");
63 _info_button.set_sensitive (false);
64 _info_button.set_visual_state (Gtkmm2ext::NoVisualState);
65 _info_button.set_elements (ArdourButton::Text);
67 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
71 last_shuttle_request = 0;
72 last_speed_displayed = -99999999;
73 shuttle_grabbed = false;
74 shuttle_speed_on_grab = 0;
76 shuttle_max_speed = 8.0f;
77 shuttle_context_menu = 0;
80 set_flags (CAN_FOCUS);
81 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);
82 set_name (X_("ShuttleControl"));
86 shuttle_max_speed = Config->get_shuttle_max_speed();
88 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
89 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
90 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
91 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
92 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
93 else { shuttle_max_speed = 1.5f; }
95 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
96 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
100 /* gtkmm 2.4: the C++ wrapper doesn't work */
101 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
102 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
105 ShuttleControl::~ShuttleControl ()
107 cairo_pattern_destroy (pattern);
108 cairo_pattern_destroy (shine_pattern);
109 delete shuttle_context_menu;
113 ShuttleControl::set_session (Session *s)
115 SessionHandlePtr::set_session (s);
118 set_sensitive (true);
119 _session->add_controllable (_controllable);
121 set_sensitive (false);
126 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
129 cairo_pattern_destroy (pattern);
131 cairo_pattern_destroy (shine_pattern);
135 CairoWidget::on_size_allocate ( alloc);
138 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
139 uint32_t col = UIConfiguration::instance().color ("shuttle");
141 UINT_TO_RGBA(col, &r, &g, &b, &a);
142 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
143 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
144 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
147 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
148 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
149 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
150 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
154 ShuttleControl::map_transport_state ()
156 float speed = _session->actual_speed ();
158 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
159 && !( speed == 1.f && last_speed_displayed != 1.f)
160 && !( speed == 0.f && last_speed_displayed != 0.f)
163 return; // nothing to see here, move along.
166 // Q: is there a good reason why we re-calculate this every time?
167 if (fabs(speed) <= (2*DBL_EPSILON)) {
170 if (Config->get_shuttle_units() == Semitones) {
172 int semi = speed_as_semitones (speed, reverse);
173 shuttle_fract = semitones_as_fract (semi, reverse);
175 shuttle_fract = speed/shuttle_max_speed;
183 ShuttleControl::build_shuttle_context_menu ()
185 using namespace Menu_Helpers;
187 shuttle_context_menu = new Menu();
188 MenuList& items = shuttle_context_menu->items();
190 Menu* speed_menu = manage (new Menu());
191 MenuList& speed_items = speed_menu->items();
193 Menu* units_menu = manage (new Menu);
194 MenuList& units_items = units_menu->items();
195 RadioMenuItem::Group units_group;
197 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
198 if (Config->get_shuttle_units() == Percentage) {
199 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
201 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
202 if (Config->get_shuttle_units() == Semitones) {
203 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
205 items.push_back (MenuElem (_("Units"), *units_menu));
207 Menu* style_menu = manage (new Menu);
208 MenuList& style_items = style_menu->items();
209 RadioMenuItem::Group style_group;
211 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
212 if (Config->get_shuttle_behaviour() == Sprung) {
213 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
215 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
216 if (Config->get_shuttle_behaviour() == Wheel) {
217 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
220 items.push_back (MenuElem (_("Mode"), *style_menu));
222 RadioMenuItem::Group speed_group;
224 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
225 if (shuttle_max_speed == 8.0) {
226 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
228 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
229 if (shuttle_max_speed == 6.0) {
230 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
232 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
233 if (shuttle_max_speed == 4.0) {
234 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
236 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
237 if (shuttle_max_speed == 3.0) {
238 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
240 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
241 if (shuttle_max_speed == 2.0) {
242 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
244 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
245 if (shuttle_max_speed == 1.5) {
246 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
249 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
251 items.push_back (SeparatorElem ());
252 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
256 ShuttleControl::reset_speed ()
258 if (_session->transport_rolling()) {
259 _session->request_transport_speed (1.0, true);
261 _session->request_transport_speed (0.0, true);
266 ShuttleControl::set_shuttle_max_speed (float speed)
268 Config->set_shuttle_max_speed (speed);
269 shuttle_max_speed = speed;
270 last_speed_displayed = -99999999;
274 ShuttleControl::on_button_press_event (GdkEventButton* ev)
280 if (binding_proxy.button_press_handler (ev)) {
284 if (Keyboard::is_context_menu_event (ev)) {
285 if (shuttle_context_menu == 0) {
286 build_shuttle_context_menu ();
288 shuttle_context_menu->popup (ev->button, ev->time);
292 switch (ev->button) {
294 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
295 if (_session->transport_rolling()) {
296 _session->request_transport_speed (1.0);
300 shuttle_grabbed = true;
301 shuttle_speed_on_grab = _session->actual_speed ();
302 requested_speed = shuttle_speed_on_grab;
303 mouse_shuttle (ev->x, true);
304 gdk_pointer_grab(ev->window,false,
305 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
320 ShuttleControl::on_button_release_event (GdkEventButton* ev)
326 switch (ev->button) {
328 if (shuttle_grabbed) {
329 shuttle_grabbed = false;
330 remove_modal_grab ();
331 gdk_pointer_ungrab (GDK_CURRENT_TIME);
333 if (Config->get_shuttle_behaviour() == Sprung) {
334 if (shuttle_speed_on_grab == 0 ) {
335 _session->request_stop ();
337 _session->request_transport_speed (shuttle_speed_on_grab);
340 mouse_shuttle (ev->x, true);
346 if (_session->transport_rolling()) {
347 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
361 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
367 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
369 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
373 bool semis = (Config->get_shuttle_units() == Semitones);
375 switch (ev->direction) {
377 case GDK_SCROLL_RIGHT:
379 if (shuttle_fract == 0) {
380 shuttle_fract = semitones_as_fract (1, false);
383 int st = fract_as_semitones (shuttle_fract, rev);
384 shuttle_fract = semitones_as_fract (st + 1, rev);
387 shuttle_fract += 0.00125;
390 case GDK_SCROLL_DOWN:
391 case GDK_SCROLL_LEFT:
393 if (shuttle_fract == 0) {
394 shuttle_fract = semitones_as_fract (1, true);
397 int st = fract_as_semitones (shuttle_fract, rev);
398 shuttle_fract = semitones_as_fract (st - 1, rev);
401 shuttle_fract -= 0.00125;
410 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
411 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
413 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
414 to the far side of it.
417 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
418 switch (ev->direction) {
420 case GDK_SCROLL_RIGHT:
421 shuttle_fract = upper_side_of_dead_zone;
423 case GDK_SCROLL_DOWN:
424 case GDK_SCROLL_LEFT:
425 shuttle_fract = lower_side_of_dead_zone;
428 /* impossible, checked above */
434 use_shuttle_fract (true);
440 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
442 if (!_session || !shuttle_grabbed) {
446 return mouse_shuttle (ev->x, false);
450 ShuttleControl::mouse_shuttle (double x, bool force)
452 double const center = get_width() / 2.0;
453 double distance_from_center = x - center;
455 if (distance_from_center > 0) {
456 distance_from_center = min (distance_from_center, center);
458 distance_from_center = max (distance_from_center, -center);
461 /* compute shuttle fract as expressing how far between the center
462 and the edge we are. positive values indicate we are right of
463 center, negative values indicate left of center
466 shuttle_fract = distance_from_center / center; // center == half the width
467 use_shuttle_fract (force);
472 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
475 use_shuttle_fract (false, zero_ok);
479 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
481 assert (speed != 0.0);
485 return (int) round (12.0 * fast_log2 (-speed));
488 return (int) round (12.0 * fast_log2 (speed));
493 ShuttleControl::semitones_as_speed (int semi, bool reverse)
496 return -pow (2.0, (semi / 12.0));
498 return pow (2.0, (semi / 12.0));
503 ShuttleControl::semitones_as_fract (int semi, bool reverse)
505 float speed = semitones_as_speed (semi, reverse);
506 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
510 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
512 assert (fract != 0.0);
513 return speed_as_semitones (fract * 4.0, reverse);
517 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
519 microseconds_t now = get_microseconds();
521 shuttle_fract = max (-1.0f, shuttle_fract);
522 shuttle_fract = min (1.0f, shuttle_fract);
524 /* do not attempt to submit a motion-driven transport speed request
525 more than once per process cycle.
528 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
532 last_shuttle_request = now;
536 if (Config->get_shuttle_units() == Semitones) {
537 if (shuttle_fract != 0.0) {
539 int semi = fract_as_semitones (shuttle_fract, reverse);
540 speed = semitones_as_speed (semi, reverse);
545 speed = shuttle_max_speed * shuttle_fract;
548 requested_speed = speed;
550 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
552 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
557 ShuttleControl::set_colors ()
561 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
563 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
570 ShuttleControl::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
572 cairo_t* cr = ctx->cobj();
573 // center slider line
574 float yc = get_height() / 2;
576 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
577 cairo_set_line_width (cr, 3);
578 cairo_move_to (cr, lw, yc);
579 cairo_line_to (cr, get_width () - lw, yc);
580 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
581 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
582 cairo_stroke_preserve (cr);
583 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
588 float acutal_speed = 0.0;
592 speed = _session->actual_speed ();
593 acutal_speed = speed;
594 if (shuttle_grabbed) {
595 speed = requested_speed;
600 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
601 float marker_size = round (get_height() * 0.66);
602 float avail_width = get_width() - marker_size;
603 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
605 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
606 cairo_set_source_rgba (cr, 0, 0, 0, 1);
608 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
610 uint32_t col = UIConfiguration::instance().color ("shuttle");
611 Gtkmm2ext::set_source_rgba (cr, col);
613 cairo_set_source (cr, pattern);
615 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
616 cairo_fill_preserve (cr);
617 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
622 if (acutal_speed != 0) {
623 if (Config->get_shuttle_units() == Percentage) {
624 if (acutal_speed == 1.0) {
625 snprintf (buf, sizeof (buf), "%s", _("Play"));
627 if (acutal_speed < 0.0) {
628 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
630 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
635 int semi = speed_as_semitones (acutal_speed, reversed);
637 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
639 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
643 snprintf (buf, sizeof (buf), "%s", _("Stop"));
646 last_speed_displayed = acutal_speed;
648 _info_button.set_text (buf);
651 if (UIConfiguration::instance().get_widget_prelight()) {
653 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
654 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
662 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
664 Config->set_shuttle_behaviour (s);
668 ShuttleControl::set_shuttle_units (ShuttleUnits s)
670 Config->set_shuttle_units (s);
673 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
674 : PBD::Controllable (X_("Shuttle"))
680 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
682 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
686 ShuttleControl::ShuttleControllable::get_value () const
688 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
692 ShuttleControl::parameter_changed (std::string p)
694 if (p == "shuttle-behaviour") {
695 switch (Config->get_shuttle_behaviour ()) {
697 /* back to Sprung - reset to speed = 1.0 if playing
700 if (_session->transport_rolling()) {
701 if (_session->actual_speed() == 1.0) {
704 /* reset current speed and
705 revert to 1.0 as the default
707 _session->request_transport_speed (1.0);
708 /* redraw when speed changes */
721 } else if (p == "shuttle-max-speed") {
723 } else if (p == "shuttle-units") {
730 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
734 if (UIConfiguration::instance().get_widget_prelight()) {
738 return CairoWidget::on_enter_notify_event (ev);
742 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
746 if (UIConfiguration::instance().get_widget_prelight()) {
750 return CairoWidget::on_leave_notify_event (ev);