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