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