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_style_menu = 0;
78 shuttle_unit_menu = 0;
79 shuttle_context_menu = 0;
82 set_flags (CAN_FOCUS);
83 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);
84 set_name (X_("ShuttleControl"));
88 shuttle_max_speed = Config->get_shuttle_max_speed();
90 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
91 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
92 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
93 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
94 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
95 else { shuttle_max_speed = 1.5f; }
97 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
98 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
102 /* gtkmm 2.4: the C++ wrapper doesn't work */
103 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
104 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
107 ShuttleControl::~ShuttleControl ()
109 cairo_pattern_destroy (pattern);
110 cairo_pattern_destroy (shine_pattern);
114 ShuttleControl::set_session (Session *s)
116 SessionHandlePtr::set_session (s);
119 set_sensitive (true);
120 _session->add_controllable (_controllable);
122 set_sensitive (false);
127 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
130 cairo_pattern_destroy (pattern);
132 cairo_pattern_destroy (shine_pattern);
136 CairoWidget::on_size_allocate ( alloc);
139 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
140 uint32_t col = UIConfiguration::instance().color ("shuttle");
142 UINT_TO_RGBA(col, &r, &g, &b, &a);
143 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
144 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
145 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
148 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
149 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
150 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
151 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
155 ShuttleControl::map_transport_state ()
157 float speed = _session->actual_speed ();
159 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
160 && !( speed == 1.f && last_speed_displayed != 1.f)
161 && !( speed == 0.f && last_speed_displayed != 0.f)
164 return; // nothing to see here, move along.
167 // Q: is there a good reason why we re-calculate this every time?
168 if (fabs(speed) <= (2*DBL_EPSILON)) {
171 if (Config->get_shuttle_units() == Semitones) {
173 int semi = speed_as_semitones (speed, reverse);
174 shuttle_fract = semitones_as_fract (semi, reverse);
176 shuttle_fract = speed/shuttle_max_speed;
184 ShuttleControl::build_shuttle_context_menu ()
186 using namespace Menu_Helpers;
188 shuttle_context_menu = new Menu();
189 MenuList& items = shuttle_context_menu->items();
191 Menu* speed_menu = manage (new Menu());
192 MenuList& speed_items = speed_menu->items();
194 Menu* units_menu = manage (new Menu);
195 MenuList& units_items = units_menu->items();
196 RadioMenuItem::Group units_group;
198 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
199 if (Config->get_shuttle_units() == Percentage) {
200 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
202 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
203 if (Config->get_shuttle_units() == Semitones) {
204 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
206 items.push_back (MenuElem (_("Units"), *units_menu));
208 Menu* style_menu = manage (new Menu);
209 MenuList& style_items = style_menu->items();
210 RadioMenuItem::Group style_group;
212 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
213 if (Config->get_shuttle_behaviour() == Sprung) {
214 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
216 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
217 if (Config->get_shuttle_behaviour() == Wheel) {
218 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
221 items.push_back (MenuElem (_("Mode"), *style_menu));
223 RadioMenuItem::Group speed_group;
225 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
226 if (shuttle_max_speed == 8.0) {
227 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
229 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
230 if (shuttle_max_speed == 6.0) {
231 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
233 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
234 if (shuttle_max_speed == 4.0) {
235 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
237 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
238 if (shuttle_max_speed == 3.0) {
239 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
241 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
242 if (shuttle_max_speed == 2.0) {
243 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
245 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
246 if (shuttle_max_speed == 1.5) {
247 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
250 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
252 items.push_back (SeparatorElem ());
253 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
257 ShuttleControl::reset_speed ()
259 if (_session->transport_rolling()) {
260 _session->request_transport_speed (1.0, true);
262 _session->request_transport_speed (0.0, true);
267 ShuttleControl::set_shuttle_max_speed (float speed)
269 Config->set_shuttle_max_speed (speed);
270 shuttle_max_speed = speed;
271 last_speed_displayed = -99999999;
275 ShuttleControl::on_button_press_event (GdkEventButton* ev)
281 if (binding_proxy.button_press_handler (ev)) {
285 if (Keyboard::is_context_menu_event (ev)) {
286 if (shuttle_context_menu == 0) {
287 build_shuttle_context_menu ();
289 shuttle_context_menu->popup (ev->button, ev->time);
293 switch (ev->button) {
295 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
296 if (_session->transport_rolling()) {
297 _session->request_transport_speed (1.0);
301 shuttle_grabbed = true;
302 shuttle_speed_on_grab = _session->actual_speed ();
303 requested_speed = shuttle_speed_on_grab;
304 mouse_shuttle (ev->x, true);
305 gdk_pointer_grab(ev->window,false,
306 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
321 ShuttleControl::on_button_release_event (GdkEventButton* ev)
327 switch (ev->button) {
329 if (shuttle_grabbed) {
330 shuttle_grabbed = false;
331 remove_modal_grab ();
332 gdk_pointer_ungrab (GDK_CURRENT_TIME);
334 if (Config->get_shuttle_behaviour() == Sprung) {
335 if (shuttle_speed_on_grab == 0 ) {
336 _session->request_stop ();
338 _session->request_transport_speed (shuttle_speed_on_grab);
341 mouse_shuttle (ev->x, true);
347 if (_session->transport_rolling()) {
348 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
362 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
368 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
370 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
374 bool semis = (Config->get_shuttle_units() == Semitones);
376 switch (ev->direction) {
378 case GDK_SCROLL_RIGHT:
380 if (shuttle_fract == 0) {
381 shuttle_fract = semitones_as_fract (1, false);
384 int st = fract_as_semitones (shuttle_fract, rev);
385 shuttle_fract = semitones_as_fract (st + 1, rev);
388 shuttle_fract += 0.00125;
391 case GDK_SCROLL_DOWN:
392 case GDK_SCROLL_LEFT:
394 if (shuttle_fract == 0) {
395 shuttle_fract = semitones_as_fract (1, true);
398 int st = fract_as_semitones (shuttle_fract, rev);
399 shuttle_fract = semitones_as_fract (st - 1, rev);
402 shuttle_fract -= 0.00125;
411 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
412 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
414 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
415 to the far side of it.
418 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
419 switch (ev->direction) {
421 case GDK_SCROLL_RIGHT:
422 shuttle_fract = upper_side_of_dead_zone;
424 case GDK_SCROLL_DOWN:
425 case GDK_SCROLL_LEFT:
426 shuttle_fract = lower_side_of_dead_zone;
429 /* impossible, checked above */
435 use_shuttle_fract (true);
441 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
443 if (!_session || !shuttle_grabbed) {
447 return mouse_shuttle (ev->x, false);
451 ShuttleControl::mouse_shuttle (double x, bool force)
453 double const center = get_width() / 2.0;
454 double distance_from_center = x - center;
456 if (distance_from_center > 0) {
457 distance_from_center = min (distance_from_center, center);
459 distance_from_center = max (distance_from_center, -center);
462 /* compute shuttle fract as expressing how far between the center
463 and the edge we are. positive values indicate we are right of
464 center, negative values indicate left of center
467 shuttle_fract = distance_from_center / center; // center == half the width
468 use_shuttle_fract (force);
473 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
476 use_shuttle_fract (false, zero_ok);
480 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
482 assert (speed != 0.0);
486 return (int) round (12.0 * fast_log2 (-speed));
489 return (int) round (12.0 * fast_log2 (speed));
494 ShuttleControl::semitones_as_speed (int semi, bool reverse)
497 return -pow (2.0, (semi / 12.0));
499 return pow (2.0, (semi / 12.0));
504 ShuttleControl::semitones_as_fract (int semi, bool reverse)
506 float speed = semitones_as_speed (semi, reverse);
507 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
511 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
513 assert (fract != 0.0);
514 return speed_as_semitones (fract * 4.0, reverse);
518 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
520 microseconds_t now = get_microseconds();
522 shuttle_fract = max (-1.0f, shuttle_fract);
523 shuttle_fract = min (1.0f, shuttle_fract);
525 /* do not attempt to submit a motion-driven transport speed request
526 more than once per process cycle.
529 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
533 last_shuttle_request = now;
537 if (Config->get_shuttle_units() == Semitones) {
538 if (shuttle_fract != 0.0) {
540 int semi = fract_as_semitones (shuttle_fract, reverse);
541 speed = semitones_as_speed (semi, reverse);
546 speed = shuttle_max_speed * shuttle_fract;
549 requested_speed = speed;
551 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
553 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
558 ShuttleControl::set_colors ()
562 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
564 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
571 ShuttleControl::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
573 cairo_t* cr = ctx->cobj();
574 // center slider line
575 float yc = get_height() / 2;
577 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
578 cairo_set_line_width (cr, 3);
579 cairo_move_to (cr, lw, yc);
580 cairo_line_to (cr, get_width () - lw, yc);
581 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
582 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
583 cairo_stroke_preserve (cr);
584 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
589 float acutal_speed = 0.0;
593 speed = _session->actual_speed ();
594 acutal_speed = speed;
595 if (shuttle_grabbed) {
596 speed = requested_speed;
601 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
602 float marker_size = round (get_height() * 0.66);
603 float avail_width = get_width() - marker_size;
604 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
606 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
607 cairo_set_source_rgba (cr, 0, 0, 0, 1);
609 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
611 uint32_t col = UIConfiguration::instance().color ("shuttle");
612 Gtkmm2ext::set_source_rgba (cr, col);
614 cairo_set_source (cr, pattern);
616 if (UIConfiguration::instance().get_widget_prelight() && _hovering) {
617 cairo_fill_preserve (cr);
618 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
623 if (acutal_speed != 0) {
624 if (Config->get_shuttle_units() == Percentage) {
625 if (acutal_speed == 1.0) {
626 snprintf (buf, sizeof (buf), "%s", _("Play"));
628 if (acutal_speed < 0.0) {
629 snprintf (buf, sizeof (buf), "< %.1f%%", -acutal_speed * 100.f);
631 snprintf (buf, sizeof (buf), "> %.1f%%", acutal_speed * 100.f);
636 int semi = speed_as_semitones (acutal_speed, reversed);
638 snprintf (buf, sizeof (buf), _("< %+2d st"), semi);
640 snprintf (buf, sizeof (buf), _("> %+2d st"), semi);
644 snprintf (buf, sizeof (buf), "%s", _("Stop"));
647 last_speed_displayed = acutal_speed;
649 _info_button.set_text (buf);
652 if (UIConfiguration::instance().get_widget_prelight()) {
654 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
655 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
663 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
665 Config->set_shuttle_behaviour (s);
669 ShuttleControl::set_shuttle_units (ShuttleUnits s)
671 Config->set_shuttle_units (s);
674 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
675 : PBD::Controllable (X_("Shuttle"))
681 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
683 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
687 ShuttleControl::ShuttleControllable::get_value () const
689 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
693 ShuttleControl::parameter_changed (std::string p)
695 if (p == "shuttle-behaviour") {
696 switch (Config->get_shuttle_behaviour ()) {
698 /* back to Sprung - reset to speed = 1.0 if playing
701 if (_session->transport_rolling()) {
702 if (_session->actual_speed() == 1.0) {
705 /* reset current speed and
706 revert to 1.0 as the default
708 _session->request_transport_speed (1.0);
709 /* redraw when speed changes */
722 } else if (p == "shuttle-max-speed") {
724 } else if (p == "shuttle-units") {
731 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
735 if (UIConfiguration::instance().get_widget_prelight()) {
739 return CairoWidget::on_enter_notify_event (ev);
743 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
747 if (UIConfiguration::instance().get_widget_prelight()) {
751 return CairoWidget::on_leave_notify_event (ev);