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