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 StereoPanner::ColorScheme StereoPanner::colors[3];
57 bool StereoPanner::have_colors = false;
59 Pango::AttrList StereoPanner::panner_font_attributes;
60 bool StereoPanner::have_font = false;
62 using namespace ARDOUR;
64 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
65 : PannerInterface (p->panner())
67 , position_control (_panner->pannable()->pan_azimuth_control)
68 , width_control (_panner->pannable()->pan_width_control)
69 , dragging_position (false)
70 , dragging_left (false)
71 , dragging_right (false)
74 , accumulated_delta (0)
76 , position_binder (position_control)
77 , width_binder (width_control)
85 Pango::FontDescription font;
86 Pango::AttrFontDesc* font_attr;
87 font = Pango::FontDescription (ARDOUR_UI::config()->get_SmallBoldMonospaceFont());
88 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
89 panner_font_attributes.change(*font_attr);
94 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
95 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
97 _panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
98 _panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&StereoPanner::pannable_handler, this), gui_context());
100 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
105 StereoPanner::~StereoPanner ()
111 StereoPanner::set_tooltip ()
113 if (_panner_shell->bypassed()) {
114 _tooltip.set_tip (_("bypassed"));
117 double pos = position_control->get_value(); // 0..1
119 /* We show the position of the center of the image relative to the left & right.
120 This is expressed as a pair of percentage values that ranges from (100,0)
121 (hard left) through (50,50) (hard center) to (0,100) (hard right).
123 This is pretty wierd, but its the way audio engineers expect it. Just remember that
124 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
128 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
129 (int) rint (100.0 * pos),
130 (int) floor (100.0 * width_control->get_value()));
131 _tooltip.set_tip (buf);
135 StereoPanner::on_expose_event (GdkEventExpose*)
137 Glib::RefPtr<Gdk::Window> win (get_window());
138 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
139 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
140 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
141 layout->set_attributes (panner_font_attributes);
145 const double pos = position_control->get_value (); /* 0..1 */
146 const double swidth = width_control->get_value (); /* -1..+1 */
147 const double fswidth = fabs (swidth);
148 uint32_t o, f, t, b, r;
152 height = get_height ();
154 const int step_down = rint(height / 3.5);
155 const double corner_radius = 5.0 * ARDOUR_UI::ui_scale;
156 const int lr_box_size = height - 2 * step_down;
157 const int pos_box_size = (int)(rint(step_down * .8)) | 1;
158 const int top_step = step_down - pos_box_size;
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 bg");
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 (S_("Panner|R"));
251 layout->set_text (S_("Panner|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 (S_("Panner|M"));
274 layout->set_text (S_("Panner|L"));
276 layout->set_text (S_("Panner|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 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
416 int usable_width = get_width() - lr_box_size;
417 double center = (lr_box_size/2.0) + (usable_width * pos);
418 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
419 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
420 const int half_box = lr_box_size/2;
422 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
424 dragging_right = true;
426 dragging_left = true;
428 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
430 dragging_left = true;
432 dragging_right = true;
435 StartWidthGesture ();
439 _tooltip.target_start_drag ();
446 StereoPanner::on_button_release_event (GdkEventButton* ev)
448 if (PannerInterface::on_button_release_event (ev)) {
452 if (ev->button != 1) {
456 if (_panner_shell->bypassed()) {
460 bool const dp = dragging_position;
463 _tooltip.target_stop_drag ();
464 dragging_position = false;
465 dragging_left = false;
466 dragging_right = false;
467 accumulated_delta = 0;
470 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
474 StopPositionGesture ();
484 StereoPanner::on_scroll_event (GdkEventScroll* ev)
486 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
487 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
488 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
491 if (_panner_shell->bypassed()) {
495 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
498 step = one_degree * 5.0;
501 switch (ev->direction) {
502 case GDK_SCROLL_LEFT:
504 width_control->set_value (wv);
508 position_control->set_value (pv);
510 case GDK_SCROLL_RIGHT:
512 width_control->set_value (wv);
514 case GDK_SCROLL_DOWN:
516 position_control->set_value (pv);
524 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
526 if (_panner_shell->bypassed()) {
533 const int lr_box_size = get_height() - 2 * rint(get_height() / 3.5);
534 int usable_width = get_width() - lr_box_size;
535 double delta = (ev->x - last_drag_x) / (double) usable_width;
536 double current_width = width_control->get_value ();
542 if (dragging_left || dragging_right) {
544 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
546 /* change width and position in a way that keeps the
547 * other side in the same place
552 double pv = position_control->get_value();
555 position_control->set_value (pv - delta);
557 position_control->set_value (pv + delta);
561 /* delta is positive, so we're about to
562 increase the width. But we need to increase it
563 by twice the required value so that the
564 other side remains in place when we set
565 the position as well.
567 width_control->set_value (current_width + (delta * 2.0));
569 width_control->set_value (current_width + delta);
576 /* maintain position as invariant as we change the width */
578 /* create a detent close to the center */
580 if (!detented && fabs (current_width) < 0.02) {
583 width_control->set_value (0);
588 accumulated_delta += delta;
590 /* have we pulled far enough to escape ? */
592 if (fabs (accumulated_delta) >= 0.025) {
593 width_control->set_value (current_width + accumulated_delta);
595 accumulated_delta = false;
599 /* width needs to change by 2 * delta because both L & R move */
600 width_control->set_value (current_width + (delta * 2.0));
604 } else if (dragging_position) {
606 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
607 position_control->set_value (pv + delta);
615 StereoPanner::on_key_press_event (GdkEventKey* ev)
617 double one_degree = 1.0/180.0;
618 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
619 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
622 if (_panner_shell->bypassed()) {
626 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
629 step = one_degree * 5.0;
632 /* up/down control width because we consider pan position more "important"
633 (and thus having higher "sense" priority) than width.
636 switch (ev->keyval) {
638 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
639 width_control->set_value (1.0);
641 width_control->set_value (wv + step);
645 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
646 width_control->set_value (-1.0);
648 width_control->set_value (wv - step);
654 position_control->set_value (pv);
658 position_control->set_value (pv);
662 width_control->set_value (0.0);
673 StereoPanner::set_colors ()
675 colors[Normal].fill = ARDOUR_UI::config()->color_mod ("stereo panner fill", "panner fill");
676 // colors[Normal].outline = ARDOUR_UI::config()->color ("stereo panner outline");
677 colors[Normal].outline = ArdourCanvas::HSV (colors[Normal].fill).outline().color ();
678 colors[Normal].text = ARDOUR_UI::config()->color ("stereo panner text");
679 colors[Normal].background = ARDOUR_UI::config()->color ("stereo panner bg");
680 colors[Normal].rule = ARDOUR_UI::config()->color ("stereo panner rule");
682 colors[Mono].fill = ARDOUR_UI::config()->color ("stereo panner mono fill");
683 colors[Mono].outline = ARDOUR_UI::config()->color ("stereo panner mono outline");
684 colors[Mono].text = ARDOUR_UI::config()->color ("stereo panner mono text");
685 colors[Mono].background = ARDOUR_UI::config()->color ("stereo panner mono bg");
686 colors[Mono].rule = ARDOUR_UI::config()->color ("stereo panner rule");
688 colors[Inverted].fill = ARDOUR_UI::config()->color_mod ("stereo panner inverted fill", "stereo panner inverted");
689 colors[Inverted].outline = ARDOUR_UI::config()->color ("stereo panner inverted outline");
690 colors[Inverted].text = ARDOUR_UI::config()->color ("stereo panner inverted text");
691 colors[Inverted].background = ARDOUR_UI::config()->color_mod ("stereo panner inverted bg", "stereo panner inverted bg");
692 colors[Inverted].rule = ARDOUR_UI::config()->color ("stereo panner rule");
696 StereoPanner::color_handler ()
703 StereoPanner::bypass_handler ()
709 StereoPanner::pannable_handler ()
711 panvalue_connections.drop_connections();
712 position_control = _panner->pannable()->pan_azimuth_control;
713 width_control = _panner->pannable()->pan_width_control;
714 position_binder.set_controllable(position_control);
715 width_binder.set_controllable(width_control);
717 position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
718 width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
723 StereoPanner::editor ()
725 return new StereoPannerEditor (this);