rework panning -- Squashed commit of the following:
[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 #include "gtkmm2ext/persistent_tooltip.h"
34
35 #include "ardour/pannable.h"
36 #include "ardour/panner.h"
37 #include "ardour/panner_shell.h"
38
39 #include "ardour_ui.h"
40 #include "global_signals.h"
41 #include "stereo_panner.h"
42 #include "stereo_panner_editor.h"
43 #include "rgb_macros.h"
44 #include "utils.h"
45
46 #include "i18n.h"
47
48 using namespace std;
49 using namespace Gtk;
50 using namespace Gtkmm2ext;
51
52 static const int pos_box_size = 8;
53 static const int lr_box_size = 15;
54 static const int step_down = 10;
55 static const int top_step = 2;
56
57 StereoPanner::ColorScheme StereoPanner::colors[3];
58 bool StereoPanner::have_colors = false;
59
60 using namespace ARDOUR;
61
62 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
63         : PannerInterface (p->panner())
64         , _panner_shell (p)
65         , position_control (_panner->pannable()->pan_azimuth_control)
66         , width_control (_panner->pannable()->pan_width_control)
67         , dragging_position (false)
68         , dragging_left (false)
69         , dragging_right (false)
70         , drag_start_x (0)
71         , last_drag_x (0)
72         , accumulated_delta (0)
73         , detented (false)
74         , position_binder (position_control)
75         , width_binder (width_control)
76         , _dragging (false)
77 {
78         if (!have_colors) {
79                 set_colors ();
80                 have_colors = true;
81         }
82
83         position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
84         width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
85         _panner_shell->Changed.connect (connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
86
87         ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
88
89         set_tooltip ();
90 }
91
92 StereoPanner::~StereoPanner ()
93 {
94
95 }
96
97 void
98 StereoPanner::set_tooltip ()
99 {
100         if (_panner_shell->bypassed()) {
101                 _tooltip.set_tip (_("bypassed"));
102                 return;
103         }
104         double pos = position_control->get_value(); // 0..1
105
106         /* We show the position of the center of the image relative to the left & right.
107            This is expressed as a pair of percentage values that ranges from (100,0)
108            (hard left) through (50,50) (hard center) to (0,100) (hard right).
109
110            This is pretty wierd, but its the way audio engineers expect it. Just remember that
111            the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
112         */
113
114         char buf[64];
115         snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
116                   (int) rint (100.0 * pos),
117                   (int) floor (100.0 * width_control->get_value()));
118         _tooltip.set_tip (buf);
119 }
120
121 bool
122 StereoPanner::on_expose_event (GdkEventExpose*)
123 {
124         Glib::RefPtr<Gdk::Window> win (get_window());
125         Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
126         Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
127
128         int width, height;
129         double pos = position_control->get_value (); /* 0..1 */
130         double swidth = width_control->get_value (); /* -1..+1 */
131         double fswidth = fabs (swidth);
132         uint32_t o, f, t, b, r;
133         State state;
134         const double corner_radius = 5.0;
135
136         width = get_width();
137         height = get_height ();
138
139         if (swidth == 0.0) {
140                 state = Mono;
141         } else if (swidth < 0.0) {
142                 state = Inverted;
143         } else {
144                 state = Normal;
145         }
146
147         o = colors[state].outline;
148         f = colors[state].fill;
149         t = colors[state].text;
150         b = colors[state].background;
151         r = colors[state].rule;
152
153         /* background */
154
155         if (!_panner_shell->bypassed()) {
156                 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
157         } else {
158                 context->set_source_rgba (0.1, 0.1, 0.1, 0.2);
159         }
160         cairo_rectangle (context->cobj(), 0, 0, width, height);
161         context->fill ();
162
163         /* the usable width is reduced from the real width, because we need space for
164            the two halves of LR boxes that will extend past the actual left/right
165            positions (indicated by the vertical line segment above them).
166         */
167
168         double usable_width = width - lr_box_size;
169
170         /* compute the centers of the L/R boxes based on the current stereo width */
171
172         if (fmod (usable_width,2.0) == 0) {
173                 /* even width, but we need odd, so that there is an exact center.
174                    So, offset cairo by 1, and reduce effective width by 1
175                 */
176                 usable_width -= 1.0;
177                 context->translate (1.0, 0.0);
178         }
179
180         double center = (lr_box_size/2.0) + (usable_width * pos);
181         const double pan_spread = (fswidth * usable_width)/2.0;
182         const double half_lr_box = lr_box_size/2.0;
183         int left;
184         int right;
185
186         left = center - pan_spread;  // center of left box
187         right = center + pan_spread; // center of right box
188
189         /* center line */
190
191         context->set_line_width (1.0);
192         context->move_to ((usable_width + lr_box_size)/2.0, 0);
193         context->rel_line_to (0, height);
194         context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
195         context->stroke ();
196
197         if (_panner_shell->bypassed()) {
198                 return true;
199         }
200
201         /* compute & draw the line through the box */
202
203         context->set_line_width (2);
204         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
205         context->move_to (left, top_step+(pos_box_size/2.0)+step_down);
206         context->line_to (left, top_step+(pos_box_size/2.0));
207         context->line_to (right, top_step+(pos_box_size/2.0));
208         context->line_to (right, top_step+(pos_box_size/2.0) + step_down);
209         context->stroke ();
210
211         /* left box */
212
213         rounded_rectangle (context, left - half_lr_box,
214                            half_lr_box+step_down,
215                            lr_box_size, lr_box_size, corner_radius);
216         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
217         context->stroke_preserve ();
218         context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
219         context->fill ();
220
221         /* add text */
222
223         context->move_to (left - half_lr_box + 3,
224                           (lr_box_size/2) + step_down + 13);
225         context->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD);
226
227         if (state != Mono) {
228                 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
229                 if (swidth < 0.0) {
230                         context->show_text (_("R"));
231                 } else {
232                         context->show_text (_("L"));
233                 }
234         }
235
236         /* right box */
237
238         rounded_rectangle (context, right - half_lr_box,
239                            half_lr_box+step_down,
240                            lr_box_size, lr_box_size, corner_radius);
241         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
242         context->stroke_preserve ();
243         context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
244         context->fill ();
245
246         /* add text */
247
248         context->move_to (right - half_lr_box + 3, (lr_box_size/2)+step_down + 13);
249         context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
250
251         if (state == Mono) {
252                 context->show_text (_("M"));
253         } else {
254                 if (swidth < 0.0) {
255                         context->show_text (_("L"));
256                 } else {
257                         context->show_text (_("R"));
258                 }
259         }
260
261         /* draw the central box */
262
263         context->set_line_width (2.0);
264         context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
265         context->rel_line_to (0.0, pos_box_size); /* lower right */
266         context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
267         context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
268         context->rel_line_to (0.0, -pos_box_size); /* upper left */
269         context->close_path ();
270
271         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
272         context->stroke_preserve ();
273         context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
274         context->fill ();
275
276         return true;
277 }
278
279 bool
280 StereoPanner::on_button_press_event (GdkEventButton* ev)
281 {
282         if (PannerInterface::on_button_press_event (ev)) {
283                 return true;
284         }
285
286         if (_panner_shell->bypassed()) {
287                 return true;
288         }
289         
290         drag_start_x = ev->x;
291         last_drag_x = ev->x;
292
293         dragging_position = false;
294         dragging_left = false;
295         dragging_right = false;
296         _dragging = false;
297         _tooltip.target_stop_drag ();
298         accumulated_delta = 0;
299         detented = false;
300
301         /* Let the binding proxies get first crack at the press event
302          */
303
304         if (ev->y < 20) {
305                 if (position_binder.button_press_handler (ev)) {
306                         return true;
307                 }
308         } else {
309                 if (width_binder.button_press_handler (ev)) {
310                         return true;
311                 }
312         }
313
314         if (ev->button != 1) {
315                 return false;
316         }
317
318         if (ev->type == GDK_2BUTTON_PRESS) {
319                 int width = get_width();
320
321                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
322                         /* handled by button release */
323                         return true;
324                 }
325
326                 if (ev->y < 20) {
327
328                         /* upper section: adjusts position, constrained by width */
329
330                         const double w = fabs (width_control->get_value ());
331                         const double max_pos = 1.0 - (w/2.0);
332                         const double min_pos = w/2.0;
333
334                         if (ev->x <= width/3) {
335                                 /* left side dbl click */
336                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
337                                         /* 2ndary-double click on left, collapse to hard left */
338                                         width_control->set_value (0);
339                                         position_control->set_value (0);
340                                 } else {
341                                         position_control->set_value (min_pos);
342                                 }
343                         } else if (ev->x > 2*width/3) {
344                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
345                                         /* 2ndary-double click on right, collapse to hard right */
346                                         width_control->set_value (0);
347                                         position_control->set_value (1.0);
348                                 } else {
349                                         position_control->set_value (max_pos);
350                                 }
351                         } else {
352                                 position_control->set_value (0.5);
353                         }
354
355                 } else {
356
357                         /* lower section: adjusts width, constrained by position */
358
359                         const double p = position_control->get_value ();
360                         const double max_width = 2.0 * min ((1.0 - p), p);
361
362                         if (ev->x <= width/3) {
363                                 /* left side dbl click */
364                                 width_control->set_value (max_width); // reset width to 100%
365                         } else if (ev->x > 2*width/3) {
366                                 /* right side dbl click */
367                                 width_control->set_value (-max_width); // reset width to inverted 100%
368                         } else {
369                                 /* center dbl click */
370                                 width_control->set_value (0); // collapse width to 0%
371                         }
372                 }
373
374                 _dragging = false;
375                 _tooltip.target_stop_drag ();
376
377         } else if (ev->type == GDK_BUTTON_PRESS) {
378
379                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
380                         /* handled by button release */
381                         return true;
382                 }
383
384                 if (ev->y < 20) {
385                         /* top section of widget is for position drags */
386                         dragging_position = true;
387                         StartPositionGesture ();
388                 } else {
389                         /* lower section is for dragging width */
390
391                         double pos = position_control->get_value (); /* 0..1 */
392                         double swidth = width_control->get_value (); /* -1..+1 */
393                         double fswidth = fabs (swidth);
394                         int usable_width = get_width() - lr_box_size;
395                         double center = (lr_box_size/2.0) + (usable_width * pos);
396                         int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
397                         int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
398                         const int half_box = lr_box_size/2;
399
400                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
401                                 if (swidth < 0.0) {
402                                         dragging_right = true;
403                                 } else {
404                                         dragging_left = true;
405                                 }
406                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
407                                 if (swidth < 0.0) {
408                                         dragging_left = true;
409                                 } else {
410                                         dragging_right = true;
411                                 }
412                         }
413                         StartWidthGesture ();
414                 }
415
416                 _dragging = true;
417                 _tooltip.target_start_drag ();
418         }
419
420         return true;
421 }
422
423 bool
424 StereoPanner::on_button_release_event (GdkEventButton* ev)
425 {
426         if (PannerInterface::on_button_release_event (ev)) {
427                 return true;
428         }
429         
430         if (ev->button != 1) {
431                 return false;
432         }
433
434         if (_panner_shell->bypassed()) {
435                 return false;
436         }
437
438         bool const dp = dragging_position;
439
440         _dragging = false;
441         _tooltip.target_stop_drag ();
442         dragging_position = false;
443         dragging_left = false;
444         dragging_right = false;
445         accumulated_delta = 0;
446         detented = false;
447
448         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
449                 _panner->reset ();
450         } else {
451                 if (dp) {
452                         StopPositionGesture ();
453                 } else {
454                         StopWidthGesture ();
455                 }
456         }
457
458         return true;
459 }
460
461 bool
462 StereoPanner::on_scroll_event (GdkEventScroll* ev)
463 {
464         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
465         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
466         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
467         double step;
468
469         if (_panner_shell->bypassed()) {
470                 return false;
471         }
472
473         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
474                 step = one_degree;
475         } else {
476                 step = one_degree * 5.0;
477         }
478
479         switch (ev->direction) {
480         case GDK_SCROLL_LEFT:
481                 wv += step;
482                 width_control->set_value (wv);
483                 break;
484         case GDK_SCROLL_UP:
485                 pv -= step;
486                 position_control->set_value (pv);
487                 break;
488         case GDK_SCROLL_RIGHT:
489                 wv -= step;
490                 width_control->set_value (wv);
491                 break;
492         case GDK_SCROLL_DOWN:
493                 pv += step;
494                 position_control->set_value (pv);
495                 break;
496         }
497
498         return true;
499 }
500
501 bool
502 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
503 {
504         if (_panner_shell->bypassed()) {
505                 _dragging = false;
506         }
507         if (!_dragging) {
508                 return false;
509         }
510
511         int usable_width = get_width() - lr_box_size;
512         double delta = (ev->x - last_drag_x) / (double) usable_width;
513         double current_width = width_control->get_value ();
514
515         if (dragging_left) {
516                 delta = -delta;
517         }
518         
519         if (dragging_left || dragging_right) {
520
521                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
522
523                         /* change width and position in a way that keeps the
524                          * other side in the same place
525                          */
526
527                         _panner->freeze ();
528                         
529                         double pv = position_control->get_value();
530
531                         if (dragging_left) {
532                                 position_control->set_value (pv - delta);
533                         } else {
534                                 position_control->set_value (pv + delta);
535                         }
536
537                         if (delta > 0.0) {
538                                 /* delta is positive, so we're about to
539                                    increase the width. But we need to increase it
540                                    by twice the required value so that the
541                                    other side remains in place when we set
542                                    the position as well.
543                                 */
544                                 width_control->set_value (current_width + (delta * 2.0));
545                         } else {
546                                 width_control->set_value (current_width + delta);
547                         }
548
549                         _panner->thaw ();
550
551                 } else {
552
553                         /* maintain position as invariant as we change the width */
554                         
555                         /* create a detent close to the center */
556                         
557                         if (!detented && fabs (current_width) < 0.02) {
558                                 detented = true;
559                                 /* snap to zero */
560                                 width_control->set_value (0);
561                         }
562                         
563                         if (detented) {
564                                 
565                                 accumulated_delta += delta;
566                                 
567                                 /* have we pulled far enough to escape ? */
568                                 
569                                 if (fabs (accumulated_delta) >= 0.025) {
570                                         width_control->set_value (current_width + accumulated_delta);
571                                         detented = false;
572                                         accumulated_delta = false;
573                                 }
574                                 
575                         } else {
576                                 /* width needs to change by 2 * delta because both L & R move */
577                                 width_control->set_value (current_width + (delta * 2.0));
578                         }
579                 }
580
581         } else if (dragging_position) {
582
583                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
584                 position_control->set_value (pv + delta);
585         }
586
587         last_drag_x = ev->x;
588         return true;
589 }
590
591 bool
592 StereoPanner::on_key_press_event (GdkEventKey* ev)
593 {
594         double one_degree = 1.0/180.0;
595         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
596         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
597         double step;
598
599         if (_panner_shell->bypassed()) {
600                 return false;
601         }
602
603         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
604                 step = one_degree;
605         } else {
606                 step = one_degree * 5.0;
607         }
608
609         /* up/down control width because we consider pan position more "important"
610            (and thus having higher "sense" priority) than width.
611         */
612
613         switch (ev->keyval) {
614         case GDK_Up:
615                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
616                         width_control->set_value (1.0);
617                 } else {
618                         width_control->set_value (wv + step);
619                 }
620                 break;
621         case GDK_Down:
622                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
623                         width_control->set_value (-1.0);
624                 } else {
625                         width_control->set_value (wv - step);
626                 }
627                 break;
628
629         case GDK_Left:
630                 pv -= step;
631                 position_control->set_value (pv);
632                 break;
633         case GDK_Right:
634                 pv += step;
635                 position_control->set_value (pv);
636                 break;
637         case GDK_0:
638         case GDK_KP_0:
639                 width_control->set_value (0.0);
640                 break;
641
642         default:
643                 return false;
644         }
645
646         return true;
647 }
648
649 void
650 StereoPanner::set_colors ()
651 {
652         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
653         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
654         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
655         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
656         colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
657
658         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
659         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
660         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
661         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
662         colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
663
664         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
665         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
666         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
667         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
668         colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
669 }
670
671 void
672 StereoPanner::color_handler ()
673 {
674         set_colors ();
675         queue_draw ();
676 }
677
678 void
679 StereoPanner::bypass_handler ()
680 {
681         queue_draw ();
682 }
683
684 PannerEditor*
685 StereoPanner::editor ()
686 {
687         return new StereoPannerEditor (this);
688 }