Rename Frames ruler to Samples.
[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                         StartPositionGesture ();
367                 } else {
368                         /* lower section is for dragging width */
369                         
370                         double pos = position_control->get_value (); /* 0..1 */
371                         double swidth = width_control->get_value (); /* -1..+1 */
372                         double fswidth = fabs (swidth);
373                         int usable_width = get_width() - lr_box_size;
374                         double center = (lr_box_size/2.0) + (usable_width * pos);
375                         int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
376                         int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
377                         const int half_box = lr_box_size/2;
378                         
379                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
380                                 if (swidth < 0.0) {
381                                         dragging_right = true;
382                                 } else {
383                                         dragging_left = true;
384                                 }
385                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
386                                 if (swidth < 0.0) {
387                                         dragging_left = true;
388                                 } else {
389                                         dragging_right = true;
390                                 }
391                         }
392                         StartWidthGesture ();
393                 }
394
395                 dragging = true;
396         }
397
398         return true;
399 }
400
401 bool
402 StereoPanner::on_button_release_event (GdkEventButton* ev)
403 {
404         if (ev->button != 1) {
405                 return false;
406         }
407
408         bool dp = dragging_position;
409
410         dragging = false;
411         dragging_position = false;
412         dragging_left = false;
413         dragging_right = false;
414         accumulated_delta = 0;
415         detented = false;
416
417         if (drag_data_window) {
418                 drag_data_window->hide ();
419         }
420         
421         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
422                 /* reset to default */
423                 position_control->set_value (0.5);
424                 width_control->set_value (1.0);
425         } else {
426                 if (dp) {
427                         StopPositionGesture ();
428                 } else {
429                         StopWidthGesture ();
430                 }
431         }
432
433         return true;
434 }
435
436 bool
437 StereoPanner::on_scroll_event (GdkEventScroll* ev)
438 {
439         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
440         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
441         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
442         double step;
443         
444         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
445                 step = one_degree;
446         } else {
447                 step = one_degree * 5.0;
448         }
449
450         switch (ev->direction) {
451         case GDK_SCROLL_LEFT:
452                 wv += step;
453                 width_control->set_value (wv);
454                 break;
455         case GDK_SCROLL_UP:
456                 pv -= step;
457                 position_control->set_value (pv);
458                 break;
459         case GDK_SCROLL_RIGHT:
460                 wv -= step;
461                 width_control->set_value (wv);
462                 break;
463         case GDK_SCROLL_DOWN:
464                 pv += step;
465                 position_control->set_value (pv);
466                 break;
467         }
468
469         return true;
470 }
471
472 bool
473 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
474 {
475         if (!dragging) {
476                 return false;
477         }
478
479         if (!drag_data_window) {
480                 drag_data_window = new Window (WINDOW_POPUP);
481                 drag_data_window->set_position (WIN_POS_MOUSE);
482                 drag_data_window->set_decorated (false);
483                 
484                 drag_data_label = manage (new Label);
485                 drag_data_label->set_use_markup (true);
486
487                 drag_data_window->set_border_width (6);
488                 drag_data_window->add (*drag_data_label);
489                 drag_data_label->show ();
490                 
491                 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
492                 if (toplevel) {
493                         drag_data_window->set_transient_for (*toplevel);
494                 }
495         }
496
497         if (!drag_data_window->is_visible ()) {
498                 /* move the window a little away from the mouse */
499                 drag_data_window->move (ev->x_root+30, ev->y_root+30);
500                 drag_data_window->present ();
501         }
502
503         int w = get_width();
504         double delta = (ev->x - last_drag_x) / (double) w;
505         double current_width = width_control->get_value ();
506         
507         if (dragging_left) {
508                 delta = -delta;
509         }
510
511         if (dragging_left || dragging_right) {
512
513                 /* maintain position as invariant as we change the width */
514
515
516                 /* create a detent close to the center */
517
518                 if (!detented && fabs (current_width) < 0.02) {
519                         detented = true;
520                         /* snap to zero */
521                         width_control->set_value (0);
522                 }
523                 
524                 if (detented) {
525
526                         accumulated_delta += delta;
527
528                         /* have we pulled far enough to escape ? */
529
530                         if (fabs (accumulated_delta) >= 0.025) {
531                                 width_control->set_value (current_width + accumulated_delta);
532                                 detented = false;
533                                 accumulated_delta = false;
534                         }
535                                 
536                 } else {
537                         width_control->set_value (current_width + delta);
538                 }
539
540         } else if (dragging_position) {
541
542                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
543                 position_control->set_value (pv + delta);
544         }
545
546         last_drag_x = ev->x;
547         return true;
548 }
549
550 bool
551 StereoPanner::on_key_press_event (GdkEventKey* ev)
552 {
553         double one_degree = 1.0/180.0;
554         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
555         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
556         double step;
557
558         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
559                 step = one_degree;
560         } else {
561                 step = one_degree * 5.0;
562         }
563
564         /* up/down control width because we consider pan position more "important"
565            (and thus having higher "sense" priority) than width.
566         */
567
568         switch (ev->keyval) {
569         case GDK_Up:
570                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
571                         width_control->set_value (1.0);
572                 } else {
573                         width_control->set_value (wv + step);
574                 }
575                 break;
576         case GDK_Down:
577                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
578                         width_control->set_value (-1.0);
579                 } else {
580                         width_control->set_value (wv - step);
581                 }
582
583         case GDK_Left:
584                 pv -= step;
585                 position_control->set_value (pv);
586                 break;
587         case GDK_Right:
588                 pv += step;
589                 position_control->set_value (pv);
590                 break;
591
592                 break;
593         case GDK_0:
594         case GDK_KP_0:
595                 width_control->set_value (0.0);
596                 break;
597
598         default: 
599                 return false;
600         }
601                 
602         return true;
603 }
604
605 bool
606 StereoPanner::on_key_release_event (GdkEventKey* ev)
607 {
608         return false;
609 }
610
611 bool
612 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
613 {
614         grab_focus ();
615         Keyboard::magic_widget_grab_focus ();
616         return false;
617 }
618
619 bool
620 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
621 {
622         Keyboard::magic_widget_drop_focus ();
623         return false;
624 }
625
626 void
627 StereoPanner::set_colors ()
628 {
629         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
630         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
631         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
632         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
633         colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
634
635         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
636         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
637         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
638         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
639         colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
640
641         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
642         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
643         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
644         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
645         colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
646 }
647
648 void
649 StereoPanner::color_handler ()
650 {
651         set_colors ();
652         queue_draw ();
653 }