2 Copyright (C) 2000-2007 Paul Davis
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.
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.
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.
24 #include <gtkmm/window.h>
25 #include <pangomm/layout.h>
27 #include "pbd/controllable.h"
28 #include "pbd/compose.h"
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"
36 #include "ardour/pannable.h"
37 #include "ardour/panner.h"
38 #include "ardour/panner_shell.h"
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"
51 using namespace Gtkmm2ext;
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;
58 StereoPanner::ColorScheme StereoPanner::colors[3];
59 bool StereoPanner::have_colors = false;
61 Pango::AttrList StereoPanner::panner_font_attributes;
62 bool StereoPanner::have_font = false;
64 using namespace ARDOUR;
66 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
67 : PannerInterface (p->panner())
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)
76 , accumulated_delta (0)
78 , position_binder (position_control)
79 , width_binder (width_control)
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);
98 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
99 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
101 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
103 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
108 StereoPanner::~StereoPanner ()
114 StereoPanner::set_tooltip ()
116 if (_panner_shell->bypassed()) {
117 _tooltip.set_tip (_("bypassed"));
120 double pos = position_control->get_value(); // 0..1
122 /* We show the position of the center of the image relative to the left & right.
123 This is expressed as a pair of percentage values that ranges from (100,0)
124 (hard left) through (50,50) (hard center) to (0,100) (hard right).
126 This is pretty wierd, but its the way audio engineers expect it. Just remember that
127 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
131 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
132 (int) rint (100.0 * pos),
133 (int) floor (100.0 * width_control->get_value()));
134 _tooltip.set_tip (buf);
138 StereoPanner::on_expose_event (GdkEventExpose*)
140 Glib::RefPtr<Gdk::Window> win (get_window());
141 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
142 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
143 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
144 layout->set_attributes (panner_font_attributes);
148 const double pos = position_control->get_value (); /* 0..1 */
149 const double swidth = width_control->get_value (); /* -1..+1 */
150 const double fswidth = fabs (swidth);
151 const double corner_radius = 5.0;
152 uint32_t o, f, t, b, r;
156 height = get_height ();
160 } else if (swidth < 0.0) {
166 o = colors[state].outline;
167 f = colors[state].fill;
168 t = colors[state].text;
169 b = colors[state].background;
170 r = colors[state].rule;
172 if (_panner_shell->bypassed()) {
182 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
183 cairo_rectangle (context->cobj(), 0, 0, width, height);
184 context->fill_preserve ();
187 /* the usable width is reduced from the real width, because we need space for
188 the two halves of LR boxes that will extend past the actual left/right
189 positions (indicated by the vertical line segment above them).
192 double usable_width = width - lr_box_size;
194 /* compute the centers of the L/R boxes based on the current stereo width */
196 if (fmod (usable_width,2.0) == 0) {
197 /* even width, but we need odd, so that there is an exact center.
198 So, offset cairo by 1, and reduce effective width by 1
201 context->translate (1.0, 0.0);
204 const double half_lr_box = lr_box_size/2.0;
205 const double center = rint(half_lr_box + (usable_width * pos));
206 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
207 const double left = center - pan_spread;
208 const double right = center + pan_spread;
211 context->set_line_width (1.0);
212 context->move_to ((usable_width + lr_box_size)/2.0, 0);
213 context->rel_line_to (0, height);
214 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
217 /* compute & draw the line through the box */
218 context->set_line_width (2);
219 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
220 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
221 context->line_to (left, top_step + (pos_box_size/2.0));
222 context->line_to (right, top_step + (pos_box_size/2.0));
223 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
226 context->set_line_width (1.0);
230 rounded_rectangle (context, left - half_lr_box,
231 half_lr_box+step_down,
232 lr_box_size, lr_box_size, corner_radius);
233 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
234 context->fill_preserve();
235 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
239 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 layout->set_text (_("R"));
243 layout->set_text (_("L"));
245 layout->get_pixel_size(tw, th);
246 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
247 pango_cairo_show_layout (context->cobj(), layout->gobj());
251 rounded_rectangle (context, right - half_lr_box,
252 half_lr_box+step_down,
253 lr_box_size, lr_box_size, corner_radius);
254 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
255 context->fill_preserve();
256 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
260 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
263 layout->set_text (_("M"));
266 layout->set_text (_("L"));
268 layout->set_text (_("R"));
271 layout->get_pixel_size(tw, th);
272 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
273 pango_cairo_show_layout (context->cobj(), layout->gobj());
275 /* draw the central box */
276 context->set_line_width (2.0);
277 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
278 context->rel_line_to (0.0, pos_box_size); /* lower right */
279 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
280 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
281 context->rel_line_to (0.0, -pos_box_size); /* upper left */
282 context->close_path ();
284 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
285 context->stroke_preserve ();
286 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
293 StereoPanner::on_button_press_event (GdkEventButton* ev)
295 if (PannerInterface::on_button_press_event (ev)) {
299 if (_panner_shell->bypassed()) {
303 drag_start_x = ev->x;
306 dragging_position = false;
307 dragging_left = false;
308 dragging_right = false;
310 _tooltip.target_stop_drag ();
311 accumulated_delta = 0;
314 /* Let the binding proxies get first crack at the press event
318 if (position_binder.button_press_handler (ev)) {
322 if (width_binder.button_press_handler (ev)) {
327 if (ev->button != 1) {
331 if (ev->type == GDK_2BUTTON_PRESS) {
332 int width = get_width();
334 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
335 /* handled by button release */
341 /* upper section: adjusts position, constrained by width */
343 const double w = fabs (width_control->get_value ());
344 const double max_pos = 1.0 - (w/2.0);
345 const double min_pos = w/2.0;
347 if (ev->x <= width/3) {
348 /* left side dbl click */
349 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
350 /* 2ndary-double click on left, collapse to hard left */
351 width_control->set_value (0);
352 position_control->set_value (0);
354 position_control->set_value (min_pos);
356 } else if (ev->x > 2*width/3) {
357 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
358 /* 2ndary-double click on right, collapse to hard right */
359 width_control->set_value (0);
360 position_control->set_value (1.0);
362 position_control->set_value (max_pos);
365 position_control->set_value (0.5);
370 /* lower section: adjusts width, constrained by position */
372 const double p = position_control->get_value ();
373 const double max_width = 2.0 * min ((1.0 - p), p);
375 if (ev->x <= width/3) {
376 /* left side dbl click */
377 width_control->set_value (max_width); // reset width to 100%
378 } else if (ev->x > 2*width/3) {
379 /* right side dbl click */
380 width_control->set_value (-max_width); // reset width to inverted 100%
382 /* center dbl click */
383 width_control->set_value (0); // collapse width to 0%
388 _tooltip.target_stop_drag ();
390 } else if (ev->type == GDK_BUTTON_PRESS) {
392 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
393 /* handled by button release */
398 /* top section of widget is for position drags */
399 dragging_position = true;
400 StartPositionGesture ();
402 /* lower section is for dragging width */
404 double pos = position_control->get_value (); /* 0..1 */
405 double swidth = width_control->get_value (); /* -1..+1 */
406 double fswidth = fabs (swidth);
407 int usable_width = get_width() - lr_box_size;
408 double center = (lr_box_size/2.0) + (usable_width * pos);
409 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
410 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
411 const int half_box = lr_box_size/2;
413 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
415 dragging_right = true;
417 dragging_left = true;
419 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
421 dragging_left = true;
423 dragging_right = true;
426 StartWidthGesture ();
430 _tooltip.target_start_drag ();
437 StereoPanner::on_button_release_event (GdkEventButton* ev)
439 if (PannerInterface::on_button_release_event (ev)) {
443 if (ev->button != 1) {
447 if (_panner_shell->bypassed()) {
451 bool const dp = dragging_position;
454 _tooltip.target_stop_drag ();
455 dragging_position = false;
456 dragging_left = false;
457 dragging_right = false;
458 accumulated_delta = 0;
461 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
465 StopPositionGesture ();
475 StereoPanner::on_scroll_event (GdkEventScroll* ev)
477 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
478 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
479 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
482 if (_panner_shell->bypassed()) {
486 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
489 step = one_degree * 5.0;
492 switch (ev->direction) {
493 case GDK_SCROLL_LEFT:
495 width_control->set_value (wv);
499 position_control->set_value (pv);
501 case GDK_SCROLL_RIGHT:
503 width_control->set_value (wv);
505 case GDK_SCROLL_DOWN:
507 position_control->set_value (pv);
515 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
517 if (_panner_shell->bypassed()) {
524 int usable_width = get_width() - lr_box_size;
525 double delta = (ev->x - last_drag_x) / (double) usable_width;
526 double current_width = width_control->get_value ();
532 if (dragging_left || dragging_right) {
534 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
536 /* change width and position in a way that keeps the
537 * other side in the same place
542 double pv = position_control->get_value();
545 position_control->set_value (pv - delta);
547 position_control->set_value (pv + delta);
551 /* delta is positive, so we're about to
552 increase the width. But we need to increase it
553 by twice the required value so that the
554 other side remains in place when we set
555 the position as well.
557 width_control->set_value (current_width + (delta * 2.0));
559 width_control->set_value (current_width + delta);
566 /* maintain position as invariant as we change the width */
568 /* create a detent close to the center */
570 if (!detented && fabs (current_width) < 0.02) {
573 width_control->set_value (0);
578 accumulated_delta += delta;
580 /* have we pulled far enough to escape ? */
582 if (fabs (accumulated_delta) >= 0.025) {
583 width_control->set_value (current_width + accumulated_delta);
585 accumulated_delta = false;
589 /* width needs to change by 2 * delta because both L & R move */
590 width_control->set_value (current_width + (delta * 2.0));
594 } else if (dragging_position) {
596 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
597 position_control->set_value (pv + delta);
605 StereoPanner::on_key_press_event (GdkEventKey* ev)
607 double one_degree = 1.0/180.0;
608 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
609 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
612 if (_panner_shell->bypassed()) {
616 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
619 step = one_degree * 5.0;
622 /* up/down control width because we consider pan position more "important"
623 (and thus having higher "sense" priority) than width.
626 switch (ev->keyval) {
628 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
629 width_control->set_value (1.0);
631 width_control->set_value (wv + step);
635 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
636 width_control->set_value (-1.0);
638 width_control->set_value (wv - step);
644 position_control->set_value (pv);
648 position_control->set_value (pv);
652 width_control->set_value (0.0);
663 StereoPanner::set_colors ()
665 colors[Normal].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerFill();
666 colors[Normal].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerOutline();
667 colors[Normal].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerText();
668 colors[Normal].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerBackground();
669 colors[Normal].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
671 colors[Mono].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoFill();
672 colors[Mono].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoOutline();
673 colors[Mono].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoText();
674 colors[Mono].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoBackground();
675 colors[Mono].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
677 colors[Inverted].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedFill();
678 colors[Inverted].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedOutline();
679 colors[Inverted].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedText();
680 colors[Inverted].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedBackground();
681 colors[Inverted].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
685 StereoPanner::color_handler ()
692 StereoPanner::bypass_handler ()
698 StereoPanner::pannable_handler ()
700 panvalue_connections.drop_connections();
701 position_control = _panner->pannable()->pan_azimuth_control;
702 width_control = _panner->pannable()->pan_width_control;
703 position_binder.set_controllable(position_control);
704 width_binder.set_controllable(width_control);
706 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
707 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
712 StereoPanner::editor ()
714 return new StereoPannerEditor (this);