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.
23 #include "ardour/ardour.h"
24 #include "ardour/audioengine.h"
25 #include "ardour/rc_configuration.h"
26 #include "ardour/session.h"
28 #include "gtkmm2ext/keyboard.h"
29 #include "gtkmm2ext/gui_thread.h"
30 #include "gtkmm2ext/cairocell.h"
31 #include "gtkmm2ext/utils.h"
32 #include "gtkmm2ext/rgb_macros.h"
35 #include "rgb_macros.h"
36 #include "shuttle_control.h"
42 using namespace Gtkmm2ext;
43 using namespace ARDOUR;
44 using namespace ARDOUR_UI_UTILS;
48 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
53 ShuttleControl::ShuttleControl ()
54 : _controllable (new ShuttleControllable (*this))
55 , binding_proxy (_controllable)
58 left_text = Pango::Layout::create (get_pango_context());
59 right_text = Pango::Layout::create (get_pango_context());
61 right_text->set_attributes (text_attributes);
62 left_text->set_attributes (text_attributes);
64 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
68 last_shuttle_request = 0;
69 last_speed_displayed = -99999999;
70 shuttle_grabbed = false;
71 shuttle_speed_on_grab = 0;
73 shuttle_max_speed = 8.0f;
74 shuttle_style_menu = 0;
75 shuttle_unit_menu = 0;
76 shuttle_context_menu = 0;
79 set_flags (CAN_FOCUS);
80 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);
81 set_size_request (85, 20);
82 set_name (X_("ShuttleControl"));
84 shuttle_max_speed = Config->get_shuttle_max_speed();
86 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
87 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
88 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
89 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
90 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
91 else { shuttle_max_speed = 1.5f; }
93 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
94 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
98 /* gtkmm 2.4: the C++ wrapper doesn't work */
99 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
100 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
103 ShuttleControl::~ShuttleControl ()
105 cairo_pattern_destroy (pattern);
106 cairo_pattern_destroy (shine_pattern);
110 ShuttleControl::set_session (Session *s)
112 SessionHandlePtr::set_session (s);
115 set_sensitive (true);
116 _session->add_controllable (_controllable);
118 set_sensitive (false);
123 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
126 cairo_pattern_destroy (pattern);
128 cairo_pattern_destroy (shine_pattern);
132 CairoWidget::on_size_allocate ( alloc);
135 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
136 uint32_t col = UIConfiguration::instance().color ("shuttle");
138 UINT_TO_RGBA(col, &r, &g, &b, &a);
139 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
140 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
141 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
144 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
145 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
146 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
147 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
149 Pango::AttrFontDesc* font_attr;
151 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (UIConfiguration::instance().get_NormalBoldFont()));
152 text_attributes.change (*font_attr);
158 ShuttleControl::map_transport_state ()
160 float speed = _session->transport_speed ();
162 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
163 && !( speed == 1.f && last_speed_displayed != 1.f)
164 && !( speed == 0.f && last_speed_displayed != 0.f)
167 return; // nothing to see here, move along.
170 // Q: is there a good reason why we re-calculate this every time?
171 if (fabs(speed) <= (2*DBL_EPSILON)) {
174 if (Config->get_shuttle_units() == Semitones) {
176 int semi = speed_as_semitones (speed, reverse);
177 shuttle_fract = semitones_as_fract (semi, reverse);
179 shuttle_fract = speed/shuttle_max_speed;
187 ShuttleControl::build_shuttle_context_menu ()
189 using namespace Menu_Helpers;
191 shuttle_context_menu = new Menu();
192 MenuList& items = shuttle_context_menu->items();
194 Menu* speed_menu = manage (new Menu());
195 MenuList& speed_items = speed_menu->items();
197 Menu* units_menu = manage (new Menu);
198 MenuList& units_items = units_menu->items();
199 RadioMenuItem::Group units_group;
201 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
202 if (Config->get_shuttle_units() == Percentage) {
203 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
205 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
206 if (Config->get_shuttle_units() == Semitones) {
207 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
209 items.push_back (MenuElem (_("Units"), *units_menu));
211 Menu* style_menu = manage (new Menu);
212 MenuList& style_items = style_menu->items();
213 RadioMenuItem::Group style_group;
215 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
216 if (Config->get_shuttle_behaviour() == Sprung) {
217 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
219 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
220 if (Config->get_shuttle_behaviour() == Wheel) {
221 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
224 items.push_back (MenuElem (_("Mode"), *style_menu));
226 RadioMenuItem::Group speed_group;
228 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
229 if (shuttle_max_speed == 8.0) {
230 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
232 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
233 if (shuttle_max_speed == 6.0) {
234 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
236 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
237 if (shuttle_max_speed == 4.0) {
238 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
240 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
241 if (shuttle_max_speed == 3.0) {
242 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
244 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
245 if (shuttle_max_speed == 2.0) {
246 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
248 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
249 if (shuttle_max_speed == 1.5) {
250 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
253 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
255 items.push_back (SeparatorElem ());
256 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
260 ShuttleControl::show_shuttle_context_menu ()
262 if (shuttle_context_menu == 0) {
263 build_shuttle_context_menu ();
266 shuttle_context_menu->popup (1, gtk_get_current_event_time());
270 ShuttleControl::reset_speed ()
272 if (_session->transport_rolling()) {
273 _session->request_transport_speed (1.0, true);
275 _session->request_transport_speed (0.0, true);
280 ShuttleControl::set_shuttle_max_speed (float speed)
282 Config->set_shuttle_max_speed (speed);
283 shuttle_max_speed = speed;
284 last_speed_displayed = -99999999;
288 ShuttleControl::on_button_press_event (GdkEventButton* ev)
294 if (binding_proxy.button_press_handler (ev)) {
298 if (Keyboard::is_context_menu_event (ev)) {
299 show_shuttle_context_menu ();
303 switch (ev->button) {
305 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
306 if (_session->transport_rolling()) {
307 _session->request_transport_speed (1.0);
311 shuttle_grabbed = true;
312 shuttle_speed_on_grab = _session->transport_speed ();
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;
559 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
561 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
566 ShuttleControl::set_colors ()
570 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
571 uint32_t text = UIConfiguration::instance().color (X_("shuttle text"));
573 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
578 UINT_TO_RGBA (text, &r, &g, &b, &a);
580 /* rescale for Pango colors ... sigh */
582 r = lrint (r * 65535.0);
583 g = lrint (g * 65535.0);
584 b = lrint (b * 65535.0);
587 text_color = new Pango::AttrColor (Pango::Attribute::create_attr_foreground (r, g, b));
588 text_attributes.change (*text_color);
592 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
595 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
596 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
602 speed = _session->transport_speed ();
606 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
607 float marker_size = get_height() - 5.0;
608 float avail_width = get_width() - marker_size - 4;
609 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
610 // cairo_set_source_rgb (cr, 0, 1, 0.0);
611 cairo_set_source (cr, pattern);
613 cairo_move_to( cr, x, 2.5);
614 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
615 cairo_line_to( cr, x, 2.5 + marker_size);
616 cairo_close_path(cr);
617 } else if ( speed ==0.0 )
618 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
620 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
621 cairo_set_line_width (cr, 1.75);
630 if (Config->get_shuttle_units() == Percentage) {
633 snprintf (buf, sizeof (buf), "%s", _("Playing"));
636 snprintf (buf, sizeof (buf), "<<< %.1f%%", -speed * 100.f);
638 snprintf (buf, sizeof (buf), ">>> %.1f%%", speed * 100.f);
645 int semi = speed_as_semitones (speed, reversed);
648 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
650 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
655 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
658 last_speed_displayed = speed;
660 const float top_text_margin = 3.0f;
661 const float side_text_margin = 5.0f;
663 left_text->set_text (buf);
664 cairo_move_to (cr, side_text_margin, top_text_margin);
665 pango_cairo_show_layout (cr, left_text->gobj());
669 switch (Config->get_shuttle_behaviour()) {
671 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
674 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
678 right_text->set_text (buf);
679 Pango::Rectangle r = right_text->get_ink_extents ();
680 cairo_move_to (cr, get_width() - ((r.get_width()/PANGO_SCALE) + side_text_margin), top_text_margin);
681 pango_cairo_show_layout (cr, right_text->gobj());
683 if (UIConfiguration::instance().get_widget_prelight()) {
685 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
686 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
693 ShuttleControl::shuttle_unit_clicked ()
695 if (shuttle_unit_menu == 0) {
696 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
698 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
702 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
704 Config->set_shuttle_behaviour (s);
708 ShuttleControl::set_shuttle_units (ShuttleUnits s)
710 Config->set_shuttle_units (s);
713 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
714 : PBD::Controllable (X_("Shuttle"))
720 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
722 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
726 ShuttleControl::ShuttleControllable::get_value () const
728 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
732 ShuttleControl::parameter_changed (std::string p)
734 if (p == "shuttle-behaviour") {
735 switch (Config->get_shuttle_behaviour ()) {
737 /* back to Sprung - reset to speed = 1.0 if playing
740 if (_session->transport_rolling()) {
741 if (_session->transport_speed() == 1.0) {
744 /* reset current speed and
745 revert to 1.0 as the default
747 _session->request_transport_speed (1.0);
748 /* redraw when speed changes */
761 } else if (p == "shuttle-units") {
768 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
772 if (UIConfiguration::instance().get_widget_prelight()) {
776 return CairoWidget::on_enter_notify_event (ev);
780 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
784 if (UIConfiguration::instance().get_widget_prelight()) {
788 return CairoWidget::on_leave_notify_event (ev);