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