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 requested_speed = shuttle_speed_on_grab;
315 mouse_shuttle (ev->x, true);
316 gdk_pointer_grab(ev->window,false,
317 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
332 ShuttleControl::on_button_release_event (GdkEventButton* ev)
338 switch (ev->button) {
340 if (shuttle_grabbed) {
341 shuttle_grabbed = false;
342 remove_modal_grab ();
343 gdk_pointer_ungrab (GDK_CURRENT_TIME);
345 if (Config->get_shuttle_behaviour() == Sprung) {
346 if (shuttle_speed_on_grab == 0 ) {
347 _session->request_stop ();
349 _session->request_transport_speed (shuttle_speed_on_grab);
352 mouse_shuttle (ev->x, true);
358 if (_session->transport_rolling()) {
359 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
373 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
379 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
381 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
385 bool semis = (Config->get_shuttle_units() == Semitones);
387 switch (ev->direction) {
389 case GDK_SCROLL_RIGHT:
391 if (shuttle_fract == 0) {
392 shuttle_fract = semitones_as_fract (1, false);
395 int st = fract_as_semitones (shuttle_fract, rev);
396 shuttle_fract = semitones_as_fract (st + 1, rev);
399 shuttle_fract += 0.00125;
402 case GDK_SCROLL_DOWN:
403 case GDK_SCROLL_LEFT:
405 if (shuttle_fract == 0) {
406 shuttle_fract = semitones_as_fract (1, true);
409 int st = fract_as_semitones (shuttle_fract, rev);
410 shuttle_fract = semitones_as_fract (st - 1, rev);
413 shuttle_fract -= 0.00125;
422 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
423 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
425 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
426 to the far side of it.
429 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
430 switch (ev->direction) {
432 case GDK_SCROLL_RIGHT:
433 shuttle_fract = upper_side_of_dead_zone;
435 case GDK_SCROLL_DOWN:
436 case GDK_SCROLL_LEFT:
437 shuttle_fract = lower_side_of_dead_zone;
440 /* impossible, checked above */
446 use_shuttle_fract (true);
452 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
454 if (!_session || !shuttle_grabbed) {
458 return mouse_shuttle (ev->x, false);
462 ShuttleControl::mouse_shuttle (double x, bool force)
464 double const center = get_width() / 2.0;
465 double distance_from_center = x - center;
467 if (distance_from_center > 0) {
468 distance_from_center = min (distance_from_center, center);
470 distance_from_center = max (distance_from_center, -center);
473 /* compute shuttle fract as expressing how far between the center
474 and the edge we are. positive values indicate we are right of
475 center, negative values indicate left of center
478 shuttle_fract = distance_from_center / center; // center == half the width
479 use_shuttle_fract (force);
484 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
487 use_shuttle_fract (false, zero_ok);
491 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
493 assert (speed != 0.0);
497 return (int) round (12.0 * fast_log2 (-speed));
500 return (int) round (12.0 * fast_log2 (speed));
505 ShuttleControl::semitones_as_speed (int semi, bool reverse)
508 return -pow (2.0, (semi / 12.0));
510 return pow (2.0, (semi / 12.0));
515 ShuttleControl::semitones_as_fract (int semi, bool reverse)
517 float speed = semitones_as_speed (semi, reverse);
518 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
522 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
524 assert (fract != 0.0);
525 return speed_as_semitones (fract * 4.0, reverse);
529 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
531 microseconds_t now = get_microseconds();
533 shuttle_fract = max (-1.0f, shuttle_fract);
534 shuttle_fract = min (1.0f, shuttle_fract);
536 /* do not attempt to submit a motion-driven transport speed request
537 more than once per process cycle.
540 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
544 last_shuttle_request = now;
548 if (Config->get_shuttle_units() == Semitones) {
549 if (shuttle_fract != 0.0) {
551 int semi = fract_as_semitones (shuttle_fract, reverse);
552 speed = semitones_as_speed (semi, reverse);
557 speed = shuttle_max_speed * shuttle_fract;
560 requested_speed = speed;
562 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
564 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
569 ShuttleControl::set_colors ()
573 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
574 uint32_t text = UIConfiguration::instance().color (X_("shuttle text"));
576 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
581 UINT_TO_RGBA (text, &r, &g, &b, &a);
583 /* rescale for Pango colors ... sigh */
585 r = lrint (r * 65535.0);
586 g = lrint (g * 65535.0);
587 b = lrint (b * 65535.0);
590 text_color = new Pango::AttrColor (Pango::Attribute::create_attr_foreground (r, g, b));
591 text_attributes.change (*text_color);
595 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
598 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
599 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 4);
603 float acutal_speed = 0.0;
606 speed = _session->transport_speed ();
607 acutal_speed = speed;
608 if (shuttle_grabbed) {
609 speed = requested_speed;
614 float visual_fraction = std::min (1.0f, speed / shuttle_max_speed);
615 float marker_size = get_height() - 5.0;
616 float avail_width = get_width() - marker_size - 4;
617 float x = get_width() * 0.5 + visual_fraction * avail_width * 0.5;
618 // cairo_set_source_rgb (cr, 0, 1, 0.0);
619 cairo_set_source (cr, pattern);
621 cairo_move_to( cr, x, 2.5);
622 cairo_line_to( cr, x + marker_size * .577, 2.5 + marker_size * 0.5);
623 cairo_line_to( cr, x, 2.5 + marker_size);
624 cairo_close_path(cr);
625 } else if ( speed ==0.0 )
626 rounded_rectangle (cr, x, 2.5, marker_size, marker_size, 1);
628 cairo_arc (cr, x, 2.5 + marker_size * .5, marker_size * 0.47, 0, 2.0 * M_PI);
629 cairo_set_line_width (cr, 1.75);
636 if (acutal_speed != 0) {
638 if (Config->get_shuttle_units() == Percentage) {
640 if (acutal_speed == 1.0) {
641 snprintf (buf, sizeof (buf), "%s", _("Playing"));
643 if (acutal_speed < 0.0) {
644 snprintf (buf, sizeof (buf), "<<< %.1f%%", -acutal_speed * 100.f);
646 snprintf (buf, sizeof (buf), ">>> %.1f%%", acutal_speed * 100.f);
653 int semi = speed_as_semitones (acutal_speed, reversed);
656 snprintf (buf, sizeof (buf), _("<<< %+d semitones"), semi);
658 snprintf (buf, sizeof (buf), _(">>> %+d semitones"), semi);
663 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
666 last_speed_displayed = acutal_speed;
668 const float top_text_margin = 3.0f;
669 const float side_text_margin = 5.0f;
671 left_text->set_text (buf);
672 cairo_move_to (cr, side_text_margin, top_text_margin);
673 pango_cairo_show_layout (cr, left_text->gobj());
677 switch (Config->get_shuttle_behaviour()) {
679 snprintf (buf, sizeof (buf), "%s", _("Sprung"));
682 snprintf (buf, sizeof (buf), "%s", _("Wheel"));
686 right_text->set_text (buf);
687 Pango::Rectangle r = right_text->get_ink_extents ();
688 cairo_move_to (cr, get_width() - ((r.get_width()/PANGO_SCALE) + side_text_margin), top_text_margin);
689 pango_cairo_show_layout (cr, right_text->gobj());
691 if (UIConfiguration::instance().get_widget_prelight()) {
693 rounded_rectangle (cr, 1, 1, get_width()-2, get_height()-2, 4.0);
694 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
701 ShuttleControl::shuttle_unit_clicked ()
703 if (shuttle_unit_menu == 0) {
704 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
706 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
710 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
712 Config->set_shuttle_behaviour (s);
716 ShuttleControl::set_shuttle_units (ShuttleUnits s)
718 Config->set_shuttle_units (s);
721 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
722 : PBD::Controllable (X_("Shuttle"))
728 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
730 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
734 ShuttleControl::ShuttleControllable::get_value () const
736 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
740 ShuttleControl::parameter_changed (std::string p)
742 if (p == "shuttle-behaviour") {
743 switch (Config->get_shuttle_behaviour ()) {
745 /* back to Sprung - reset to speed = 1.0 if playing
748 if (_session->transport_rolling()) {
749 if (_session->transport_speed() == 1.0) {
752 /* reset current speed and
753 revert to 1.0 as the default
755 _session->request_transport_speed (1.0);
756 /* redraw when speed changes */
769 } else if (p == "shuttle-units") {
776 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
780 if (UIConfiguration::instance().get_widget_prelight()) {
784 return CairoWidget::on_enter_notify_event (ev);
788 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
792 if (UIConfiguration::instance().get_widget_prelight()) {
796 return CairoWidget::on_leave_notify_event (ev);