2 * Copyright (C) 2010-2012 Carl Hetherington <carl@carlh.net>
3 * Copyright (C) 2010-2016 Paul Davis <paul@linuxaudiosystems.com>
4 * Copyright (C) 2012-2017 Robin Gareus <robin@gareus.org>
5 * Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27 #include <gtkmm/window.h>
28 #include <pangomm/layout.h>
30 #include "pbd/compose.h"
32 #include "gtkmm2ext/gui_thread.h"
33 #include "gtkmm2ext/gtk_ui.h"
34 #include "gtkmm2ext/keyboard.h"
35 #include "gtkmm2ext/utils.h"
36 #include "gtkmm2ext/persistent_tooltip.h"
38 #include "ardour/pannable.h"
39 #include "ardour/panner.h"
40 #include "ardour/panner_shell.h"
42 #include "gtkmm2ext/colors.h"
44 #include "stereo_panner.h"
45 #include "stereo_panner_editor.h"
46 #include "rgb_macros.h"
48 #include "ui_config.h"
54 using namespace Gtkmm2ext;
55 using namespace ARDOUR_UI_UTILS;
57 using PBD::Controllable;
59 StereoPanner::ColorScheme StereoPanner::colors[3];
60 bool StereoPanner::have_colors = false;
62 Pango::AttrList StereoPanner::panner_font_attributes;
63 bool StereoPanner::have_font = false;
65 using namespace ARDOUR;
67 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
68 : PannerInterface (p->panner())
70 , position_control (_panner->pannable()->pan_azimuth_control)
71 , width_control (_panner->pannable()->pan_width_control)
72 , dragging_position (false)
73 , dragging_left (false)
74 , dragging_right (false)
77 , accumulated_delta (0)
79 , position_binder (position_control)
80 , width_binder (width_control)
88 Pango::FontDescription font;
89 Pango::AttrFontDesc* font_attr;
90 font = Pango::FontDescription (UIConfiguration::instance().get_SmallBoldMonospaceFont());
91 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
92 panner_font_attributes.change(*font_attr);
97 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
98 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
100 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
101 _panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::pannable_handler, this), gui_context());
103 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
108 StereoPanner::~StereoPanner ()
114 StereoPanner::set_tooltip ()
116 if (_panner_shell->bypassed()) {
117 _tooltip.set_tip (_("bypassed"));
120 double pos = position_control->get_value(); // 0..1
122 /* We show the position of the center of the image relative to the left & right.
123 This is expressed as a pair of percentage values that ranges from (100,0)
124 (hard left) through (50,50) (hard center) to (0,100) (hard right).
126 This is pretty wierd, but its the way audio engineers expect it. Just remember that
127 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
131 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
132 (int) rint (100.0 * pos),
133 (int) floor (100.0 * width_control->get_value()));
134 _tooltip.set_tip (buf);
138 StereoPanner::on_expose_event (GdkEventExpose*)
140 Glib::RefPtr<Gdk::Window> win (get_window());
141 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
142 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
143 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
144 layout->set_attributes (panner_font_attributes);
148 const double pos = position_control->get_value (); /* 0..1 */
149 const double swidth = width_control->get_value (); /* -1..+1 */
150 const double fswidth = fabs (swidth);
151 uint32_t o, f, t, b, r;
155 height = get_height ();
157 const int step_down = rint(height / 3.5);
158 const double corner_radius = 5.0 * UIConfiguration::instance().get_ui_scale();
159 const int lr_box_size = height - 2 * step_down;
160 const int pos_box_size = (int)(rint(step_down * .8)) | 1;
161 const int top_step = step_down - pos_box_size;
165 } else if (swidth < 0.0) {
171 o = colors[state].outline;
172 f = colors[state].fill;
173 t = colors[state].text;
174 b = colors[state].background;
175 r = colors[state].rule;
177 if (_panner_shell->bypassed()) {
186 b = UIConfiguration::instance().color ("send bg");
187 // b = rgba_from_style("SendStripBase",
188 // UINT_RGBA_R(b), UINT_RGBA_G(b), UINT_RGBA_B(b), 255,
193 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
194 cairo_rectangle (context->cobj(), 0, 0, width, height);
195 context->fill_preserve ();
198 /* the usable width is reduced from the real width, because we need space for
199 the two halves of LR boxes that will extend past the actual left/right
200 positions (indicated by the vertical line segment above them).
203 double usable_width = width - lr_box_size;
205 /* compute the centers of the L/R boxes based on the current stereo width */
207 if (fmod (usable_width,2.0) == 0) {
208 /* even width, but we need odd, so that there is an exact center.
209 So, offset cairo by 1, and reduce effective width by 1
212 context->translate (1.0, 0.0);
215 const double half_lr_box = lr_box_size/2.0;
216 const double center = rint(half_lr_box + (usable_width * pos));
217 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
218 const double left = center - pan_spread;
219 const double right = center + pan_spread;
222 context->set_line_width (1.0);
223 context->move_to ((usable_width + lr_box_size)/2.0, 0);
224 context->rel_line_to (0, height);
225 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
228 /* compute & draw the line through the box */
229 context->set_line_width (2);
230 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
231 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
232 context->line_to (left, top_step + (pos_box_size/2.0));
233 context->line_to (right, top_step + (pos_box_size/2.0));
234 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
237 context->set_line_width (1.0);
241 rounded_rectangle (context, left - half_lr_box,
242 half_lr_box+step_down,
243 lr_box_size, lr_box_size, corner_radius);
244 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
245 context->fill_preserve();
246 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
250 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
252 layout->set_text (S_("Panner|R"));
254 layout->set_text (S_("Panner|L"));
256 layout->get_pixel_size(tw, th);
257 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
258 pango_cairo_show_layout (context->cobj(), layout->gobj());
262 rounded_rectangle (context, right - half_lr_box,
263 half_lr_box+step_down,
264 lr_box_size, lr_box_size, corner_radius);
265 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
266 context->fill_preserve();
267 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
271 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
274 layout->set_text (S_("Panner|M"));
277 layout->set_text (S_("Panner|L"));
279 layout->set_text (S_("Panner|R"));
282 layout->get_pixel_size(tw, th);
283 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
284 pango_cairo_show_layout (context->cobj(), layout->gobj());
286 /* draw the central box */
287 context->set_line_width (2.0);
288 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
289 context->rel_line_to (0.0, pos_box_size); /* lower right */
290 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
291 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
292 context->rel_line_to (0.0, -pos_box_size); /* upper left */
293 context->close_path ();
295 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
296 context->stroke_preserve ();
297 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
304 StereoPanner::on_button_press_event (GdkEventButton* ev)
306 if (PannerInterface::on_button_press_event (ev)) {
310 if (_panner_shell->bypassed()) {
314 drag_start_x = ev->x;
317 dragging_position = false;
318 dragging_left = false;
319 dragging_right = false;
321 _tooltip.target_stop_drag ();
322 accumulated_delta = 0;
325 /* Let the binding proxies get first crack at the press event
329 if (position_binder.button_press_handler (ev)) {
333 if (width_binder.button_press_handler (ev)) {
338 if (ev->button != 1) {
342 if (ev->type == GDK_2BUTTON_PRESS) {
343 int width = get_width();
345 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
346 /* handled by button release */
352 /* upper section: adjusts position, constrained by width */
354 const double w = fabs (width_control->get_value ());
355 const double max_pos = 1.0 - (w/2.0);
356 const double min_pos = w/2.0;
358 if (ev->x <= width/3) {
359 /* left side dbl click */
360 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
361 /* 2ndary-double click on left, collapse to hard left */
362 width_control->set_value (0, Controllable::NoGroup);
363 position_control->set_value (0, Controllable::NoGroup);
365 position_control->set_value (min_pos, Controllable::NoGroup);
367 } else if (ev->x > 2*width/3) {
368 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
369 /* 2ndary-double click on right, collapse to hard right */
370 width_control->set_value (0, Controllable::NoGroup);
371 position_control->set_value (1.0, Controllable::NoGroup);
373 position_control->set_value (max_pos, Controllable::NoGroup);
376 position_control->set_value (0.5, Controllable::NoGroup);
381 /* lower section: adjusts width, constrained by position */
383 const double p = position_control->get_value ();
384 const double max_width = 2.0 * min ((1.0 - p), p);
386 if (ev->x <= width/3) {
387 /* left side dbl click */
388 width_control->set_value (max_width, Controllable::NoGroup); // reset width to 100%
389 } else if (ev->x > 2*width/3) {
390 /* right side dbl click */
391 width_control->set_value (-max_width, Controllable::NoGroup); // reset width to inverted 100%
393 /* center dbl click */
394 width_control->set_value (0, Controllable::NoGroup); // collapse width to 0%
399 _tooltip.target_stop_drag ();
401 } else if (ev->type == GDK_BUTTON_PRESS) {
403 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
404 /* handled by button release */
409 /* top section of widget is for position drags */
410 dragging_position = true;
411 StartPositionGesture ();
413 /* lower section is for dragging width */
415 double pos = position_control->get_value (); /* 0..1 */
416 double swidth = width_control->get_value (); /* -1..+1 */
417 double fswidth = fabs (swidth);
418 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
419 int usable_width = get_width() - lr_box_size;
420 double center = (lr_box_size/2.0) + (usable_width * pos);
421 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
422 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
423 const int half_box = lr_box_size/2;
425 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
427 dragging_right = true;
429 dragging_left = true;
431 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
433 dragging_left = true;
435 dragging_right = true;
438 StartWidthGesture ();
442 _tooltip.target_start_drag ();
449 StereoPanner::on_button_release_event (GdkEventButton* ev)
451 if (PannerInterface::on_button_release_event (ev)) {
455 if (ev->button != 1) {
459 if (_panner_shell->bypassed()) {
463 bool const dp = dragging_position;
466 _tooltip.target_stop_drag ();
467 dragging_position = false;
468 dragging_left = false;
469 dragging_right = false;
470 accumulated_delta = 0;
473 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
477 StopPositionGesture ();
487 StereoPanner::on_scroll_event (GdkEventScroll* ev)
489 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
490 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
491 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
494 if (_panner_shell->bypassed()) {
498 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
501 step = one_degree * 5.0;
504 switch (ev->direction) {
505 case GDK_SCROLL_LEFT:
507 width_control->set_value (wv, Controllable::NoGroup);
511 position_control->set_value (pv, Controllable::NoGroup);
513 case GDK_SCROLL_RIGHT:
515 width_control->set_value (wv, Controllable::NoGroup);
517 case GDK_SCROLL_DOWN:
519 position_control->set_value (pv, Controllable::NoGroup);
527 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
529 if (_panner_shell->bypassed()) {
536 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
537 int usable_width = get_width() - lr_box_size;
538 double delta = (ev->x - last_drag_x) / (double) usable_width;
539 double current_width = width_control->get_value ();
545 if (dragging_left || dragging_right) {
547 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
549 /* change width and position in a way that keeps the
550 * other side in the same place
555 double pv = position_control->get_value();
558 position_control->set_value (pv - delta, Controllable::NoGroup);
560 position_control->set_value (pv + delta, Controllable::NoGroup);
564 /* delta is positive, so we're about to
565 increase the width. But we need to increase it
566 by twice the required value so that the
567 other side remains in place when we set
568 the position as well.
570 width_control->set_value (current_width + (delta * 2.0), Controllable::NoGroup);
572 width_control->set_value (current_width + delta, Controllable::NoGroup);
579 /* maintain position as invariant as we change the width */
581 /* create a detent close to the center */
583 if (!detented && fabs (current_width) < 0.02) {
586 width_control->set_value (0, Controllable::NoGroup);
591 accumulated_delta += delta;
593 /* have we pulled far enough to escape ? */
595 if (fabs (accumulated_delta) >= 0.025) {
596 width_control->set_value (current_width + accumulated_delta, Controllable::NoGroup);
598 accumulated_delta = false;
602 /* width needs to change by 2 * delta because both L & R move */
603 width_control->set_value (current_width + (delta * 2.0), Controllable::NoGroup);
607 } else if (dragging_position) {
609 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
610 position_control->set_value (pv + delta, Controllable::NoGroup);
618 StereoPanner::on_key_press_event (GdkEventKey* ev)
620 double one_degree = 1.0/180.0;
621 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
622 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
625 if (_panner_shell->bypassed()) {
629 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
632 step = one_degree * 5.0;
635 /* up/down control width because we consider pan position more "important"
636 (and thus having higher "sense" priority) than width.
639 switch (ev->keyval) {
641 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
642 width_control->set_value (1.0, Controllable::NoGroup);
644 width_control->set_value (wv + step, Controllable::NoGroup);
648 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
649 width_control->set_value (-1.0, Controllable::NoGroup);
651 width_control->set_value (wv - step, Controllable::NoGroup);
657 position_control->set_value (pv, Controllable::NoGroup);
661 position_control->set_value (pv, Controllable::NoGroup);
665 width_control->set_value (0.0, Controllable::NoGroup);
676 StereoPanner::set_colors ()
678 colors[Normal].fill = UIConfiguration::instance().color ("stereo panner fill");
679 colors[Normal].outline = UIConfiguration::instance().color ("stereo panner outline");
680 colors[Normal].text = UIConfiguration::instance().color ("stereo panner text");
681 colors[Normal].background = UIConfiguration::instance().color ("stereo panner bg");
682 colors[Normal].rule = UIConfiguration::instance().color ("stereo panner rule");
684 colors[Mono].fill = UIConfiguration::instance().color ("stereo panner mono fill");
685 colors[Mono].outline = UIConfiguration::instance().color ("stereo panner mono outline");
686 colors[Mono].text = UIConfiguration::instance().color ("stereo panner mono text");
687 colors[Mono].background = UIConfiguration::instance().color ("stereo panner mono bg");
688 colors[Mono].rule = UIConfiguration::instance().color ("stereo panner rule");
690 colors[Inverted].fill = UIConfiguration::instance().color ("stereo panner inverted fill");
691 colors[Inverted].outline = UIConfiguration::instance().color ("stereo panner inverted outline");
692 colors[Inverted].text = UIConfiguration::instance().color ("stereo panner inverted text");
693 colors[Inverted].background = UIConfiguration::instance().color ("stereo panner inverted bg");
694 colors[Inverted].rule = UIConfiguration::instance().color ("stereo panner rule");
698 StereoPanner::color_handler ()
705 StereoPanner::bypass_handler ()
711 StereoPanner::pannable_handler ()
713 panvalue_connections.drop_connections();
714 position_control = _panner->pannable()->pan_azimuth_control;
715 width_control = _panner->pannable()->pan_width_control;
716 position_binder.set_controllable(position_control);
717 width_binder.set_controllable(width_control);
719 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
720 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
725 StereoPanner::editor ()
727 return new StereoPannerEditor (this);