7fe204fbfda52e3772d61135971bdd7a3704d60c
[ardour.git] / gtk2_ardour / stereo_panner.cc
1 /*
2   Copyright (C) 2000-2007 Paul Davis
3
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.
8
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.
13
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.
17 */
18
19 #include <iostream>
20 #include <iomanip>
21 #include <cstring>
22 #include <cmath>
23
24 #include <gtkmm/window.h>
25
26 #include "pbd/controllable.h"
27 #include "pbd/compose.h"
28
29 #include "gtkmm2ext/gui_thread.h"
30 #include "gtkmm2ext/gtk_ui.h"
31 #include "gtkmm2ext/keyboard.h"
32 #include "gtkmm2ext/utils.h"
33
34 #include "ardour/pannable.h"
35 #include "ardour/panner.h"
36
37 #include "ardour_ui.h"
38 #include "global_signals.h"
39 #include "stereo_panner.h"
40 #include "rgb_macros.h"
41 #include "utils.h"
42
43 #include "i18n.h"
44
45 using namespace std;
46 using namespace Gtk;
47 using namespace Gtkmm2ext;
48
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;
53
54 StereoPanner::ColorScheme StereoPanner::colors[3];
55 bool StereoPanner::have_colors = false;
56
57 using namespace ARDOUR;
58
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)
75 {
76         if (!have_colors) {
77                 set_colors ();
78                 have_colors = true;
79         }
80
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());
83
84         set_flags (Gtk::CAN_FOCUS);
85
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);
91
92         ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
93 }
94
95 StereoPanner::~StereoPanner ()
96 {
97         delete drag_data_window;
98 }
99
100 void
101 StereoPanner::set_drag_data ()
102 {
103         if (!drag_data_label) {
104                 return;
105         }
106
107         double pos = position_control->get_value(); // 0..1
108
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).
112
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.
115         */
116
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);
122 }
123
124 void
125 StereoPanner::value_change ()
126 {
127         set_drag_data ();
128         queue_draw ();
129 }
130
131 bool
132 StereoPanner::on_expose_event (GdkEventExpose* ev)
133 {
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();
137
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;
145
146         width = get_width();
147         height = get_height ();
148
149         if (swidth == 0.0) {
150                 state = Mono;
151         } else if (swidth < 0.0) {
152                 state = Inverted;
153         } else {
154                 state = Normal;
155         }
156
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;
162
163         /* background */
164
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 ();
168
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).
172         */
173
174         double usable_width = width - lr_box_size;
175
176         /* compute the centers of the L/R boxes based on the current stereo width */
177
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
181                 */
182                 usable_width -= 1.0;
183                 context->translate (1.0, 0.0);
184         }
185
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;
191
192         left = center - pan_spread;  // center of left box
193         right = center + pan_spread; // center of right box
194
195         /* center line */
196
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 ();
202
203         /* compute & draw the line through the box */
204
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 ();
212
213         /* left box */
214
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 ();
222
223         /* add text */
224
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);
228
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"));
235                 }
236         }
237
238         /* right box */
239
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 ();
247
248         /* add text */
249
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));
252
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"));
260                 }
261         }
262
263         /* draw the central box */
264
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 ();
272
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 ();
277
278         return true;
279 }
280
281 bool
282 StereoPanner::on_button_press_event (GdkEventButton* ev)
283 {
284         drag_start_x = ev->x;
285         last_drag_x = ev->x;
286
287         dragging_position = false;
288         dragging_left = false;
289         dragging_right = false;
290         dragging = false;
291         accumulated_delta = 0;
292         detented = false;
293
294         /* Let the binding proxies get first crack at the press event
295          */
296
297         if (ev->y < 20) {
298                 if (position_binder.button_press_handler (ev)) {
299                         return true;
300                 }
301         } else {
302                 if (width_binder.button_press_handler (ev)) {
303                         return true;
304                 }
305         }
306
307         if (ev->button != 1) {
308                 return false;
309         }
310
311         if (ev->type == GDK_2BUTTON_PRESS) {
312                 int width = get_width();
313
314                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
315                         /* handled by button release */
316                         return true;
317                 }
318
319                 if (ev->y < 20) {
320
321                         /* upper section: adjusts position, constrained by width */
322
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;
326
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);
335                                 }
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);
343                                 }
344                         } else {
345                                 position_control->set_value (0.5);
346                         }
347
348                 } else {
349
350                         /* lower section: adjusts width, constrained by position */
351
352                         const double p = position_control->get_value ();
353                         const double max_width = 2.0 * min ((1.0 - p), p);
354
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%
364                         }
365                 }
366
367                 dragging = false;
368
369         } else if (ev->type == GDK_BUTTON_PRESS) {
370
371                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
372                         /* handled by button release */
373                         return true;
374                 }
375
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 */
382
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;
391
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;
397                                 }
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;
403                                 }
404                         }
405                         StartWidthGesture ();
406                 }
407
408                 dragging = true;
409         }
410
411         return true;
412 }
413
414 bool
415 StereoPanner::on_button_release_event (GdkEventButton* ev)
416 {
417         if (ev->button != 1) {
418                 return false;
419         }
420
421         bool dp = dragging_position;
422
423         dragging = false;
424         dragging_position = false;
425         dragging_left = false;
426         dragging_right = false;
427         accumulated_delta = 0;
428         detented = false;
429
430         if (drag_data_window) {
431                 drag_data_window->hide ();
432         }
433
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 ();
443                 }
444         }
445
446         return true;
447 }
448
449 bool
450 StereoPanner::on_scroll_event (GdkEventScroll* ev)
451 {
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;
456
457         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
458                 step = one_degree;
459         } else {
460                 step = one_degree * 5.0;
461         }
462
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;
480         }
481
482         return true;
483 }
484
485 bool
486 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
487 {
488         if (!dragging) {
489                 return false;
490         }
491
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);
497
498                 drag_data_label = manage (new Label);
499                 drag_data_label->set_use_markup (true);
500
501                 drag_data_window->set_border_width (6);
502                 drag_data_window->add (*drag_data_label);
503                 drag_data_label->show ();
504
505                 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
506                 if (toplevel) {
507                         drag_data_window->set_transient_for (*toplevel);
508                 }
509         }
510
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 ();
517         }
518
519         int w = get_width();
520         double delta = (ev->x - last_drag_x) / (double) w;
521         double current_width = width_control->get_value ();
522
523         if (dragging_left) {
524                 delta = -delta;
525         }
526
527         if (dragging_left || dragging_right) {
528
529                 /* maintain position as invariant as we change the width */
530
531
532                 /* create a detent close to the center */
533
534                 if (!detented && fabs (current_width) < 0.02) {
535                         detented = true;
536                         /* snap to zero */
537                         width_control->set_value (0);
538                 }
539
540                 if (detented) {
541
542                         accumulated_delta += delta;
543
544                         /* have we pulled far enough to escape ? */
545
546                         if (fabs (accumulated_delta) >= 0.025) {
547                                 width_control->set_value (current_width + accumulated_delta);
548                                 detented = false;
549                                 accumulated_delta = false;
550                         }
551
552                 } else {
553                         width_control->set_value (current_width + delta);
554                 }
555
556         } else if (dragging_position) {
557
558                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
559                 position_control->set_value (pv + delta);
560         }
561
562         last_drag_x = ev->x;
563         return true;
564 }
565
566 bool
567 StereoPanner::on_key_press_event (GdkEventKey* ev)
568 {
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;
573
574         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
575                 step = one_degree;
576         } else {
577                 step = one_degree * 5.0;
578         }
579
580         /* up/down control width because we consider pan position more "important"
581            (and thus having higher "sense" priority) than width.
582         */
583
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);
590                 }
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);
597                 }
598
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;
607
608                 break;
609         case GDK_0:
610         case GDK_KP_0:
611                 width_control->set_value (0.0);
612                 break;
613
614         default:
615                 return false;
616         }
617
618         return true;
619 }
620
621 bool
622 StereoPanner::on_key_release_event (GdkEventKey* ev)
623 {
624         return false;
625 }
626
627 bool
628 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
629 {
630         grab_focus ();
631         Keyboard::magic_widget_grab_focus ();
632         return false;
633 }
634
635 bool
636 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
637 {
638         Keyboard::magic_widget_drop_focus ();
639         return false;
640 }
641
642 void
643 StereoPanner::set_colors ()
644 {
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();
650
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();
656
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();
662 }
663
664 void
665 StereoPanner::color_handler ()
666 {
667         set_colors ();
668         queue_draw ();
669 }