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 using PBD::Controllable;
57 StereoPanner::ColorScheme StereoPanner::colors[3];
58 bool StereoPanner::have_colors = false;
60 Pango::AttrList StereoPanner::panner_font_attributes;
61 bool StereoPanner::have_font = false;
63 using namespace ARDOUR;
65 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
66 : PannerInterface (p->panner())
68 , position_control (_panner->pannable()->pan_azimuth_control)
69 , width_control (_panner->pannable()->pan_width_control)
70 , dragging_position (false)
71 , dragging_left (false)
72 , dragging_right (false)
75 , accumulated_delta (0)
77 , position_binder (position_control)
78 , width_binder (width_control)
86 Pango::FontDescription font;
87 Pango::AttrFontDesc* font_attr;
88 font = Pango::FontDescription (UIConfiguration::instance().get_SmallBoldMonospaceFont());
89 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
90 panner_font_attributes.change(*font_attr);
95 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
96 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
98 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
99 _panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::pannable_handler, this), gui_context());
101 UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
106 StereoPanner::~StereoPanner ()
112 StereoPanner::set_tooltip ()
114 if (_panner_shell->bypassed()) {
115 _tooltip.set_tip (_("bypassed"));
118 double pos = position_control->get_value(); // 0..1
120 /* We show the position of the center of the image relative to the left & right.
121 This is expressed as a pair of percentage values that ranges from (100,0)
122 (hard left) through (50,50) (hard center) to (0,100) (hard right).
124 This is pretty wierd, but its the way audio engineers expect it. Just remember that
125 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
129 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
130 (int) rint (100.0 * pos),
131 (int) floor (100.0 * width_control->get_value()));
132 _tooltip.set_tip (buf);
136 StereoPanner::on_expose_event (GdkEventExpose*)
138 Glib::RefPtr<Gdk::Window> win (get_window());
139 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
140 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
141 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
142 layout->set_attributes (panner_font_attributes);
146 const double pos = position_control->get_value (); /* 0..1 */
147 const double swidth = width_control->get_value (); /* -1..+1 */
148 const double fswidth = fabs (swidth);
149 uint32_t o, f, t, b, r;
153 height = get_height ();
155 const int step_down = rint(height / 3.5);
156 const double corner_radius = 5.0 * UIConfiguration::instance().get_ui_scale();
157 const int lr_box_size = height - 2 * step_down;
158 const int pos_box_size = (int)(rint(step_down * .8)) | 1;
159 const int top_step = step_down - pos_box_size;
163 } else if (swidth < 0.0) {
169 o = colors[state].outline;
170 f = colors[state].fill;
171 t = colors[state].text;
172 b = colors[state].background;
173 r = colors[state].rule;
175 if (_panner_shell->bypassed()) {
184 b = UIConfiguration::instance().color ("send bg");
185 // b = rgba_from_style("SendStripBase",
186 // UINT_RGBA_R(b), UINT_RGBA_G(b), UINT_RGBA_B(b), 255,
191 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
192 cairo_rectangle (context->cobj(), 0, 0, width, height);
193 context->fill_preserve ();
196 /* the usable width is reduced from the real width, because we need space for
197 the two halves of LR boxes that will extend past the actual left/right
198 positions (indicated by the vertical line segment above them).
201 double usable_width = width - lr_box_size;
203 /* compute the centers of the L/R boxes based on the current stereo width */
205 if (fmod (usable_width,2.0) == 0) {
206 /* even width, but we need odd, so that there is an exact center.
207 So, offset cairo by 1, and reduce effective width by 1
210 context->translate (1.0, 0.0);
213 const double half_lr_box = lr_box_size/2.0;
214 const double center = rint(half_lr_box + (usable_width * pos));
215 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
216 const double left = center - pan_spread;
217 const double right = center + pan_spread;
220 context->set_line_width (1.0);
221 context->move_to ((usable_width + lr_box_size)/2.0, 0);
222 context->rel_line_to (0, height);
223 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
226 /* compute & draw the line through the box */
227 context->set_line_width (2);
228 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
229 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
230 context->line_to (left, top_step + (pos_box_size/2.0));
231 context->line_to (right, top_step + (pos_box_size/2.0));
232 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
235 context->set_line_width (1.0);
239 rounded_rectangle (context, left - half_lr_box,
240 half_lr_box+step_down,
241 lr_box_size, lr_box_size, corner_radius);
242 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
243 context->fill_preserve();
244 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
248 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
250 layout->set_text (S_("Panner|R"));
252 layout->set_text (S_("Panner|L"));
254 layout->get_pixel_size(tw, th);
255 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
256 pango_cairo_show_layout (context->cobj(), layout->gobj());
260 rounded_rectangle (context, right - half_lr_box,
261 half_lr_box+step_down,
262 lr_box_size, lr_box_size, corner_radius);
263 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
264 context->fill_preserve();
265 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
269 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
272 layout->set_text (S_("Panner|M"));
275 layout->set_text (S_("Panner|L"));
277 layout->set_text (S_("Panner|R"));
280 layout->get_pixel_size(tw, th);
281 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
282 pango_cairo_show_layout (context->cobj(), layout->gobj());
284 /* draw the central box */
285 context->set_line_width (2.0);
286 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
287 context->rel_line_to (0.0, pos_box_size); /* lower right */
288 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
289 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
290 context->rel_line_to (0.0, -pos_box_size); /* upper left */
291 context->close_path ();
293 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
294 context->stroke_preserve ();
295 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
302 StereoPanner::on_button_press_event (GdkEventButton* ev)
304 if (PannerInterface::on_button_press_event (ev)) {
308 if (_panner_shell->bypassed()) {
312 drag_start_x = ev->x;
315 dragging_position = false;
316 dragging_left = false;
317 dragging_right = false;
319 _tooltip.target_stop_drag ();
320 accumulated_delta = 0;
323 /* Let the binding proxies get first crack at the press event
327 if (position_binder.button_press_handler (ev)) {
331 if (width_binder.button_press_handler (ev)) {
336 if (ev->button != 1) {
340 if (ev->type == GDK_2BUTTON_PRESS) {
341 int width = get_width();
343 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
344 /* handled by button release */
350 /* upper section: adjusts position, constrained by width */
352 const double w = fabs (width_control->get_value ());
353 const double max_pos = 1.0 - (w/2.0);
354 const double min_pos = w/2.0;
356 if (ev->x <= width/3) {
357 /* left side dbl click */
358 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
359 /* 2ndary-double click on left, collapse to hard left */
360 width_control->set_value (0, Controllable::NoGroup);
361 position_control->set_value (0, Controllable::NoGroup);
363 position_control->set_value (min_pos, Controllable::NoGroup);
365 } else if (ev->x > 2*width/3) {
366 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
367 /* 2ndary-double click on right, collapse to hard right */
368 width_control->set_value (0, Controllable::NoGroup);
369 position_control->set_value (1.0, Controllable::NoGroup);
371 position_control->set_value (max_pos, Controllable::NoGroup);
374 position_control->set_value (0.5, Controllable::NoGroup);
379 /* lower section: adjusts width, constrained by position */
381 const double p = position_control->get_value ();
382 const double max_width = 2.0 * min ((1.0 - p), p);
384 if (ev->x <= width/3) {
385 /* left side dbl click */
386 width_control->set_value (max_width, Controllable::NoGroup); // reset width to 100%
387 } else if (ev->x > 2*width/3) {
388 /* right side dbl click */
389 width_control->set_value (-max_width, Controllable::NoGroup); // reset width to inverted 100%
391 /* center dbl click */
392 width_control->set_value (0, Controllable::NoGroup); // collapse width to 0%
397 _tooltip.target_stop_drag ();
399 } else if (ev->type == GDK_BUTTON_PRESS) {
401 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
402 /* handled by button release */
407 /* top section of widget is for position drags */
408 dragging_position = true;
409 StartPositionGesture ();
411 /* lower section is for dragging width */
413 double pos = position_control->get_value (); /* 0..1 */
414 double swidth = width_control->get_value (); /* -1..+1 */
415 double fswidth = fabs (swidth);
416 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
417 int usable_width = get_width() - lr_box_size;
418 double center = (lr_box_size/2.0) + (usable_width * pos);
419 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
420 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
421 const int half_box = lr_box_size/2;
423 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
425 dragging_right = true;
427 dragging_left = true;
429 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
431 dragging_left = true;
433 dragging_right = true;
436 StartWidthGesture ();
440 _tooltip.target_start_drag ();
447 StereoPanner::on_button_release_event (GdkEventButton* ev)
449 if (PannerInterface::on_button_release_event (ev)) {
453 if (ev->button != 1) {
457 if (_panner_shell->bypassed()) {
461 bool const dp = dragging_position;
464 _tooltip.target_stop_drag ();
465 dragging_position = false;
466 dragging_left = false;
467 dragging_right = false;
468 accumulated_delta = 0;
471 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
475 StopPositionGesture ();
485 StereoPanner::on_scroll_event (GdkEventScroll* ev)
487 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
488 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
489 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
492 if (_panner_shell->bypassed()) {
496 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
499 step = one_degree * 5.0;
502 switch (ev->direction) {
503 case GDK_SCROLL_LEFT:
505 width_control->set_value (wv, Controllable::NoGroup);
509 position_control->set_value (pv, Controllable::NoGroup);
511 case GDK_SCROLL_RIGHT:
513 width_control->set_value (wv, Controllable::NoGroup);
515 case GDK_SCROLL_DOWN:
517 position_control->set_value (pv, Controllable::NoGroup);
525 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
527 if (_panner_shell->bypassed()) {
534 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
535 int usable_width = get_width() - lr_box_size;
536 double delta = (ev->x - last_drag_x) / (double) usable_width;
537 double current_width = width_control->get_value ();
543 if (dragging_left || dragging_right) {
545 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
547 /* change width and position in a way that keeps the
548 * other side in the same place
553 double pv = position_control->get_value();
556 position_control->set_value (pv - delta, Controllable::NoGroup);
558 position_control->set_value (pv + delta, Controllable::NoGroup);
562 /* delta is positive, so we're about to
563 increase the width. But we need to increase it
564 by twice the required value so that the
565 other side remains in place when we set
566 the position as well.
568 width_control->set_value (current_width + (delta * 2.0), Controllable::NoGroup);
570 width_control->set_value (current_width + delta, Controllable::NoGroup);
577 /* maintain position as invariant as we change the width */
579 /* create a detent close to the center */
581 if (!detented && fabs (current_width) < 0.02) {
584 width_control->set_value (0, Controllable::NoGroup);
589 accumulated_delta += delta;
591 /* have we pulled far enough to escape ? */
593 if (fabs (accumulated_delta) >= 0.025) {
594 width_control->set_value (current_width + accumulated_delta, Controllable::NoGroup);
596 accumulated_delta = false;
600 /* width needs to change by 2 * delta because both L & R move */
601 width_control->set_value (current_width + (delta * 2.0), Controllable::NoGroup);
605 } else if (dragging_position) {
607 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
608 position_control->set_value (pv + delta, Controllable::NoGroup);
616 StereoPanner::on_key_press_event (GdkEventKey* ev)
618 double one_degree = 1.0/180.0;
619 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
620 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
623 if (_panner_shell->bypassed()) {
627 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
630 step = one_degree * 5.0;
633 /* up/down control width because we consider pan position more "important"
634 (and thus having higher "sense" priority) than width.
637 switch (ev->keyval) {
639 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
640 width_control->set_value (1.0, Controllable::NoGroup);
642 width_control->set_value (wv + step, Controllable::NoGroup);
646 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
647 width_control->set_value (-1.0, Controllable::NoGroup);
649 width_control->set_value (wv - step, Controllable::NoGroup);
655 position_control->set_value (pv, Controllable::NoGroup);
659 position_control->set_value (pv, Controllable::NoGroup);
663 width_control->set_value (0.0, Controllable::NoGroup);
674 StereoPanner::set_colors ()
676 colors[Normal].fill = UIConfiguration::instance().color_mod ("stereo panner fill", "panner fill");
677 // colors[Normal].outline = UIConfiguration::instance().color ("stereo panner outline");
678 colors[Normal].outline = ArdourCanvas::HSV (colors[Normal].fill).outline().color ();
679 colors[Normal].text = UIConfiguration::instance().color ("stereo panner text");
680 colors[Normal].background = UIConfiguration::instance().color ("stereo panner bg");
681 colors[Normal].rule = UIConfiguration::instance().color ("stereo panner rule");
683 colors[Mono].fill = UIConfiguration::instance().color ("stereo panner mono fill");
684 colors[Mono].outline = UIConfiguration::instance().color ("stereo panner mono outline");
685 colors[Mono].text = UIConfiguration::instance().color ("stereo panner mono text");
686 colors[Mono].background = UIConfiguration::instance().color ("stereo panner mono bg");
687 colors[Mono].rule = UIConfiguration::instance().color ("stereo panner rule");
689 colors[Inverted].fill = UIConfiguration::instance().color_mod ("stereo panner inverted fill", "stereo panner inverted");
690 colors[Inverted].outline = UIConfiguration::instance().color ("stereo panner inverted outline");
691 colors[Inverted].text = UIConfiguration::instance().color ("stereo panner inverted text");
692 colors[Inverted].background = UIConfiguration::instance().color_mod ("stereo panner inverted bg", "stereo panner inverted bg");
693 colors[Inverted].rule = UIConfiguration::instance().color ("stereo panner rule");
697 StereoPanner::color_handler ()
704 StereoPanner::bypass_handler ()
710 StereoPanner::pannable_handler ()
712 panvalue_connections.drop_connections();
713 position_control = _panner->pannable()->pan_azimuth_control;
714 width_control = _panner->pannable()->pan_width_control;
715 position_binder.set_controllable(position_control);
716 width_binder.set_controllable(width_control);
718 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
719 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
724 StereoPanner::editor ()
726 return new StereoPannerEditor (this);