allow alt-drag on stereo panner to move just one side of the stereo field. this wiggl...
[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         : 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)
66         , drag_start_x (0)
67         , last_drag_x (0)
68         , accumulated_delta (0)
69         , detented (false)
70         , position_binder (position_control)
71         , width_binder (width_control)
72 {
73         if (!have_colors) {
74                 set_colors ();
75                 have_colors = true;
76         }
77
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());
80
81         ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
82 }
83
84 StereoPanner::~StereoPanner ()
85 {
86
87 }
88
89 void
90 StereoPanner::set_drag_data ()
91 {
92         if (!_drag_data_label) {
93                 return;
94         }
95
96         double pos = position_control->get_value(); // 0..1
97
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).
101
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.
104         */
105
106         char buf[64];
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);
111 }
112
113 bool
114 StereoPanner::on_expose_event (GdkEventExpose*)
115 {
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();
119
120         int width, height;
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;
125         State state;
126         const double corner_radius = 5.0;
127
128         width = get_width();
129         height = get_height ();
130
131         if (swidth == 0.0) {
132                 state = Mono;
133         } else if (swidth < 0.0) {
134                 state = Inverted;
135         } else {
136                 state = Normal;
137         }
138
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;
144
145         /* background */
146
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);
149         context->fill ();
150
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).
154         */
155
156         double usable_width = width - lr_box_size;
157
158         /* compute the centers of the L/R boxes based on the current stereo width */
159
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
163                 */
164                 usable_width -= 1.0;
165                 context->translate (1.0, 0.0);
166         }
167
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;
171         int left;
172         int right;
173
174         left = center - pan_spread;  // center of left box
175         right = center + pan_spread; // center of right box
176
177         /* center line */
178
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));
183         context->stroke ();
184
185         /* compute & draw the line through the box */
186
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);
193         context->stroke ();
194
195         /* left box */
196
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));
203         context->fill ();
204
205         /* add text */
206
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);
210
211         if (state != Mono) {
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));
213                 if (swidth < 0.0) {
214                         context->show_text (_("R"));
215                 } else {
216                         context->show_text (_("L"));
217                 }
218         }
219
220         /* right box */
221
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));
228         context->fill ();
229
230         /* add text */
231
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));
234
235         if (state == Mono) {
236                 context->show_text (_("M"));
237         } else {
238                 if (swidth < 0.0) {
239                         context->show_text (_("L"));
240                 } else {
241                         context->show_text (_("R"));
242                 }
243         }
244
245         /* draw the central box */
246
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 ();
254
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));
258         context->fill ();
259
260         return true;
261 }
262
263 bool
264 StereoPanner::on_button_press_event (GdkEventButton* ev)
265 {
266         drag_start_x = ev->x;
267         last_drag_x = ev->x;
268
269         dragging_position = false;
270         dragging_left = false;
271         dragging_right = false;
272         _dragging = false;
273         accumulated_delta = 0;
274         detented = false;
275
276         /* Let the binding proxies get first crack at the press event
277          */
278
279         if (ev->y < 20) {
280                 if (position_binder.button_press_handler (ev)) {
281                         return true;
282                 }
283         } else {
284                 if (width_binder.button_press_handler (ev)) {
285                         return true;
286                 }
287         }
288
289         if (ev->button != 1) {
290                 return false;
291         }
292
293         if (ev->type == GDK_2BUTTON_PRESS) {
294                 int width = get_width();
295
296                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
297                         /* handled by button release */
298                         return true;
299                 }
300
301                 if (ev->y < 20) {
302
303                         /* upper section: adjusts position, constrained by width */
304
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;
308
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);
315                                 } else {
316                                         position_control->set_value (min_pos);
317                                 }
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);
323                                 } else {
324                                         position_control->set_value (max_pos);
325                                 }
326                         } else {
327                                 position_control->set_value (0.5);
328                         }
329
330                 } else {
331
332                         /* lower section: adjusts width, constrained by position */
333
334                         const double p = position_control->get_value ();
335                         const double max_width = 2.0 * min ((1.0 - p), p);
336
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%
343                         } else {
344                                 /* center dbl click */
345                                 width_control->set_value (0); // collapse width to 0%
346                         }
347                 }
348
349                 _dragging = false;
350
351         } else if (ev->type == GDK_BUTTON_PRESS) {
352
353                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
354                         /* handled by button release */
355                         return true;
356                 }
357
358                 show_drag_data_window ();
359
360                 if (ev->y < 20) {
361                         /* top section of widget is for position drags */
362                         dragging_position = true;
363                         StartPositionGesture ();
364                 } else {
365                         /* lower section is for dragging width */
366
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;
375
376                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
377                                 if (swidth < 0.0) {
378                                         dragging_right = true;
379                                 } else {
380                                         dragging_left = true;
381                                 }
382                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
383                                 if (swidth < 0.0) {
384                                         dragging_left = true;
385                                 } else {
386                                         dragging_right = true;
387                                 }
388                         }
389                         StartWidthGesture ();
390                 }
391
392                 _dragging = true;
393         }
394
395         return true;
396 }
397
398 bool
399 StereoPanner::on_button_release_event (GdkEventButton* ev)
400 {
401         if (ev->button != 1) {
402                 return false;
403         }
404
405         bool const dp = dragging_position;
406
407         _dragging = false;
408         dragging_position = false;
409         dragging_left = false;
410         dragging_right = false;
411         accumulated_delta = 0;
412         detented = false;
413
414         hide_drag_data_window ();
415
416         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
417                 _panner->reset ();
418         } else {
419                 if (dp) {
420                         StopPositionGesture ();
421                 } else {
422                         StopWidthGesture ();
423                 }
424         }
425
426         return true;
427 }
428
429 bool
430 StereoPanner::on_scroll_event (GdkEventScroll* ev)
431 {
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
435         double step;
436
437         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
438                 step = one_degree;
439         } else {
440                 step = one_degree * 5.0;
441         }
442
443         switch (ev->direction) {
444         case GDK_SCROLL_LEFT:
445                 wv += step;
446                 width_control->set_value (wv);
447                 break;
448         case GDK_SCROLL_UP:
449                 pv -= step;
450                 position_control->set_value (pv);
451                 break;
452         case GDK_SCROLL_RIGHT:
453                 wv -= step;
454                 width_control->set_value (wv);
455                 break;
456         case GDK_SCROLL_DOWN:
457                 pv += step;
458                 position_control->set_value (pv);
459                 break;
460         }
461
462         return true;
463 }
464
465 bool
466 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
467 {
468         if (!_dragging) {
469                 return false;
470         }
471
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 ();
475
476         if (dragging_left) {
477                 delta = -delta;
478         }
479         
480         if (dragging_left || dragging_right) {
481
482                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
483
484                         /* change width and position in a way that keeps the
485                          * other side in the same place
486                          */
487
488                         _panner->freeze ();
489                         
490                         double pv = position_control->get_value();
491
492                         if (dragging_left) {
493                                 position_control->set_value (pv - delta);
494                         } else {
495                                 position_control->set_value (pv + delta);
496                         }
497
498                         if (delta > 0.0) {
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.
504                                 */
505                                 width_control->set_value (current_width + (delta * 2.0));
506                         } else {
507                                 width_control->set_value (current_width + delta);
508                         }
509
510                         _panner->thaw ();
511
512                 } else {
513
514                         /* maintain position as invariant as we change the width */
515                         
516                         /* create a detent close to the center */
517                         
518                         if (!detented && fabs (current_width) < 0.02) {
519                                 detented = true;
520                                 /* snap to zero */
521                                 width_control->set_value (0);
522                         }
523                         
524                         if (detented) {
525                                 
526                                 accumulated_delta += delta;
527                                 
528                                 /* have we pulled far enough to escape ? */
529                                 
530                                 if (fabs (accumulated_delta) >= 0.025) {
531                                         width_control->set_value (current_width + accumulated_delta);
532                                         detented = false;
533                                         accumulated_delta = false;
534                                 }
535                                 
536                         } else {
537                                 /* width needs to change by 2 * delta because both L & R move */
538                                 width_control->set_value (current_width + (delta * 2.0));
539                         }
540                 }
541
542         } else if (dragging_position) {
543
544                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
545                 position_control->set_value (pv + delta);
546         }
547
548         last_drag_x = ev->x;
549         return true;
550 }
551
552 bool
553 StereoPanner::on_key_press_event (GdkEventKey* ev)
554 {
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
558         double step;
559
560         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
561                 step = one_degree;
562         } else {
563                 step = one_degree * 5.0;
564         }
565
566         /* up/down control width because we consider pan position more "important"
567            (and thus having higher "sense" priority) than width.
568         */
569
570         switch (ev->keyval) {
571         case GDK_Up:
572                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
573                         width_control->set_value (1.0);
574                 } else {
575                         width_control->set_value (wv + step);
576                 }
577                 break;
578         case GDK_Down:
579                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
580                         width_control->set_value (-1.0);
581                 } else {
582                         width_control->set_value (wv - step);
583                 }
584                 break;
585
586         case GDK_Left:
587                 pv -= step;
588                 position_control->set_value (pv);
589                 break;
590         case GDK_Right:
591                 pv += step;
592                 position_control->set_value (pv);
593                 break;
594         case GDK_0:
595         case GDK_KP_0:
596                 width_control->set_value (0.0);
597                 break;
598
599         default:
600                 return false;
601         }
602
603         return true;
604 }
605
606 void
607 StereoPanner::set_colors ()
608 {
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();
614
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();
620
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();
626 }
627
628 void
629 StereoPanner::color_handler ()
630 {
631         set_colors ();
632         queue_draw ();
633 }