better detent handling, and dbl-click behaviour for lower half improvements
[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->type == GDK_2BUTTON_PRESS) {
298                 int width = get_width();
299
300                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
301                         /* handled by button release */
302                         return true;
303                 }
304
305                 if (ev->y < 20) {
306                         /* lower section: adjusts position, constrained by width */
307
308                         if (ev->x >= width/2 - 10 && ev->x <= width/2 + 10) {
309                                 /* double click near center, reset position to center */
310                                 position_control->set_value (0.5); 
311                         } else {
312                                 if (ev->x < width/2) {
313                                         /* double click on left, collapse to hard left */
314                                         width_control->set_value (0);
315                                         position_control->set_value (0);
316                                 } else {
317                                         /* double click on right, collapse to hard right */
318                                         width_control->set_value (0);
319                                         position_control->set_value (1.0);
320                                 }
321                         }
322
323                 } else {
324                         /* lower section: adjusts width, constrained by position */
325
326                         if (ev->x <= width/3) {
327                                 /* left side dbl click */
328                                 width_control->set_value (1.0); // reset width to 100%
329                         } else if (ev->x > 2*width/3) {
330                                 /* right side dbl click */
331                                 width_control->set_value (-1.0); // reset width to inverted 100%
332                         } else {
333                                 /* center dbl click */
334                                 width_control->set_value (0); // collapse width to 0%
335                         }
336                 }
337
338                 dragging = false;
339
340         } else if (ev->type == GDK_BUTTON_PRESS) {
341
342                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
343                         /* handled by button release */
344                         return true;
345                 }
346
347                 if (ev->y < 20) {
348                         /* top section of widget is for position drags */
349                         dragging_position = true;
350                 } else {
351                         /* lower section is for dragging width */
352                         
353                         double pos = position_control->get_value (); /* 0..1 */
354                         double swidth = width_control->get_value (); /* -1..+1 */
355                         double fswidth = fabs (swidth);
356                         int usable_width = get_width() - lr_box_size;
357                         double center = (lr_box_size/2.0) + (usable_width * pos);
358                         int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
359                         int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
360                         const int half_box = lr_box_size/2;
361                         
362                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
363                                 dragging_left = true;
364                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
365                                 dragging_right = true;
366                         }
367                         
368                 }
369
370                 dragging = true;
371         }
372
373         return true;
374 }
375
376 bool
377 StereoPanner::on_button_release_event (GdkEventButton* ev)
378 {
379         dragging = false;
380         dragging_position = false;
381         dragging_left = false;
382         dragging_right = false;
383         accumulated_delta = 0;
384         detented = false;
385
386         if (drag_data_window) {
387                 drag_data_window->hide ();
388         }
389         
390         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
391                 /* reset to default */
392                 position_control->set_value (0.5);
393                 width_control->set_value (1.0);
394         }
395
396         set_tooltip ();
397
398         return true;
399 }
400
401 bool
402 StereoPanner::on_scroll_event (GdkEventScroll* ev)
403 {
404         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
405         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
406         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
407         double step;
408         
409         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
410                 step = one_degree;
411         } else {
412                 step = one_degree * 5.0;
413         }
414
415         switch (ev->direction) {
416         case GDK_SCROLL_LEFT:
417                 wv += step;
418                 width_control->set_value (wv);
419                 break;
420         case GDK_SCROLL_UP:
421                 pv -= step;
422                 position_control->set_value (pv);
423                 break;
424         case GDK_SCROLL_RIGHT:
425                 wv -= step;
426                 width_control->set_value (wv);
427                 break;
428         case GDK_SCROLL_DOWN:
429                 pv += step;
430                 position_control->set_value (pv);
431                 break;
432         }
433
434         return true;
435 }
436
437 bool
438 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
439 {
440         if (!dragging) {
441                 return false;
442         }
443
444         if (!drag_data_window) {
445                 drag_data_window = new Window (WINDOW_POPUP);
446                 drag_data_window->set_position (WIN_POS_MOUSE);
447                 drag_data_window->set_decorated (false);
448                 
449                 drag_data_label = manage (new Label);
450                 drag_data_label->set_use_markup (true);
451
452                 drag_data_window->set_border_width (6);
453                 drag_data_window->add (*drag_data_label);
454                 drag_data_label->show ();
455                 
456                 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
457                 if (toplevel) {
458                         drag_data_window->set_transient_for (*toplevel);
459                 }
460         }
461
462         if (!drag_data_window->is_visible ()) {
463                 /* move the window a little away from the mouse */
464                 drag_data_window->move (ev->x_root+30, ev->y_root+30);
465                 drag_data_window->present ();
466                 unset_tooltip ();
467         }
468
469         int w = get_width();
470         double delta = (ev->x - last_drag_x) / (double) w;
471         
472         if (dragging_left) {
473                 delta = -delta;
474         }
475
476         if (dragging_left || dragging_right) {
477
478                 /* maintain position as invariant as we change the width */
479
480                 double current_width = width_control->get_value ();
481
482                 /* create a detent close to the center */
483
484                 if (!detented && fabs (current_width) < 0.02) {
485                         detented = true;
486                         /* snap to zero */
487                         width_control->set_value (0);
488                 }
489                 
490                 if (detented) {
491
492                         accumulated_delta += delta;
493
494                         /* have we pulled far enough to escape ? */
495
496                         if (fabs (accumulated_delta) >= 0.1) {
497                                 width_control->set_value (current_width + accumulated_delta);
498                                 detented = false;
499                                 accumulated_delta = false;
500                         }
501                                 
502                 } else {
503                         width_control->set_value (current_width + delta);
504                 }
505
506         } else if (dragging_position) {
507
508                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
509                 position_control->set_value (pv + delta);
510         }
511
512         last_drag_x = ev->x;
513         return true;
514 }
515
516 bool
517 StereoPanner::on_key_press_event (GdkEventKey* ev)
518 {
519         double one_degree = 1.0/180.0;
520         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
521         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
522         double step;
523
524         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
525                 step = one_degree;
526         } else {
527                 step = one_degree * 5.0;
528         }
529
530         /* up/down control width because we consider pan position more "important"
531            (and thus having higher "sense" priority) than width.
532         */
533
534         switch (ev->keyval) {
535         case GDK_Up:
536                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
537                         width_control->set_value (1.0);
538                 } else {
539                         width_control->set_value (wv + step);
540                 }
541                 break;
542         case GDK_Down:
543                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
544                         width_control->set_value (-1.0);
545                 } else {
546                         width_control->set_value (wv - step);
547                 }
548
549         case GDK_Left:
550                 pv -= step;
551                 position_control->set_value (pv);
552                 break;
553         case GDK_Right:
554                 pv += step;
555                 position_control->set_value (pv);
556                 break;
557
558                 break;
559         case GDK_0:
560         case GDK_KP_0:
561                 width_control->set_value (0.0);
562                 break;
563
564         default: 
565                 return false;
566         }
567                 
568         return true;
569 }
570
571 bool
572 StereoPanner::on_key_release_event (GdkEventKey* ev)
573 {
574         return false;
575 }
576
577 bool
578 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
579 {
580         grab_focus ();
581         Keyboard::magic_widget_grab_focus ();
582         return false;
583 }
584
585 bool
586 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
587 {
588         Keyboard::magic_widget_drop_focus ();
589         return false;
590 }
591
592 void
593 StereoPanner::set_colors ()
594 {
595         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
596         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
597         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
598         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
599
600         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
601         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
602         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
603         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
604
605         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
606         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
607         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
608         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
609
610         color_change (); /* EMIT SIGNAL */
611 }