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 (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
99 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
100 _panner_shell->Changed.connect (connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
102 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
107 StereoPanner::~StereoPanner ()
113 StereoPanner::set_tooltip ()
115 if (_panner_shell->bypassed()) {
116 _tooltip.set_tip (_("bypassed"));
119 double pos = position_control->get_value(); // 0..1
121 /* We show the position of the center of the image relative to the left & right.
122 This is expressed as a pair of percentage values that ranges from (100,0)
123 (hard left) through (50,50) (hard center) to (0,100) (hard right).
125 This is pretty wierd, but its the way audio engineers expect it. Just remember that
126 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
130 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
131 (int) rint (100.0 * pos),
132 (int) floor (100.0 * width_control->get_value()));
133 _tooltip.set_tip (buf);
137 StereoPanner::on_expose_event (GdkEventExpose*)
139 Glib::RefPtr<Gdk::Window> win (get_window());
140 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
141 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
142 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
143 layout->set_attributes (panner_font_attributes);
147 const double pos = position_control->get_value (); /* 0..1 */
148 const double swidth = width_control->get_value (); /* -1..+1 */
149 const double fswidth = fabs (swidth);
150 const double corner_radius = 5.0;
151 uint32_t o, f, t, b, r;
155 height = get_height ();
159 } else if (swidth < 0.0) {
165 o = colors[state].outline;
166 f = colors[state].fill;
167 t = colors[state].text;
168 b = colors[state].background;
169 r = colors[state].rule;
171 if (_panner_shell->bypassed()) {
181 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
182 cairo_rectangle (context->cobj(), 0, 0, width, height);
183 context->fill_preserve ();
186 /* the usable width is reduced from the real width, because we need space for
187 the two halves of LR boxes that will extend past the actual left/right
188 positions (indicated by the vertical line segment above them).
191 double usable_width = width - lr_box_size;
193 /* compute the centers of the L/R boxes based on the current stereo width */
195 if (fmod (usable_width,2.0) == 0) {
196 /* even width, but we need odd, so that there is an exact center.
197 So, offset cairo by 1, and reduce effective width by 1
200 context->translate (1.0, 0.0);
203 const double half_lr_box = lr_box_size/2.0;
204 const double center = rint(half_lr_box + (usable_width * pos));
205 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
206 const double left = center - pan_spread;
207 const double right = center + pan_spread;
209 printf("L %.2f R %.2f PW:%.2f\n", left, right, 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()->canvasvar_StereoPannerFill.get();
667 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
668 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
669 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
670 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
672 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
673 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
674 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
675 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
676 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
678 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
679 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
680 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
681 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
682 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
686 StereoPanner::color_handler ()
693 StereoPanner::bypass_handler ()
699 StereoPanner::editor ()
701 return new StereoPannerEditor (this);