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 "stereo_panner.h"
43 #include "stereo_panner_editor.h"
44 #include "rgb_macros.h"
46 #include "ui_config.h"
52 using namespace Gtkmm2ext;
53 using namespace ARDOUR_UI_UTILS;
55 StereoPanner::ColorScheme StereoPanner::colors[3];
56 bool StereoPanner::have_colors = false;
58 Pango::AttrList StereoPanner::panner_font_attributes;
59 bool StereoPanner::have_font = false;
61 using namespace ARDOUR;
63 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
64 : PannerInterface (p->panner())
66 , position_control (_panner->pannable()->pan_azimuth_control)
67 , width_control (_panner->pannable()->pan_width_control)
68 , dragging_position (false)
69 , dragging_left (false)
70 , dragging_right (false)
73 , accumulated_delta (0)
75 , position_binder (position_control)
76 , width_binder (width_control)
84 Pango::FontDescription font;
85 Pango::AttrFontDesc* font_attr;
86 font = Pango::FontDescription (UIConfiguration::instance().get_SmallBoldMonospaceFont());
87 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
88 panner_font_attributes.change(*font_attr);
93 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
94 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
96 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
97 _panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::pannable_handler, this), gui_context());
99 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
104 StereoPanner::~StereoPanner ()
110 StereoPanner::set_tooltip ()
112 if (_panner_shell->bypassed()) {
113 _tooltip.set_tip (_("bypassed"));
116 double pos = position_control->get_value(); // 0..1
118 /* We show the position of the center of the image relative to the left & right.
119 This is expressed as a pair of percentage values that ranges from (100,0)
120 (hard left) through (50,50) (hard center) to (0,100) (hard right).
122 This is pretty wierd, but its the way audio engineers expect it. Just remember that
123 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
127 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
128 (int) rint (100.0 * pos),
129 (int) floor (100.0 * width_control->get_value()));
130 _tooltip.set_tip (buf);
134 StereoPanner::on_expose_event (GdkEventExpose*)
136 Glib::RefPtr<Gdk::Window> win (get_window());
137 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
138 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
139 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
140 layout->set_attributes (panner_font_attributes);
144 const double pos = position_control->get_value (); /* 0..1 */
145 const double swidth = width_control->get_value (); /* -1..+1 */
146 const double fswidth = fabs (swidth);
147 uint32_t o, f, t, b, r;
151 height = get_height ();
153 const int step_down = rint(height / 3.5);
154 const double corner_radius = 5.0 * UIConfiguration::instance().get_ui_scale();
155 const int lr_box_size = height - 2 * step_down;
156 const int pos_box_size = (int)(rint(step_down * .8)) | 1;
157 const int top_step = step_down - pos_box_size;
161 } else if (swidth < 0.0) {
167 o = colors[state].outline;
168 f = colors[state].fill;
169 t = colors[state].text;
170 b = colors[state].background;
171 r = colors[state].rule;
173 if (_panner_shell->bypassed()) {
182 b = UIConfiguration::instance().color ("send bg");
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 (S_("Panner|R"));
250 layout->set_text (S_("Panner|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 (S_("Panner|M"));
273 layout->set_text (S_("Panner|L"));
275 layout->set_text (S_("Panner|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 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
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 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
533 int usable_width = get_width() - lr_box_size;
534 double delta = (ev->x - last_drag_x) / (double) usable_width;
535 double current_width = width_control->get_value ();
541 if (dragging_left || dragging_right) {
543 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
545 /* change width and position in a way that keeps the
546 * other side in the same place
551 double pv = position_control->get_value();
554 position_control->set_value (pv - delta);
556 position_control->set_value (pv + delta);
560 /* delta is positive, so we're about to
561 increase the width. But we need to increase it
562 by twice the required value so that the
563 other side remains in place when we set
564 the position as well.
566 width_control->set_value (current_width + (delta * 2.0));
568 width_control->set_value (current_width + delta);
575 /* maintain position as invariant as we change the width */
577 /* create a detent close to the center */
579 if (!detented && fabs (current_width) < 0.02) {
582 width_control->set_value (0);
587 accumulated_delta += delta;
589 /* have we pulled far enough to escape ? */
591 if (fabs (accumulated_delta) >= 0.025) {
592 width_control->set_value (current_width + accumulated_delta);
594 accumulated_delta = false;
598 /* width needs to change by 2 * delta because both L & R move */
599 width_control->set_value (current_width + (delta * 2.0));
603 } else if (dragging_position) {
605 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
606 position_control->set_value (pv + delta);
614 StereoPanner::on_key_press_event (GdkEventKey* ev)
616 double one_degree = 1.0/180.0;
617 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
618 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
621 if (_panner_shell->bypassed()) {
625 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
628 step = one_degree * 5.0;
631 /* up/down control width because we consider pan position more "important"
632 (and thus having higher "sense" priority) than width.
635 switch (ev->keyval) {
637 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
638 width_control->set_value (1.0);
640 width_control->set_value (wv + step);
644 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
645 width_control->set_value (-1.0);
647 width_control->set_value (wv - step);
653 position_control->set_value (pv);
657 position_control->set_value (pv);
661 width_control->set_value (0.0);
672 StereoPanner::set_colors ()
674 colors[Normal].fill = UIConfiguration::instance().color_mod ("stereo panner fill", "panner fill");
675 // colors[Normal].outline = UIConfiguration::instance().color ("stereo panner outline");
676 colors[Normal].outline = ArdourCanvas::HSV (colors[Normal].fill).outline().color ();
677 colors[Normal].text = UIConfiguration::instance().color ("stereo panner text");
678 colors[Normal].background = UIConfiguration::instance().color ("stereo panner bg");
679 colors[Normal].rule = UIConfiguration::instance().color ("stereo panner rule");
681 colors[Mono].fill = UIConfiguration::instance().color ("stereo panner mono fill");
682 colors[Mono].outline = UIConfiguration::instance().color ("stereo panner mono outline");
683 colors[Mono].text = UIConfiguration::instance().color ("stereo panner mono text");
684 colors[Mono].background = UIConfiguration::instance().color ("stereo panner mono bg");
685 colors[Mono].rule = UIConfiguration::instance().color ("stereo panner rule");
687 colors[Inverted].fill = UIConfiguration::instance().color_mod ("stereo panner inverted fill", "stereo panner inverted");
688 colors[Inverted].outline = UIConfiguration::instance().color ("stereo panner inverted outline");
689 colors[Inverted].text = UIConfiguration::instance().color ("stereo panner inverted text");
690 colors[Inverted].background = UIConfiguration::instance().color_mod ("stereo panner inverted bg", "stereo panner inverted bg");
691 colors[Inverted].rule = UIConfiguration::instance().color ("stereo panner rule");
695 StereoPanner::color_handler ()
702 StereoPanner::bypass_handler ()
708 StereoPanner::pannable_handler ()
710 panvalue_connections.drop_connections();
711 position_control = _panner->pannable()->pan_azimuth_control;
712 width_control = _panner->pannable()->pan_width_control;
713 position_binder.set_controllable(position_control);
714 width_binder.set_controllable(width_control);
716 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
717 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
722 StereoPanner::editor ()
724 return new StereoPannerEditor (this);