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 rounded_rectangle (context, 0, 0, width, height, corner_radius);
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 accumulated_delta = 0;
281 /* Let the binding proxies get first crack at the press event
285 if (position_binder.button_press_handler (ev)) {
289 if (width_binder.button_press_handler (ev)) {
294 if (ev->button != 1) {
298 if (ev->type == GDK_2BUTTON_PRESS) {
299 int width = get_width();
301 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
302 /* handled by button release */
308 /* upper section: adjusts position, constrained by width */
310 const double w = fabs (width_control->get_value ());
311 const double max_pos = 1.0 - (w/2.0);
312 const double min_pos = w/2.0;
314 if (ev->x <= width/3) {
315 /* left side dbl click */
316 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
317 /* 2ndary-double click on left, collapse to hard left */
318 width_control->set_value (0);
319 position_control->set_value (0);
321 position_control->set_value (min_pos);
323 } else if (ev->x > 2*width/3) {
324 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
325 /* 2ndary-double click on right, collapse to hard right */
326 width_control->set_value (0);
327 position_control->set_value (1.0);
329 position_control->set_value (max_pos);
332 position_control->set_value (0.5);
337 /* lower section: adjusts width, constrained by position */
339 const double p = position_control->get_value ();
340 const double max_width = 2.0 * min ((1.0 - p), p);
342 if (ev->x <= width/3) {
343 /* left side dbl click */
344 width_control->set_value (max_width); // reset width to 100%
345 } else if (ev->x > 2*width/3) {
346 /* right side dbl click */
347 width_control->set_value (-max_width); // reset width to inverted 100%
349 /* center dbl click */
350 width_control->set_value (0); // collapse width to 0%
356 } else if (ev->type == GDK_BUTTON_PRESS) {
358 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
359 /* handled by button release */
364 /* top section of widget is for position drags */
365 dragging_position = true;
366 StartPositionGesture ();
368 /* lower section is for dragging width */
370 double pos = position_control->get_value (); /* 0..1 */
371 double swidth = width_control->get_value (); /* -1..+1 */
372 double fswidth = fabs (swidth);
373 int usable_width = get_width() - lr_box_size;
374 double center = (lr_box_size/2.0) + (usable_width * pos);
375 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
376 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
377 const int half_box = lr_box_size/2;
379 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
381 dragging_right = true;
383 dragging_left = true;
385 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
387 dragging_left = true;
389 dragging_right = true;
392 StartWidthGesture ();
396 _tooltip.target_start_drag ();
403 StereoPanner::on_button_release_event (GdkEventButton* ev)
405 if (PannerInterface::on_button_release_event (ev)) {
409 if (ev->button != 1) {
413 bool const dp = dragging_position;
416 _tooltip.target_stop_drag ();
417 dragging_position = false;
418 dragging_left = false;
419 dragging_right = false;
420 accumulated_delta = 0;
423 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
427 StopPositionGesture ();
437 StereoPanner::on_scroll_event (GdkEventScroll* ev)
439 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
440 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
441 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
444 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
447 step = one_degree * 5.0;
450 switch (ev->direction) {
451 case GDK_SCROLL_LEFT:
453 width_control->set_value (wv);
457 position_control->set_value (pv);
459 case GDK_SCROLL_RIGHT:
461 width_control->set_value (wv);
463 case GDK_SCROLL_DOWN:
465 position_control->set_value (pv);
473 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
479 int usable_width = get_width() - lr_box_size;
480 double delta = (ev->x - last_drag_x) / (double) usable_width;
481 double current_width = width_control->get_value ();
487 if (dragging_left || dragging_right) {
489 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
491 /* change width and position in a way that keeps the
492 * other side in the same place
497 double pv = position_control->get_value();
500 position_control->set_value (pv - delta);
502 position_control->set_value (pv + delta);
506 /* delta is positive, so we're about to
507 increase the width. But we need to increase it
508 by twice the required value so that the
509 other side remains in place when we set
510 the position as well.
512 width_control->set_value (current_width + (delta * 2.0));
514 width_control->set_value (current_width + delta);
521 /* maintain position as invariant as we change the width */
523 /* create a detent close to the center */
525 if (!detented && fabs (current_width) < 0.02) {
528 width_control->set_value (0);
533 accumulated_delta += delta;
535 /* have we pulled far enough to escape ? */
537 if (fabs (accumulated_delta) >= 0.025) {
538 width_control->set_value (current_width + accumulated_delta);
540 accumulated_delta = false;
544 /* width needs to change by 2 * delta because both L & R move */
545 width_control->set_value (current_width + (delta * 2.0));
549 } else if (dragging_position) {
551 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
552 position_control->set_value (pv + delta);
560 StereoPanner::on_key_press_event (GdkEventKey* ev)
562 double one_degree = 1.0/180.0;
563 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
564 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
567 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
570 step = one_degree * 5.0;
573 /* up/down control width because we consider pan position more "important"
574 (and thus having higher "sense" priority) than width.
577 switch (ev->keyval) {
579 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
580 width_control->set_value (1.0);
582 width_control->set_value (wv + step);
586 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
587 width_control->set_value (-1.0);
589 width_control->set_value (wv - step);
595 position_control->set_value (pv);
599 position_control->set_value (pv);
603 width_control->set_value (0.0);
614 StereoPanner::set_colors ()
616 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
617 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
618 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
619 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
620 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
622 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
623 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
624 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
625 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
626 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
628 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
629 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
630 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
631 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
632 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
636 StereoPanner::color_handler ()
643 StereoPanner::editor ()
645 return new StereoPannerEditor (this);