various improvements for stereo panner. note that dbl-click in "top" section will...
[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 {
68         if (!have_colors) {
69                 set_colors ();
70                 have_colors = true;
71         }
72
73         position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
74         width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
75         set_tooltip ();
76
77         set_flags (Gtk::CAN_FOCUS);
78
79         add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
80                     Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
81                     Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
82                     Gdk::SCROLL_MASK|
83                     Gdk::POINTER_MOTION_MASK);
84
85         color_change.connect (connections, invalidator (*this), boost::bind (&DrawingArea::queue_draw, this), gui_context());
86 }
87
88 StereoPanner::~StereoPanner ()
89 {
90 }
91
92 void
93 StereoPanner::set_tooltip ()
94 {
95         double pos = position_control->get_value(); // 0..1
96         
97         /* We show the position of the center of the image relative to the left & right.
98            This is expressed as a pair of percentage values that ranges from (100,0) 
99            (hard left) through (50,50) (hard center) to (0,100) (hard right).
100
101            This is pretty wierd, but its the way audio engineers expect it. Just remember that
102            the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
103         */
104
105         Gtkmm2ext::UI::instance()->set_tip (this, 
106                                             string_compose (_("L:%1 R:%2 Width: %3%%\n\n0 -> set width to zero\n%4-uparrow -> set width to 100\n%4-downarrow -> set width to -100"), 
107                                                             (int) rint (100.0 * (1.0 - pos)),
108                                                             (int) rint (100.0 * pos),
109                                                             (int) floor (100.0 * width_control->get_value()),
110                                                             Keyboard::secondary_modifier_name()).c_str());
111 }
112
113 void
114 StereoPanner::value_change ()
115 {
116         set_tooltip ();
117         queue_draw ();
118 }
119
120 bool
121 StereoPanner::on_expose_event (GdkEventExpose* ev)
122 {
123         Glib::RefPtr<Gdk::Window> win (get_window());
124         Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
125
126         cairo_t* cr = gdk_cairo_create (win->gobj());
127        
128         int width, height;
129         double pos = position_control->get_value (); /* 0..1 */
130         double swidth = width_control->get_value (); /* -1..+1 */
131         double fswidth = fabs (swidth);
132         uint32_t o, f, t, b;
133         State state;
134
135         width = get_width();
136         height = get_height ();
137
138         if (swidth == 0.0) {
139                 state = Mono;
140         } else if (swidth < 0.0) {
141                 state = Inverted;
142         } else { 
143                 state = Normal;
144         }
145
146         o = colors[state].outline;
147         f = colors[state].fill;
148         t = colors[state].text;
149         b = colors[state].background;
150
151         /* background */
152
153         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));
154         cairo_rectangle (cr, 0, 0, width, height);
155         cairo_fill (cr);
156
157         double usable_width = width - lr_box_size;
158
159         /* compute the centers of the L/R boxes based on the current stereo width */
160
161         if (fmod (usable_width,2.0) == 0) {
162                 /* even width, but we need odd, so that there is an exact center.
163                    So, offset cairo by 1, and reduce effective width by 1 
164                 */
165                 usable_width -= 1.0;
166                 cairo_translate (cr, 1.0, 0.0);
167         }
168
169         double center = (lr_box_size/2.0) + (usable_width * pos);
170         const double pan_spread = (fswidth * usable_width)/2.0;
171         const double half_lr_box = lr_box_size/2.0;
172         int left;
173         int right;
174
175         left = center - pan_spread;  // center of left box
176         right = center + pan_spread; // right of right box
177
178         /* compute & draw the line through the box */
179         
180         cairo_set_line_width (cr, 2);
181         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));
182         cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
183         cairo_line_to (cr, left, top_step+(pos_box_size/2));
184         cairo_line_to (cr, right, top_step+(pos_box_size/2));
185         cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
186         cairo_stroke (cr);
187
188         /* left box */
189
190         cairo_rectangle (cr, 
191                          left - half_lr_box,
192                          (lr_box_size/2)+step_down, 
193                          lr_box_size, lr_box_size);
194         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));
195         cairo_stroke_preserve (cr);
196         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));
197         cairo_fill (cr);
198         
199         /* add text */
200
201         cairo_move_to (cr, 
202                        left - half_lr_box + 3,
203                        (lr_box_size/2) + step_down + 13);
204         cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
205
206         if (state != Mono) {
207                 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));
208                 if (swidth < 0.0) {
209                         cairo_show_text (cr, _("R"));
210                 } else {
211                         cairo_show_text (cr, _("L"));
212                 }
213         }
214
215         /* right box */
216
217         cairo_rectangle (cr, 
218                          right - half_lr_box,
219                          (lr_box_size/2)+step_down, 
220                          lr_box_size, lr_box_size);
221         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));
222         cairo_stroke_preserve (cr);
223         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));
224         cairo_fill (cr);
225
226         /* add text */
227
228         cairo_move_to (cr, 
229                        right - half_lr_box + 3,
230                        (lr_box_size/2)+step_down + 13);
231         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));
232
233         if (state == Mono) {
234                 cairo_show_text (cr, _("M"));
235         } else {
236                 if (swidth < 0.0) {
237                         cairo_show_text (cr, _("L"));
238                 } else {
239                         cairo_show_text (cr, _("R"));
240                 }
241         }
242
243         /* draw the central box */
244
245         cairo_set_line_width (cr, 1);
246         cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
247         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));
248         cairo_stroke_preserve (cr);
249         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));
250         cairo_fill (cr);
251
252         /* done */
253
254         cairo_destroy (cr);
255         return true;
256 }
257
258 bool
259 StereoPanner::on_button_press_event (GdkEventButton* ev)
260 {
261         drag_start_x = ev->x;
262         last_drag_x = ev->x;
263         
264         dragging_position = false;
265         dragging_left = false;
266         dragging_right = false;
267         accumulated_delta = 0;
268
269         if (ev->y < 20) {
270                 /* top section of widget is for position drags */
271                 dragging_position = true;
272         } else {
273                 /* lower section is for dragging width */
274
275                 double pos = position_control->get_value (); /* 0..1 */
276                 double swidth = width_control->get_value (); /* -1..+1 */
277                 double fswidth = fabs (swidth);
278                 int usable_width = get_width() - lr_box_size;
279                 double center = (lr_box_size/2.0) + (usable_width * pos);
280                 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
281                 int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
282                 const int half_box = lr_box_size/2;
283
284                 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
285                         dragging_left = true;
286                 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
287                         dragging_right = true;
288                 }
289
290         }
291
292         if (ev->type == GDK_2BUTTON_PRESS) {
293                 if (dragging_position) {
294                         int width = get_width();
295                         if (ev->x >= width/2 - 10 && ev->x <= width/2 + 10) {
296                                 /* double click near center, reset position to center */
297                                 position_control->set_value (0.5); 
298                         } else {
299                                 if (ev->x < width/2) {
300                                         /* double click on left, collapse to hard left */
301                                         width_control->set_value (0);
302                                         position_control->set_value (0);
303                                 } else {
304                                         /* double click on right, collapse to hard right */
305                                         width_control->set_value (0);
306                                         position_control->set_value (1.0);
307                                 }
308                         }
309                 } else {
310                         if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
311                                 width_control->set_value (-1.0); // reset position to reversed full, LR
312                         } else {
313                                 width_control->set_value (1.0); // reset position to full, LR
314                         }
315                 }
316                 dragging = false;
317         } else {
318                 dragging = true;
319         }
320
321         return true;
322 }
323
324 bool
325 StereoPanner::on_button_release_event (GdkEventButton* ev)
326 {
327         dragging = false;
328         dragging_position = false;
329         dragging_left = false;
330         dragging_right = false;
331         accumulated_delta = 0;
332         return true;
333 }
334
335 bool
336 StereoPanner::on_scroll_event (GdkEventScroll* ev)
337 {
338         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
339         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
340         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
341         double step;
342         
343         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
344                 step = one_degree;
345         } else {
346                 step = one_degree * 5.0;
347         }
348
349         switch (ev->direction) {
350         case GDK_SCROLL_LEFT:
351                 wv += step;
352                 width_control->set_value (wv);
353                 break;
354         case GDK_SCROLL_UP:
355                 pv -= step;
356                 position_control->set_value (pv);
357                 break;
358         case GDK_SCROLL_RIGHT:
359                 wv -= step;
360                 width_control->set_value (wv);
361                 break;
362         case GDK_SCROLL_DOWN:
363                 pv += step;
364                 position_control->set_value (pv);
365                 break;
366         }
367
368         return true;
369 }
370
371 bool
372 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
373 {
374         if (!dragging) {
375                 return false;
376         }
377
378         int w = get_width();
379         double delta = (ev->x - last_drag_x) / (double) w;
380         
381         if (dragging_left) {
382                 delta = -delta;
383         }
384
385         if (dragging_left || dragging_right) {
386
387                 /* maintain position as invariant as we change the width */
388
389                 double current_width = width_control->get_value ();
390
391                 if (fabs (current_width) < 0.1) {
392                         accumulated_delta += delta;
393                         /* in the detent - have we pulled far enough to escape ? */
394                         if (fabs (accumulated_delta) >= 0.1) {
395                                 width_control->set_value (current_width + accumulated_delta);
396                                 accumulated_delta = 0;
397                         }
398                 } else {
399                         width_control->set_value (current_width + delta);
400                 }
401
402         } else if (dragging_position) {
403
404                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
405                 position_control->set_value (pv + delta);
406         }
407
408         last_drag_x = ev->x;
409         return true;
410 }
411
412 bool
413 StereoPanner::on_key_press_event (GdkEventKey* ev)
414 {
415         double one_degree = 1.0/180.0;
416         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
417         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
418         double step;
419
420         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
421                 step = one_degree;
422         } else {
423                 step = one_degree * 5.0;
424         }
425
426         /* up/down control width because we consider pan position more "important"
427            (and thus having higher "sense" priority) than width.
428         */
429
430         switch (ev->keyval) {
431         case GDK_Up:
432                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
433                         width_control->set_value (1.0);
434                 } else {
435                         width_control->set_value (wv + step);
436                 }
437                 break;
438         case GDK_Down:
439                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
440                         width_control->set_value (-1.0);
441                 } else {
442                         width_control->set_value (wv - step);
443                 }
444
445         case GDK_Left:
446                 pv -= step;
447                 position_control->set_value (pv);
448                 break;
449         case GDK_Right:
450                 pv += step;
451                 position_control->set_value (pv);
452                 break;
453
454                 break;
455         case GDK_0:
456         case GDK_KP_0:
457                 width_control->set_value (0.0);
458                 break;
459
460         default: 
461                 return false;
462         }
463                 
464         return true;
465 }
466
467 bool
468 StereoPanner::on_key_release_event (GdkEventKey* ev)
469 {
470         return false;
471 }
472
473 bool
474 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
475 {
476         grab_focus ();
477         Keyboard::magic_widget_grab_focus ();
478         return false;
479 }
480
481 bool
482 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
483 {
484         Keyboard::magic_widget_drop_focus ();
485         return false;
486 }
487
488 void
489 StereoPanner::set_colors ()
490 {
491         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
492         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
493         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
494         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
495
496         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
497         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
498         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
499         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
500
501         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
502         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
503         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
504         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
505
506         color_change (); /* EMIT SIGNAL */
507 }