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.
25 #include <gtkmm/window.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"
35 #include "ardour/pannable.h"
36 #include "ardour/panner.h"
38 #include "ardour_ui.h"
39 #include "global_signals.h"
40 #include "stereo_panner.h"
41 #include "rgb_macros.h"
48 using namespace Gtkmm2ext;
50 static const int pos_box_size = 9;
51 static const int lr_box_size = 15;
52 static const int step_down = 10;
53 static const int top_step = 2;
55 StereoPanner::ColorScheme StereoPanner::colors[3];
56 bool StereoPanner::have_colors = false;
58 using namespace ARDOUR;
60 StereoPanner::StereoPanner (boost::shared_ptr<Panner> panner)
62 , position_control (_panner->pannable()->pan_azimuth_control)
63 , width_control (_panner->pannable()->pan_width_control)
65 , dragging_position (false)
66 , dragging_left (false)
67 , dragging_right (false)
70 , accumulated_delta (0)
72 , drag_data_window (0)
74 , position_binder (position_control)
75 , width_binder (width_control)
82 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
83 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
85 set_flags (Gtk::CAN_FOCUS);
87 add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
88 Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
89 Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
91 Gdk::POINTER_MOTION_MASK);
93 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
96 StereoPanner::~StereoPanner ()
98 delete drag_data_window;
102 StereoPanner::set_drag_data ()
104 if (!drag_data_label) {
108 double pos = position_control->get_value(); // 0..1
110 /* We show the position of the center of the image relative to the left & right.
111 This is expressed as a pair of percentage values that ranges from (100,0)
112 (hard left) through (50,50) (hard center) to (0,100) (hard right).
114 This is pretty wierd, but its the way audio engineers expect it. Just remember that
115 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
119 snprintf (buf, sizeof (buf), "L:%3d R:%3d Width:%d%%", (int) rint (100.0 * (1.0 - pos)),
120 (int) rint (100.0 * pos),
121 (int) floor (100.0 * width_control->get_value()));
122 drag_data_label->set_markup (buf);
126 StereoPanner::value_change ()
133 StereoPanner::on_expose_event (GdkEventExpose* ev)
135 Glib::RefPtr<Gdk::Window> win (get_window());
136 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
137 Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
140 double pos = position_control->get_value (); /* 0..1 */
141 double swidth = width_control->get_value (); /* -1..+1 */
142 double fswidth = fabs (swidth);
145 const double corner_radius = 5.0;
148 height = get_height ();
152 } else if (swidth < 0.0) {
158 o = colors[state].outline;
159 f = colors[state].fill;
160 t = colors[state].text;
161 b = colors[state].background;
165 context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
166 rounded_rectangle (context, 0, 0, width, height, corner_radius);
169 /* the usable width is reduced from the real width, because we need space for
170 the two halves of LR boxes that will extend past the actual left/right
171 positions (indicated by the vertical line segment above them).
174 double usable_width = width - lr_box_size;
176 /* compute the centers of the L/R boxes based on the current stereo width */
178 if (fmod (usable_width,2.0) == 0) {
179 /* even width, but we need odd, so that there is an exact center.
180 So, offset cairo by 1, and reduce effective width by 1
183 context->translate (1.0, 0.0);
186 double center = (lr_box_size/2.0) + (usable_width * pos);
187 const double pan_spread = (fswidth * usable_width)/2.0;
188 const double half_lr_box = lr_box_size/2.0;
192 left = center - pan_spread; // center of left box
193 right = center + pan_spread; // center of right box
195 /* compute & draw the line through the box */
197 context->set_line_width (2);
198 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
199 context->move_to (left, top_step+(pos_box_size/2.0)+step_down);
200 context->line_to (left, top_step+(pos_box_size/2.0));
201 context->line_to (right, top_step+(pos_box_size/2.0));
202 context->line_to (right, top_step+(pos_box_size/2.0) + step_down);
207 rounded_rectangle (context, left - half_lr_box,
208 half_lr_box+step_down,
209 lr_box_size, lr_box_size, corner_radius);
210 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
211 context->stroke_preserve ();
212 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
217 context->move_to (left - half_lr_box + 3,
218 (lr_box_size/2) + step_down + 13);
219 context->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD);
222 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
224 context->show_text (_("R"));
226 context->show_text (_("L"));
232 rounded_rectangle (context, right - half_lr_box,
233 half_lr_box+step_down,
234 lr_box_size, lr_box_size, corner_radius);
235 context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
236 context->stroke_preserve ();
237 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->move_to (right - half_lr_box + 3, (lr_box_size/2)+step_down + 13);
243 context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
246 context->show_text (_("M"));
249 context->show_text (_("L"));
251 context->show_text (_("R"));
255 /* draw the central box */
257 double spos = (pos_box_size/2.0) + (usable_width * pos);
259 context->set_line_width (2.0);
260 context->move_to (spos + (pos_box_size/2.0), top_step); /* top right */
261 context->rel_line_to (0.0, pos_box_size); /* lower right */
262 context->rel_line_to (-pos_box_size/2.0, 4.0); /* bottom point */
263 context->rel_line_to (-pos_box_size/2.0, -4.0); /* lower left */
264 context->rel_line_to (0.0, -pos_box_size); /* upper left */
265 context->close_path ();
267 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->stroke_preserve ();
269 context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
276 StereoPanner::on_button_press_event (GdkEventButton* ev)
278 drag_start_x = ev->x;
281 dragging_position = false;
282 dragging_left = false;
283 dragging_right = false;
285 accumulated_delta = 0;
288 /* Let the binding proxies get first crack at the press event
292 if (position_binder.button_press_handler (ev)) {
296 if (width_binder.button_press_handler (ev)) {
301 if (ev->button != 1) {
305 if (ev->type == GDK_2BUTTON_PRESS) {
306 int width = get_width();
308 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
309 /* handled by button release */
315 /* upper section: adjusts position, constrained by width */
317 const double w = fabs (width_control->get_value ());
318 const double max_pos = 1.0 - (w/2.0);
319 const double min_pos = w/2.0;
321 if (ev->x <= width/3) {
322 /* left side dbl click */
323 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
324 /* 2ndary-double click on left, collapse to hard left */
325 width_control->set_value (0);
326 position_control->set_value (0);
328 position_control->set_value (min_pos);
330 } else if (ev->x > 2*width/3) {
331 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
332 /* 2ndary-double click on right, collapse to hard right */
333 width_control->set_value (0);
334 position_control->set_value (1.0);
336 position_control->set_value (max_pos);
339 position_control->set_value (0.5);
344 /* lower section: adjusts width, constrained by position */
346 const double p = position_control->get_value ();
347 const double max_width = 2.0 * min ((1.0 - p), p);
349 if (ev->x <= width/3) {
350 /* left side dbl click */
351 width_control->set_value (max_width); // reset width to 100%
352 } else if (ev->x > 2*width/3) {
353 /* right side dbl click */
354 width_control->set_value (-max_width); // reset width to inverted 100%
356 /* center dbl click */
357 width_control->set_value (0); // collapse width to 0%
363 } else if (ev->type == GDK_BUTTON_PRESS) {
365 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
366 /* handled by button release */
371 /* top section of widget is for position drags */
372 dragging_position = true;
373 StartPositionGesture ();
375 /* lower section is for dragging width */
377 double pos = position_control->get_value (); /* 0..1 */
378 double swidth = width_control->get_value (); /* -1..+1 */
379 double fswidth = fabs (swidth);
380 int usable_width = get_width() - lr_box_size;
381 double center = (lr_box_size/2.0) + (usable_width * pos);
382 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
383 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
384 const int half_box = lr_box_size/2;
386 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
388 dragging_right = true;
390 dragging_left = true;
392 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
394 dragging_left = true;
396 dragging_right = true;
399 StartWidthGesture ();
409 StereoPanner::on_button_release_event (GdkEventButton* ev)
411 if (ev->button != 1) {
415 bool dp = dragging_position;
418 dragging_position = false;
419 dragging_left = false;
420 dragging_right = false;
421 accumulated_delta = 0;
424 if (drag_data_window) {
425 drag_data_window->hide ();
428 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
429 /* reset to default */
430 position_control->set_value (0.5);
431 width_control->set_value (1.0);
434 StopPositionGesture ();
444 StereoPanner::on_scroll_event (GdkEventScroll* ev)
446 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
447 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
448 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
451 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
454 step = one_degree * 5.0;
457 switch (ev->direction) {
458 case GDK_SCROLL_LEFT:
460 width_control->set_value (wv);
464 position_control->set_value (pv);
466 case GDK_SCROLL_RIGHT:
468 width_control->set_value (wv);
470 case GDK_SCROLL_DOWN:
472 position_control->set_value (pv);
480 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
486 if (!drag_data_window) {
487 drag_data_window = new Window (WINDOW_POPUP);
488 drag_data_window->set_name (X_("ContrastingPopup"));
489 drag_data_window->set_position (WIN_POS_MOUSE);
490 drag_data_window->set_decorated (false);
492 drag_data_label = manage (new Label);
493 drag_data_label->set_use_markup (true);
495 drag_data_window->set_border_width (6);
496 drag_data_window->add (*drag_data_label);
497 drag_data_label->show ();
499 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
501 drag_data_window->set_transient_for (*toplevel);
505 if (!drag_data_window->is_visible ()) {
506 /* move the popup window vertically down from the panner display */
508 get_window()->get_origin (rx, ry);
509 drag_data_window->move (rx, ry+get_height());
510 drag_data_window->present ();
514 double delta = (ev->x - last_drag_x) / (double) w;
515 double current_width = width_control->get_value ();
521 if (dragging_left || dragging_right) {
523 /* maintain position as invariant as we change the width */
526 /* create a detent close to the center */
528 if (!detented && fabs (current_width) < 0.02) {
531 width_control->set_value (0);
536 accumulated_delta += delta;
538 /* have we pulled far enough to escape ? */
540 if (fabs (accumulated_delta) >= 0.025) {
541 width_control->set_value (current_width + accumulated_delta);
543 accumulated_delta = false;
547 width_control->set_value (current_width + delta);
550 } else if (dragging_position) {
552 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
553 position_control->set_value (pv + delta);
561 StereoPanner::on_key_press_event (GdkEventKey* ev)
563 double one_degree = 1.0/180.0;
564 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
565 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
568 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
571 step = one_degree * 5.0;
574 /* up/down control width because we consider pan position more "important"
575 (and thus having higher "sense" priority) than width.
578 switch (ev->keyval) {
580 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
581 width_control->set_value (1.0);
583 width_control->set_value (wv + step);
587 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
588 width_control->set_value (-1.0);
590 width_control->set_value (wv - step);
595 position_control->set_value (pv);
599 position_control->set_value (pv);
605 width_control->set_value (0.0);
616 StereoPanner::on_key_release_event (GdkEventKey* ev)
622 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
625 Keyboard::magic_widget_grab_focus ();
630 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
632 Keyboard::magic_widget_drop_focus ();
637 StereoPanner::set_colors ()
639 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
640 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
641 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
642 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
643 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
645 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
646 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
647 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
648 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
649 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
651 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
652 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
653 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
654 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
655 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
659 StereoPanner::color_handler ()