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/keyboard.h"
31 #include "gtkmm2ext/gui_thread.h"
32 #include "gtkmm2ext/cairocell.h"
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/rgb_macros.h"
37 #include "rgb_macros.h"
38 #include "shuttle_control.h"
44 using namespace Gtkmm2ext;
45 using namespace ARDOUR;
46 using namespace ARDOUR_UI_UTILS;
50 gboolean qt (gboolean, gint, gint, gboolean, Gtk::Tooltip*, gpointer)
55 ShuttleControl::ShuttleControl ()
56 : _controllable (new ShuttleControllable (*this))
57 , binding_proxy (_controllable)
61 Pango::AttrFontDesc* font_attr;
62 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (UIConfiguration::instance().get_NormalFont()));
63 text_attributes.change (*font_attr);
66 _text = Pango::Layout::create (get_pango_context());
67 _text->set_attributes (text_attributes);
69 set_tooltip (*this, _("Shuttle speed control (Context-click for options)"));
73 last_shuttle_request = 0;
74 last_speed_displayed = -99999999;
75 shuttle_grabbed = false;
76 shuttle_speed_on_grab = 0;
78 shuttle_max_speed = 8.0f;
79 shuttle_style_menu = 0;
80 shuttle_unit_menu = 0;
81 shuttle_context_menu = 0;
84 set_flags (CAN_FOCUS);
85 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);
86 set_name (X_("ShuttleControl"));
90 _text->set_text ("@Sp");
91 _text->get_pixel_size (tw, th);
92 h = (int) ceil(th * BASELINESTRETCH + 1.0);
93 if ((h - th) & 1) { ++h; }
94 set_size_request (85, h);
96 shuttle_max_speed = Config->get_shuttle_max_speed();
98 if (shuttle_max_speed >= 8.f) { shuttle_max_speed = 8.0f; }
99 else if (shuttle_max_speed >= 6.f) { shuttle_max_speed = 6.0f; }
100 else if (shuttle_max_speed >= 4.f) { shuttle_max_speed = 4.0f; }
101 else if (shuttle_max_speed >= 3.f) { shuttle_max_speed = 3.0f; }
102 else if (shuttle_max_speed >= 2.f) { shuttle_max_speed = 2.0f; }
103 else { shuttle_max_speed = 1.5f; }
105 Config->ParameterChanged.connect (parameter_connection, MISSING_INVALIDATOR, boost::bind (&ShuttleControl::parameter_changed, this, _1), gui_context());
106 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ShuttleControl::set_colors));
110 /* gtkmm 2.4: the C++ wrapper doesn't work */
111 g_signal_connect ((GObject*) gobj(), "query-tooltip", G_CALLBACK (qt), NULL);
112 // signal_query_tooltip().connect (sigc::mem_fun (*this, &ShuttleControl::on_query_tooltip));
115 ShuttleControl::~ShuttleControl ()
117 cairo_pattern_destroy (pattern);
118 cairo_pattern_destroy (shine_pattern);
123 ShuttleControl::set_session (Session *s)
125 SessionHandlePtr::set_session (s);
128 set_sensitive (true);
129 _session->add_controllable (_controllable);
131 set_sensitive (false);
136 ShuttleControl::on_size_allocate (Gtk::Allocation& alloc)
139 cairo_pattern_destroy (pattern);
141 cairo_pattern_destroy (shine_pattern);
145 CairoWidget::on_size_allocate ( alloc);
148 pattern = cairo_pattern_create_linear (0, 0, 0, alloc.get_height());
149 uint32_t col = UIConfiguration::instance().color ("shuttle");
151 UINT_TO_RGBA(col, &r, &g, &b, &a);
152 cairo_pattern_add_color_stop_rgb (pattern, 0.0, r/400.0, g/400.0, b/400.0);
153 cairo_pattern_add_color_stop_rgb (pattern, 0.4, r/255.0, g/255.0, b/255.0);
154 cairo_pattern_add_color_stop_rgb (pattern, 1.0, r/512.0, g/512.0, b/512.0);
157 shine_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, 10);
158 cairo_pattern_add_color_stop_rgba (shine_pattern, 0, 1,1,1,0.0);
159 cairo_pattern_add_color_stop_rgba (shine_pattern, 0.2, 1,1,1,0.4);
160 cairo_pattern_add_color_stop_rgba (shine_pattern, 1, 1,1,1,0.1);
164 ShuttleControl::map_transport_state ()
166 float speed = _session->transport_speed ();
168 if ( (fabsf( speed - last_speed_displayed) < 0.005f) // dead-zone
169 && !( speed == 1.f && last_speed_displayed != 1.f)
170 && !( speed == 0.f && last_speed_displayed != 0.f)
173 return; // nothing to see here, move along.
176 // Q: is there a good reason why we re-calculate this every time?
177 if (fabs(speed) <= (2*DBL_EPSILON)) {
180 if (Config->get_shuttle_units() == Semitones) {
182 int semi = speed_as_semitones (speed, reverse);
183 shuttle_fract = semitones_as_fract (semi, reverse);
185 shuttle_fract = speed/shuttle_max_speed;
193 ShuttleControl::build_shuttle_context_menu ()
195 using namespace Menu_Helpers;
197 shuttle_context_menu = new Menu();
198 MenuList& items = shuttle_context_menu->items();
200 Menu* speed_menu = manage (new Menu());
201 MenuList& speed_items = speed_menu->items();
203 Menu* units_menu = manage (new Menu);
204 MenuList& units_items = units_menu->items();
205 RadioMenuItem::Group units_group;
207 units_items.push_back (RadioMenuElem (units_group, _("Percent"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Percentage)));
208 if (Config->get_shuttle_units() == Percentage) {
209 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
211 units_items.push_back (RadioMenuElem (units_group, _("Semitones"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_units), Semitones)));
212 if (Config->get_shuttle_units() == Semitones) {
213 static_cast<RadioMenuItem*>(&units_items.back())->set_active();
215 items.push_back (MenuElem (_("Units"), *units_menu));
217 Menu* style_menu = manage (new Menu);
218 MenuList& style_items = style_menu->items();
219 RadioMenuItem::Group style_group;
221 style_items.push_back (RadioMenuElem (style_group, _("Sprung"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Sprung)));
222 if (Config->get_shuttle_behaviour() == Sprung) {
223 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
225 style_items.push_back (RadioMenuElem (style_group, _("Wheel"), sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_style), Wheel)));
226 if (Config->get_shuttle_behaviour() == Wheel) {
227 static_cast<RadioMenuItem*>(&style_items.back())->set_active();
230 items.push_back (MenuElem (_("Mode"), *style_menu));
232 RadioMenuItem::Group speed_group;
234 speed_items.push_back (RadioMenuElem (speed_group, "8", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 8.0f)));
235 if (shuttle_max_speed == 8.0) {
236 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
238 speed_items.push_back (RadioMenuElem (speed_group, "6", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 6.0f)));
239 if (shuttle_max_speed == 6.0) {
240 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
242 speed_items.push_back (RadioMenuElem (speed_group, "4", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 4.0f)));
243 if (shuttle_max_speed == 4.0) {
244 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
246 speed_items.push_back (RadioMenuElem (speed_group, "3", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 3.0f)));
247 if (shuttle_max_speed == 3.0) {
248 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
250 speed_items.push_back (RadioMenuElem (speed_group, "2", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 2.0f)));
251 if (shuttle_max_speed == 2.0) {
252 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
254 speed_items.push_back (RadioMenuElem (speed_group, "1.5", sigc::bind (sigc::mem_fun (*this, &ShuttleControl::set_shuttle_max_speed), 1.5f)));
255 if (shuttle_max_speed == 1.5) {
256 static_cast<RadioMenuItem*>(&speed_items.back())->set_active ();
259 items.push_back (MenuElem (_("Maximum speed"), *speed_menu));
261 items.push_back (SeparatorElem ());
262 items.push_back (MenuElem (_("Reset to 100%"), sigc::mem_fun (*this, &ShuttleControl::reset_speed)));
266 ShuttleControl::show_shuttle_context_menu ()
268 if (shuttle_context_menu == 0) {
269 build_shuttle_context_menu ();
272 shuttle_context_menu->popup (1, gtk_get_current_event_time());
276 ShuttleControl::reset_speed ()
278 if (_session->transport_rolling()) {
279 _session->request_transport_speed (1.0, true);
281 _session->request_transport_speed (0.0, true);
286 ShuttleControl::set_shuttle_max_speed (float speed)
288 Config->set_shuttle_max_speed (speed);
289 shuttle_max_speed = speed;
290 last_speed_displayed = -99999999;
294 ShuttleControl::on_button_press_event (GdkEventButton* ev)
300 if (binding_proxy.button_press_handler (ev)) {
304 if (Keyboard::is_context_menu_event (ev)) {
305 show_shuttle_context_menu ();
309 switch (ev->button) {
311 if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
312 if (_session->transport_rolling()) {
313 _session->request_transport_speed (1.0);
317 shuttle_grabbed = true;
318 shuttle_speed_on_grab = _session->transport_speed ();
319 requested_speed = shuttle_speed_on_grab;
320 mouse_shuttle (ev->x, true);
321 gdk_pointer_grab(ev->window,false,
322 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
337 ShuttleControl::on_button_release_event (GdkEventButton* ev)
343 switch (ev->button) {
345 if (shuttle_grabbed) {
346 shuttle_grabbed = false;
347 remove_modal_grab ();
348 gdk_pointer_ungrab (GDK_CURRENT_TIME);
350 if (Config->get_shuttle_behaviour() == Sprung) {
351 if (shuttle_speed_on_grab == 0 ) {
352 _session->request_stop ();
354 _session->request_transport_speed (shuttle_speed_on_grab);
357 mouse_shuttle (ev->x, true);
363 if (_session->transport_rolling()) {
364 _session->request_transport_speed (1.0, Config->get_shuttle_behaviour() == Wheel);
378 ShuttleControl::on_query_tooltip (int, int, bool, const Glib::RefPtr<Gtk::Tooltip>&)
384 ShuttleControl::on_scroll_event (GdkEventScroll* ev)
386 if (!_session || Config->get_shuttle_behaviour() != Wheel) {
390 bool semis = (Config->get_shuttle_units() == Semitones);
392 switch (ev->direction) {
394 case GDK_SCROLL_RIGHT:
396 if (shuttle_fract == 0) {
397 shuttle_fract = semitones_as_fract (1, false);
400 int st = fract_as_semitones (shuttle_fract, rev);
401 shuttle_fract = semitones_as_fract (st + 1, rev);
404 shuttle_fract += 0.00125;
407 case GDK_SCROLL_DOWN:
408 case GDK_SCROLL_LEFT:
410 if (shuttle_fract == 0) {
411 shuttle_fract = semitones_as_fract (1, true);
414 int st = fract_as_semitones (shuttle_fract, rev);
415 shuttle_fract = semitones_as_fract (st - 1, rev);
418 shuttle_fract -= 0.00125;
427 float lower_side_of_dead_zone = semitones_as_fract (-24, true);
428 float upper_side_of_dead_zone = semitones_as_fract (-24, false);
430 /* if we entered the "dead zone" (-24 semitones in forward or reverse), jump
431 to the far side of it.
434 if (shuttle_fract > lower_side_of_dead_zone && shuttle_fract < upper_side_of_dead_zone) {
435 switch (ev->direction) {
437 case GDK_SCROLL_RIGHT:
438 shuttle_fract = upper_side_of_dead_zone;
440 case GDK_SCROLL_DOWN:
441 case GDK_SCROLL_LEFT:
442 shuttle_fract = lower_side_of_dead_zone;
445 /* impossible, checked above */
451 use_shuttle_fract (true);
457 ShuttleControl::on_motion_notify_event (GdkEventMotion* ev)
459 if (!_session || !shuttle_grabbed) {
463 return mouse_shuttle (ev->x, false);
467 ShuttleControl::mouse_shuttle (double x, bool force)
469 double const center = get_width() / 2.0;
470 double distance_from_center = x - center;
472 if (distance_from_center > 0) {
473 distance_from_center = min (distance_from_center, center);
475 distance_from_center = max (distance_from_center, -center);
478 /* compute shuttle fract as expressing how far between the center
479 and the edge we are. positive values indicate we are right of
480 center, negative values indicate left of center
483 shuttle_fract = distance_from_center / center; // center == half the width
484 use_shuttle_fract (force);
489 ShuttleControl::set_shuttle_fract (double f, bool zero_ok)
492 use_shuttle_fract (false, zero_ok);
496 ShuttleControl::speed_as_semitones (float speed, bool& reverse)
498 assert (speed != 0.0);
502 return (int) round (12.0 * fast_log2 (-speed));
505 return (int) round (12.0 * fast_log2 (speed));
510 ShuttleControl::semitones_as_speed (int semi, bool reverse)
513 return -pow (2.0, (semi / 12.0));
515 return pow (2.0, (semi / 12.0));
520 ShuttleControl::semitones_as_fract (int semi, bool reverse)
522 float speed = semitones_as_speed (semi, reverse);
523 return speed/4.0; /* 4.0 is the maximum speed for a 24 semitone shift */
527 ShuttleControl::fract_as_semitones (float fract, bool& reverse)
529 assert (fract != 0.0);
530 return speed_as_semitones (fract * 4.0, reverse);
534 ShuttleControl::use_shuttle_fract (bool force, bool zero_ok)
536 microseconds_t now = get_microseconds();
538 shuttle_fract = max (-1.0f, shuttle_fract);
539 shuttle_fract = min (1.0f, shuttle_fract);
541 /* do not attempt to submit a motion-driven transport speed request
542 more than once per process cycle.
545 if (!force && (last_shuttle_request - now) < (microseconds_t) AudioEngine::instance()->usecs_per_cycle()) {
549 last_shuttle_request = now;
553 if (Config->get_shuttle_units() == Semitones) {
554 if (shuttle_fract != 0.0) {
556 int semi = fract_as_semitones (shuttle_fract, reverse);
557 speed = semitones_as_speed (semi, reverse);
562 speed = shuttle_max_speed * shuttle_fract;
565 requested_speed = speed;
567 _session->request_transport_speed (speed, Config->get_shuttle_behaviour() == Wheel);
569 _session->request_transport_speed_nonzero (speed, Config->get_shuttle_behaviour() == Wheel);
574 ShuttleControl::set_colors ()
578 uint32_t bg_color = UIConfiguration::instance().color (X_("shuttle bg"));
579 uint32_t text = UIConfiguration::instance().color (X_("shuttle text"));
581 UINT_TO_RGBA (bg_color, &r, &g, &b, &a);
586 UINT_TO_RGBA (text, &r, &g, &b, &a);
588 /* rescale for Pango colors ... sigh */
590 r = lrint (r * 65535.0);
591 g = lrint (g * 65535.0);
592 b = lrint (b * 65535.0);
595 text_color = new Pango::AttrColor (Pango::Attribute::create_attr_foreground (r, g, b));
596 text_attributes.change (*text_color);
600 ShuttleControl::render (cairo_t* cr, cairo_rectangle_t*)
602 // center slider line
603 float yc = get_height() / 2;
605 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
606 cairo_set_line_width (cr, 3);
607 cairo_move_to (cr, lw, yc);
608 cairo_line_to (cr, get_width () - lw, yc);
609 cairo_set_source_rgb (cr, bg_r, bg_g, bg_b);
613 float acutal_speed = 0.0;
617 speed = _session->transport_speed ();
618 acutal_speed = speed;
619 if (shuttle_grabbed) {
620 speed = requested_speed;
625 float visual_fraction = std::max (-1.0f, std::min (1.0f, speed / shuttle_max_speed));
626 float marker_size = round (get_height() * 0.66);
627 float avail_width = get_width() - marker_size;
628 float x = 0.5 * (get_width() + visual_fraction * avail_width - marker_size);
630 rounded_rectangle (cr, x, 0, marker_size, get_height(), 5);
631 cairo_set_source_rgba (cr, 0, 0, 0, 1);
633 rounded_rectangle (cr, x + 1, 1, marker_size - 2, get_height() - 2, 3.5);
634 cairo_set_source (cr, pattern);
638 if (acutal_speed != 0) {
639 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);
651 int semi = speed_as_semitones (acutal_speed, reversed);
653 snprintf (buf, sizeof (buf), _("< %+2d semi"), semi);
655 snprintf (buf, sizeof (buf), _("> %+2d semi"), semi);
659 snprintf (buf, sizeof (buf), "%s", _("Stopped"));
662 last_speed_displayed = acutal_speed;
665 _text->set_text (buf);
666 _text->get_pixel_size (tw, th);
667 cairo_move_to (cr, 0.5 * (get_width() - tw), 0.5 * (get_height() - th));
668 pango_cairo_show_layout (cr, _text->gobj());
670 if (UIConfiguration::instance().get_widget_prelight()) {
672 rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3.5);
673 cairo_set_source_rgba (cr, 1, 1, 1, 0.15);
680 ShuttleControl::shuttle_unit_clicked ()
682 if (shuttle_unit_menu == 0) {
683 shuttle_unit_menu = dynamic_cast<Menu*> (ActionManager::get_widget ("/ShuttleUnitPopup"));
685 shuttle_unit_menu->popup (1, gtk_get_current_event_time());
689 ShuttleControl::set_shuttle_style (ShuttleBehaviour s)
691 Config->set_shuttle_behaviour (s);
695 ShuttleControl::set_shuttle_units (ShuttleUnits s)
697 Config->set_shuttle_units (s);
700 ShuttleControl::ShuttleControllable::ShuttleControllable (ShuttleControl& s)
701 : PBD::Controllable (X_("Shuttle"))
707 ShuttleControl::ShuttleControllable::set_value (double val, PBD::Controllable::GroupControlDisposition /*group_override*/)
709 sc.set_shuttle_fract ((val - lower()) / (upper() - lower()), true);
713 ShuttleControl::ShuttleControllable::get_value () const
715 return lower() + (sc.get_shuttle_fract () * (upper() - lower()));
719 ShuttleControl::parameter_changed (std::string p)
721 if (p == "shuttle-behaviour") {
722 switch (Config->get_shuttle_behaviour ()) {
724 /* back to Sprung - reset to speed = 1.0 if playing
727 if (_session->transport_rolling()) {
728 if (_session->transport_speed() == 1.0) {
731 /* reset current speed and
732 revert to 1.0 as the default
734 _session->request_transport_speed (1.0);
735 /* redraw when speed changes */
748 } else if (p == "shuttle-max-speed") {
750 } else if (p == "shuttle-units") {
757 ShuttleControl::on_enter_notify_event (GdkEventCrossing* ev)
761 if (UIConfiguration::instance().get_widget_prelight()) {
765 return CairoWidget::on_enter_notify_event (ev);
769 ShuttleControl::on_leave_notify_event (GdkEventCrossing* ev)
773 if (UIConfiguration::instance().get_widget_prelight()) {
777 return CairoWidget::on_leave_notify_event (ev);