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 = rgba_from_style("SendStripBase",
184 UINT_RGBA_R(b), UINT_RGBA_G(b), UINT_RGBA_B(b), 255,
189 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
190 cairo_rectangle (context->cobj(), 0, 0, width, height);
191 context->fill_preserve ();
194 /* the usable width is reduced from the real width, because we need space for
195 the two halves of LR boxes that will extend past the actual left/right
196 positions (indicated by the vertical line segment above them).
199 double usable_width = width - lr_box_size;
201 /* compute the centers of the L/R boxes based on the current stereo width */
203 if (fmod (usable_width,2.0) == 0) {
204 /* even width, but we need odd, so that there is an exact center.
205 So, offset cairo by 1, and reduce effective width by 1
208 context->translate (1.0, 0.0);
211 const double half_lr_box = lr_box_size/2.0;
212 const double center = rint(half_lr_box + (usable_width * pos));
213 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
214 const double left = center - pan_spread;
215 const double right = center + pan_spread;
218 context->set_line_width (1.0);
219 context->move_to ((usable_width + lr_box_size)/2.0, 0);
220 context->rel_line_to (0, height);
221 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
224 /* compute & draw the line through the box */
225 context->set_line_width (2);
226 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
227 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
228 context->line_to (left, top_step + (pos_box_size/2.0));
229 context->line_to (right, top_step + (pos_box_size/2.0));
230 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
233 context->set_line_width (1.0);
237 rounded_rectangle (context, left - half_lr_box,
238 half_lr_box+step_down,
239 lr_box_size, lr_box_size, corner_radius);
240 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
241 context->fill_preserve();
242 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
246 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
248 layout->set_text (_("R"));
250 layout->set_text (_("L"));
252 layout->get_pixel_size(tw, th);
253 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
254 pango_cairo_show_layout (context->cobj(), layout->gobj());
258 rounded_rectangle (context, right - half_lr_box,
259 half_lr_box+step_down,
260 lr_box_size, lr_box_size, corner_radius);
261 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
262 context->fill_preserve();
263 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
267 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
270 layout->set_text (_("M"));
273 layout->set_text (_("L"));
275 layout->set_text (_("R"));
278 layout->get_pixel_size(tw, th);
279 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
280 pango_cairo_show_layout (context->cobj(), layout->gobj());
282 /* draw the central box */
283 context->set_line_width (2.0);
284 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
285 context->rel_line_to (0.0, pos_box_size); /* lower right */
286 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
287 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
288 context->rel_line_to (0.0, -pos_box_size); /* upper left */
289 context->close_path ();
291 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
292 context->stroke_preserve ();
293 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
300 StereoPanner::on_button_press_event (GdkEventButton* ev)
302 if (PannerInterface::on_button_press_event (ev)) {
306 if (_panner_shell->bypassed()) {
310 drag_start_x = ev->x;
313 dragging_position = false;
314 dragging_left = false;
315 dragging_right = false;
317 _tooltip.target_stop_drag ();
318 accumulated_delta = 0;
321 /* Let the binding proxies get first crack at the press event
325 if (position_binder.button_press_handler (ev)) {
329 if (width_binder.button_press_handler (ev)) {
334 if (ev->button != 1) {
338 if (ev->type == GDK_2BUTTON_PRESS) {
339 int width = get_width();
341 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
342 /* handled by button release */
348 /* upper section: adjusts position, constrained by width */
350 const double w = fabs (width_control->get_value ());
351 const double max_pos = 1.0 - (w/2.0);
352 const double min_pos = w/2.0;
354 if (ev->x <= width/3) {
355 /* left side dbl click */
356 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
357 /* 2ndary-double click on left, collapse to hard left */
358 width_control->set_value (0);
359 position_control->set_value (0);
361 position_control->set_value (min_pos);
363 } else if (ev->x > 2*width/3) {
364 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
365 /* 2ndary-double click on right, collapse to hard right */
366 width_control->set_value (0);
367 position_control->set_value (1.0);
369 position_control->set_value (max_pos);
372 position_control->set_value (0.5);
377 /* lower section: adjusts width, constrained by position */
379 const double p = position_control->get_value ();
380 const double max_width = 2.0 * min ((1.0 - p), p);
382 if (ev->x <= width/3) {
383 /* left side dbl click */
384 width_control->set_value (max_width); // reset width to 100%
385 } else if (ev->x > 2*width/3) {
386 /* right side dbl click */
387 width_control->set_value (-max_width); // reset width to inverted 100%
389 /* center dbl click */
390 width_control->set_value (0); // collapse width to 0%
395 _tooltip.target_stop_drag ();
397 } else if (ev->type == GDK_BUTTON_PRESS) {
399 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
400 /* handled by button release */
405 /* top section of widget is for position drags */
406 dragging_position = true;
407 StartPositionGesture ();
409 /* lower section is for dragging width */
411 double pos = position_control->get_value (); /* 0..1 */
412 double swidth = width_control->get_value (); /* -1..+1 */
413 double fswidth = fabs (swidth);
414 int usable_width = get_width() - lr_box_size;
415 double center = (lr_box_size/2.0) + (usable_width * pos);
416 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
417 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
418 const int half_box = lr_box_size/2;
420 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
422 dragging_right = true;
424 dragging_left = true;
426 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
428 dragging_left = true;
430 dragging_right = true;
433 StartWidthGesture ();
437 _tooltip.target_start_drag ();
444 StereoPanner::on_button_release_event (GdkEventButton* ev)
446 if (PannerInterface::on_button_release_event (ev)) {
450 if (ev->button != 1) {
454 if (_panner_shell->bypassed()) {
458 bool const dp = dragging_position;
461 _tooltip.target_stop_drag ();
462 dragging_position = false;
463 dragging_left = false;
464 dragging_right = false;
465 accumulated_delta = 0;
468 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
472 StopPositionGesture ();
482 StereoPanner::on_scroll_event (GdkEventScroll* ev)
484 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
485 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
486 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
489 if (_panner_shell->bypassed()) {
493 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
496 step = one_degree * 5.0;
499 switch (ev->direction) {
500 case GDK_SCROLL_LEFT:
502 width_control->set_value (wv);
506 position_control->set_value (pv);
508 case GDK_SCROLL_RIGHT:
510 width_control->set_value (wv);
512 case GDK_SCROLL_DOWN:
514 position_control->set_value (pv);
522 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
524 if (_panner_shell->bypassed()) {
531 int usable_width = get_width() - lr_box_size;
532 double delta = (ev->x - last_drag_x) / (double) usable_width;
533 double current_width = width_control->get_value ();
539 if (dragging_left || dragging_right) {
541 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
543 /* change width and position in a way that keeps the
544 * other side in the same place
549 double pv = position_control->get_value();
552 position_control->set_value (pv - delta);
554 position_control->set_value (pv + delta);
558 /* delta is positive, so we're about to
559 increase the width. But we need to increase it
560 by twice the required value so that the
561 other side remains in place when we set
562 the position as well.
564 width_control->set_value (current_width + (delta * 2.0));
566 width_control->set_value (current_width + delta);
573 /* maintain position as invariant as we change the width */
575 /* create a detent close to the center */
577 if (!detented && fabs (current_width) < 0.02) {
580 width_control->set_value (0);
585 accumulated_delta += delta;
587 /* have we pulled far enough to escape ? */
589 if (fabs (accumulated_delta) >= 0.025) {
590 width_control->set_value (current_width + accumulated_delta);
592 accumulated_delta = false;
596 /* width needs to change by 2 * delta because both L & R move */
597 width_control->set_value (current_width + (delta * 2.0));
601 } else if (dragging_position) {
603 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
604 position_control->set_value (pv + delta);
612 StereoPanner::on_key_press_event (GdkEventKey* ev)
614 double one_degree = 1.0/180.0;
615 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
616 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
619 if (_panner_shell->bypassed()) {
623 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
626 step = one_degree * 5.0;
629 /* up/down control width because we consider pan position more "important"
630 (and thus having higher "sense" priority) than width.
633 switch (ev->keyval) {
635 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
636 width_control->set_value (1.0);
638 width_control->set_value (wv + step);
642 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
643 width_control->set_value (-1.0);
645 width_control->set_value (wv - step);
651 position_control->set_value (pv);
655 position_control->set_value (pv);
659 width_control->set_value (0.0);
670 StereoPanner::set_colors ()
672 colors[Normal].fill = ARDOUR_UI::config()->get_StereoPannerFill();
673 // colors[Normal].outline = ARDOUR_UI::config()->get_StereoPannerOutline();
674 colors[Normal].outline = ArdourCanvas::HSV (colors[Normal].fill).outline().color ();
675 colors[Normal].text = ARDOUR_UI::config()->get_StereoPannerText();
676 colors[Normal].background = ARDOUR_UI::config()->get_StereoPannerBackground();
677 colors[Normal].rule = ARDOUR_UI::config()->get_StereoPannerRule();
679 colors[Mono].fill = ARDOUR_UI::config()->get_StereoPannerMonoFill();
680 colors[Mono].outline = ARDOUR_UI::config()->get_StereoPannerMonoOutline();
681 colors[Mono].text = ARDOUR_UI::config()->get_StereoPannerMonoText();
682 colors[Mono].background = ARDOUR_UI::config()->get_StereoPannerMonoBackground();
683 colors[Mono].rule = ARDOUR_UI::config()->get_StereoPannerRule();
685 colors[Inverted].fill = ARDOUR_UI::config()->get_StereoPannerInvertedFill();
686 colors[Inverted].outline = ARDOUR_UI::config()->get_StereoPannerInvertedOutline();
687 colors[Inverted].text = ARDOUR_UI::config()->get_StereoPannerInvertedText();
688 colors[Inverted].background = ARDOUR_UI::config()->get_StereoPannerInvertedBackground();
689 colors[Inverted].rule = ARDOUR_UI::config()->get_StereoPannerRule();
693 StereoPanner::color_handler ()
700 StereoPanner::bypass_handler ()
706 StereoPanner::pannable_handler ()
708 panvalue_connections.drop_connections();
709 position_control = _panner->pannable()->pan_azimuth_control;
710 width_control = _panner->pannable()->pan_width_control;
711 position_binder.set_controllable(position_control);
712 width_binder.set_controllable(width_control);
714 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
715 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
720 StereoPanner::editor ()
722 return new StereoPannerEditor (this);