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