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"
38 #include "ardour_ui.h"
39 #include "global_signals.h"
40 #include "stereo_panner.h"
41 #include "stereo_panner_editor.h"
42 #include "rgb_macros.h"
49 using namespace Gtkmm2ext;
51 static const int pos_box_size = 8;
52 static const int lr_box_size = 15;
53 static const int step_down = 10;
54 static const int top_step = 2;
56 StereoPanner::ColorScheme StereoPanner::colors[3];
57 bool StereoPanner::have_colors = false;
59 using namespace ARDOUR;
61 StereoPanner::StereoPanner (boost::shared_ptr<Panner> panner)
62 : PannerInterface (panner)
63 , position_control (_panner->pannable()->pan_azimuth_control)
64 , width_control (_panner->pannable()->pan_width_control)
65 , dragging_position (false)
66 , dragging_left (false)
67 , dragging_right (false)
70 , accumulated_delta (0)
72 , position_binder (position_control)
73 , width_binder (width_control)
81 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
82 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
84 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
89 StereoPanner::~StereoPanner ()
95 StereoPanner::set_tooltip ()
97 double pos = position_control->get_value(); // 0..1
99 /* We show the position of the center of the image relative to the left & right.
100 This is expressed as a pair of percentage values that ranges from (100,0)
101 (hard left) through (50,50) (hard center) to (0,100) (hard right).
103 This is pretty wierd, but its the way audio engineers expect it. Just remember that
104 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
108 snprintf (buf, sizeof (buf), "L:%3d R:%3d Width:%d%%", (int) rint (100.0 * (1.0 - pos)),
109 (int) rint (100.0 * pos),
110 (int) floor (100.0 * width_control->get_value()));
111 _tooltip.set_tip (buf);
115 StereoPanner::on_expose_event (GdkEventExpose*)
117 Glib::RefPtr<Gdk::Window> win (get_window());
118 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
119 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
122 double pos = position_control->get_value (); /* 0..1 */
123 double swidth = width_control->get_value (); /* -1..+1 */
124 double fswidth = fabs (swidth);
125 uint32_t o, f, t, b, r;
127 const double corner_radius = 5.0;
130 height = get_height ();
134 } else if (swidth < 0.0) {
140 o = colors[state].outline;
141 f = colors[state].fill;
142 t = colors[state].text;
143 b = colors[state].background;
144 r = colors[state].rule;
148 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
149 cairo_rectangle (context->cobj(), 0, 0, width, height);
152 /* the usable width is reduced from the real width, because we need space for
153 the two halves of LR boxes that will extend past the actual left/right
154 positions (indicated by the vertical line segment above them).
157 double usable_width = width - lr_box_size;
159 /* compute the centers of the L/R boxes based on the current stereo width */
161 if (fmod (usable_width,2.0) == 0) {
162 /* even width, but we need odd, so that there is an exact center.
163 So, offset cairo by 1, and reduce effective width by 1
166 context->translate (1.0, 0.0);
169 double center = (lr_box_size/2.0) + (usable_width * pos);
170 const double pan_spread = (fswidth * usable_width)/2.0;
171 const double half_lr_box = lr_box_size/2.0;
175 left = center - pan_spread; // center of left box
176 right = center + pan_spread; // center of right box
180 context->set_line_width (1.0);
181 context->move_to ((usable_width + lr_box_size)/2.0, 0);
182 context->rel_line_to (0, height);
183 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
186 /* compute & draw the line through the box */
188 context->set_line_width (2);
189 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
190 context->move_to (left, top_step+(pos_box_size/2.0)+step_down);
191 context->line_to (left, top_step+(pos_box_size/2.0));
192 context->line_to (right, top_step+(pos_box_size/2.0));
193 context->line_to (right, top_step+(pos_box_size/2.0) + step_down);
198 rounded_rectangle (context, left - half_lr_box,
199 half_lr_box+step_down,
200 lr_box_size, lr_box_size, corner_radius);
201 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
202 context->stroke_preserve ();
203 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
208 context->move_to (left - half_lr_box + 3,
209 (lr_box_size/2) + step_down + 13);
210 context->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD);
213 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
215 context->show_text (_("R"));
217 context->show_text (_("L"));
223 rounded_rectangle (context, right - half_lr_box,
224 half_lr_box+step_down,
225 lr_box_size, lr_box_size, corner_radius);
226 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
227 context->stroke_preserve ();
228 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
233 context->move_to (right - half_lr_box + 3, (lr_box_size/2)+step_down + 13);
234 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
237 context->show_text (_("M"));
240 context->show_text (_("L"));
242 context->show_text (_("R"));
246 /* draw the central box */
248 context->set_line_width (2.0);
249 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
250 context->rel_line_to (0.0, pos_box_size); /* lower right */
251 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
252 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
253 context->rel_line_to (0.0, -pos_box_size); /* upper left */
254 context->close_path ();
256 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
257 context->stroke_preserve ();
258 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
265 StereoPanner::on_button_press_event (GdkEventButton* ev)
267 if (PannerInterface::on_button_press_event (ev)) {
271 drag_start_x = ev->x;
274 dragging_position = false;
275 dragging_left = false;
276 dragging_right = false;
278 _tooltip.target_stop_drag ();
279 accumulated_delta = 0;
282 /* Let the binding proxies get first crack at the press event
286 if (position_binder.button_press_handler (ev)) {
290 if (width_binder.button_press_handler (ev)) {
295 if (ev->button != 1) {
299 if (ev->type == GDK_2BUTTON_PRESS) {
300 int width = get_width();
302 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
303 /* handled by button release */
309 /* upper section: adjusts position, constrained by width */
311 const double w = fabs (width_control->get_value ());
312 const double max_pos = 1.0 - (w/2.0);
313 const double min_pos = w/2.0;
315 if (ev->x <= width/3) {
316 /* left side dbl click */
317 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
318 /* 2ndary-double click on left, collapse to hard left */
319 width_control->set_value (0);
320 position_control->set_value (0);
322 position_control->set_value (min_pos);
324 } else if (ev->x > 2*width/3) {
325 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
326 /* 2ndary-double click on right, collapse to hard right */
327 width_control->set_value (0);
328 position_control->set_value (1.0);
330 position_control->set_value (max_pos);
333 position_control->set_value (0.5);
338 /* lower section: adjusts width, constrained by position */
340 const double p = position_control->get_value ();
341 const double max_width = 2.0 * min ((1.0 - p), p);
343 if (ev->x <= width/3) {
344 /* left side dbl click */
345 width_control->set_value (max_width); // reset width to 100%
346 } else if (ev->x > 2*width/3) {
347 /* right side dbl click */
348 width_control->set_value (-max_width); // reset width to inverted 100%
350 /* center dbl click */
351 width_control->set_value (0); // collapse width to 0%
356 _tooltip.target_stop_drag ();
358 } else if (ev->type == GDK_BUTTON_PRESS) {
360 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
361 /* handled by button release */
366 /* top section of widget is for position drags */
367 dragging_position = true;
368 StartPositionGesture ();
370 /* lower section is for dragging width */
372 double pos = position_control->get_value (); /* 0..1 */
373 double swidth = width_control->get_value (); /* -1..+1 */
374 double fswidth = fabs (swidth);
375 int usable_width = get_width() - lr_box_size;
376 double center = (lr_box_size/2.0) + (usable_width * pos);
377 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
378 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
379 const int half_box = lr_box_size/2;
381 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
383 dragging_right = true;
385 dragging_left = true;
387 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
389 dragging_left = true;
391 dragging_right = true;
394 StartWidthGesture ();
398 _tooltip.target_start_drag ();
405 StereoPanner::on_button_release_event (GdkEventButton* ev)
407 if (PannerInterface::on_button_release_event (ev)) {
411 if (ev->button != 1) {
415 bool const dp = dragging_position;
418 _tooltip.target_stop_drag ();
419 dragging_position = false;
420 dragging_left = false;
421 dragging_right = false;
422 accumulated_delta = 0;
425 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
429 StopPositionGesture ();
439 StereoPanner::on_scroll_event (GdkEventScroll* ev)
441 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
442 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
443 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
446 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
449 step = one_degree * 5.0;
452 switch (ev->direction) {
453 case GDK_SCROLL_LEFT:
455 width_control->set_value (wv);
459 position_control->set_value (pv);
461 case GDK_SCROLL_RIGHT:
463 width_control->set_value (wv);
465 case GDK_SCROLL_DOWN:
467 position_control->set_value (pv);
475 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
481 int usable_width = get_width() - lr_box_size;
482 double delta = (ev->x - last_drag_x) / (double) usable_width;
483 double current_width = width_control->get_value ();
489 if (dragging_left || dragging_right) {
491 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
493 /* change width and position in a way that keeps the
494 * other side in the same place
499 double pv = position_control->get_value();
502 position_control->set_value (pv - delta);
504 position_control->set_value (pv + delta);
508 /* delta is positive, so we're about to
509 increase the width. But we need to increase it
510 by twice the required value so that the
511 other side remains in place when we set
512 the position as well.
514 width_control->set_value (current_width + (delta * 2.0));
516 width_control->set_value (current_width + delta);
523 /* maintain position as invariant as we change the width */
525 /* create a detent close to the center */
527 if (!detented && fabs (current_width) < 0.02) {
530 width_control->set_value (0);
535 accumulated_delta += delta;
537 /* have we pulled far enough to escape ? */
539 if (fabs (accumulated_delta) >= 0.025) {
540 width_control->set_value (current_width + accumulated_delta);
542 accumulated_delta = false;
546 /* width needs to change by 2 * delta because both L & R move */
547 width_control->set_value (current_width + (delta * 2.0));
551 } else if (dragging_position) {
553 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
554 position_control->set_value (pv + delta);
562 StereoPanner::on_key_press_event (GdkEventKey* ev)
564 double one_degree = 1.0/180.0;
565 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
566 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
569 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
572 step = one_degree * 5.0;
575 /* up/down control width because we consider pan position more "important"
576 (and thus having higher "sense" priority) than width.
579 switch (ev->keyval) {
581 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
582 width_control->set_value (1.0);
584 width_control->set_value (wv + step);
588 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
589 width_control->set_value (-1.0);
591 width_control->set_value (wv - step);
597 position_control->set_value (pv);
601 position_control->set_value (pv);
605 width_control->set_value (0.0);
616 StereoPanner::set_colors ()
618 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
619 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
620 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
621 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
622 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
624 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
625 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
626 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
627 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
628 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
630 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
631 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
632 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
633 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
634 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
638 StereoPanner::color_handler ()
645 StereoPanner::editor ()
647 return new StereoPannerEditor (this);