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