151b4f6bead3124c1311ab8e5f56113c98809f48
[ardour.git] / gtk2_ardour / stereo_panner.cc
1 /*
2     Copyright (C) 2000-2007 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <iostream>
21 #include <iomanip>
22 #include <cstring>
23 #include <cmath>
24
25 #include <gtkmm/window.h>
26
27 #include "pbd/controllable.h"
28 #include "pbd/compose.h"
29
30 #include "gtkmm2ext/gui_thread.h"
31 #include "gtkmm2ext/gtk_ui.h"
32 #include "gtkmm2ext/keyboard.h"
33
34 #include "ardour/panner.h"
35
36 #include "ardour_ui.h"
37 #include "global_signals.h"
38 #include "stereo_panner.h"
39 #include "rgb_macros.h"
40 #include "utils.h"
41
42 #include "i18n.h"
43
44 using namespace std;
45 using namespace Gtk;
46 using namespace Gtkmm2ext;
47
48 static const int pos_box_size = 10;
49 static const int lr_box_size = 15;
50 static const int step_down = 10;
51 static const int top_step = 2;
52
53 StereoPanner::ColorScheme StereoPanner::colors[3];
54 bool StereoPanner::have_colors = false;
55 PBD::Signal0<void> StereoPanner::color_change;
56
57 StereoPanner::StereoPanner (boost::shared_ptr<PBD::Controllable> position, boost::shared_ptr<PBD::Controllable> width)
58         : position_control (position)
59         , width_control (width)
60         , dragging (false)
61         , dragging_position (false)
62         , dragging_left (false)
63         , dragging_right (false)
64         , drag_start_x (0)
65         , last_drag_x (0)
66         , accumulated_delta (0)
67         , drag_data_window (0)
68         , drag_data_label (0)
69 {
70         if (!have_colors) {
71                 set_colors ();
72                 have_colors = true;
73         }
74
75         position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
76         width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
77
78         set_tooltip ();
79         set_flags (Gtk::CAN_FOCUS);
80
81         add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
82                     Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
83                     Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
84                     Gdk::SCROLL_MASK|
85                     Gdk::POINTER_MOTION_MASK);
86
87         color_change.connect (connections, invalidator (*this), boost::bind (&DrawingArea::queue_draw, this), gui_context());
88 }
89
90 StereoPanner::~StereoPanner ()
91 {
92         delete drag_data_window;
93 }
94
95 void
96 StereoPanner::set_tooltip ()
97 {
98         Gtkmm2ext::UI::instance()->set_tip (this, 
99                                             string_compose (_("0 -> set width to zero\n%4-uparrow -> set width to 100\n%4-downarrow -> set width to -100"), 
100                                                             Keyboard::secondary_modifier_name()).c_str());
101 }
102
103 void
104 StereoPanner::unset_tooltip ()
105 {
106         Gtkmm2ext::UI::instance()->set_tip (this, "");
107 }
108
109 void
110 StereoPanner::set_drag_data ()
111 {
112         if (!drag_data_label) {
113                 return;
114         }
115
116         double pos = position_control->get_value(); // 0..1
117         
118         /* We show the position of the center of the image relative to the left & right.
119            This is expressed as a pair of percentage values that ranges from (100,0) 
120            (hard left) through (50,50) (hard center) to (0,100) (hard right).
121
122            This is pretty wierd, but its the way audio engineers expect it. Just remember that
123            the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
124         */
125
126         drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
127                                                      (int) rint (100.0 * (1.0 - pos)),
128                                                      (int) rint (100.0 * pos),
129                                                      (int) floor (100.0 * width_control->get_value())));
130 }
131
132 void
133 StereoPanner::value_change ()
134 {
135         set_drag_data ();
136         queue_draw ();
137 }
138
139 bool
140 StereoPanner::on_expose_event (GdkEventExpose* ev)
141 {
142         Glib::RefPtr<Gdk::Window> win (get_window());
143         Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
144
145         cairo_t* cr = gdk_cairo_create (win->gobj());
146        
147         int width, height;
148         double pos = position_control->get_value (); /* 0..1 */
149         double swidth = width_control->get_value (); /* -1..+1 */
150         double fswidth = fabs (swidth);
151         uint32_t o, f, t, b;
152         State state;
153
154         width = get_width();
155         height = get_height ();
156
157         if (swidth == 0.0) {
158                 state = Mono;
159         } else if (swidth < 0.0) {
160                 state = Inverted;
161         } else { 
162                 state = Normal;
163         }
164
165         o = colors[state].outline;
166         f = colors[state].fill;
167         t = colors[state].text;
168         b = colors[state].background;
169
170         /* background */
171
172         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));
173         cairo_rectangle (cr, 0, 0, width, height);
174         cairo_fill (cr);
175
176         double usable_width = width - lr_box_size;
177
178         /* compute the centers of the L/R boxes based on the current stereo width */
179
180         if (fmod (usable_width,2.0) == 0) {
181                 /* even width, but we need odd, so that there is an exact center.
182                    So, offset cairo by 1, and reduce effective width by 1 
183                 */
184                 usable_width -= 1.0;
185                 cairo_translate (cr, 1.0, 0.0);
186         }
187
188         double center = (lr_box_size/2.0) + (usable_width * pos);
189         const double pan_spread = (fswidth * usable_width)/2.0;
190         const double half_lr_box = lr_box_size/2.0;
191         int left;
192         int right;
193
194         left = center - pan_spread;  // center of left box
195         right = center + pan_spread; // right of right box
196
197         /* compute & draw the line through the box */
198         
199         cairo_set_line_width (cr, 2);
200         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));
201         cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
202         cairo_line_to (cr, left, top_step+(pos_box_size/2));
203         cairo_line_to (cr, right, top_step+(pos_box_size/2));
204         cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
205         cairo_stroke (cr);
206
207         /* left box */
208
209         cairo_rectangle (cr, 
210                          left - half_lr_box,
211                          (lr_box_size/2)+step_down, 
212                          lr_box_size, lr_box_size);
213         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));
214         cairo_stroke_preserve (cr);
215         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));
216         cairo_fill (cr);
217         
218         /* add text */
219
220         cairo_move_to (cr, 
221                        left - half_lr_box + 3,
222                        (lr_box_size/2) + step_down + 13);
223         cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
224
225         if (state != Mono) {
226                 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));
227                 if (swidth < 0.0) {
228                         cairo_show_text (cr, _("R"));
229                 } else {
230                         cairo_show_text (cr, _("L"));
231                 }
232         }
233
234         /* right box */
235
236         cairo_rectangle (cr, 
237                          right - half_lr_box,
238                          (lr_box_size/2)+step_down, 
239                          lr_box_size, lr_box_size);
240         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));
241         cairo_stroke_preserve (cr);
242         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));
243         cairo_fill (cr);
244
245         /* add text */
246
247         cairo_move_to (cr, 
248                        right - half_lr_box + 3,
249                        (lr_box_size/2)+step_down + 13);
250         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));
251
252         if (state == Mono) {
253                 cairo_show_text (cr, _("M"));
254         } else {
255                 if (swidth < 0.0) {
256                         cairo_show_text (cr, _("L"));
257                 } else {
258                         cairo_show_text (cr, _("R"));
259                 }
260         }
261
262         /* draw the central box */
263
264         cairo_set_line_width (cr, 1);
265         cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
266         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));
267         cairo_stroke_preserve (cr);
268         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));
269         cairo_fill (cr);
270
271         /* done */
272
273         cairo_destroy (cr);
274         return true;
275 }
276
277 bool
278 StereoPanner::on_button_press_event (GdkEventButton* ev)
279 {
280         drag_start_x = ev->x;
281         last_drag_x = ev->x;
282         
283         dragging_position = false;
284         dragging_left = false;
285         dragging_right = false;
286         accumulated_delta = 0;
287
288         if (ev->y < 20) {
289                 /* top section of widget is for position drags */
290                 dragging_position = true;
291         } else {
292                 /* lower section is for dragging width */
293
294                 double pos = position_control->get_value (); /* 0..1 */
295                 double swidth = width_control->get_value (); /* -1..+1 */
296                 double fswidth = fabs (swidth);
297                 int usable_width = get_width() - lr_box_size;
298                 double center = (lr_box_size/2.0) + (usable_width * pos);
299                 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
300                 int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
301                 const int half_box = lr_box_size/2;
302
303                 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
304                         dragging_left = true;
305                 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
306                         dragging_right = true;
307                 }
308
309         }
310
311         if (ev->type == GDK_2BUTTON_PRESS) {
312                 if (dragging_position) {
313                         int width = get_width();
314                         if (ev->x >= width/2 - 10 && ev->x <= width/2 + 10) {
315                                 /* double click near center, reset position to center */
316                                 position_control->set_value (0.5); 
317                         } else {
318                                 if (ev->x < width/2) {
319                                         /* double click on left, collapse to hard left */
320                                         width_control->set_value (0);
321                                         position_control->set_value (0);
322                                 } else {
323                                         /* double click on right, collapse to hard right */
324                                         width_control->set_value (0);
325                                         position_control->set_value (1.0);
326                                 }
327                         }
328                 } else {
329                         if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
330                                 width_control->set_value (-1.0); // reset position to reversed full, LR
331                         } else {
332                                 width_control->set_value (1.0); // reset position to full, LR
333                         }
334                 }
335                 dragging = false;
336         } else {
337                 dragging = true;
338         }
339
340         return true;
341 }
342
343 bool
344 StereoPanner::on_button_release_event (GdkEventButton* ev)
345 {
346         dragging = false;
347         dragging_position = false;
348         dragging_left = false;
349         dragging_right = false;
350         accumulated_delta = 0;
351
352         if (drag_data_window) {
353                 drag_data_window->hide ();
354         }
355
356         set_tooltip ();
357
358         return true;
359 }
360
361 bool
362 StereoPanner::on_scroll_event (GdkEventScroll* ev)
363 {
364         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
365         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
366         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
367         double step;
368         
369         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
370                 step = one_degree;
371         } else {
372                 step = one_degree * 5.0;
373         }
374
375         switch (ev->direction) {
376         case GDK_SCROLL_LEFT:
377                 wv += step;
378                 width_control->set_value (wv);
379                 break;
380         case GDK_SCROLL_UP:
381                 pv -= step;
382                 position_control->set_value (pv);
383                 break;
384         case GDK_SCROLL_RIGHT:
385                 wv -= step;
386                 width_control->set_value (wv);
387                 break;
388         case GDK_SCROLL_DOWN:
389                 pv += step;
390                 position_control->set_value (pv);
391                 break;
392         }
393
394         return true;
395 }
396
397 bool
398 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
399 {
400         if (!dragging) {
401                 return false;
402         }
403
404         if (!drag_data_window) {
405                 drag_data_window = new Window (WINDOW_POPUP);
406                 drag_data_window->set_position (WIN_POS_MOUSE);
407                 drag_data_window->set_decorated (false);
408                 
409                 drag_data_label = manage (new Label);
410                 drag_data_label->set_use_markup (true);
411
412                 drag_data_window->set_border_width (6);
413                 drag_data_window->add (*drag_data_label);
414                 drag_data_label->show ();
415                 
416                 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
417                 if (toplevel) {
418                         drag_data_window->set_transient_for (*toplevel);
419                 }
420         }
421
422         if (!drag_data_window->is_visible ()) {
423                 /* move the window a little away from the mouse */
424                 drag_data_window->move (ev->x_root+30, ev->y_root+30);
425                 drag_data_window->present ();
426                 unset_tooltip ();
427         }
428
429         int w = get_width();
430         double delta = (ev->x - last_drag_x) / (double) w;
431         
432         if (dragging_left) {
433                 delta = -delta;
434         }
435
436         if (dragging_left || dragging_right) {
437
438                 /* maintain position as invariant as we change the width */
439
440                 double current_width = width_control->get_value ();
441
442                 if (fabs (current_width) < 0.1) {
443                         accumulated_delta += delta;
444                         /* in the detent - have we pulled far enough to escape ? */
445                         if (fabs (accumulated_delta) >= 0.1) {
446                                 width_control->set_value (current_width + accumulated_delta);
447                                 accumulated_delta = 0;
448                         }
449                 } else {
450                         width_control->set_value (current_width + delta);
451                 }
452
453         } else if (dragging_position) {
454
455                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
456                 position_control->set_value (pv + delta);
457         }
458
459         last_drag_x = ev->x;
460         return true;
461 }
462
463 bool
464 StereoPanner::on_key_press_event (GdkEventKey* ev)
465 {
466         double one_degree = 1.0/180.0;
467         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
468         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
469         double step;
470
471         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
472                 step = one_degree;
473         } else {
474                 step = one_degree * 5.0;
475         }
476
477         /* up/down control width because we consider pan position more "important"
478            (and thus having higher "sense" priority) than width.
479         */
480
481         switch (ev->keyval) {
482         case GDK_Up:
483                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
484                         width_control->set_value (1.0);
485                 } else {
486                         width_control->set_value (wv + step);
487                 }
488                 break;
489         case GDK_Down:
490                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
491                         width_control->set_value (-1.0);
492                 } else {
493                         width_control->set_value (wv - step);
494                 }
495
496         case GDK_Left:
497                 pv -= step;
498                 position_control->set_value (pv);
499                 break;
500         case GDK_Right:
501                 pv += step;
502                 position_control->set_value (pv);
503                 break;
504
505                 break;
506         case GDK_0:
507         case GDK_KP_0:
508                 width_control->set_value (0.0);
509                 break;
510
511         default: 
512                 return false;
513         }
514                 
515         return true;
516 }
517
518 bool
519 StereoPanner::on_key_release_event (GdkEventKey* ev)
520 {
521         return false;
522 }
523
524 bool
525 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
526 {
527         grab_focus ();
528         Keyboard::magic_widget_grab_focus ();
529         return false;
530 }
531
532 bool
533 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
534 {
535         Keyboard::magic_widget_drop_focus ();
536         return false;
537 }
538
539 void
540 StereoPanner::set_colors ()
541 {
542         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
543         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
544         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
545         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
546
547         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
548         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
549         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
550         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
551
552         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
553         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
554         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
555         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
556
557         color_change (); /* EMIT SIGNAL */
558 }