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