new mono panner widget; make stereo panner respond to changes in colors immediately
[ardour.git] / gtk2_ardour / mono_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 "mono_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 MonoPanner::ColorScheme MonoPanner::colors;
54 bool MonoPanner::have_colors = false;
55
56 MonoPanner::MonoPanner (boost::shared_ptr<PBD::Controllable> position)
57         : position_control (position)
58         , dragging (false)
59         , drag_start_x (0)
60         , last_drag_x (0)
61         , accumulated_delta (0)
62         , detented (false)
63         , drag_data_window (0)
64         , drag_data_label (0)
65         , position_binder (position)
66 {
67         if (!have_colors) {
68                 set_colors ();
69                 have_colors = true;
70         }
71
72         position_control->Changed.connect (connections, invalidator(*this), boost::bind (&MonoPanner::value_change, this), gui_context());
73
74         set_flags (Gtk::CAN_FOCUS);
75
76         add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
77                     Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
78                     Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
79                     Gdk::SCROLL_MASK|
80                     Gdk::POINTER_MOTION_MASK);
81
82         ColorsChanged.connect (sigc::mem_fun (*this, &MonoPanner::color_handler));
83 }
84
85 MonoPanner::~MonoPanner ()
86 {
87         delete drag_data_window;
88 }
89
90 void
91 MonoPanner::set_drag_data ()
92 {
93         if (!drag_data_label) {
94                 return;
95         }
96
97         double pos = position_control->get_value(); // 0..1
98         
99         /* We show the position of the center of the image relative to the left & right.
100            This is expressed as a pair of percentage values that ranges from (100,0) 
101            (hard left) through (50,50) (hard center) to (0,100) (hard right).
102
103            This is pretty wierd, but its the way audio engineers expect it. Just remember that
104            the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
105         */
106
107         drag_data_label->set_markup (string_compose (_("L:%1 R:%2"),
108                                                      (int) rint (100.0 * (1.0 - pos)),
109                                                      (int) rint (100.0 * pos)));
110 }
111
112 void
113 MonoPanner::value_change ()
114 {
115         set_drag_data ();
116         queue_draw ();
117 }
118
119 bool
120 MonoPanner::on_expose_event (GdkEventExpose* ev)
121 {
122         Glib::RefPtr<Gdk::Window> win (get_window());
123         Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
124
125         cairo_t* cr = gdk_cairo_create (win->gobj());
126        
127         int width, height;
128         double pos = position_control->get_value (); /* 0..1 */
129         uint32_t o, f, t, b, pf, po;
130
131         width = get_width();
132         height = get_height ();
133
134         o = colors.outline;
135         f = colors.fill;
136         t = colors.text;
137         b = colors.background;
138         pf = colors.pos_fill;
139         po = colors.pos_outline;
140
141         /* background */
142
143         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));
144         cairo_rectangle (cr, 0, 0, width, height);
145         cairo_fill (cr);
146
147         /* the usable width is reduced from the real width, because we need space for 
148            the two halves of LR boxes that will extend past the actual left/right
149            positions (indicated by the vertical line segment above them).
150         */
151
152         double usable_width = width - lr_box_size;
153
154         /* compute the centers of the L/R boxes based on the current stereo width */
155
156         if (fmod (usable_width,2.0) == 0) {
157                 /* even width, but we need odd, so that there is an exact center.
158                    So, offset cairo by 1, and reduce effective width by 1 
159                 */
160                 usable_width -= 1.0;
161                 cairo_translate (cr, 1.0, 0.0);
162         }
163
164         double center = (lr_box_size/2.0) + (usable_width * pos);
165         const double half_lr_box = lr_box_size/2.0;
166         double left;
167         double right;
168
169         left = 4 + half_lr_box; // center of left box
170         right = width  - 4 - half_lr_box; // center of right box
171
172         /* center line */
173         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));
174         cairo_set_line_width (cr, 1.0);
175         cairo_move_to (cr, width/2.0, 0);
176         cairo_line_to (cr, width/2.0, height);
177         cairo_stroke (cr);
178
179         /* left box */
180
181         cairo_rectangle (cr, 
182                          left - half_lr_box,
183                          half_lr_box+step_down, 
184                          lr_box_size, lr_box_size);
185         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));
186         cairo_stroke_preserve (cr);
187         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));
188         cairo_fill (cr);
189         
190         /* add text */
191
192         cairo_move_to (cr, 
193                        left - half_lr_box + 3,
194                        (lr_box_size/2) + step_down + 13);
195         cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
196         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));
197         cairo_show_text (cr, _("L"));
198
199         /* right box */
200
201         cairo_rectangle (cr, 
202                          right - half_lr_box,
203                          half_lr_box+step_down, 
204                          lr_box_size, lr_box_size);
205         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));
206         cairo_stroke_preserve (cr);
207         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));
208         cairo_fill (cr);
209
210         /* add text */
211
212         cairo_move_to (cr, 
213                        right - half_lr_box + 3,
214                        (lr_box_size/2)+step_down + 13);
215         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));
216         cairo_show_text (cr, _("R"));
217
218         /* 2 lines that connect them both */
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_set_line_width (cr, 1.0);
221         cairo_move_to (cr, left + half_lr_box, half_lr_box+step_down);
222         cairo_line_to (cr, right - half_lr_box, half_lr_box+step_down);
223         cairo_stroke (cr);
224
225
226         cairo_move_to (cr, left + half_lr_box, half_lr_box+step_down+lr_box_size);
227         cairo_line_to (cr, right - half_lr_box, half_lr_box+step_down+lr_box_size);
228         cairo_stroke (cr);
229
230         /* draw the position indicator */
231
232         cairo_set_line_width (cr, 2.0);
233         cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
234         cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(po), UINT_RGBA_G_FLT(po), UINT_RGBA_B_FLT(po), UINT_RGBA_A_FLT(po));
235         cairo_stroke_preserve (cr);
236         cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(pf), UINT_RGBA_G_FLT(pf), UINT_RGBA_B_FLT(pf), UINT_RGBA_A_FLT(pf));
237         cairo_fill (cr);
238
239         /* done */
240
241         cairo_destroy (cr);
242         return true;
243 }
244
245 bool
246 MonoPanner::on_button_press_event (GdkEventButton* ev)
247 {
248         drag_start_x = ev->x;
249         last_drag_x = ev->x;
250         
251         dragging = false;
252         accumulated_delta = 0;
253         detented = false;
254
255         /* Let the binding proxies get first crack at the press event
256          */
257
258         if (ev->y < 20) {
259                 if (position_binder.button_press_handler (ev)) {
260                         return true;
261                 }
262         }
263         
264         if (ev->button != 1) {
265                 return false;
266         }
267
268         if (ev->type == GDK_2BUTTON_PRESS) {
269                 int width = get_width();
270
271                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
272                         /* handled by button release */
273                         return true;
274                 }
275
276                         
277                 if (ev->x <= width/3) {
278                         /* left side dbl click */
279                         position_control->set_value (0);
280                 } else if (ev->x > 2*width/3) {
281                         position_control->set_value (1.0);
282                 } else {
283                         position_control->set_value (0.5);
284                 }
285
286                 dragging = false;
287
288         } else if (ev->type == GDK_BUTTON_PRESS) {
289
290                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
291                         /* handled by button release */
292                         return true;
293                 }
294
295                 dragging = true;
296         }
297
298         return true;
299 }
300
301 bool
302 MonoPanner::on_button_release_event (GdkEventButton* ev)
303 {
304         if (ev->button != 1) {
305                 return false;
306         }
307
308         dragging = false;
309         accumulated_delta = 0;
310         detented = false;
311
312         if (drag_data_window) {
313                 drag_data_window->hide ();
314         }
315         
316         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
317                 /* reset to default */
318                 position_control->set_value (0.5);
319         }
320
321         return true;
322 }
323
324 bool
325 MonoPanner::on_scroll_event (GdkEventScroll* ev)
326 {
327         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
328         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
329         double step;
330         
331         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
332                 step = one_degree;
333         } else {
334                 step = one_degree * 5.0;
335         }
336
337         switch (ev->direction) {
338         case GDK_SCROLL_UP:
339         case GDK_SCROLL_LEFT:
340                 pv -= step;
341                 position_control->set_value (pv);
342                 break;
343         case GDK_SCROLL_DOWN:
344         case GDK_SCROLL_RIGHT:
345                 pv += step;
346                 position_control->set_value (pv);
347                 break;
348         }
349
350         return true;
351 }
352
353 bool
354 MonoPanner::on_motion_notify_event (GdkEventMotion* ev)
355 {
356         if (!dragging) {
357                 return false;
358         }
359
360         if (!drag_data_window) {
361                 drag_data_window = new Window (WINDOW_POPUP);
362                 drag_data_window->set_position (WIN_POS_MOUSE);
363                 drag_data_window->set_decorated (false);
364                 
365                 drag_data_label = manage (new Label);
366                 drag_data_label->set_use_markup (true);
367
368                 drag_data_window->set_border_width (6);
369                 drag_data_window->add (*drag_data_label);
370                 drag_data_label->show ();
371                 
372                 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
373                 if (toplevel) {
374                         drag_data_window->set_transient_for (*toplevel);
375                 }
376         }
377
378         if (!drag_data_window->is_visible ()) {
379                 /* move the window a little away from the mouse */
380                 drag_data_window->move (ev->x_root+30, ev->y_root+30);
381                 drag_data_window->present ();
382         }
383
384         int w = get_width();
385         double delta = (ev->x - last_drag_x) / (double) w;
386         
387         /* create a detent close to the center */
388         
389         if (!detented && ARDOUR::Panner::equivalent (position_control->get_value(), 0.5)) {
390                 detented = true;
391                 /* snap to center */
392                 position_control->set_value (0.5);
393         }
394         
395         if (detented) {
396                 accumulated_delta += delta;
397                 
398                 /* have we pulled far enough to escape ? */
399                 
400                 if (fabs (accumulated_delta) >= 0.025) {
401                         position_control->set_value (position_control->get_value() + accumulated_delta);
402                         detented = false;
403                         accumulated_delta = false;
404                 }
405         } else {
406                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
407                 position_control->set_value (pv + delta);
408         }
409
410         last_drag_x = ev->x;
411         return true;
412 }
413
414 bool
415 MonoPanner::on_key_press_event (GdkEventKey* ev)
416 {
417         double one_degree = 1.0/180.0;
418         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
419         double step;
420
421         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
422                 step = one_degree;
423         } else {
424                 step = one_degree * 5.0;
425         }
426
427         /* up/down control width because we consider pan position more "important"
428            (and thus having higher "sense" priority) than width.
429         */
430
431         switch (ev->keyval) {
432         case GDK_Left:
433                 pv -= step;
434                 position_control->set_value (pv);
435                 break;
436         case GDK_Right:
437                 pv += step;
438                 position_control->set_value (pv);
439                 break;
440         default: 
441                 return false;
442         }
443                 
444         return true;
445 }
446
447 bool
448 MonoPanner::on_key_release_event (GdkEventKey* ev)
449 {
450         return false;
451 }
452
453 bool
454 MonoPanner::on_enter_notify_event (GdkEventCrossing* ev)
455 {
456         grab_focus ();
457         Keyboard::magic_widget_grab_focus ();
458         return false;
459 }
460
461 bool
462 MonoPanner::on_leave_notify_event (GdkEventCrossing*)
463 {
464         Keyboard::magic_widget_drop_focus ();
465         return false;
466 }
467
468 void
469 MonoPanner::set_colors ()
470 {
471         colors.fill = ARDOUR_UI::config()->canvasvar_MonoPannerFill.get();
472         colors.outline = ARDOUR_UI::config()->canvasvar_MonoPannerOutline.get();
473         colors.text = ARDOUR_UI::config()->canvasvar_MonoPannerText.get();
474         colors.background = ARDOUR_UI::config()->canvasvar_MonoPannerBackground.get();
475         colors.pos_outline = ARDOUR_UI::config()->canvasvar_MonoPannerPositionOutline.get();
476         colors.pos_fill = ARDOUR_UI::config()->canvasvar_MonoPannerPositionFill.get();
477 }
478
479 void
480 MonoPanner::color_handler ()
481 {
482         set_colors ();
483         queue_draw ();
484 }