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