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