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