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;
53 static const int pos_box_size = 8;
54 static const int lr_box_size = 15;
55 static const int step_down = 10;
56 static const int top_step = 2;
58 StereoPanner::ColorScheme StereoPanner::colors[3];
59 bool StereoPanner::have_colors = false;
61 Pango::AttrList StereoPanner::panner_font_attributes;
62 bool StereoPanner::have_font = false;
64 using namespace ARDOUR;
66 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
67 : PannerInterface (p->panner())
69 , position_control (_panner->pannable()->pan_azimuth_control)
70 , width_control (_panner->pannable()->pan_width_control)
71 , dragging_position (false)
72 , dragging_left (false)
73 , dragging_right (false)
76 , accumulated_delta (0)
78 , position_binder (position_control)
79 , width_binder (width_control)
87 Pango::FontDescription font;
88 Pango::AttrFontDesc* font_attr;
89 font = Pango::FontDescription ("ArdourMono");
90 font.set_weight (Pango::WEIGHT_BOLD);
91 font.set_size(9 * PANGO_SCALE);
92 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
93 panner_font_attributes.change(*font_attr);
98 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
99 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
101 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
102 _panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::pannable_handler, this), gui_context());
104 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
109 StereoPanner::~StereoPanner ()
115 StereoPanner::set_tooltip ()
117 if (_panner_shell->bypassed()) {
118 _tooltip.set_tip (_("bypassed"));
121 double pos = position_control->get_value(); // 0..1
123 /* We show the position of the center of the image relative to the left & right.
124 This is expressed as a pair of percentage values that ranges from (100,0)
125 (hard left) through (50,50) (hard center) to (0,100) (hard right).
127 This is pretty wierd, but its the way audio engineers expect it. Just remember that
128 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
132 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
133 (int) rint (100.0 * pos),
134 (int) floor (100.0 * width_control->get_value()));
135 _tooltip.set_tip (buf);
139 StereoPanner::on_expose_event (GdkEventExpose*)
141 Glib::RefPtr<Gdk::Window> win (get_window());
142 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
143 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
144 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
145 layout->set_attributes (panner_font_attributes);
149 const double pos = position_control->get_value (); /* 0..1 */
150 const double swidth = width_control->get_value (); /* -1..+1 */
151 const double fswidth = fabs (swidth);
152 const double corner_radius = 5.0;
153 uint32_t o, f, t, b, r;
157 height = get_height ();
161 } else if (swidth < 0.0) {
167 o = colors[state].outline;
168 f = colors[state].fill;
169 t = colors[state].text;
170 b = colors[state].background;
171 r = colors[state].rule;
173 if (_panner_shell->bypassed()) {
183 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
184 cairo_rectangle (context->cobj(), 0, 0, width, height);
185 context->fill_preserve ();
188 /* the usable width is reduced from the real width, because we need space for
189 the two halves of LR boxes that will extend past the actual left/right
190 positions (indicated by the vertical line segment above them).
193 double usable_width = width - lr_box_size;
195 /* compute the centers of the L/R boxes based on the current stereo width */
197 if (fmod (usable_width,2.0) == 0) {
198 /* even width, but we need odd, so that there is an exact center.
199 So, offset cairo by 1, and reduce effective width by 1
202 context->translate (1.0, 0.0);
205 const double half_lr_box = lr_box_size/2.0;
206 const double center = rint(half_lr_box + (usable_width * pos));
207 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
208 const double left = center - pan_spread;
209 const double right = center + pan_spread;
212 context->set_line_width (1.0);
213 context->move_to ((usable_width + lr_box_size)/2.0, 0);
214 context->rel_line_to (0, height);
215 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
218 /* compute & draw the line through the box */
219 context->set_line_width (2);
220 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
221 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
222 context->line_to (left, top_step + (pos_box_size/2.0));
223 context->line_to (right, top_step + (pos_box_size/2.0));
224 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
227 context->set_line_width (1.0);
231 rounded_rectangle (context, left - half_lr_box,
232 half_lr_box+step_down,
233 lr_box_size, lr_box_size, corner_radius);
234 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
235 context->fill_preserve();
236 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
240 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
242 layout->set_text (_("R"));
244 layout->set_text (_("L"));
246 layout->get_pixel_size(tw, th);
247 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
248 pango_cairo_show_layout (context->cobj(), layout->gobj());
252 rounded_rectangle (context, right - half_lr_box,
253 half_lr_box+step_down,
254 lr_box_size, lr_box_size, corner_radius);
255 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
256 context->fill_preserve();
257 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
261 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
264 layout->set_text (_("M"));
267 layout->set_text (_("L"));
269 layout->set_text (_("R"));
272 layout->get_pixel_size(tw, th);
273 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
274 pango_cairo_show_layout (context->cobj(), layout->gobj());
276 /* draw the central box */
277 context->set_line_width (2.0);
278 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
279 context->rel_line_to (0.0, pos_box_size); /* lower right */
280 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
281 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
282 context->rel_line_to (0.0, -pos_box_size); /* upper left */
283 context->close_path ();
285 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
286 context->stroke_preserve ();
287 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
294 StereoPanner::on_button_press_event (GdkEventButton* ev)
296 if (PannerInterface::on_button_press_event (ev)) {
300 if (_panner_shell->bypassed()) {
304 drag_start_x = ev->x;
307 dragging_position = false;
308 dragging_left = false;
309 dragging_right = false;
311 _tooltip.target_stop_drag ();
312 accumulated_delta = 0;
315 /* Let the binding proxies get first crack at the press event
319 if (position_binder.button_press_handler (ev)) {
323 if (width_binder.button_press_handler (ev)) {
328 if (ev->button != 1) {
332 if (ev->type == GDK_2BUTTON_PRESS) {
333 int width = get_width();
335 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
336 /* handled by button release */
342 /* upper section: adjusts position, constrained by width */
344 const double w = fabs (width_control->get_value ());
345 const double max_pos = 1.0 - (w/2.0);
346 const double min_pos = w/2.0;
348 if (ev->x <= width/3) {
349 /* left side dbl click */
350 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
351 /* 2ndary-double click on left, collapse to hard left */
352 width_control->set_value (0);
353 position_control->set_value (0);
355 position_control->set_value (min_pos);
357 } else if (ev->x > 2*width/3) {
358 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
359 /* 2ndary-double click on right, collapse to hard right */
360 width_control->set_value (0);
361 position_control->set_value (1.0);
363 position_control->set_value (max_pos);
366 position_control->set_value (0.5);
371 /* lower section: adjusts width, constrained by position */
373 const double p = position_control->get_value ();
374 const double max_width = 2.0 * min ((1.0 - p), p);
376 if (ev->x <= width/3) {
377 /* left side dbl click */
378 width_control->set_value (max_width); // reset width to 100%
379 } else if (ev->x > 2*width/3) {
380 /* right side dbl click */
381 width_control->set_value (-max_width); // reset width to inverted 100%
383 /* center dbl click */
384 width_control->set_value (0); // collapse width to 0%
389 _tooltip.target_stop_drag ();
391 } else if (ev->type == GDK_BUTTON_PRESS) {
393 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
394 /* handled by button release */
399 /* top section of widget is for position drags */
400 dragging_position = true;
401 StartPositionGesture ();
403 /* lower section is for dragging width */
405 double pos = position_control->get_value (); /* 0..1 */
406 double swidth = width_control->get_value (); /* -1..+1 */
407 double fswidth = fabs (swidth);
408 int usable_width = get_width() - lr_box_size;
409 double center = (lr_box_size/2.0) + (usable_width * pos);
410 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
411 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
412 const int half_box = lr_box_size/2;
414 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
416 dragging_right = true;
418 dragging_left = true;
420 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
422 dragging_left = true;
424 dragging_right = true;
427 StartWidthGesture ();
431 _tooltip.target_start_drag ();
438 StereoPanner::on_button_release_event (GdkEventButton* ev)
440 if (PannerInterface::on_button_release_event (ev)) {
444 if (ev->button != 1) {
448 if (_panner_shell->bypassed()) {
452 bool const dp = dragging_position;
455 _tooltip.target_stop_drag ();
456 dragging_position = false;
457 dragging_left = false;
458 dragging_right = false;
459 accumulated_delta = 0;
462 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
466 StopPositionGesture ();
476 StereoPanner::on_scroll_event (GdkEventScroll* ev)
478 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
479 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
480 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
483 if (_panner_shell->bypassed()) {
487 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
490 step = one_degree * 5.0;
493 switch (ev->direction) {
494 case GDK_SCROLL_LEFT:
496 width_control->set_value (wv);
500 position_control->set_value (pv);
502 case GDK_SCROLL_RIGHT:
504 width_control->set_value (wv);
506 case GDK_SCROLL_DOWN:
508 position_control->set_value (pv);
516 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
518 if (_panner_shell->bypassed()) {
525 int usable_width = get_width() - lr_box_size;
526 double delta = (ev->x - last_drag_x) / (double) usable_width;
527 double current_width = width_control->get_value ();
533 if (dragging_left || dragging_right) {
535 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
537 /* change width and position in a way that keeps the
538 * other side in the same place
543 double pv = position_control->get_value();
546 position_control->set_value (pv - delta);
548 position_control->set_value (pv + delta);
552 /* delta is positive, so we're about to
553 increase the width. But we need to increase it
554 by twice the required value so that the
555 other side remains in place when we set
556 the position as well.
558 width_control->set_value (current_width + (delta * 2.0));
560 width_control->set_value (current_width + delta);
567 /* maintain position as invariant as we change the width */
569 /* create a detent close to the center */
571 if (!detented && fabs (current_width) < 0.02) {
574 width_control->set_value (0);
579 accumulated_delta += delta;
581 /* have we pulled far enough to escape ? */
583 if (fabs (accumulated_delta) >= 0.025) {
584 width_control->set_value (current_width + accumulated_delta);
586 accumulated_delta = false;
590 /* width needs to change by 2 * delta because both L & R move */
591 width_control->set_value (current_width + (delta * 2.0));
595 } else if (dragging_position) {
597 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
598 position_control->set_value (pv + delta);
606 StereoPanner::on_key_press_event (GdkEventKey* ev)
608 double one_degree = 1.0/180.0;
609 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
610 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
613 if (_panner_shell->bypassed()) {
617 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
620 step = one_degree * 5.0;
623 /* up/down control width because we consider pan position more "important"
624 (and thus having higher "sense" priority) than width.
627 switch (ev->keyval) {
629 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
630 width_control->set_value (1.0);
632 width_control->set_value (wv + step);
636 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
637 width_control->set_value (-1.0);
639 width_control->set_value (wv - step);
645 position_control->set_value (pv);
649 position_control->set_value (pv);
653 width_control->set_value (0.0);
664 StereoPanner::set_colors ()
666 colors[Normal].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerFill();
667 colors[Normal].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerOutline();
668 colors[Normal].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerText();
669 colors[Normal].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerBackground();
670 colors[Normal].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
672 colors[Mono].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoFill();
673 colors[Mono].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoOutline();
674 colors[Mono].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoText();
675 colors[Mono].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoBackground();
676 colors[Mono].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
678 colors[Inverted].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedFill();
679 colors[Inverted].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedOutline();
680 colors[Inverted].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedText();
681 colors[Inverted].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedBackground();
682 colors[Inverted].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
686 StereoPanner::color_handler ()
693 StereoPanner::bypass_handler ()
699 StereoPanner::pannable_handler ()
701 panvalue_connections.drop_connections();
702 position_control = _panner->pannable()->pan_azimuth_control;
703 width_control = _panner->pannable()->pan_width_control;
704 position_binder.set_controllable(position_control);
705 width_binder.set_controllable(width_control);
707 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
708 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
713 StereoPanner::editor ()
715 return new StereoPannerEditor (this);