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 "canvas/colors.h"
42 #include "ardour_ui.h"
43 #include "global_signals.h"
44 #include "stereo_panner.h"
45 #include "stereo_panner_editor.h"
46 #include "rgb_macros.h"
53 using namespace Gtkmm2ext;
54 using namespace ARDOUR_UI_UTILS;
56 static const int pos_box_size = 8;
57 static const int lr_box_size = 15;
58 static const int step_down = 10;
59 static const int top_step = 2;
61 StereoPanner::ColorScheme StereoPanner::colors[3];
62 bool StereoPanner::have_colors = false;
64 Pango::AttrList StereoPanner::panner_font_attributes;
65 bool StereoPanner::have_font = false;
67 using namespace ARDOUR;
69 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
70 : PannerInterface (p->panner())
72 , position_control (_panner->pannable()->pan_azimuth_control)
73 , width_control (_panner->pannable()->pan_width_control)
74 , dragging_position (false)
75 , dragging_left (false)
76 , dragging_right (false)
79 , accumulated_delta (0)
81 , position_binder (position_control)
82 , width_binder (width_control)
90 Pango::FontDescription font;
91 Pango::AttrFontDesc* font_attr;
92 font = Pango::FontDescription (ARDOUR_UI::config()->get_SmallBoldMonospaceFont());
93 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
94 panner_font_attributes.change(*font_attr);
99 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
100 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
102 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
103 _panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::pannable_handler, this), gui_context());
105 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
110 StereoPanner::~StereoPanner ()
116 StereoPanner::set_tooltip ()
118 if (_panner_shell->bypassed()) {
119 _tooltip.set_tip (_("bypassed"));
122 double pos = position_control->get_value(); // 0..1
124 /* We show the position of the center of the image relative to the left & right.
125 This is expressed as a pair of percentage values that ranges from (100,0)
126 (hard left) through (50,50) (hard center) to (0,100) (hard right).
128 This is pretty wierd, but its the way audio engineers expect it. Just remember that
129 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
133 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
134 (int) rint (100.0 * pos),
135 (int) floor (100.0 * width_control->get_value()));
136 _tooltip.set_tip (buf);
140 StereoPanner::on_expose_event (GdkEventExpose*)
142 Glib::RefPtr<Gdk::Window> win (get_window());
143 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
144 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
145 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
146 layout->set_attributes (panner_font_attributes);
150 const double pos = position_control->get_value (); /* 0..1 */
151 const double swidth = width_control->get_value (); /* -1..+1 */
152 const double fswidth = fabs (swidth);
153 const double corner_radius = 5.0;
154 uint32_t o, f, t, b, r;
158 height = get_height ();
162 } else if (swidth < 0.0) {
168 o = colors[state].outline;
169 f = colors[state].fill;
170 t = colors[state].text;
171 b = colors[state].background;
172 r = colors[state].rule;
174 if (_panner_shell->bypassed()) {
183 b = ARDOUR_UI::config()->color ("send strip base");
184 // b = rgba_from_style("SendStripBase",
185 // UINT_RGBA_R(b), UINT_RGBA_G(b), UINT_RGBA_B(b), 255,
190 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
191 cairo_rectangle (context->cobj(), 0, 0, width, height);
192 context->fill_preserve ();
195 /* the usable width is reduced from the real width, because we need space for
196 the two halves of LR boxes that will extend past the actual left/right
197 positions (indicated by the vertical line segment above them).
200 double usable_width = width - lr_box_size;
202 /* compute the centers of the L/R boxes based on the current stereo width */
204 if (fmod (usable_width,2.0) == 0) {
205 /* even width, but we need odd, so that there is an exact center.
206 So, offset cairo by 1, and reduce effective width by 1
209 context->translate (1.0, 0.0);
212 const double half_lr_box = lr_box_size/2.0;
213 const double center = rint(half_lr_box + (usable_width * pos));
214 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
215 const double left = center - pan_spread;
216 const double right = center + pan_spread;
219 context->set_line_width (1.0);
220 context->move_to ((usable_width + lr_box_size)/2.0, 0);
221 context->rel_line_to (0, height);
222 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
225 /* compute & draw the line through the box */
226 context->set_line_width (2);
227 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
228 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
229 context->line_to (left, top_step + (pos_box_size/2.0));
230 context->line_to (right, top_step + (pos_box_size/2.0));
231 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
234 context->set_line_width (1.0);
238 rounded_rectangle (context, left - half_lr_box,
239 half_lr_box+step_down,
240 lr_box_size, lr_box_size, corner_radius);
241 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
242 context->fill_preserve();
243 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
247 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
249 layout->set_text (_("R"));
251 layout->set_text (_("L"));
253 layout->get_pixel_size(tw, th);
254 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
255 pango_cairo_show_layout (context->cobj(), layout->gobj());
259 rounded_rectangle (context, right - half_lr_box,
260 half_lr_box+step_down,
261 lr_box_size, lr_box_size, corner_radius);
262 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
263 context->fill_preserve();
264 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
268 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
271 layout->set_text (_("M"));
274 layout->set_text (_("L"));
276 layout->set_text (_("R"));
279 layout->get_pixel_size(tw, th);
280 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
281 pango_cairo_show_layout (context->cobj(), layout->gobj());
283 /* draw the central box */
284 context->set_line_width (2.0);
285 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
286 context->rel_line_to (0.0, pos_box_size); /* lower right */
287 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
288 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
289 context->rel_line_to (0.0, -pos_box_size); /* upper left */
290 context->close_path ();
292 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
293 context->stroke_preserve ();
294 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
301 StereoPanner::on_button_press_event (GdkEventButton* ev)
303 if (PannerInterface::on_button_press_event (ev)) {
307 if (_panner_shell->bypassed()) {
311 drag_start_x = ev->x;
314 dragging_position = false;
315 dragging_left = false;
316 dragging_right = false;
318 _tooltip.target_stop_drag ();
319 accumulated_delta = 0;
322 /* Let the binding proxies get first crack at the press event
326 if (position_binder.button_press_handler (ev)) {
330 if (width_binder.button_press_handler (ev)) {
335 if (ev->button != 1) {
339 if (ev->type == GDK_2BUTTON_PRESS) {
340 int width = get_width();
342 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
343 /* handled by button release */
349 /* upper section: adjusts position, constrained by width */
351 const double w = fabs (width_control->get_value ());
352 const double max_pos = 1.0 - (w/2.0);
353 const double min_pos = w/2.0;
355 if (ev->x <= width/3) {
356 /* left side dbl click */
357 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
358 /* 2ndary-double click on left, collapse to hard left */
359 width_control->set_value (0);
360 position_control->set_value (0);
362 position_control->set_value (min_pos);
364 } else if (ev->x > 2*width/3) {
365 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
366 /* 2ndary-double click on right, collapse to hard right */
367 width_control->set_value (0);
368 position_control->set_value (1.0);
370 position_control->set_value (max_pos);
373 position_control->set_value (0.5);
378 /* lower section: adjusts width, constrained by position */
380 const double p = position_control->get_value ();
381 const double max_width = 2.0 * min ((1.0 - p), p);
383 if (ev->x <= width/3) {
384 /* left side dbl click */
385 width_control->set_value (max_width); // reset width to 100%
386 } else if (ev->x > 2*width/3) {
387 /* right side dbl click */
388 width_control->set_value (-max_width); // reset width to inverted 100%
390 /* center dbl click */
391 width_control->set_value (0); // collapse width to 0%
396 _tooltip.target_stop_drag ();
398 } else if (ev->type == GDK_BUTTON_PRESS) {
400 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
401 /* handled by button release */
406 /* top section of widget is for position drags */
407 dragging_position = true;
408 StartPositionGesture ();
410 /* lower section is for dragging width */
412 double pos = position_control->get_value (); /* 0..1 */
413 double swidth = width_control->get_value (); /* -1..+1 */
414 double fswidth = fabs (swidth);
415 int usable_width = get_width() - lr_box_size;
416 double center = (lr_box_size/2.0) + (usable_width * pos);
417 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
418 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
419 const int half_box = lr_box_size/2;
421 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
423 dragging_right = true;
425 dragging_left = true;
427 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
429 dragging_left = true;
431 dragging_right = true;
434 StartWidthGesture ();
438 _tooltip.target_start_drag ();
445 StereoPanner::on_button_release_event (GdkEventButton* ev)
447 if (PannerInterface::on_button_release_event (ev)) {
451 if (ev->button != 1) {
455 if (_panner_shell->bypassed()) {
459 bool const dp = dragging_position;
462 _tooltip.target_stop_drag ();
463 dragging_position = false;
464 dragging_left = false;
465 dragging_right = false;
466 accumulated_delta = 0;
469 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
473 StopPositionGesture ();
483 StereoPanner::on_scroll_event (GdkEventScroll* ev)
485 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
486 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
487 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
490 if (_panner_shell->bypassed()) {
494 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
497 step = one_degree * 5.0;
500 switch (ev->direction) {
501 case GDK_SCROLL_LEFT:
503 width_control->set_value (wv);
507 position_control->set_value (pv);
509 case GDK_SCROLL_RIGHT:
511 width_control->set_value (wv);
513 case GDK_SCROLL_DOWN:
515 position_control->set_value (pv);
523 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
525 if (_panner_shell->bypassed()) {
532 int usable_width = get_width() - lr_box_size;
533 double delta = (ev->x - last_drag_x) / (double) usable_width;
534 double current_width = width_control->get_value ();
540 if (dragging_left || dragging_right) {
542 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
544 /* change width and position in a way that keeps the
545 * other side in the same place
550 double pv = position_control->get_value();
553 position_control->set_value (pv - delta);
555 position_control->set_value (pv + delta);
559 /* delta is positive, so we're about to
560 increase the width. But we need to increase it
561 by twice the required value so that the
562 other side remains in place when we set
563 the position as well.
565 width_control->set_value (current_width + (delta * 2.0));
567 width_control->set_value (current_width + delta);
574 /* maintain position as invariant as we change the width */
576 /* create a detent close to the center */
578 if (!detented && fabs (current_width) < 0.02) {
581 width_control->set_value (0);
586 accumulated_delta += delta;
588 /* have we pulled far enough to escape ? */
590 if (fabs (accumulated_delta) >= 0.025) {
591 width_control->set_value (current_width + accumulated_delta);
593 accumulated_delta = false;
597 /* width needs to change by 2 * delta because both L & R move */
598 width_control->set_value (current_width + (delta * 2.0));
602 } else if (dragging_position) {
604 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
605 position_control->set_value (pv + delta);
613 StereoPanner::on_key_press_event (GdkEventKey* ev)
615 double one_degree = 1.0/180.0;
616 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
617 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
620 if (_panner_shell->bypassed()) {
624 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
627 step = one_degree * 5.0;
630 /* up/down control width because we consider pan position more "important"
631 (and thus having higher "sense" priority) than width.
634 switch (ev->keyval) {
636 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
637 width_control->set_value (1.0);
639 width_control->set_value (wv + step);
643 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
644 width_control->set_value (-1.0);
646 width_control->set_value (wv - step);
652 position_control->set_value (pv);
656 position_control->set_value (pv);
660 width_control->set_value (0.0);
671 StereoPanner::set_colors ()
673 colors[Normal].fill = ARDOUR_UI::config()->color_mod ("stereo panner fill", "panner fill");
674 // colors[Normal].outline = ARDOUR_UI::config()->color ("stereo panner outline");
675 colors[Normal].outline = ArdourCanvas::HSV (colors[Normal].fill).outline().color ();
676 colors[Normal].text = ARDOUR_UI::config()->color ("stereo panner text");
677 colors[Normal].background = ARDOUR_UI::config()->color ("stereo panner bg");
678 colors[Normal].rule = ARDOUR_UI::config()->color ("stereo panner rule");
680 colors[Mono].fill = ARDOUR_UI::config()->color ("stereo panner mono fill");
681 colors[Mono].outline = ARDOUR_UI::config()->color ("stereo panner mono outline");
682 colors[Mono].text = ARDOUR_UI::config()->color ("stereo panner mono text");
683 colors[Mono].background = ARDOUR_UI::config()->color ("stereo panner mono bg");
684 colors[Mono].rule = ARDOUR_UI::config()->color ("stereo panner rule");
686 colors[Inverted].fill = ARDOUR_UI::config()->color_mod ("stereo panner inverted fill", "stereo panner inverted");
687 colors[Inverted].outline = ARDOUR_UI::config()->color ("stereo panner inverted outline");
688 colors[Inverted].text = ARDOUR_UI::config()->color ("stereo panner inverted text");
689 colors[Inverted].background = ARDOUR_UI::config()->color_mod ("stereo panner inverted bg", "stereo panner inverted bg");
690 colors[Inverted].rule = ARDOUR_UI::config()->color ("stereo panner rule");
694 StereoPanner::color_handler ()
701 StereoPanner::bypass_handler ()
707 StereoPanner::pannable_handler ()
709 panvalue_connections.drop_connections();
710 position_control = _panner->pannable()->pan_azimuth_control;
711 width_control = _panner->pannable()->pan_width_control;
712 position_binder.set_controllable(position_control);
713 width_binder.set_controllable(width_control);
715 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
716 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
721 StereoPanner::editor ()
723 return new StereoPannerEditor (this);