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