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"
34 #include "ardour/pannable.h"
35 #include "ardour/panner.h"
37 #include "ardour_ui.h"
38 #include "global_signals.h"
39 #include "stereo_panner.h"
40 #include "rgb_macros.h"
47 using namespace Gtkmm2ext;
49 static const int pos_box_size = 8;
50 static const int lr_box_size = 15;
51 static const int step_down = 10;
52 static const int top_step = 2;
54 StereoPanner::ColorScheme StereoPanner::colors[3];
55 bool StereoPanner::have_colors = false;
57 using namespace ARDOUR;
59 StereoPanner::StereoPanner (boost::shared_ptr<Panner> panner)
60 : PannerInterface (panner)
61 , position_control (_panner->pannable()->pan_azimuth_control)
62 , width_control (_panner->pannable()->pan_width_control)
63 , dragging_position (false)
64 , dragging_left (false)
65 , dragging_right (false)
68 , accumulated_delta (0)
70 , position_binder (position_control)
71 , width_binder (width_control)
78 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
79 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
81 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
84 StereoPanner::~StereoPanner ()
90 StereoPanner::set_drag_data ()
92 if (!_drag_data_label) {
96 double pos = position_control->get_value(); // 0..1
98 /* We show the position of the center of the image relative to the left & right.
99 This is expressed as a pair of percentage values that ranges from (100,0)
100 (hard left) through (50,50) (hard center) to (0,100) (hard right).
102 This is pretty wierd, but its the way audio engineers expect it. Just remember that
103 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
107 snprintf (buf, sizeof (buf), "L:%3d R:%3d Width:%d%%", (int) rint (100.0 * (1.0 - pos)),
108 (int) rint (100.0 * pos),
109 (int) floor (100.0 * width_control->get_value()));
110 _drag_data_label->set_markup (buf);
114 StereoPanner::on_expose_event (GdkEventExpose*)
116 Glib::RefPtr<Gdk::Window> win (get_window());
117 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
118 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
121 double pos = position_control->get_value (); /* 0..1 */
122 double swidth = width_control->get_value (); /* -1..+1 */
123 double fswidth = fabs (swidth);
124 uint32_t o, f, t, b, r;
126 const double corner_radius = 5.0;
129 height = get_height ();
133 } else if (swidth < 0.0) {
139 o = colors[state].outline;
140 f = colors[state].fill;
141 t = colors[state].text;
142 b = colors[state].background;
143 r = colors[state].rule;
147 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
148 rounded_rectangle (context, 0, 0, width, height, corner_radius);
151 /* the usable width is reduced from the real width, because we need space for
152 the two halves of LR boxes that will extend past the actual left/right
153 positions (indicated by the vertical line segment above them).
156 double usable_width = width - lr_box_size;
158 /* compute the centers of the L/R boxes based on the current stereo width */
160 if (fmod (usable_width,2.0) == 0) {
161 /* even width, but we need odd, so that there is an exact center.
162 So, offset cairo by 1, and reduce effective width by 1
165 context->translate (1.0, 0.0);
168 double center = (lr_box_size/2.0) + (usable_width * pos);
169 const double pan_spread = (fswidth * usable_width)/2.0;
170 const double half_lr_box = lr_box_size/2.0;
174 left = center - pan_spread; // center of left box
175 right = center + pan_spread; // center of right box
179 context->set_line_width (1.0);
180 context->move_to ((usable_width + lr_box_size)/2.0, 0);
181 context->rel_line_to (0, height);
182 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
185 /* compute & draw the line through the box */
187 context->set_line_width (2);
188 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
189 context->move_to (left, top_step+(pos_box_size/2.0)+step_down);
190 context->line_to (left, top_step+(pos_box_size/2.0));
191 context->line_to (right, top_step+(pos_box_size/2.0));
192 context->line_to (right, top_step+(pos_box_size/2.0) + step_down);
197 rounded_rectangle (context, left - half_lr_box,
198 half_lr_box+step_down,
199 lr_box_size, lr_box_size, corner_radius);
200 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
201 context->stroke_preserve ();
202 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
207 context->move_to (left - half_lr_box + 3,
208 (lr_box_size/2) + step_down + 13);
209 context->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD);
212 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
214 context->show_text (_("R"));
216 context->show_text (_("L"));
222 rounded_rectangle (context, right - half_lr_box,
223 half_lr_box+step_down,
224 lr_box_size, lr_box_size, corner_radius);
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->stroke_preserve ();
227 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
232 context->move_to (right - half_lr_box + 3, (lr_box_size/2)+step_down + 13);
233 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
236 context->show_text (_("M"));
239 context->show_text (_("L"));
241 context->show_text (_("R"));
245 /* draw the central box */
247 context->set_line_width (2.0);
248 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
249 context->rel_line_to (0.0, pos_box_size); /* lower right */
250 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
251 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
252 context->rel_line_to (0.0, -pos_box_size); /* upper left */
253 context->close_path ();
255 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
256 context->stroke_preserve ();
257 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
264 StereoPanner::on_button_press_event (GdkEventButton* ev)
266 drag_start_x = ev->x;
269 dragging_position = false;
270 dragging_left = false;
271 dragging_right = false;
273 accumulated_delta = 0;
276 /* Let the binding proxies get first crack at the press event
280 if (position_binder.button_press_handler (ev)) {
284 if (width_binder.button_press_handler (ev)) {
289 if (ev->button != 1) {
293 if (ev->type == GDK_2BUTTON_PRESS) {
294 int width = get_width();
296 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
297 /* handled by button release */
303 /* upper section: adjusts position, constrained by width */
305 const double w = fabs (width_control->get_value ());
306 const double max_pos = 1.0 - (w/2.0);
307 const double min_pos = w/2.0;
309 if (ev->x <= width/3) {
310 /* left side dbl click */
311 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
312 /* 2ndary-double click on left, collapse to hard left */
313 width_control->set_value (0);
314 position_control->set_value (0);
316 position_control->set_value (min_pos);
318 } else if (ev->x > 2*width/3) {
319 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
320 /* 2ndary-double click on right, collapse to hard right */
321 width_control->set_value (0);
322 position_control->set_value (1.0);
324 position_control->set_value (max_pos);
327 position_control->set_value (0.5);
332 /* lower section: adjusts width, constrained by position */
334 const double p = position_control->get_value ();
335 const double max_width = 2.0 * min ((1.0 - p), p);
337 if (ev->x <= width/3) {
338 /* left side dbl click */
339 width_control->set_value (max_width); // reset width to 100%
340 } else if (ev->x > 2*width/3) {
341 /* right side dbl click */
342 width_control->set_value (-max_width); // reset width to inverted 100%
344 /* center dbl click */
345 width_control->set_value (0); // collapse width to 0%
351 } else if (ev->type == GDK_BUTTON_PRESS) {
353 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
354 /* handled by button release */
358 show_drag_data_window ();
361 /* top section of widget is for position drags */
362 dragging_position = true;
363 StartPositionGesture ();
365 /* lower section is for dragging width */
367 double pos = position_control->get_value (); /* 0..1 */
368 double swidth = width_control->get_value (); /* -1..+1 */
369 double fswidth = fabs (swidth);
370 int usable_width = get_width() - lr_box_size;
371 double center = (lr_box_size/2.0) + (usable_width * pos);
372 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
373 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
374 const int half_box = lr_box_size/2;
376 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
378 dragging_right = true;
380 dragging_left = true;
382 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
384 dragging_left = true;
386 dragging_right = true;
389 StartWidthGesture ();
399 StereoPanner::on_button_release_event (GdkEventButton* ev)
401 if (ev->button != 1) {
405 bool const dp = dragging_position;
408 dragging_position = false;
409 dragging_left = false;
410 dragging_right = false;
411 accumulated_delta = 0;
414 hide_drag_data_window ();
416 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
420 StopPositionGesture ();
430 StereoPanner::on_scroll_event (GdkEventScroll* ev)
432 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
433 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
434 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
437 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
440 step = one_degree * 5.0;
443 switch (ev->direction) {
444 case GDK_SCROLL_LEFT:
446 width_control->set_value (wv);
450 position_control->set_value (pv);
452 case GDK_SCROLL_RIGHT:
454 width_control->set_value (wv);
456 case GDK_SCROLL_DOWN:
458 position_control->set_value (pv);
466 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
472 int usable_width = get_width() - lr_box_size;
473 double delta = (ev->x - last_drag_x) / (double) usable_width;
474 double current_width = width_control->get_value ();
480 if (dragging_left || dragging_right) {
482 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
484 /* change width and position in a way that keeps the
485 * other side in the same place
490 double pv = position_control->get_value();
493 position_control->set_value (pv - delta);
495 position_control->set_value (pv + delta);
499 /* delta is positive, so we're about to
500 increase the width. But we need to increase it
501 by twice the required value so that the
502 other side remains in place when we set
503 the position as well.
505 width_control->set_value (current_width + (delta * 2.0));
507 width_control->set_value (current_width + delta);
514 /* maintain position as invariant as we change the width */
516 /* create a detent close to the center */
518 if (!detented && fabs (current_width) < 0.02) {
521 width_control->set_value (0);
526 accumulated_delta += delta;
528 /* have we pulled far enough to escape ? */
530 if (fabs (accumulated_delta) >= 0.025) {
531 width_control->set_value (current_width + accumulated_delta);
533 accumulated_delta = false;
537 /* width needs to change by 2 * delta because both L & R move */
538 width_control->set_value (current_width + (delta * 2.0));
542 } else if (dragging_position) {
544 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
545 position_control->set_value (pv + delta);
553 StereoPanner::on_key_press_event (GdkEventKey* ev)
555 double one_degree = 1.0/180.0;
556 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
557 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
560 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
563 step = one_degree * 5.0;
566 /* up/down control width because we consider pan position more "important"
567 (and thus having higher "sense" priority) than width.
570 switch (ev->keyval) {
572 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
573 width_control->set_value (1.0);
575 width_control->set_value (wv + step);
579 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
580 width_control->set_value (-1.0);
582 width_control->set_value (wv - step);
588 position_control->set_value (pv);
592 position_control->set_value (pv);
596 width_control->set_value (0.0);
607 StereoPanner::set_colors ()
609 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
610 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
611 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
612 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
613 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
615 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
616 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
617 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
618 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
619 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
621 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
622 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
623 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
624 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
625 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
629 StereoPanner::color_handler ()