refine stereo-panner GUI:
[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 #include <iostream>
20 #include <iomanip>
21 #include <cstring>
22 #include <cmath>
23
24 #include <gtkmm/window.h>
25 #include <pangomm/layout.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 #include "gtkmm2ext/persistent_tooltip.h"
35
36 #include "ardour/pannable.h"
37 #include "ardour/panner.h"
38 #include "ardour/panner_shell.h"
39
40 #include "ardour_ui.h"
41 #include "global_signals.h"
42 #include "stereo_panner.h"
43 #include "stereo_panner_editor.h"
44 #include "rgb_macros.h"
45 #include "utils.h"
46
47 #include "i18n.h"
48
49 using namespace std;
50 using namespace Gtk;
51 using namespace Gtkmm2ext;
52
53 static const int pos_box_size = 8;
54 static const int lr_box_size = 15;
55 static const int step_down = 10;
56 static const int top_step = 2;
57
58 StereoPanner::ColorScheme StereoPanner::colors[3];
59 bool StereoPanner::have_colors = false;
60
61 Pango::AttrList StereoPanner::panner_font_attributes;
62 bool            StereoPanner::have_font = false;
63
64 using namespace ARDOUR;
65
66 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
67         : PannerInterface (p->panner())
68         , _panner_shell (p)
69         , position_control (_panner->pannable()->pan_azimuth_control)
70         , width_control (_panner->pannable()->pan_width_control)
71         , dragging_position (false)
72         , dragging_left (false)
73         , dragging_right (false)
74         , drag_start_x (0)
75         , last_drag_x (0)
76         , accumulated_delta (0)
77         , detented (false)
78         , position_binder (position_control)
79         , width_binder (width_control)
80         , _dragging (false)
81 {
82         if (!have_colors) {
83                 set_colors ();
84                 have_colors = true;
85         }
86         if (!have_font) {
87                 Pango::FontDescription font;
88                 Pango::AttrFontDesc* font_attr;
89                 font = Pango::FontDescription ("ArdourMono");
90                 font.set_weight (Pango::WEIGHT_BOLD);
91                 font.set_size(9 * PANGO_SCALE);
92                 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
93                 panner_font_attributes.change(*font_attr);
94                 delete font_attr;
95                 have_font = true;
96         }
97
98         position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
99         width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
100         _panner_shell->Changed.connect (connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
101
102         ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
103
104         set_tooltip ();
105 }
106
107 StereoPanner::~StereoPanner ()
108 {
109
110 }
111
112 void
113 StereoPanner::set_tooltip ()
114 {
115         if (_panner_shell->bypassed()) {
116                 _tooltip.set_tip (_("bypassed"));
117                 return;
118         }
119         double pos = position_control->get_value(); // 0..1
120
121         /* We show the position of the center of the image relative to the left & right.
122            This is expressed as a pair of percentage values that ranges from (100,0)
123            (hard left) through (50,50) (hard center) to (0,100) (hard right).
124
125            This is pretty wierd, but its the way audio engineers expect it. Just remember that
126            the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
127         */
128
129         char buf[64];
130         snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
131                   (int) rint (100.0 * pos),
132                   (int) floor (100.0 * width_control->get_value()));
133         _tooltip.set_tip (buf);
134 }
135
136 bool
137 StereoPanner::on_expose_event (GdkEventExpose*)
138 {
139         Glib::RefPtr<Gdk::Window> win (get_window());
140         Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
141         Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
142         Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
143         layout->set_attributes (panner_font_attributes);
144
145         int tw, th;
146         int width, height;
147         const double pos = position_control->get_value (); /* 0..1 */
148         const double swidth = width_control->get_value (); /* -1..+1 */
149         const double fswidth = fabs (swidth);
150         const double corner_radius = 5.0;
151         uint32_t o, f, t, b, r;
152         State state;
153
154         width = get_width();
155         height = get_height ();
156
157         if (swidth == 0.0) {
158                 state = Mono;
159         } else if (swidth < 0.0) {
160                 state = Inverted;
161         } else {
162                 state = Normal;
163         }
164
165         o = colors[state].outline;
166         f = colors[state].fill;
167         t = colors[state].text;
168         b = colors[state].background;
169         r = colors[state].rule;
170
171         if (_panner_shell->bypassed()) {
172                 b  = 0x20202040;
173                 f  = 0x404040ff;
174                 o  = 0x606060ff;
175                 t  = 0x606060ff;
176                 r  = 0x606060ff;
177         }
178
179         /* background */
180
181         context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
182         cairo_rectangle (context->cobj(), 0, 0, width, height);
183         context->fill_preserve ();
184         context->clip();
185
186         /* the usable width is reduced from the real width, because we need space for
187            the two halves of LR boxes that will extend past the actual left/right
188            positions (indicated by the vertical line segment above them).
189         */
190
191         double usable_width = width - lr_box_size;
192
193         /* compute the centers of the L/R boxes based on the current stereo width */
194
195         if (fmod (usable_width,2.0) == 0) {
196                 /* even width, but we need odd, so that there is an exact center.
197                    So, offset cairo by 1, and reduce effective width by 1
198                 */
199                 usable_width -= 1.0;
200                 context->translate (1.0, 0.0);
201         }
202
203         const double half_lr_box = lr_box_size/2.0;
204         const double center = rint(half_lr_box + (usable_width * pos));
205         const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
206         const double left  = center - pan_spread;
207         const double right = center + pan_spread;
208
209         printf("L %.2f  R %.2f  PW:%.2f\n", left, right, pan_spread);
210
211         /* center line */
212         context->set_line_width (1.0);
213         context->move_to ((usable_width + lr_box_size)/2.0, 0);
214         context->rel_line_to (0, height);
215         context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
216         context->stroke ();
217
218         /* compute & draw the line through the box */
219         context->set_line_width (2);
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->move_to (left,  top_step + (pos_box_size/2.0) + step_down + 1.0);
222         context->line_to (left,  top_step + (pos_box_size/2.0));
223         context->line_to (right, top_step + (pos_box_size/2.0));
224         context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
225         context->stroke ();
226
227         context->set_line_width (1.0);
228
229         /* left box */
230         if (state != Mono) {
231                 rounded_rectangle (context, left - half_lr_box,
232                                 half_lr_box+step_down,
233                                 lr_box_size, lr_box_size, corner_radius);
234                 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
235                 context->fill_preserve();
236                 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
237                 context->stroke();
238
239                 /* add text */
240                 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
241                 if (swidth < 0.0) {
242                         layout->set_text (_("R"));
243                 } else {
244                         layout->set_text (_("L"));
245                 }
246                 layout->get_pixel_size(tw, th);
247                 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
248                 pango_cairo_show_layout (context->cobj(), layout->gobj());
249         }
250
251         /* right box */
252         rounded_rectangle (context, right - half_lr_box,
253                         half_lr_box+step_down,
254                         lr_box_size, lr_box_size, corner_radius);
255         context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
256         context->fill_preserve();
257         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
258         context->stroke();
259
260         /* add text */
261         context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
262
263         if (state == Mono) {
264                 layout->set_text (_("M"));
265         } else {
266                 if (swidth < 0.0) {
267                         layout->set_text (_("L"));
268                 } else {
269                         layout->set_text (_("R"));
270                 }
271         }
272         layout->get_pixel_size(tw, th);
273         context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
274         pango_cairo_show_layout (context->cobj(), layout->gobj());
275
276         /* draw the central box */
277         context->set_line_width (2.0);
278         context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
279         context->rel_line_to (0.0, pos_box_size); /* lower right */
280         context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
281         context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
282         context->rel_line_to (0.0, -pos_box_size); /* upper left */
283         context->close_path ();
284
285         context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
286         context->stroke_preserve ();
287         context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
288         context->fill ();
289
290         return true;
291 }
292
293 bool
294 StereoPanner::on_button_press_event (GdkEventButton* ev)
295 {
296         if (PannerInterface::on_button_press_event (ev)) {
297                 return true;
298         }
299
300         if (_panner_shell->bypassed()) {
301                 return true;
302         }
303         
304         drag_start_x = ev->x;
305         last_drag_x = ev->x;
306
307         dragging_position = false;
308         dragging_left = false;
309         dragging_right = false;
310         _dragging = false;
311         _tooltip.target_stop_drag ();
312         accumulated_delta = 0;
313         detented = false;
314
315         /* Let the binding proxies get first crack at the press event
316          */
317
318         if (ev->y < 20) {
319                 if (position_binder.button_press_handler (ev)) {
320                         return true;
321                 }
322         } else {
323                 if (width_binder.button_press_handler (ev)) {
324                         return true;
325                 }
326         }
327
328         if (ev->button != 1) {
329                 return false;
330         }
331
332         if (ev->type == GDK_2BUTTON_PRESS) {
333                 int width = get_width();
334
335                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
336                         /* handled by button release */
337                         return true;
338                 }
339
340                 if (ev->y < 20) {
341
342                         /* upper section: adjusts position, constrained by width */
343
344                         const double w = fabs (width_control->get_value ());
345                         const double max_pos = 1.0 - (w/2.0);
346                         const double min_pos = w/2.0;
347
348                         if (ev->x <= width/3) {
349                                 /* left side dbl click */
350                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
351                                         /* 2ndary-double click on left, collapse to hard left */
352                                         width_control->set_value (0);
353                                         position_control->set_value (0);
354                                 } else {
355                                         position_control->set_value (min_pos);
356                                 }
357                         } else if (ev->x > 2*width/3) {
358                                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
359                                         /* 2ndary-double click on right, collapse to hard right */
360                                         width_control->set_value (0);
361                                         position_control->set_value (1.0);
362                                 } else {
363                                         position_control->set_value (max_pos);
364                                 }
365                         } else {
366                                 position_control->set_value (0.5);
367                         }
368
369                 } else {
370
371                         /* lower section: adjusts width, constrained by position */
372
373                         const double p = position_control->get_value ();
374                         const double max_width = 2.0 * min ((1.0 - p), p);
375
376                         if (ev->x <= width/3) {
377                                 /* left side dbl click */
378                                 width_control->set_value (max_width); // reset width to 100%
379                         } else if (ev->x > 2*width/3) {
380                                 /* right side dbl click */
381                                 width_control->set_value (-max_width); // reset width to inverted 100%
382                         } else {
383                                 /* center dbl click */
384                                 width_control->set_value (0); // collapse width to 0%
385                         }
386                 }
387
388                 _dragging = false;
389                 _tooltip.target_stop_drag ();
390
391         } else if (ev->type == GDK_BUTTON_PRESS) {
392
393                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
394                         /* handled by button release */
395                         return true;
396                 }
397
398                 if (ev->y < 20) {
399                         /* top section of widget is for position drags */
400                         dragging_position = true;
401                         StartPositionGesture ();
402                 } else {
403                         /* lower section is for dragging width */
404
405                         double pos = position_control->get_value (); /* 0..1 */
406                         double swidth = width_control->get_value (); /* -1..+1 */
407                         double fswidth = fabs (swidth);
408                         int usable_width = get_width() - lr_box_size;
409                         double center = (lr_box_size/2.0) + (usable_width * pos);
410                         int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
411                         int right = lrint (center +  (fswidth * usable_width / 2.0)); // center of rightmost box
412                         const int half_box = lr_box_size/2;
413
414                         if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
415                                 if (swidth < 0.0) {
416                                         dragging_right = true;
417                                 } else {
418                                         dragging_left = true;
419                                 }
420                         } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
421                                 if (swidth < 0.0) {
422                                         dragging_left = true;
423                                 } else {
424                                         dragging_right = true;
425                                 }
426                         }
427                         StartWidthGesture ();
428                 }
429
430                 _dragging = true;
431                 _tooltip.target_start_drag ();
432         }
433
434         return true;
435 }
436
437 bool
438 StereoPanner::on_button_release_event (GdkEventButton* ev)
439 {
440         if (PannerInterface::on_button_release_event (ev)) {
441                 return true;
442         }
443         
444         if (ev->button != 1) {
445                 return false;
446         }
447
448         if (_panner_shell->bypassed()) {
449                 return false;
450         }
451
452         bool const dp = dragging_position;
453
454         _dragging = false;
455         _tooltip.target_stop_drag ();
456         dragging_position = false;
457         dragging_left = false;
458         dragging_right = false;
459         accumulated_delta = 0;
460         detented = false;
461
462         if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
463                 _panner->reset ();
464         } else {
465                 if (dp) {
466                         StopPositionGesture ();
467                 } else {
468                         StopWidthGesture ();
469                 }
470         }
471
472         return true;
473 }
474
475 bool
476 StereoPanner::on_scroll_event (GdkEventScroll* ev)
477 {
478         double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
479         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
480         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
481         double step;
482
483         if (_panner_shell->bypassed()) {
484                 return false;
485         }
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         switch (ev->direction) {
494         case GDK_SCROLL_LEFT:
495                 wv += step;
496                 width_control->set_value (wv);
497                 break;
498         case GDK_SCROLL_UP:
499                 pv -= step;
500                 position_control->set_value (pv);
501                 break;
502         case GDK_SCROLL_RIGHT:
503                 wv -= step;
504                 width_control->set_value (wv);
505                 break;
506         case GDK_SCROLL_DOWN:
507                 pv += step;
508                 position_control->set_value (pv);
509                 break;
510         }
511
512         return true;
513 }
514
515 bool
516 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
517 {
518         if (_panner_shell->bypassed()) {
519                 _dragging = false;
520         }
521         if (!_dragging) {
522                 return false;
523         }
524
525         int usable_width = get_width() - lr_box_size;
526         double delta = (ev->x - last_drag_x) / (double) usable_width;
527         double current_width = width_control->get_value ();
528
529         if (dragging_left) {
530                 delta = -delta;
531         }
532         
533         if (dragging_left || dragging_right) {
534
535                 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
536
537                         /* change width and position in a way that keeps the
538                          * other side in the same place
539                          */
540
541                         _panner->freeze ();
542                         
543                         double pv = position_control->get_value();
544
545                         if (dragging_left) {
546                                 position_control->set_value (pv - delta);
547                         } else {
548                                 position_control->set_value (pv + delta);
549                         }
550
551                         if (delta > 0.0) {
552                                 /* delta is positive, so we're about to
553                                    increase the width. But we need to increase it
554                                    by twice the required value so that the
555                                    other side remains in place when we set
556                                    the position as well.
557                                 */
558                                 width_control->set_value (current_width + (delta * 2.0));
559                         } else {
560                                 width_control->set_value (current_width + delta);
561                         }
562
563                         _panner->thaw ();
564
565                 } else {
566
567                         /* maintain position as invariant as we change the width */
568                         
569                         /* create a detent close to the center */
570                         
571                         if (!detented && fabs (current_width) < 0.02) {
572                                 detented = true;
573                                 /* snap to zero */
574                                 width_control->set_value (0);
575                         }
576                         
577                         if (detented) {
578                                 
579                                 accumulated_delta += delta;
580                                 
581                                 /* have we pulled far enough to escape ? */
582                                 
583                                 if (fabs (accumulated_delta) >= 0.025) {
584                                         width_control->set_value (current_width + accumulated_delta);
585                                         detented = false;
586                                         accumulated_delta = false;
587                                 }
588                                 
589                         } else {
590                                 /* width needs to change by 2 * delta because both L & R move */
591                                 width_control->set_value (current_width + (delta * 2.0));
592                         }
593                 }
594
595         } else if (dragging_position) {
596
597                 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
598                 position_control->set_value (pv + delta);
599         }
600
601         last_drag_x = ev->x;
602         return true;
603 }
604
605 bool
606 StereoPanner::on_key_press_event (GdkEventKey* ev)
607 {
608         double one_degree = 1.0/180.0;
609         double pv = position_control->get_value(); // 0..1.0 ; 0 = left
610         double wv = width_control->get_value(); // 0..1.0 ; 0 = left
611         double step;
612
613         if (_panner_shell->bypassed()) {
614                 return false;
615         }
616
617         if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
618                 step = one_degree;
619         } else {
620                 step = one_degree * 5.0;
621         }
622
623         /* up/down control width because we consider pan position more "important"
624            (and thus having higher "sense" priority) than width.
625         */
626
627         switch (ev->keyval) {
628         case GDK_Up:
629                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
630                         width_control->set_value (1.0);
631                 } else {
632                         width_control->set_value (wv + step);
633                 }
634                 break;
635         case GDK_Down:
636                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
637                         width_control->set_value (-1.0);
638                 } else {
639                         width_control->set_value (wv - step);
640                 }
641                 break;
642
643         case GDK_Left:
644                 pv -= step;
645                 position_control->set_value (pv);
646                 break;
647         case GDK_Right:
648                 pv += step;
649                 position_control->set_value (pv);
650                 break;
651         case GDK_0:
652         case GDK_KP_0:
653                 width_control->set_value (0.0);
654                 break;
655
656         default:
657                 return false;
658         }
659
660         return true;
661 }
662
663 void
664 StereoPanner::set_colors ()
665 {
666         colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
667         colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
668         colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
669         colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
670         colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
671
672         colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
673         colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
674         colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
675         colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
676         colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
677
678         colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
679         colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
680         colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
681         colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
682         colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
683 }
684
685 void
686 StereoPanner::color_handler ()
687 {
688         set_colors ();
689         queue_draw ();
690 }
691
692 void
693 StereoPanner::bypass_handler ()
694 {
695         queue_draw ();
696 }
697
698 PannerEditor*
699 StereoPanner::editor ()
700 {
701         return new StereoPannerEditor (this);
702 }