stereo and mono panner tweaks to use cairomm and rounded rectangles and more
[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 = 9;
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         double spos = (pos_box_size/2.0) + (usable_width * pos);
258
259         context->set_line_width (2.0);
260         context->move_to (spos + (pos_box_size/2.0), top_step); /* top right */
261         context->rel_line_to (0.0, pos_box_size); /* lower right */
262         context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
263         context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
264         context->rel_line_to (0.0, -pos_box_size); /* upper left */
265         context->close_path ();
266
267         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
268         context->stroke_preserve ();
269         context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
270         context->fill ();
271
272         return true;
273 }
274
275 bool
276 StereoPanner::on_button_press_event (GdkEventButton* ev)
277 {
278         drag_start_x = ev->x;
279         last_drag_x = ev->x;
280         
281         dragging_position = false;
282         dragging_left = false;
283         dragging_right = false;
284         dragging = false;
285         accumulated_delta = 0;
286         detented = false;
287
288         /* Let the binding proxies get first crack at the press event
289          */
290
291         if (ev->y < 20) {
292                 if (position_binder.button_press_handler (ev)) {
293                         return true;
294                 }
295         } else {
296                 if (width_binder.button_press_handler (ev)) {
297                         return true;
298                 }
299         }
300         
301         if (ev->button != 1) {
302                 return false;
303         }
304
305         if (ev->type == GDK_2BUTTON_PRESS) {
306                 int width = get_width();
307
308                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
309                         /* handled by button release */
310                         return true;
311                 }
312
313                 if (ev->y < 20) {
314                         
315                         /* upper section: adjusts position, constrained by width */
316
317                         const double w = fabs (width_control->get_value ());
318                         const double max_pos = 1.0 - (w/2.0);
319                         const double min_pos = w/2.0;
320
321                         if (ev->x <= width/3) {
322                                 /* left side dbl click */
323                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
324                                         /* 2ndary-double click on left, collapse to hard left */
325                                         width_control->set_value (0);
326                                         position_control->set_value (0);
327                                 } else {
328                                         position_control->set_value (min_pos);
329                                 }
330                         } else if (ev->x > 2*width/3) {
331                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
332                                         /* 2ndary-double click on right, collapse to hard right */
333                                         width_control->set_value (0);
334                                         position_control->set_value (1.0);
335                                 } else {
336                                         position_control->set_value (max_pos);
337                                 }
338                         } else {
339                                 position_control->set_value (0.5);
340                         }
341
342                 } else {
343
344                         /* lower section: adjusts width, constrained by position */
345
346                         const double p = position_control->get_value ();
347                         const double max_width = 2.0 * min ((1.0 - p), p);
348
349                         if (ev->x <= width/3) {
350                                 /* left side dbl click */
351                                 width_control->set_value (max_width); // reset width to 100%
352                         } else if (ev->x > 2*width/3) {
353                                 /* right side dbl click */
354                                 width_control->set_value (-max_width); // reset width to inverted 100%
355                         } else {
356                                 /* center dbl click */
357                                 width_control->set_value (0); // collapse width to 0%
358                         }
359                 }
360
361                 dragging = false;
362
363         } else if (ev->type == GDK_BUTTON_PRESS) {
364
365                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
366                         /* handled by button release */
367                         return true;
368                 }
369
370                 if (ev->y < 20) {
371                         /* top section of widget is for position drags */
372                         dragging_position = true;
373                         StartPositionGesture ();
374                 } else {
375                         /* lower section is for dragging width */
376                         
377                         double pos = position_control->get_value (); /* 0..1 */
378                         double swidth = width_control->get_value (); /* -1..+1 */
379                         double fswidth = fabs (swidth);
380                         int usable_width = get_width() - lr_box_size;
381                         double center = (lr_box_size/2.0) + (usable_width * pos);
382                         int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
383                         int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
384                         const int half_box = lr_box_size/2;
385                         
386                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
387                                 if (swidth < 0.0) {
388                                         dragging_right = true;
389                                 } else {
390                                         dragging_left = true;
391                                 }
392                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
393                                 if (swidth < 0.0) {
394                                         dragging_left = true;
395                                 } else {
396                                         dragging_right = true;
397                                 }
398                         }
399                         StartWidthGesture ();
400                 }
401
402                 dragging = true;
403         }
404
405         return true;
406 }
407
408 bool
409 StereoPanner::on_button_release_event (GdkEventButton* ev)
410 {
411         if (ev->button != 1) {
412                 return false;
413         }
414
415         bool dp = dragging_position;
416
417         dragging = false;
418         dragging_position = false;
419         dragging_left = false;
420         dragging_right = false;
421         accumulated_delta = 0;
422         detented = false;
423
424         if (drag_data_window) {
425                 drag_data_window->hide ();
426         }
427         
428         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
429                 /* reset to default */
430                 position_control->set_value (0.5);
431                 width_control->set_value (1.0);
432         } else {
433                 if (dp) {
434                         StopPositionGesture ();
435                 } else {
436                         StopWidthGesture ();
437                 }
438         }
439
440         return true;
441 }
442
443 bool
444 StereoPanner::on_scroll_event (GdkEventScroll* ev)
445 {
446         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
447         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
448         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
449         double step;
450         
451         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
452                 step = one_degree;
453         } else {
454                 step = one_degree * 5.0;
455         }
456
457         switch (ev->direction) {
458         case GDK_SCROLL_LEFT:
459                 wv += step;
460                 width_control->set_value (wv);
461                 break;
462         case GDK_SCROLL_UP:
463                 pv -= step;
464                 position_control->set_value (pv);
465                 break;
466         case GDK_SCROLL_RIGHT:
467                 wv -= step;
468                 width_control->set_value (wv);
469                 break;
470         case GDK_SCROLL_DOWN:
471                 pv += step;
472                 position_control->set_value (pv);
473                 break;
474         }
475
476         return true;
477 }
478
479 bool
480 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
481 {
482         if (!dragging) {
483                 return false;
484         }
485
486         if (!drag_data_window) {
487                 drag_data_window = new Window (WINDOW_POPUP);
488                 drag_data_window->set_name (X_("ContrastingPopup"));
489                 drag_data_window->set_position (WIN_POS_MOUSE);
490                 drag_data_window->set_decorated (false);
491                 
492                 drag_data_label = manage (new Label);
493                 drag_data_label->set_use_markup (true);
494
495                 drag_data_window->set_border_width (6);
496                 drag_data_window->add (*drag_data_label);
497                 drag_data_label->show ();
498                 
499                 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
500                 if (toplevel) {
501                         drag_data_window->set_transient_for (*toplevel);
502                 }
503         }
504
505         if (!drag_data_window->is_visible ()) {
506                 /* move the popup window vertically down from the panner display */
507                 int rx, ry;
508                 get_window()->get_origin (rx, ry);
509                 drag_data_window->move (rx, ry+get_height());
510                 drag_data_window->present ();
511         }
512
513         int w = get_width();
514         double delta = (ev->x - last_drag_x) / (double) w;
515         double current_width = width_control->get_value ();
516         
517         if (dragging_left) {
518                 delta = -delta;
519         }
520
521         if (dragging_left || dragging_right) {
522
523                 /* maintain position as invariant as we change the width */
524
525
526                 /* create a detent close to the center */
527
528                 if (!detented && fabs (current_width) < 0.02) {
529                         detented = true;
530                         /* snap to zero */
531                         width_control->set_value (0);
532                 }
533                 
534                 if (detented) {
535
536                         accumulated_delta += delta;
537
538                         /* have we pulled far enough to escape ? */
539
540                         if (fabs (accumulated_delta) >= 0.025) {
541                                 width_control->set_value (current_width + accumulated_delta);
542                                 detented = false;
543                                 accumulated_delta = false;
544                         }
545                                 
546                 } else {
547                         width_control->set_value (current_width + delta);
548                 }
549
550         } else if (dragging_position) {
551
552                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
553                 position_control->set_value (pv + delta);
554         }
555
556         last_drag_x = ev->x;
557         return true;
558 }
559
560 bool
561 StereoPanner::on_key_press_event (GdkEventKey* ev)
562 {
563         double one_degree = 1.0/180.0;
564         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
565         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
566         double step;
567
568         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
569                 step = one_degree;
570         } else {
571                 step = one_degree * 5.0;
572         }
573
574         /* up/down control width because we consider pan position more "important"
575            (and thus having higher "sense" priority) than width.
576         */
577
578         switch (ev->keyval) {
579         case GDK_Up:
580                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
581                         width_control->set_value (1.0);
582                 } else {
583                         width_control->set_value (wv + step);
584                 }
585                 break;
586         case GDK_Down:
587                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
588                         width_control->set_value (-1.0);
589                 } else {
590                         width_control->set_value (wv - step);
591                 }
592
593         case GDK_Left:
594                 pv -= step;
595                 position_control->set_value (pv);
596                 break;
597         case GDK_Right:
598                 pv += step;
599                 position_control->set_value (pv);
600                 break;
601
602                 break;
603         case GDK_0:
604         case GDK_KP_0:
605                 width_control->set_value (0.0);
606                 break;
607
608         default: 
609                 return false;
610         }
611                 
612         return true;
613 }
614
615 bool
616 StereoPanner::on_key_release_event (GdkEventKey* ev)
617 {
618         return false;
619 }
620
621 bool
622 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
623 {
624         grab_focus ();
625         Keyboard::magic_widget_grab_focus ();
626         return false;
627 }
628
629 bool
630 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
631 {
632         Keyboard::magic_widget_drop_focus ();
633         return false;
634 }
635
636 void
637 StereoPanner::set_colors ()
638 {
639         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
640         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
641         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
642         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
643         colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
644
645         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
646         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
647         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
648         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
649         colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
650
651         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
652         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
653         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
654         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
655         colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
656 }
657
658 void
659 StereoPanner::color_handler ()
660 {
661         set_colors ();
662         queue_draw ();
663 }