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