several theme changes. changed drawing of rounded_rectangle to be more correct....
[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         accumulated_delta = 0;
279         detented = false;
280
281         /* Let the binding proxies get first crack at the press event
282          */
283
284         if (ev->y < 20) {
285                 if (position_binder.button_press_handler (ev)) {
286                         return true;
287                 }
288         } else {
289                 if (width_binder.button_press_handler (ev)) {
290                         return true;
291                 }
292         }
293
294         if (ev->button != 1) {
295                 return false;
296         }
297
298         if (ev->type == GDK_2BUTTON_PRESS) {
299                 int width = get_width();
300
301                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
302                         /* handled by button release */
303                         return true;
304                 }
305
306                 if (ev->y < 20) {
307
308                         /* upper section: adjusts position, constrained by width */
309
310                         const double w = fabs (width_control->get_value ());
311                         const double max_pos = 1.0 - (w/2.0);
312                         const double min_pos = w/2.0;
313
314                         if (ev->x <= width/3) {
315                                 /* left side dbl click */
316                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
317                                         /* 2ndary-double click on left, collapse to hard left */
318                                         width_control->set_value (0);
319                                         position_control->set_value (0);
320                                 } else {
321                                         position_control->set_value (min_pos);
322                                 }
323                         } else if (ev->x > 2*width/3) {
324                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
325                                         /* 2ndary-double click on right, collapse to hard right */
326                                         width_control->set_value (0);
327                                         position_control->set_value (1.0);
328                                 } else {
329                                         position_control->set_value (max_pos);
330                                 }
331                         } else {
332                                 position_control->set_value (0.5);
333                         }
334
335                 } else {
336
337                         /* lower section: adjusts width, constrained by position */
338
339                         const double p = position_control->get_value ();
340                         const double max_width = 2.0 * min ((1.0 - p), p);
341
342                         if (ev->x <= width/3) {
343                                 /* left side dbl click */
344                                 width_control->set_value (max_width); // reset width to 100%
345                         } else if (ev->x > 2*width/3) {
346                                 /* right side dbl click */
347                                 width_control->set_value (-max_width); // reset width to inverted 100%
348                         } else {
349                                 /* center dbl click */
350                                 width_control->set_value (0); // collapse width to 0%
351                         }
352                 }
353
354                 _dragging = false;
355
356         } else if (ev->type == GDK_BUTTON_PRESS) {
357
358                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
359                         /* handled by button release */
360                         return true;
361                 }
362
363                 if (ev->y < 20) {
364                         /* top section of widget is for position drags */
365                         dragging_position = true;
366                         StartPositionGesture ();
367                 } else {
368                         /* lower section is for dragging width */
369
370                         double pos = position_control->get_value (); /* 0..1 */
371                         double swidth = width_control->get_value (); /* -1..+1 */
372                         double fswidth = fabs (swidth);
373                         int usable_width = get_width() - lr_box_size;
374                         double center = (lr_box_size/2.0) + (usable_width * pos);
375                         int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
376                         int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
377                         const int half_box = lr_box_size/2;
378
379                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
380                                 if (swidth < 0.0) {
381                                         dragging_right = true;
382                                 } else {
383                                         dragging_left = true;
384                                 }
385                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
386                                 if (swidth < 0.0) {
387                                         dragging_left = true;
388                                 } else {
389                                         dragging_right = true;
390                                 }
391                         }
392                         StartWidthGesture ();
393                 }
394
395                 _dragging = true;
396                 _tooltip.target_start_drag ();
397         }
398
399         return true;
400 }
401
402 bool
403 StereoPanner::on_button_release_event (GdkEventButton* ev)
404 {
405         if (PannerInterface::on_button_release_event (ev)) {
406                 return true;
407         }
408         
409         if (ev->button != 1) {
410                 return false;
411         }
412
413         bool const dp = dragging_position;
414
415         _dragging = false;
416         _tooltip.target_stop_drag ();
417         dragging_position = false;
418         dragging_left = false;
419         dragging_right = false;
420         accumulated_delta = 0;
421         detented = false;
422
423         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
424                 _panner->reset ();
425         } else {
426                 if (dp) {
427                         StopPositionGesture ();
428                 } else {
429                         StopWidthGesture ();
430                 }
431         }
432
433         return true;
434 }
435
436 bool
437 StereoPanner::on_scroll_event (GdkEventScroll* ev)
438 {
439         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
440         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
441         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
442         double step;
443
444         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
445                 step = one_degree;
446         } else {
447                 step = one_degree * 5.0;
448         }
449
450         switch (ev->direction) {
451         case GDK_SCROLL_LEFT:
452                 wv += step;
453                 width_control->set_value (wv);
454                 break;
455         case GDK_SCROLL_UP:
456                 pv -= step;
457                 position_control->set_value (pv);
458                 break;
459         case GDK_SCROLL_RIGHT:
460                 wv -= step;
461                 width_control->set_value (wv);
462                 break;
463         case GDK_SCROLL_DOWN:
464                 pv += step;
465                 position_control->set_value (pv);
466                 break;
467         }
468
469         return true;
470 }
471
472 bool
473 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
474 {
475         if (!_dragging) {
476                 return false;
477         }
478
479         int usable_width = get_width() - lr_box_size;
480         double delta = (ev->x - last_drag_x) / (double) usable_width;
481         double current_width = width_control->get_value ();
482
483         if (dragging_left) {
484                 delta = -delta;
485         }
486         
487         if (dragging_left || dragging_right) {
488
489                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
490
491                         /* change width and position in a way that keeps the
492                          * other side in the same place
493                          */
494
495                         _panner->freeze ();
496                         
497                         double pv = position_control->get_value();
498
499                         if (dragging_left) {
500                                 position_control->set_value (pv - delta);
501                         } else {
502                                 position_control->set_value (pv + delta);
503                         }
504
505                         if (delta > 0.0) {
506                                 /* delta is positive, so we're about to
507                                    increase the width. But we need to increase it
508                                    by twice the required value so that the
509                                    other side remains in place when we set
510                                    the position as well.
511                                 */
512                                 width_control->set_value (current_width + (delta * 2.0));
513                         } else {
514                                 width_control->set_value (current_width + delta);
515                         }
516
517                         _panner->thaw ();
518
519                 } else {
520
521                         /* maintain position as invariant as we change the width */
522                         
523                         /* create a detent close to the center */
524                         
525                         if (!detented && fabs (current_width) < 0.02) {
526                                 detented = true;
527                                 /* snap to zero */
528                                 width_control->set_value (0);
529                         }
530                         
531                         if (detented) {
532                                 
533                                 accumulated_delta += delta;
534                                 
535                                 /* have we pulled far enough to escape ? */
536                                 
537                                 if (fabs (accumulated_delta) >= 0.025) {
538                                         width_control->set_value (current_width + accumulated_delta);
539                                         detented = false;
540                                         accumulated_delta = false;
541                                 }
542                                 
543                         } else {
544                                 /* width needs to change by 2 * delta because both L & R move */
545                                 width_control->set_value (current_width + (delta * 2.0));
546                         }
547                 }
548
549         } else if (dragging_position) {
550
551                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
552                 position_control->set_value (pv + delta);
553         }
554
555         last_drag_x = ev->x;
556         return true;
557 }
558
559 bool
560 StereoPanner::on_key_press_event (GdkEventKey* ev)
561 {
562         double one_degree = 1.0/180.0;
563         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
564         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
565         double step;
566
567         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
568                 step = one_degree;
569         } else {
570                 step = one_degree * 5.0;
571         }
572
573         /* up/down control width because we consider pan position more "important"
574            (and thus having higher "sense" priority) than width.
575         */
576
577         switch (ev->keyval) {
578         case GDK_Up:
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         case GDK_Down:
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
593         case GDK_Left:
594                 pv -= step;
595                 position_control->set_value (pv);
596                 break;
597         case GDK_Right:
598                 pv += step;
599                 position_control->set_value (pv);
600                 break;
601         case GDK_0:
602         case GDK_KP_0:
603                 width_control->set_value (0.0);
604                 break;
605
606         default:
607                 return false;
608         }
609
610         return true;
611 }
612
613 void
614 StereoPanner::set_colors ()
615 {
616         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
617         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
618         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
619         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
620         colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
621
622         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
623         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
624         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
625         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
626         colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
627
628         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
629         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
630         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
631         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
632         colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
633 }
634
635 void
636 StereoPanner::color_handler ()
637 {
638         set_colors ();
639         queue_draw ();
640 }
641
642 PannerEditor*
643 StereoPanner::editor ()
644 {
645         return new StereoPannerEditor (this);
646 }