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 "ardour_ui.h"
41 #include "global_signals.h"
42 #include "stereo_panner.h"
43 #include "stereo_panner_editor.h"
44 #include "rgb_macros.h"
51 using namespace Gtkmm2ext;
53 static const int pos_box_size = 8;
54 static const int lr_box_size = 15;
55 static const int step_down = 10;
56 static const int top_step = 2;
58 StereoPanner::ColorScheme StereoPanner::colors[3];
59 bool StereoPanner::have_colors = false;
61 Pango::AttrList StereoPanner::panner_font_attributes;
62 bool StereoPanner::have_font = false;
64 using namespace ARDOUR;
66 StereoPanner::StereoPanner (boost::shared_ptr<PannerShell> p)
67 : PannerInterface (p->panner())
69 , position_control (_panner->pannable()->pan_azimuth_control)
70 , width_control (_panner->pannable()->pan_width_control)
71 , dragging_position (false)
72 , dragging_left (false)
73 , dragging_right (false)
76 , accumulated_delta (0)
78 , position_binder (position_control)
79 , width_binder (width_control)
87 Pango::FontDescription font;
88 Pango::AttrFontDesc* font_attr;
89 font = Pango::FontDescription ("ArdourMono");
90 font.set_weight (Pango::WEIGHT_BOLD);
91 font.set_size(9 * PANGO_SCALE);
92 font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
93 panner_font_attributes.change(*font_attr);
98 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
99 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
100 _panner_shell->Changed.connect (connections, invalidator (*this), boost::bind (&StereoPanner::bypass_handler, this), gui_context());
102 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
107 StereoPanner::~StereoPanner ()
113 StereoPanner::set_tooltip ()
115 if (_panner_shell->bypassed()) {
116 _tooltip.set_tip (_("bypassed"));
119 double pos = position_control->get_value(); // 0..1
121 /* We show the position of the center of the image relative to the left & right.
122 This is expressed as a pair of percentage values that ranges from (100,0)
123 (hard left) through (50,50) (hard center) to (0,100) (hard right).
125 This is pretty wierd, but its the way audio engineers expect it. Just remember that
126 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
130 snprintf (buf, sizeof (buf), _("L:%3d R:%3d Width:%d%%"), (int) rint (100.0 * (1.0 - pos)),
131 (int) rint (100.0 * pos),
132 (int) floor (100.0 * width_control->get_value()));
133 _tooltip.set_tip (buf);
137 StereoPanner::on_expose_event (GdkEventExpose*)
139 Glib::RefPtr<Gdk::Window> win (get_window());
140 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
141 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
142 Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
143 layout->set_attributes (panner_font_attributes);
147 const double pos = position_control->get_value (); /* 0..1 */
148 const double swidth = width_control->get_value (); /* -1..+1 */
149 const double fswidth = fabs (swidth);
150 const double corner_radius = 5.0;
151 uint32_t o, f, t, b, r;
155 height = get_height ();
159 } else if (swidth < 0.0) {
165 o = colors[state].outline;
166 f = colors[state].fill;
167 t = colors[state].text;
168 b = colors[state].background;
169 r = colors[state].rule;
171 if (_panner_shell->bypassed()) {
181 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
182 cairo_rectangle (context->cobj(), 0, 0, width, height);
183 context->fill_preserve ();
186 /* the usable width is reduced from the real width, because we need space for
187 the two halves of LR boxes that will extend past the actual left/right
188 positions (indicated by the vertical line segment above them).
191 double usable_width = width - lr_box_size;
193 /* compute the centers of the L/R boxes based on the current stereo width */
195 if (fmod (usable_width,2.0) == 0) {
196 /* even width, but we need odd, so that there is an exact center.
197 So, offset cairo by 1, and reduce effective width by 1
200 context->translate (1.0, 0.0);
203 const double half_lr_box = lr_box_size/2.0;
204 const double center = rint(half_lr_box + (usable_width * pos));
205 const double pan_spread = rint((fswidth * (usable_width-1.0))/2.0);
206 const double left = center - pan_spread;
207 const double right = center + pan_spread;
210 context->set_line_width (1.0);
211 context->move_to ((usable_width + lr_box_size)/2.0, 0);
212 context->rel_line_to (0, height);
213 context->set_source_rgba (UINT_RGBA_R_FLT(r), UINT_RGBA_G_FLT(r), UINT_RGBA_B_FLT(r), UINT_RGBA_A_FLT(r));
216 /* compute & draw the line through the box */
217 context->set_line_width (2);
218 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
219 context->move_to (left, top_step + (pos_box_size/2.0) + step_down + 1.0);
220 context->line_to (left, top_step + (pos_box_size/2.0));
221 context->line_to (right, top_step + (pos_box_size/2.0));
222 context->line_to (right, top_step + (pos_box_size/2.0) + step_down + 1.0);
225 context->set_line_width (1.0);
229 rounded_rectangle (context, left - half_lr_box,
230 half_lr_box+step_down,
231 lr_box_size, lr_box_size, corner_radius);
232 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
233 context->fill_preserve();
234 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
238 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
240 layout->set_text (_("R"));
242 layout->set_text (_("L"));
244 layout->get_pixel_size(tw, th);
245 context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
246 pango_cairo_show_layout (context->cobj(), layout->gobj());
250 rounded_rectangle (context, right - half_lr_box,
251 half_lr_box+step_down,
252 lr_box_size, lr_box_size, corner_radius);
253 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
254 context->fill_preserve();
255 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
259 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
262 layout->set_text (_("M"));
265 layout->set_text (_("L"));
267 layout->set_text (_("R"));
270 layout->get_pixel_size(tw, th);
271 context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
272 pango_cairo_show_layout (context->cobj(), layout->gobj());
274 /* draw the central box */
275 context->set_line_width (2.0);
276 context->move_to (center + (pos_box_size/2.0), top_step); /* top right */
277 context->rel_line_to (0.0, pos_box_size); /* lower right */
278 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
279 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
280 context->rel_line_to (0.0, -pos_box_size); /* upper left */
281 context->close_path ();
283 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
284 context->stroke_preserve ();
285 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
292 StereoPanner::on_button_press_event (GdkEventButton* ev)
294 if (PannerInterface::on_button_press_event (ev)) {
298 if (_panner_shell->bypassed()) {
302 drag_start_x = ev->x;
305 dragging_position = false;
306 dragging_left = false;
307 dragging_right = false;
309 _tooltip.target_stop_drag ();
310 accumulated_delta = 0;
313 /* Let the binding proxies get first crack at the press event
317 if (position_binder.button_press_handler (ev)) {
321 if (width_binder.button_press_handler (ev)) {
326 if (ev->button != 1) {
330 if (ev->type == GDK_2BUTTON_PRESS) {
331 int width = get_width();
333 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
334 /* handled by button release */
340 /* upper section: adjusts position, constrained by width */
342 const double w = fabs (width_control->get_value ());
343 const double max_pos = 1.0 - (w/2.0);
344 const double min_pos = w/2.0;
346 if (ev->x <= width/3) {
347 /* left side dbl click */
348 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
349 /* 2ndary-double click on left, collapse to hard left */
350 width_control->set_value (0);
351 position_control->set_value (0);
353 position_control->set_value (min_pos);
355 } else if (ev->x > 2*width/3) {
356 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
357 /* 2ndary-double click on right, collapse to hard right */
358 width_control->set_value (0);
359 position_control->set_value (1.0);
361 position_control->set_value (max_pos);
364 position_control->set_value (0.5);
369 /* lower section: adjusts width, constrained by position */
371 const double p = position_control->get_value ();
372 const double max_width = 2.0 * min ((1.0 - p), p);
374 if (ev->x <= width/3) {
375 /* left side dbl click */
376 width_control->set_value (max_width); // reset width to 100%
377 } else if (ev->x > 2*width/3) {
378 /* right side dbl click */
379 width_control->set_value (-max_width); // reset width to inverted 100%
381 /* center dbl click */
382 width_control->set_value (0); // collapse width to 0%
387 _tooltip.target_stop_drag ();
389 } else if (ev->type == GDK_BUTTON_PRESS) {
391 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
392 /* handled by button release */
397 /* top section of widget is for position drags */
398 dragging_position = true;
399 StartPositionGesture ();
401 /* lower section is for dragging width */
403 double pos = position_control->get_value (); /* 0..1 */
404 double swidth = width_control->get_value (); /* -1..+1 */
405 double fswidth = fabs (swidth);
406 int usable_width = get_width() - lr_box_size;
407 double center = (lr_box_size/2.0) + (usable_width * pos);
408 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
409 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
410 const int half_box = lr_box_size/2;
412 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
414 dragging_right = true;
416 dragging_left = true;
418 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
420 dragging_left = true;
422 dragging_right = true;
425 StartWidthGesture ();
429 _tooltip.target_start_drag ();
436 StereoPanner::on_button_release_event (GdkEventButton* ev)
438 if (PannerInterface::on_button_release_event (ev)) {
442 if (ev->button != 1) {
446 if (_panner_shell->bypassed()) {
450 bool const dp = dragging_position;
453 _tooltip.target_stop_drag ();
454 dragging_position = false;
455 dragging_left = false;
456 dragging_right = false;
457 accumulated_delta = 0;
460 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
464 StopPositionGesture ();
474 StereoPanner::on_scroll_event (GdkEventScroll* ev)
476 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
477 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
478 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
481 if (_panner_shell->bypassed()) {
485 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
488 step = one_degree * 5.0;
491 switch (ev->direction) {
492 case GDK_SCROLL_LEFT:
494 width_control->set_value (wv);
498 position_control->set_value (pv);
500 case GDK_SCROLL_RIGHT:
502 width_control->set_value (wv);
504 case GDK_SCROLL_DOWN:
506 position_control->set_value (pv);
514 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
516 if (_panner_shell->bypassed()) {
523 int usable_width = get_width() - lr_box_size;
524 double delta = (ev->x - last_drag_x) / (double) usable_width;
525 double current_width = width_control->get_value ();
531 if (dragging_left || dragging_right) {
533 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
535 /* change width and position in a way that keeps the
536 * other side in the same place
541 double pv = position_control->get_value();
544 position_control->set_value (pv - delta);
546 position_control->set_value (pv + delta);
550 /* delta is positive, so we're about to
551 increase the width. But we need to increase it
552 by twice the required value so that the
553 other side remains in place when we set
554 the position as well.
556 width_control->set_value (current_width + (delta * 2.0));
558 width_control->set_value (current_width + delta);
565 /* maintain position as invariant as we change the width */
567 /* create a detent close to the center */
569 if (!detented && fabs (current_width) < 0.02) {
572 width_control->set_value (0);
577 accumulated_delta += delta;
579 /* have we pulled far enough to escape ? */
581 if (fabs (accumulated_delta) >= 0.025) {
582 width_control->set_value (current_width + accumulated_delta);
584 accumulated_delta = false;
588 /* width needs to change by 2 * delta because both L & R move */
589 width_control->set_value (current_width + (delta * 2.0));
593 } else if (dragging_position) {
595 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
596 position_control->set_value (pv + delta);
604 StereoPanner::on_key_press_event (GdkEventKey* ev)
606 double one_degree = 1.0/180.0;
607 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
608 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
611 if (_panner_shell->bypassed()) {
615 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
618 step = one_degree * 5.0;
621 /* up/down control width because we consider pan position more "important"
622 (and thus having higher "sense" priority) than width.
625 switch (ev->keyval) {
627 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
628 width_control->set_value (1.0);
630 width_control->set_value (wv + step);
634 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
635 width_control->set_value (-1.0);
637 width_control->set_value (wv - step);
643 position_control->set_value (pv);
647 position_control->set_value (pv);
651 width_control->set_value (0.0);
662 StereoPanner::set_colors ()
664 colors[Normal].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerFill();
665 colors[Normal].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerOutline();
666 colors[Normal].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerText();
667 colors[Normal].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerBackground();
668 colors[Normal].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
670 colors[Mono].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoFill();
671 colors[Mono].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoOutline();
672 colors[Mono].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoText();
673 colors[Mono].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerMonoBackground();
674 colors[Mono].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
676 colors[Inverted].fill = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedFill();
677 colors[Inverted].outline = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedOutline();
678 colors[Inverted].text = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedText();
679 colors[Inverted].background = ARDOUR_UI::config()->get_canvasvar_StereoPannerInvertedBackground();
680 colors[Inverted].rule = ARDOUR_UI::config()->get_canvasvar_StereoPannerRule();
684 StereoPanner::color_handler ()
691 StereoPanner::bypass_handler ()
697 StereoPanner::editor ()
699 return new StereoPannerEditor (this);