Fix a couple of minor typos.
[ardour2.git] / gtk2_ardour / stereo_panner.cc
blob7fe204fbfda52e3772d61135971bdd7a3704d60c
1 /*
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.
19 #include <iostream>
20 #include <iomanip>
21 #include <cstring>
22 #include <cmath>
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"
41 #include "utils.h"
43 #include "i18n.h"
45 using namespace std;
46 using namespace Gtk;
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 : _panner (panner)
61 , position_control (_panner->pannable()->pan_azimuth_control)
62 , width_control (_panner->pannable()->pan_width_control)
63 , dragging (false)
64 , dragging_position (false)
65 , dragging_left (false)
66 , dragging_right (false)
67 , drag_start_x (0)
68 , last_drag_x (0)
69 , accumulated_delta (0)
70 , detented (false)
71 , drag_data_window (0)
72 , drag_data_label (0)
73 , position_binder (position_control)
74 , width_binder (width_control)
76 if (!have_colors) {
77 set_colors ();
78 have_colors = true;
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 set_flags (Gtk::CAN_FOCUS);
86 add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
87 Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
88 Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
89 Gdk::SCROLL_MASK|
90 Gdk::POINTER_MOTION_MASK);
92 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
95 StereoPanner::~StereoPanner ()
97 delete drag_data_window;
100 void
101 StereoPanner::set_drag_data ()
103 if (!drag_data_label) {
104 return;
107 double pos = position_control->get_value(); // 0..1
109 /* We show the position of the center of the image relative to the left & right.
110 This is expressed as a pair of percentage values that ranges from (100,0)
111 (hard left) through (50,50) (hard center) to (0,100) (hard right).
113 This is pretty wierd, but its the way audio engineers expect it. Just remember that
114 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
117 char buf[64];
118 snprintf (buf, sizeof (buf), "L:%3d R:%3d Width:%d%%", (int) rint (100.0 * (1.0 - pos)),
119 (int) rint (100.0 * pos),
120 (int) floor (100.0 * width_control->get_value()));
121 drag_data_label->set_markup (buf);
124 void
125 StereoPanner::value_change ()
127 set_drag_data ();
128 queue_draw ();
131 bool
132 StereoPanner::on_expose_event (GdkEventExpose* ev)
134 Glib::RefPtr<Gdk::Window> win (get_window());
135 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
136 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
138 int width, height;
139 double pos = position_control->get_value (); /* 0..1 */
140 double swidth = width_control->get_value (); /* -1..+1 */
141 double fswidth = fabs (swidth);
142 uint32_t o, f, t, b, r;
143 State state;
144 const double corner_radius = 5.0;
146 width = get_width();
147 height = get_height ();
149 if (swidth == 0.0) {
150 state = Mono;
151 } else if (swidth < 0.0) {
152 state = Inverted;
153 } else {
154 state = Normal;
157 o = colors[state].outline;
158 f = colors[state].fill;
159 t = colors[state].text;
160 b = colors[state].background;
161 r = colors[state].rule;
163 /* background */
165 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
166 rounded_rectangle (context, 0, 0, width, height, corner_radius);
167 context->fill ();
169 /* the usable width is reduced from the real width, because we need space for
170 the two halves of LR boxes that will extend past the actual left/right
171 positions (indicated by the vertical line segment above them).
174 double usable_width = width - lr_box_size;
176 /* compute the centers of the L/R boxes based on the current stereo width */
178 if (fmod (usable_width,2.0) == 0) {
179 /* even width, but we need odd, so that there is an exact center.
180 So, offset cairo by 1, and reduce effective width by 1
182 usable_width -= 1.0;
183 context->translate (1.0, 0.0);
186 double center = (lr_box_size/2.0) + (usable_width * pos);
187 const double pan_spread = (fswidth * usable_width)/2.0;
188 const double half_lr_box = lr_box_size/2.0;
189 int left;
190 int right;
192 left = center - pan_spread; // center of left box
193 right = center + pan_spread; // center of right box
195 /* center line */
197 context->set_line_width (1.0);
198 context->move_to ((usable_width + lr_box_size)/2.0, 0);
199 context->rel_line_to (0, height);
200 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
201 context->stroke ();
203 /* compute & draw the line through the box */
205 context->set_line_width (2);
206 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
207 context->move_to (left, top_step+(pos_box_size/2.0)+step_down);
208 context->line_to (left, top_step+(pos_box_size/2.0));
209 context->line_to (right, top_step+(pos_box_size/2.0));
210 context->line_to (right, top_step+(pos_box_size/2.0) + step_down);
211 context->stroke ();
213 /* left box */
215 rounded_rectangle (context, left - half_lr_box,
216 half_lr_box+step_down,
217 lr_box_size, lr_box_size, corner_radius);
218 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
219 context->stroke_preserve ();
220 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
221 context->fill ();
223 /* add text */
225 context->move_to (left - half_lr_box + 3,
226 (lr_box_size/2) + step_down + 13);
227 context->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD);
229 if (state != Mono) {
230 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
231 if (swidth < 0.0) {
232 context->show_text (_("R"));
233 } else {
234 context->show_text (_("L"));
238 /* right box */
240 rounded_rectangle (context, right - half_lr_box,
241 half_lr_box+step_down,
242 lr_box_size, lr_box_size, corner_radius);
243 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
244 context->stroke_preserve ();
245 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
246 context->fill ();
248 /* add text */
250 context->move_to (right - half_lr_box + 3, (lr_box_size/2)+step_down + 13);
251 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
253 if (state == Mono) {
254 context->show_text (_("M"));
255 } else {
256 if (swidth < 0.0) {
257 context->show_text (_("L"));
258 } else {
259 context->show_text (_("R"));
263 /* draw the central box */
265 context->set_line_width (2.0);
266 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
267 context->rel_line_to (0.0, pos_box_size); /* lower right */
268 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
269 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
270 context->rel_line_to (0.0, -pos_box_size); /* upper left */
271 context->close_path ();
273 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
274 context->stroke_preserve ();
275 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
276 context->fill ();
278 return true;
281 bool
282 StereoPanner::on_button_press_event (GdkEventButton* ev)
284 drag_start_x = ev->x;
285 last_drag_x = ev->x;
287 dragging_position = false;
288 dragging_left = false;
289 dragging_right = false;
290 dragging = false;
291 accumulated_delta = 0;
292 detented = false;
294 /* Let the binding proxies get first crack at the press event
297 if (ev->y < 20) {
298 if (position_binder.button_press_handler (ev)) {
299 return true;
301 } else {
302 if (width_binder.button_press_handler (ev)) {
303 return true;
307 if (ev->button != 1) {
308 return false;
311 if (ev->type == GDK_2BUTTON_PRESS) {
312 int width = get_width();
314 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
315 /* handled by button release */
316 return true;
319 if (ev->y < 20) {
321 /* upper section: adjusts position, constrained by width */
323 const double w = fabs (width_control->get_value ());
324 const double max_pos = 1.0 - (w/2.0);
325 const double min_pos = w/2.0;
327 if (ev->x <= width/3) {
328 /* left side dbl click */
329 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
330 /* 2ndary-double click on left, collapse to hard left */
331 width_control->set_value (0);
332 position_control->set_value (0);
333 } else {
334 position_control->set_value (min_pos);
336 } else if (ev->x > 2*width/3) {
337 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
338 /* 2ndary-double click on right, collapse to hard right */
339 width_control->set_value (0);
340 position_control->set_value (1.0);
341 } else {
342 position_control->set_value (max_pos);
344 } else {
345 position_control->set_value (0.5);
348 } else {
350 /* lower section: adjusts width, constrained by position */
352 const double p = position_control->get_value ();
353 const double max_width = 2.0 * min ((1.0 - p), p);
355 if (ev->x <= width/3) {
356 /* left side dbl click */
357 width_control->set_value (max_width); // reset width to 100%
358 } else if (ev->x > 2*width/3) {
359 /* right side dbl click */
360 width_control->set_value (-max_width); // reset width to inverted 100%
361 } else {
362 /* center dbl click */
363 width_control->set_value (0); // collapse width to 0%
367 dragging = false;
369 } else if (ev->type == GDK_BUTTON_PRESS) {
371 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
372 /* handled by button release */
373 return true;
376 if (ev->y < 20) {
377 /* top section of widget is for position drags */
378 dragging_position = true;
379 StartPositionGesture ();
380 } else {
381 /* lower section is for dragging width */
383 double pos = position_control->get_value (); /* 0..1 */
384 double swidth = width_control->get_value (); /* -1..+1 */
385 double fswidth = fabs (swidth);
386 int usable_width = get_width() - lr_box_size;
387 double center = (lr_box_size/2.0) + (usable_width * pos);
388 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
389 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
390 const int half_box = lr_box_size/2;
392 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
393 if (swidth < 0.0) {
394 dragging_right = true;
395 } else {
396 dragging_left = true;
398 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
399 if (swidth < 0.0) {
400 dragging_left = true;
401 } else {
402 dragging_right = true;
405 StartWidthGesture ();
408 dragging = true;
411 return true;
414 bool
415 StereoPanner::on_button_release_event (GdkEventButton* ev)
417 if (ev->button != 1) {
418 return false;
421 bool dp = dragging_position;
423 dragging = false;
424 dragging_position = false;
425 dragging_left = false;
426 dragging_right = false;
427 accumulated_delta = 0;
428 detented = false;
430 if (drag_data_window) {
431 drag_data_window->hide ();
434 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
435 /* reset to default */
436 position_control->set_value (0.5);
437 width_control->set_value (1.0);
438 } else {
439 if (dp) {
440 StopPositionGesture ();
441 } else {
442 StopWidthGesture ();
446 return true;
449 bool
450 StereoPanner::on_scroll_event (GdkEventScroll* ev)
452 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
453 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
454 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
455 double step;
457 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
458 step = one_degree;
459 } else {
460 step = one_degree * 5.0;
463 switch (ev->direction) {
464 case GDK_SCROLL_LEFT:
465 wv += step;
466 width_control->set_value (wv);
467 break;
468 case GDK_SCROLL_UP:
469 pv -= step;
470 position_control->set_value (pv);
471 break;
472 case GDK_SCROLL_RIGHT:
473 wv -= step;
474 width_control->set_value (wv);
475 break;
476 case GDK_SCROLL_DOWN:
477 pv += step;
478 position_control->set_value (pv);
479 break;
482 return true;
485 bool
486 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
488 if (!dragging) {
489 return false;
492 if (!drag_data_window) {
493 drag_data_window = new Window (WINDOW_POPUP);
494 drag_data_window->set_name (X_("ContrastingPopup"));
495 drag_data_window->set_position (WIN_POS_MOUSE);
496 drag_data_window->set_decorated (false);
498 drag_data_label = manage (new Label);
499 drag_data_label->set_use_markup (true);
501 drag_data_window->set_border_width (6);
502 drag_data_window->add (*drag_data_label);
503 drag_data_label->show ();
505 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
506 if (toplevel) {
507 drag_data_window->set_transient_for (*toplevel);
511 if (!drag_data_window->is_visible ()) {
512 /* move the popup window vertically down from the panner display */
513 int rx, ry;
514 get_window()->get_origin (rx, ry);
515 drag_data_window->move (rx, ry+get_height());
516 drag_data_window->present ();
519 int w = get_width();
520 double delta = (ev->x - last_drag_x) / (double) w;
521 double current_width = width_control->get_value ();
523 if (dragging_left) {
524 delta = -delta;
527 if (dragging_left || dragging_right) {
529 /* maintain position as invariant as we change the width */
532 /* create a detent close to the center */
534 if (!detented && fabs (current_width) < 0.02) {
535 detented = true;
536 /* snap to zero */
537 width_control->set_value (0);
540 if (detented) {
542 accumulated_delta += delta;
544 /* have we pulled far enough to escape ? */
546 if (fabs (accumulated_delta) >= 0.025) {
547 width_control->set_value (current_width + accumulated_delta);
548 detented = false;
549 accumulated_delta = false;
552 } else {
553 width_control->set_value (current_width + delta);
556 } else if (dragging_position) {
558 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
559 position_control->set_value (pv + delta);
562 last_drag_x = ev->x;
563 return true;
566 bool
567 StereoPanner::on_key_press_event (GdkEventKey* ev)
569 double one_degree = 1.0/180.0;
570 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
571 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
572 double step;
574 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
575 step = one_degree;
576 } else {
577 step = one_degree * 5.0;
580 /* up/down control width because we consider pan position more "important"
581 (and thus having higher "sense" priority) than width.
584 switch (ev->keyval) {
585 case GDK_Up:
586 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
587 width_control->set_value (1.0);
588 } else {
589 width_control->set_value (wv + step);
591 break;
592 case GDK_Down:
593 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
594 width_control->set_value (-1.0);
595 } else {
596 width_control->set_value (wv - step);
599 case GDK_Left:
600 pv -= step;
601 position_control->set_value (pv);
602 break;
603 case GDK_Right:
604 pv += step;
605 position_control->set_value (pv);
606 break;
608 break;
609 case GDK_0:
610 case GDK_KP_0:
611 width_control->set_value (0.0);
612 break;
614 default:
615 return false;
618 return true;
621 bool
622 StereoPanner::on_key_release_event (GdkEventKey* ev)
624 return false;
627 bool
628 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
630 grab_focus ();
631 Keyboard::magic_widget_grab_focus ();
632 return false;
635 bool
636 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
638 Keyboard::magic_widget_drop_focus ();
639 return false;
642 void
643 StereoPanner::set_colors ()
645 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
646 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
647 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
648 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
649 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
651 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
652 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
653 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
654 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
655 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
657 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
658 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
659 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
660 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
661 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
664 void
665 StereoPanner::color_handler ()
667 set_colors ();
668 queue_draw ();