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);
111 ShuttleControl::set_session (Session *s)
113 SessionHandlePtr::set_session (s);
116 set_sensitive (true);
117 _session->add_controllable (_controllable);
119 set_sensitive (false);
124 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
127 cairo_pattern_destroy (pattern);
129 cairo_pattern_destroy (shine_pattern);
133 CairoWidget::on_size_allocate ( alloc);
136 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
137 uint32_t col = UIConfiguration::instance().color ("shuttle");
139 UINT_TO_RGBA(col, &r, &g, &b, &a);
140 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
141 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
142 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
145 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
146 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
147 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
148 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
150 Pango::AttrFontDesc* font_attr;
152 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (UIConfiguration::instance().get_NormalBoldFont()));
153 text_attributes.change (*font_attr);
159 ShuttleControl::map_transport_state ()
161 float speed = _session->transport_speed ();
163 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
164 && !( speed == 1.f && last_speed_displayed != 1.f)
165 && !( speed == 0.f && last_speed_displayed != 0.f)
168 return; // nothing to see here, move along.
171 // Q: is there a good reason why we re-calculate this every time?
172 if (fabs(speed) <= (2*DBL_EPSILON)) {
175 if (Config->get_shuttle_units() == Semitones) {
177 int semi = speed_as_semitones (speed, reverse);
178 shuttle_fract = semitones_as_fract (semi, reverse);
180 shuttle_fract = speed/shuttle_max_speed;
188 ShuttleControl::build_shuttle_context_menu ()
190 using namespace Menu_Helpers;
192 shuttle_context_menu = new Menu();
193 MenuList& items = shuttle_context_menu->items();
195 Menu* speed_menu = manage (new Menu());
196 MenuList& speed_items = speed_menu->items();
198 Menu* units_menu = manage (new Menu);
199 MenuList& units_items = units_menu->items();
200 RadioMenuItem::Group units_group;
202 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
203 if (Config->get_shuttle_units() == Percentage) {
204 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
206 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
207 if (Config->get_shuttle_units() == Semitones) {
208 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
210 items.push_back (MenuElem (_("Units"), *units_menu));
212 Menu* style_menu = manage (new Menu);
213 MenuList& style_items = style_menu->items();
214 RadioMenuItem::Group style_group;
216 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
217 if (Config->get_shuttle_behaviour() == Sprung) {
218 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
220 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
221 if (Config->get_shuttle_behaviour() == Wheel) {
222 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
225 items.push_back (MenuElem (_("Mode"), *style_menu));
227 RadioMenuItem::Group speed_group;
229 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
230 if (shuttle_max_speed == 8.0) {
231 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
233 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
234 if (shuttle_max_speed == 6.0) {
235 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
237 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
238 if (shuttle_max_speed == 4.0) {
239 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
241 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
242 if (shuttle_max_speed == 3.0) {
243 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
245 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
246 if (shuttle_max_speed == 2.0) {
247 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
249 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
250 if (shuttle_max_speed == 1.5) {
251 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
254 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
256 items.push_back (SeparatorElem ());
257 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
261 ShuttleControl::show_shuttle_context_menu ()
263 if (shuttle_context_menu == 0) {
264 build_shuttle_context_menu ();
267 shuttle_context_menu->popup (1, gtk_get_current_event_time());
271 ShuttleControl::reset_speed ()
273 if (_session->transport_rolling()) {
274 _session->request_transport_speed (1.0, true);
276 _session->request_transport_speed (0.0, true);
281 ShuttleControl::set_shuttle_max_speed (float speed)
283 Config->set_shuttle_max_speed (speed);
284 shuttle_max_speed = speed;
285 last_speed_displayed = -99999999;
289 ShuttleControl::on_button_press_event (GdkEventButton* ev)
295 if (binding_proxy.button_press_handler (ev)) {
299 if (Keyboard::is_context_menu_event (ev)) {
300 show_shuttle_context_menu ();
304 switch (ev->button) {
306 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
307 if (_session->transport_rolling()) {
308 _session->request_transport_speed (1.0);
312 shuttle_grabbed = true;
313 shuttle_speed_on_grab = _session->transport_speed ();
314 mouse_shuttle (ev->x, true);
315 gdk_pointer_grab(ev->window,false,
316 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
331 ShuttleControl::on_button_release_event (GdkEventButton* ev)
337 switch (ev->button) {
339 if (shuttle_grabbed) {
340 shuttle_grabbed = false;
341 remove_modal_grab ();
342 gdk_pointer_ungrab (GDK_CURRENT_TIME);
344 if (Config->get_shuttle_behaviour() == Sprung) {
345 if (shuttle_speed_on_grab == 0 ) {
346 _session->request_stop ();
348 _session->request_transport_speed (shuttle_speed_on_grab);
351 mouse_shuttle (ev->x, true);
357 if (_session->transport_rolling()) {
358 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
372 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
378 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
380 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
384 bool semis = (Config->get_shuttle_units() == Semitones);
386 switch (ev->direction) {
388 case GDK_SCROLL_RIGHT:
390 if (shuttle_fract == 0) {
391 shuttle_fract = semitones_as_fract (1, false);
394 int st = fract_as_semitones (shuttle_fract, rev);
395 shuttle_fract = semitones_as_fract (st + 1, rev);
398 shuttle_fract += 0.00125;
401 case GDK_SCROLL_DOWN:
402 case GDK_SCROLL_LEFT:
404 if (shuttle_fract == 0) {
405 shuttle_fract = semitones_as_fract (1, true);
408 int st = fract_as_semitones (shuttle_fract, rev);
409 shuttle_fract = semitones_as_fract (st - 1, rev);
412 shuttle_fract -= 0.00125;
421 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
422 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
424 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
425 to the far side of it.
428 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
429 switch (ev->direction) {
431 case GDK_SCROLL_RIGHT:
432 shuttle_fract = upper_side_of_dead_zone;
434 case GDK_SCROLL_DOWN:
435 case GDK_SCROLL_LEFT:
436 shuttle_fract = lower_side_of_dead_zone;
439 /* impossible, checked above */
445 use_shuttle_fract (true);
451 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
453 if (!_session || !shuttle_grabbed) {
457 return mouse_shuttle (ev->x, false);
461 ShuttleControl::mouse_shuttle (double x, bool force)
463 double const center = get_width() / 2.0;
464 double distance_from_center = x - center;
466 if (distance_from_center > 0) {
467 distance_from_center = min (distance_from_center, center);
469 distance_from_center = max (distance_from_center, -center);
472 /* compute shuttle fract as expressing how far between the center
473 and the edge we are. positive values indicate we are right of
474 center, negative values indicate left of center
477 shuttle_fract = distance_from_center / center; // center == half the width
478 use_shuttle_fract (force);
483 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
486 use_shuttle_fract (false, zero_ok);
490 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
492 assert (speed != 0.0);
496 return (int) round (12.0 * fast_log2 (-speed));
499 return (int) round (12.0 * fast_log2 (speed));
504 ShuttleControl::semitones_as_speed (int semi, bool reverse)
507 return -pow (2.0, (semi / 12.0));
509 return pow (2.0, (semi / 12.0));
514 ShuttleControl::semitones_as_fract (int semi, bool reverse)
516 float speed = semitones_as_speed (semi, reverse);
517 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
521 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
523 assert (fract != 0.0);
524 return speed_as_semitones (fract * 4.0, reverse);
528 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
530 microseconds_t now = get_microseconds();
532 shuttle_fract = max (-1.0f, shuttle_fract);
533 shuttle_fract = min (1.0f, shuttle_fract);
535 /* do not attempt to submit a motion-driven transport speed request
536 more than once per process cycle.
539 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
543 last_shuttle_request = now;
547 if (Config->get_shuttle_units() == Semitones) {
548 if (shuttle_fract != 0.0) {
550 int semi = fract_as_semitones (shuttle_fract, reverse);
551 speed = semitones_as_speed (semi, reverse);
556 speed = shuttle_max_speed * shuttle_fract;
560 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
562 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
567 ShuttleControl::set_colors ()
571 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
572 uint32_t text = UIConfiguration::instance().color (X_("shuttle text"));
574 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
579 UINT_TO_RGBA (text, &r, &g, &b, &a);
581 /* rescale for Pango colors ... sigh */
583 r = lrint (r * 65535.0);
584 g = lrint (g * 65535.0);
585 b = lrint (b * 65535.0);
588 text_color = new Pango::AttrColor (Pango::Attribute::create_attr_foreground (r, g, b));
589 text_attributes.change (*text_color);
593 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
596 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
597 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
603 speed = _session->transport_speed ();
607 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
608 float marker_size = get_height() - 5.0;
609 float avail_width = get_width() - marker_size - 4;
610 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
611 // cairo_set_source_rgb (cr, 0, 1, 0.0);
612 cairo_set_source (cr, pattern);
614 cairo_move_to( cr, x, 2.5);
615 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
616 cairo_line_to( cr, x, 2.5 + marker_size);
617 cairo_close_path(cr);
618 } else if ( speed ==0.0 )
619 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
621 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
622 cairo_set_line_width (cr, 1.75);
631 if (Config->get_shuttle_units() == Percentage) {
634 snprintf (buf, sizeof (buf), "%s", _("Playing"));
637 snprintf (buf, sizeof (buf), "<<< %.1f%%", -speed * 100.f);
639 snprintf (buf, sizeof (buf), ">>> %.1f%%", speed * 100.f);
646 int semi = speed_as_semitones (speed, reversed);
649 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
651 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
656 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
659 last_speed_displayed = speed;
661 const float top_text_margin = 3.0f;
662 const float side_text_margin = 5.0f;
664 left_text->set_text (buf);
665 cairo_move_to (cr, side_text_margin, top_text_margin);
666 pango_cairo_show_layout (cr, left_text->gobj());
670 switch (Config->get_shuttle_behaviour()) {
672 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
675 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
679 right_text->set_text (buf);
680 Pango::Rectangle r = right_text->get_ink_extents ();
681 cairo_move_to (cr, get_width() - ((r.get_width()/PANGO_SCALE) + side_text_margin), top_text_margin);
682 pango_cairo_show_layout (cr, right_text->gobj());
684 if (UIConfiguration::instance().get_widget_prelight()) {
686 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
687 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
694 ShuttleControl::shuttle_unit_clicked ()
696 if (shuttle_unit_menu == 0) {
697 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
699 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
703 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
705 Config->set_shuttle_behaviour (s);
709 ShuttleControl::set_shuttle_units (ShuttleUnits s)
711 Config->set_shuttle_units (s);
714 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
715 : PBD::Controllable (X_("Shuttle"))
721 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
723 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
727 ShuttleControl::ShuttleControllable::get_value () const
729 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
733 ShuttleControl::parameter_changed (std::string p)
735 if (p == "shuttle-behaviour") {
736 switch (Config->get_shuttle_behaviour ()) {
738 /* back to Sprung - reset to speed = 1.0 if playing
741 if (_session->transport_rolling()) {
742 if (_session->transport_speed() == 1.0) {
745 /* reset current speed and
746 revert to 1.0 as the default
748 _session->request_transport_speed (1.0);
749 /* redraw when speed changes */
762 } else if (p == "shuttle-units") {
769 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
773 if (UIConfiguration::instance().get_widget_prelight()) {
777 return CairoWidget::on_enter_notify_event (ev);
781 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
785 if (UIConfiguration::instance().get_widget_prelight()) {
789 return CairoWidget::on_leave_notify_event (ev);