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()) {
182 b = rgba_from_style("SendStripBase",
183 UINT_RGBA_R(b), UINT_RGBA_G(b), UINT_RGBA_B(b), 255,
188 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
189 cairo_rectangle (context->cobj(), 0, 0, width, height);
190 context->fill_preserve ();
193 /* the usable width is reduced from the real width, because we need space for
194 the two halves of LR boxes that will extend past the actual left/right
195 positions (indicated by the vertical line segment above them).
198 double usable_width = width - lr_box_size;
200 /* compute the centers of the L/R boxes based on the current stereo width */
202 if (fmod (usable_width,2.0) == 0) {
203 /* even width, but we need odd, so that there is an exact center.
204 So, offset cairo by 1, and reduce effective width by 1
207 context->translate (1.0, 0.0);
210 const double half_lr_box = lr_box_size/2.0;
211 const double center = rint(half_lr_box + (usable_width * pos));
212 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
213 const double left = center - pan_spread;
214 const double right = center + pan_spread;
217 context->set_line_width (1.0);
218 context->move_to ((usable_width + lr_box_size)/2.0, 0);
219 context->rel_line_to (0, height);
220 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
223 /* compute & draw the line through the box */
224 context->set_line_width (2);
225 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
226 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
227 context->line_to (left, top_step + (pos_box_size/2.0));
228 context->line_to (right, top_step + (pos_box_size/2.0));
229 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
232 context->set_line_width (1.0);
236 rounded_rectangle (context, left - half_lr_box,
237 half_lr_box+step_down,
238 lr_box_size, lr_box_size, corner_radius);
239 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
240 context->fill_preserve();
241 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
245 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
247 layout->set_text (_("R"));
249 layout->set_text (_("L"));
251 layout->get_pixel_size(tw, th);
252 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
253 pango_cairo_show_layout (context->cobj(), layout->gobj());
257 rounded_rectangle (context, right - half_lr_box,
258 half_lr_box+step_down,
259 lr_box_size, lr_box_size, corner_radius);
260 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
261 context->fill_preserve();
262 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
266 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
269 layout->set_text (_("M"));
272 layout->set_text (_("L"));
274 layout->set_text (_("R"));
277 layout->get_pixel_size(tw, th);
278 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
279 pango_cairo_show_layout (context->cobj(), layout->gobj());
281 /* draw the central box */
282 context->set_line_width (2.0);
283 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
284 context->rel_line_to (0.0, pos_box_size); /* lower right */
285 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
286 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
287 context->rel_line_to (0.0, -pos_box_size); /* upper left */
288 context->close_path ();
290 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
291 context->stroke_preserve ();
292 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
299 StereoPanner::on_button_press_event (GdkEventButton* ev)
301 if (PannerInterface::on_button_press_event (ev)) {
305 if (_panner_shell->bypassed()) {
309 drag_start_x = ev->x;
312 dragging_position = false;
313 dragging_left = false;
314 dragging_right = false;
316 _tooltip.target_stop_drag ();
317 accumulated_delta = 0;
320 /* Let the binding proxies get first crack at the press event
324 if (position_binder.button_press_handler (ev)) {
328 if (width_binder.button_press_handler (ev)) {
333 if (ev->button != 1) {
337 if (ev->type == GDK_2BUTTON_PRESS) {
338 int width = get_width();
340 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
341 /* handled by button release */
347 /* upper section: adjusts position, constrained by width */
349 const double w = fabs (width_control->get_value ());
350 const double max_pos = 1.0 - (w/2.0);
351 const double min_pos = w/2.0;
353 if (ev->x <= width/3) {
354 /* left side dbl click */
355 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
356 /* 2ndary-double click on left, collapse to hard left */
357 width_control->set_value (0);
358 position_control->set_value (0);
360 position_control->set_value (min_pos);
362 } else if (ev->x > 2*width/3) {
363 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
364 /* 2ndary-double click on right, collapse to hard right */
365 width_control->set_value (0);
366 position_control->set_value (1.0);
368 position_control->set_value (max_pos);
371 position_control->set_value (0.5);
376 /* lower section: adjusts width, constrained by position */
378 const double p = position_control->get_value ();
379 const double max_width = 2.0 * min ((1.0 - p), p);
381 if (ev->x <= width/3) {
382 /* left side dbl click */
383 width_control->set_value (max_width); // reset width to 100%
384 } else if (ev->x > 2*width/3) {
385 /* right side dbl click */
386 width_control->set_value (-max_width); // reset width to inverted 100%
388 /* center dbl click */
389 width_control->set_value (0); // collapse width to 0%
394 _tooltip.target_stop_drag ();
396 } else if (ev->type == GDK_BUTTON_PRESS) {
398 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
399 /* handled by button release */
404 /* top section of widget is for position drags */
405 dragging_position = true;
406 StartPositionGesture ();
408 /* lower section is for dragging width */
410 double pos = position_control->get_value (); /* 0..1 */
411 double swidth = width_control->get_value (); /* -1..+1 */
412 double fswidth = fabs (swidth);
413 int usable_width = get_width() - lr_box_size;
414 double center = (lr_box_size/2.0) + (usable_width * pos);
415 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
416 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
417 const int half_box = lr_box_size/2;
419 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
421 dragging_right = true;
423 dragging_left = true;
425 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
427 dragging_left = true;
429 dragging_right = true;
432 StartWidthGesture ();
436 _tooltip.target_start_drag ();
443 StereoPanner::on_button_release_event (GdkEventButton* ev)
445 if (PannerInterface::on_button_release_event (ev)) {
449 if (ev->button != 1) {
453 if (_panner_shell->bypassed()) {
457 bool const dp = dragging_position;
460 _tooltip.target_stop_drag ();
461 dragging_position = false;
462 dragging_left = false;
463 dragging_right = false;
464 accumulated_delta = 0;
467 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
471 StopPositionGesture ();
481 StereoPanner::on_scroll_event (GdkEventScroll* ev)
483 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
484 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
485 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
488 if (_panner_shell->bypassed()) {
492 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
495 step = one_degree * 5.0;
498 switch (ev->direction) {
499 case GDK_SCROLL_LEFT:
501 width_control->set_value (wv);
505 position_control->set_value (pv);
507 case GDK_SCROLL_RIGHT:
509 width_control->set_value (wv);
511 case GDK_SCROLL_DOWN:
513 position_control->set_value (pv);
521 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
523 if (_panner_shell->bypassed()) {
530 int usable_width = get_width() - lr_box_size;
531 double delta = (ev->x - last_drag_x) / (double) usable_width;
532 double current_width = width_control->get_value ();
538 if (dragging_left || dragging_right) {
540 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
542 /* change width and position in a way that keeps the
543 * other side in the same place
548 double pv = position_control->get_value();
551 position_control->set_value (pv - delta);
553 position_control->set_value (pv + delta);
557 /* delta is positive, so we're about to
558 increase the width. But we need to increase it
559 by twice the required value so that the
560 other side remains in place when we set
561 the position as well.
563 width_control->set_value (current_width + (delta * 2.0));
565 width_control->set_value (current_width + delta);
572 /* maintain position as invariant as we change the width */
574 /* create a detent close to the center */
576 if (!detented && fabs (current_width) < 0.02) {
579 width_control->set_value (0);
584 accumulated_delta += delta;
586 /* have we pulled far enough to escape ? */
588 if (fabs (accumulated_delta) >= 0.025) {
589 width_control->set_value (current_width + accumulated_delta);
591 accumulated_delta = false;
595 /* width needs to change by 2 * delta because both L & R move */
596 width_control->set_value (current_width + (delta * 2.0));
600 } else if (dragging_position) {
602 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
603 position_control->set_value (pv + delta);
611 StereoPanner::on_key_press_event (GdkEventKey* ev)
613 double one_degree = 1.0/180.0;
614 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
615 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
618 if (_panner_shell->bypassed()) {
622 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
625 step = one_degree * 5.0;
628 /* up/down control width because we consider pan position more "important"
629 (and thus having higher "sense" priority) than width.
632 switch (ev->keyval) {
634 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
635 width_control->set_value (1.0);
637 width_control->set_value (wv + step);
641 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
642 width_control->set_value (-1.0);
644 width_control->set_value (wv - step);
650 position_control->set_value (pv);
654 position_control->set_value (pv);
658 width_control->set_value (0.0);
669 StereoPanner::set_colors ()
671 colors[Normal].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerFill();
672 colors[Normal].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerOutline();
673 colors[Normal].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerText();
674 colors[Normal].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerBackground();
675 colors[Normal].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
677 colors[Mono].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoFill();
678 colors[Mono].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoOutline();
679 colors[Mono].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoText();
680 colors[Mono].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoBackground();
681 colors[Mono].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
683 colors[Inverted].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedFill();
684 colors[Inverted].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedOutline();
685 colors[Inverted].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedText();
686 colors[Inverted].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedBackground();
687 colors[Inverted].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
691 StereoPanner::color_handler ()
698 StereoPanner::bypass_handler ()
704 StereoPanner::pannable_handler ()
706 panvalue_connections.drop_connections();
707 position_control = _panner->pannable()->pan_azimuth_control;
708 width_control = _panner->pannable()->pan_width_control;
709 position_binder.set_controllable(position_control);
710 width_binder.set_controllable(width_control);
712 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
713 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
718 StereoPanner::editor ()
720 return new StereoPannerEditor (this);