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>
26 #include "pbd/controllable.h"
27 #include "pbd/compose.h"
29 #include "gtkmm2ext/gui_thread.h"
30 #include "gtkmm2ext/gtk_ui.h"
31 #include "gtkmm2ext/keyboard.h"
32 #include "gtkmm2ext/utils.h"
33 #include "gtkmm2ext/persistent_tooltip.h"
35 #include "ardour/pannable.h"
36 #include "ardour/panner.h"
37 #include "ardour/panner_shell.h"
39 #include "ardour_ui.h"
40 #include "global_signals.h"
41 #include "stereo_panner.h"
42 #include "stereo_panner_editor.h"
43 #include "rgb_macros.h"
50 using namespace Gtkmm2ext;
52 static const int pos_box_size = 8;
53 static const int lr_box_size = 15;
54 static const int step_down = 10;
55 static const int top_step = 2;
57 StereoPanner::ColorScheme StereoPanner::colors[3];
58 bool StereoPanner::have_colors = false;
60 using namespace ARDOUR;
62 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
63 : PannerInterface (p->panner())
65 , position_control (_panner->pannable()->pan_azimuth_control)
66 , width_control (_panner->pannable()->pan_width_control)
67 , dragging_position (false)
68 , dragging_left (false)
69 , dragging_right (false)
72 , accumulated_delta (0)
74 , position_binder (position_control)
75 , width_binder (width_control)
83 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
84 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
85 _panner_shell->Changed.connect (connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
87 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
92 StereoPanner::~StereoPanner ()
98 StereoPanner::set_tooltip ()
100 if (_panner_shell->bypassed()) {
101 _tooltip.set_tip (_("bypassed"));
104 double pos = position_control->get_value(); // 0..1
106 /* We show the position of the center of the image relative to the left & right.
107 This is expressed as a pair of percentage values that ranges from (100,0)
108 (hard left) through (50,50) (hard center) to (0,100) (hard right).
110 This is pretty wierd, but its the way audio engineers expect it. Just remember that
111 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
115 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
116 (int) rint (100.0 * pos),
117 (int) floor (100.0 * width_control->get_value()));
118 _tooltip.set_tip (buf);
122 StereoPanner::on_expose_event (GdkEventExpose*)
124 Glib::RefPtr<Gdk::Window> win (get_window());
125 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
126 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
129 double pos = position_control->get_value (); /* 0..1 */
130 double swidth = width_control->get_value (); /* -1..+1 */
131 double fswidth = fabs (swidth);
132 uint32_t o, f, t, b, r;
134 const double corner_radius = 5.0;
137 height = get_height ();
141 } else if (swidth < 0.0) {
147 o = colors[state].outline;
148 f = colors[state].fill;
149 t = colors[state].text;
150 b = colors[state].background;
151 r = colors[state].rule;
155 if (!_panner_shell->bypassed()) {
156 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
158 context->set_source_rgba (0.1, 0.1, 0.1, 0.2);
160 cairo_rectangle (context->cobj(), 0, 0, width, height);
163 /* the usable width is reduced from the real width, because we need space for
164 the two halves of LR boxes that will extend past the actual left/right
165 positions (indicated by the vertical line segment above them).
168 double usable_width = width - lr_box_size;
170 /* compute the centers of the L/R boxes based on the current stereo width */
172 if (fmod (usable_width,2.0) == 0) {
173 /* even width, but we need odd, so that there is an exact center.
174 So, offset cairo by 1, and reduce effective width by 1
177 context->translate (1.0, 0.0);
180 double center = (lr_box_size/2.0) + (usable_width * pos);
181 const double pan_spread = (fswidth * usable_width)/2.0;
182 const double half_lr_box = lr_box_size/2.0;
186 left = center - pan_spread; // center of left box
187 right = center + pan_spread; // center of right box
191 context->set_line_width (1.0);
192 context->move_to ((usable_width + lr_box_size)/2.0, 0);
193 context->rel_line_to (0, height);
194 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
197 if (_panner_shell->bypassed()) {
201 /* compute & draw the line through the box */
203 context->set_line_width (2);
204 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
205 context->move_to (left, top_step+(pos_box_size/2.0)+step_down);
206 context->line_to (left, top_step+(pos_box_size/2.0));
207 context->line_to (right, top_step+(pos_box_size/2.0));
208 context->line_to (right, top_step+(pos_box_size/2.0) + step_down);
213 rounded_rectangle (context, left - half_lr_box,
214 half_lr_box+step_down,
215 lr_box_size, lr_box_size, corner_radius);
216 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
217 context->stroke_preserve ();
218 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
223 context->move_to (left - half_lr_box + 3,
224 (lr_box_size/2) + step_down + 13);
225 context->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD);
228 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
230 context->show_text (_("R"));
232 context->show_text (_("L"));
238 rounded_rectangle (context, right - half_lr_box,
239 half_lr_box+step_down,
240 lr_box_size, lr_box_size, corner_radius);
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));
242 context->stroke_preserve ();
243 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
248 context->move_to (right - half_lr_box + 3, (lr_box_size/2)+step_down + 13);
249 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 context->show_text (_("M"));
255 context->show_text (_("L"));
257 context->show_text (_("R"));
261 /* draw the central box */
263 context->set_line_width (2.0);
264 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
265 context->rel_line_to (0.0, pos_box_size); /* lower right */
266 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
267 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
268 context->rel_line_to (0.0, -pos_box_size); /* upper left */
269 context->close_path ();
271 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
272 context->stroke_preserve ();
273 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
280 StereoPanner::on_button_press_event (GdkEventButton* ev)
282 if (PannerInterface::on_button_press_event (ev)) {
286 if (_panner_shell->bypassed()) {
290 drag_start_x = ev->x;
293 dragging_position = false;
294 dragging_left = false;
295 dragging_right = false;
297 _tooltip.target_stop_drag ();
298 accumulated_delta = 0;
301 /* Let the binding proxies get first crack at the press event
305 if (position_binder.button_press_handler (ev)) {
309 if (width_binder.button_press_handler (ev)) {
314 if (ev->button != 1) {
318 if (ev->type == GDK_2BUTTON_PRESS) {
319 int width = get_width();
321 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
322 /* handled by button release */
328 /* upper section: adjusts position, constrained by width */
330 const double w = fabs (width_control->get_value ());
331 const double max_pos = 1.0 - (w/2.0);
332 const double min_pos = w/2.0;
334 if (ev->x <= width/3) {
335 /* left side dbl click */
336 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
337 /* 2ndary-double click on left, collapse to hard left */
338 width_control->set_value (0);
339 position_control->set_value (0);
341 position_control->set_value (min_pos);
343 } else if (ev->x > 2*width/3) {
344 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
345 /* 2ndary-double click on right, collapse to hard right */
346 width_control->set_value (0);
347 position_control->set_value (1.0);
349 position_control->set_value (max_pos);
352 position_control->set_value (0.5);
357 /* lower section: adjusts width, constrained by position */
359 const double p = position_control->get_value ();
360 const double max_width = 2.0 * min ((1.0 - p), p);
362 if (ev->x <= width/3) {
363 /* left side dbl click */
364 width_control->set_value (max_width); // reset width to 100%
365 } else if (ev->x > 2*width/3) {
366 /* right side dbl click */
367 width_control->set_value (-max_width); // reset width to inverted 100%
369 /* center dbl click */
370 width_control->set_value (0); // collapse width to 0%
375 _tooltip.target_stop_drag ();
377 } else if (ev->type == GDK_BUTTON_PRESS) {
379 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
380 /* handled by button release */
385 /* top section of widget is for position drags */
386 dragging_position = true;
387 StartPositionGesture ();
389 /* lower section is for dragging width */
391 double pos = position_control->get_value (); /* 0..1 */
392 double swidth = width_control->get_value (); /* -1..+1 */
393 double fswidth = fabs (swidth);
394 int usable_width = get_width() - lr_box_size;
395 double center = (lr_box_size/2.0) + (usable_width * pos);
396 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
397 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
398 const int half_box = lr_box_size/2;
400 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
402 dragging_right = true;
404 dragging_left = true;
406 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
408 dragging_left = true;
410 dragging_right = true;
413 StartWidthGesture ();
417 _tooltip.target_start_drag ();
424 StereoPanner::on_button_release_event (GdkEventButton* ev)
426 if (PannerInterface::on_button_release_event (ev)) {
430 if (ev->button != 1) {
434 if (_panner_shell->bypassed()) {
438 bool const dp = dragging_position;
441 _tooltip.target_stop_drag ();
442 dragging_position = false;
443 dragging_left = false;
444 dragging_right = false;
445 accumulated_delta = 0;
448 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
452 StopPositionGesture ();
462 StereoPanner::on_scroll_event (GdkEventScroll* ev)
464 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
465 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
466 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
469 if (_panner_shell->bypassed()) {
473 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
476 step = one_degree * 5.0;
479 switch (ev->direction) {
480 case GDK_SCROLL_LEFT:
482 width_control->set_value (wv);
486 position_control->set_value (pv);
488 case GDK_SCROLL_RIGHT:
490 width_control->set_value (wv);
492 case GDK_SCROLL_DOWN:
494 position_control->set_value (pv);
502 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
504 if (_panner_shell->bypassed()) {
511 int usable_width = get_width() - lr_box_size;
512 double delta = (ev->x - last_drag_x) / (double) usable_width;
513 double current_width = width_control->get_value ();
519 if (dragging_left || dragging_right) {
521 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
523 /* change width and position in a way that keeps the
524 * other side in the same place
529 double pv = position_control->get_value();
532 position_control->set_value (pv - delta);
534 position_control->set_value (pv + delta);
538 /* delta is positive, so we're about to
539 increase the width. But we need to increase it
540 by twice the required value so that the
541 other side remains in place when we set
542 the position as well.
544 width_control->set_value (current_width + (delta * 2.0));
546 width_control->set_value (current_width + delta);
553 /* maintain position as invariant as we change the width */
555 /* create a detent close to the center */
557 if (!detented && fabs (current_width) < 0.02) {
560 width_control->set_value (0);
565 accumulated_delta += delta;
567 /* have we pulled far enough to escape ? */
569 if (fabs (accumulated_delta) >= 0.025) {
570 width_control->set_value (current_width + accumulated_delta);
572 accumulated_delta = false;
576 /* width needs to change by 2 * delta because both L & R move */
577 width_control->set_value (current_width + (delta * 2.0));
581 } else if (dragging_position) {
583 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
584 position_control->set_value (pv + delta);
592 StereoPanner::on_key_press_event (GdkEventKey* ev)
594 double one_degree = 1.0/180.0;
595 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
596 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
599 if (_panner_shell->bypassed()) {
603 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
606 step = one_degree * 5.0;
609 /* up/down control width because we consider pan position more "important"
610 (and thus having higher "sense" priority) than width.
613 switch (ev->keyval) {
615 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
616 width_control->set_value (1.0);
618 width_control->set_value (wv + step);
622 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
623 width_control->set_value (-1.0);
625 width_control->set_value (wv - step);
631 position_control->set_value (pv);
635 position_control->set_value (pv);
639 width_control->set_value (0.0);
650 StereoPanner::set_colors ()
652 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
653 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
654 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
655 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
656 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
658 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
659 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
660 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
661 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
662 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
664 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
665 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
666 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
667 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
668 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
672 StereoPanner::color_handler ()
679 StereoPanner::bypass_handler ()
685 StereoPanner::editor ()
687 return new StereoPannerEditor (this);