2 Copyright (C) 2000-2007 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.
24 #include <gtkmm/window.h>
25 #include <pangomm/layout.h>
27 #include "pbd/controllable.h"
28 #include "pbd/compose.h"
30 #include "gtkmm2ext/gui_thread.h"
31 #include "gtkmm2ext/gtk_ui.h"
32 #include "gtkmm2ext/keyboard.h"
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/persistent_tooltip.h"
36 #include "ardour/pannable.h"
37 #include "ardour/panner.h"
38 #include "ardour/panner_shell.h"
40 #include "ardour_ui.h"
41 #include "global_signals.h"
42 #include "stereo_panner.h"
43 #include "stereo_panner_editor.h"
44 #include "rgb_macros.h"
51 using namespace Gtkmm2ext;
52 using namespace ARDOUR_UI_UTILS;
54 static const int pos_box_size = 8;
55 static const int lr_box_size = 15;
56 static const int step_down = 10;
57 static const int top_step = 2;
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 (ARDOUR_UI::config()->get_canvasvar_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 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 const double corner_radius = 5.0;
152 uint32_t o, f, t, b, r;
156 height = get_height ();
160 } else if (swidth < 0.0) {
166 o = colors[state].outline;
167 f = colors[state].fill;
168 t = colors[state].text;
169 b = colors[state].background;
170 r = colors[state].rule;
172 if (_panner_shell->bypassed()) {
181 b = rgba_from_style("SendStripBase",
182 UINT_RGBA_R(b), UINT_RGBA_G(b), UINT_RGBA_B(b), 255,
187 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
188 cairo_rectangle (context->cobj(), 0, 0, width, height);
189 context->fill_preserve ();
192 /* the usable width is reduced from the real width, because we need space for
193 the two halves of LR boxes that will extend past the actual left/right
194 positions (indicated by the vertical line segment above them).
197 double usable_width = width - lr_box_size;
199 /* compute the centers of the L/R boxes based on the current stereo width */
201 if (fmod (usable_width,2.0) == 0) {
202 /* even width, but we need odd, so that there is an exact center.
203 So, offset cairo by 1, and reduce effective width by 1
206 context->translate (1.0, 0.0);
209 const double half_lr_box = lr_box_size/2.0;
210 const double center = rint(half_lr_box + (usable_width * pos));
211 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
212 const double left = center - pan_spread;
213 const double right = center + pan_spread;
216 context->set_line_width (1.0);
217 context->move_to ((usable_width + lr_box_size)/2.0, 0);
218 context->rel_line_to (0, height);
219 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
222 /* compute & draw the line through the box */
223 context->set_line_width (2);
224 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
225 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
226 context->line_to (left, top_step + (pos_box_size/2.0));
227 context->line_to (right, top_step + (pos_box_size/2.0));
228 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
231 context->set_line_width (1.0);
235 rounded_rectangle (context, left - half_lr_box,
236 half_lr_box+step_down,
237 lr_box_size, lr_box_size, corner_radius);
238 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
239 context->fill_preserve();
240 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
244 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
246 layout->set_text (_("R"));
248 layout->set_text (_("L"));
250 layout->get_pixel_size(tw, th);
251 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
252 pango_cairo_show_layout (context->cobj(), layout->gobj());
256 rounded_rectangle (context, right - half_lr_box,
257 half_lr_box+step_down,
258 lr_box_size, lr_box_size, corner_radius);
259 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
260 context->fill_preserve();
261 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
265 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
268 layout->set_text (_("M"));
271 layout->set_text (_("L"));
273 layout->set_text (_("R"));
276 layout->get_pixel_size(tw, th);
277 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
278 pango_cairo_show_layout (context->cobj(), layout->gobj());
280 /* draw the central box */
281 context->set_line_width (2.0);
282 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
283 context->rel_line_to (0.0, pos_box_size); /* lower right */
284 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
285 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
286 context->rel_line_to (0.0, -pos_box_size); /* upper left */
287 context->close_path ();
289 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
290 context->stroke_preserve ();
291 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
298 StereoPanner::on_button_press_event (GdkEventButton* ev)
300 if (PannerInterface::on_button_press_event (ev)) {
304 if (_panner_shell->bypassed()) {
308 drag_start_x = ev->x;
311 dragging_position = false;
312 dragging_left = false;
313 dragging_right = false;
315 _tooltip.target_stop_drag ();
316 accumulated_delta = 0;
319 /* Let the binding proxies get first crack at the press event
323 if (position_binder.button_press_handler (ev)) {
327 if (width_binder.button_press_handler (ev)) {
332 if (ev->button != 1) {
336 if (ev->type == GDK_2BUTTON_PRESS) {
337 int width = get_width();
339 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
340 /* handled by button release */
346 /* upper section: adjusts position, constrained by width */
348 const double w = fabs (width_control->get_value ());
349 const double max_pos = 1.0 - (w/2.0);
350 const double min_pos = w/2.0;
352 if (ev->x <= width/3) {
353 /* left side dbl click */
354 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
355 /* 2ndary-double click on left, collapse to hard left */
356 width_control->set_value (0);
357 position_control->set_value (0);
359 position_control->set_value (min_pos);
361 } else if (ev->x > 2*width/3) {
362 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
363 /* 2ndary-double click on right, collapse to hard right */
364 width_control->set_value (0);
365 position_control->set_value (1.0);
367 position_control->set_value (max_pos);
370 position_control->set_value (0.5);
375 /* lower section: adjusts width, constrained by position */
377 const double p = position_control->get_value ();
378 const double max_width = 2.0 * min ((1.0 - p), p);
380 if (ev->x <= width/3) {
381 /* left side dbl click */
382 width_control->set_value (max_width); // reset width to 100%
383 } else if (ev->x > 2*width/3) {
384 /* right side dbl click */
385 width_control->set_value (-max_width); // reset width to inverted 100%
387 /* center dbl click */
388 width_control->set_value (0); // collapse width to 0%
393 _tooltip.target_stop_drag ();
395 } else if (ev->type == GDK_BUTTON_PRESS) {
397 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
398 /* handled by button release */
403 /* top section of widget is for position drags */
404 dragging_position = true;
405 StartPositionGesture ();
407 /* lower section is for dragging width */
409 double pos = position_control->get_value (); /* 0..1 */
410 double swidth = width_control->get_value (); /* -1..+1 */
411 double fswidth = fabs (swidth);
412 int usable_width = get_width() - lr_box_size;
413 double center = (lr_box_size/2.0) + (usable_width * pos);
414 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
415 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
416 const int half_box = lr_box_size/2;
418 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
420 dragging_right = true;
422 dragging_left = true;
424 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
426 dragging_left = true;
428 dragging_right = true;
431 StartWidthGesture ();
435 _tooltip.target_start_drag ();
442 StereoPanner::on_button_release_event (GdkEventButton* ev)
444 if (PannerInterface::on_button_release_event (ev)) {
448 if (ev->button != 1) {
452 if (_panner_shell->bypassed()) {
456 bool const dp = dragging_position;
459 _tooltip.target_stop_drag ();
460 dragging_position = false;
461 dragging_left = false;
462 dragging_right = false;
463 accumulated_delta = 0;
466 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
470 StopPositionGesture ();
480 StereoPanner::on_scroll_event (GdkEventScroll* ev)
482 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
483 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
484 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
487 if (_panner_shell->bypassed()) {
491 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
494 step = one_degree * 5.0;
497 switch (ev->direction) {
498 case GDK_SCROLL_LEFT:
500 width_control->set_value (wv);
504 position_control->set_value (pv);
506 case GDK_SCROLL_RIGHT:
508 width_control->set_value (wv);
510 case GDK_SCROLL_DOWN:
512 position_control->set_value (pv);
520 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
522 if (_panner_shell->bypassed()) {
529 int usable_width = get_width() - lr_box_size;
530 double delta = (ev->x - last_drag_x) / (double) usable_width;
531 double current_width = width_control->get_value ();
537 if (dragging_left || dragging_right) {
539 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
541 /* change width and position in a way that keeps the
542 * other side in the same place
547 double pv = position_control->get_value();
550 position_control->set_value (pv - delta);
552 position_control->set_value (pv + delta);
556 /* delta is positive, so we're about to
557 increase the width. But we need to increase it
558 by twice the required value so that the
559 other side remains in place when we set
560 the position as well.
562 width_control->set_value (current_width + (delta * 2.0));
564 width_control->set_value (current_width + delta);
571 /* maintain position as invariant as we change the width */
573 /* create a detent close to the center */
575 if (!detented && fabs (current_width) < 0.02) {
578 width_control->set_value (0);
583 accumulated_delta += delta;
585 /* have we pulled far enough to escape ? */
587 if (fabs (accumulated_delta) >= 0.025) {
588 width_control->set_value (current_width + accumulated_delta);
590 accumulated_delta = false;
594 /* width needs to change by 2 * delta because both L & R move */
595 width_control->set_value (current_width + (delta * 2.0));
599 } else if (dragging_position) {
601 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
602 position_control->set_value (pv + delta);
610 StereoPanner::on_key_press_event (GdkEventKey* ev)
612 double one_degree = 1.0/180.0;
613 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
614 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
617 if (_panner_shell->bypassed()) {
621 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
624 step = one_degree * 5.0;
627 /* up/down control width because we consider pan position more "important"
628 (and thus having higher "sense" priority) than width.
631 switch (ev->keyval) {
633 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
634 width_control->set_value (1.0);
636 width_control->set_value (wv + step);
640 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
641 width_control->set_value (-1.0);
643 width_control->set_value (wv - step);
649 position_control->set_value (pv);
653 position_control->set_value (pv);
657 width_control->set_value (0.0);
668 StereoPanner::set_colors ()
670 colors[Normal].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerFill();
671 colors[Normal].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerOutline();
672 colors[Normal].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerText();
673 colors[Normal].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerBackground();
674 colors[Normal].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
676 colors[Mono].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoFill();
677 colors[Mono].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoOutline();
678 colors[Mono].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoText();
679 colors[Mono].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoBackground();
680 colors[Mono].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
682 colors[Inverted].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedFill();
683 colors[Inverted].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedOutline();
684 colors[Inverted].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedText();
685 colors[Inverted].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedBackground();
686 colors[Inverted].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
690 StereoPanner::color_handler ()
697 StereoPanner::bypass_handler ()
703 StereoPanner::pannable_handler ()
705 panvalue_connections.drop_connections();
706 position_control = _panner->pannable()->pan_azimuth_control;
707 width_control = _panner->pannable()->pan_width_control;
708 position_binder.set_controllable(position_control);
709 width_binder.set_controllable(width_control);
711 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
712 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
717 StereoPanner::editor ()
719 return new StereoPannerEditor (this);