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"
34 #include "ardour/pannable.h"
35 #include "ardour/panner.h"
37 #include "ardour_ui.h"
38 #include "global_signals.h"
39 #include "stereo_panner.h"
40 #include "rgb_macros.h"
47 using namespace Gtkmm2ext;
49 static const int pos_box_size = 10;
50 static const int lr_box_size = 15;
51 static const int step_down = 10;
52 static const int top_step = 2;
54 StereoPanner::ColorScheme StereoPanner::colors[3];
55 bool StereoPanner::have_colors = false;
57 using namespace ARDOUR;
59 StereoPanner::StereoPanner (boost::shared_ptr<Panner> panner)
61 , position_control (_panner->pannable()->pan_azimuth_control)
62 , width_control (_panner->pannable()->pan_width_control)
64 , dragging_position (false)
65 , dragging_left (false)
66 , dragging_right (false)
69 , accumulated_delta (0)
71 , drag_data_window (0)
73 , position_binder (position_control)
74 , width_binder (width_control)
81 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
82 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
84 set_flags (Gtk::CAN_FOCUS);
86 add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
87 Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
88 Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
90 Gdk::POINTER_MOTION_MASK);
92 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
95 StereoPanner::~StereoPanner ()
97 delete drag_data_window;
101 StereoPanner::set_drag_data ()
103 if (!drag_data_label) {
107 double pos = position_control->get_value(); // 0..1
109 /* We show the position of the center of the image relative to the left & right.
110 This is expressed as a pair of percentage values that ranges from (100,0)
111 (hard left) through (50,50) (hard center) to (0,100) (hard right).
113 This is pretty wierd, but its the way audio engineers expect it. Just remember that
114 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
118 snprintf (buf, sizeof (buf), "L:%3d R:%3d Width:%d%%", (int) rint (100.0 * (1.0 - pos)),
119 (int) rint (100.0 * pos),
120 (int) floor (100.0 * width_control->get_value()));
121 drag_data_label->set_markup (buf);
125 StereoPanner::value_change ()
132 StereoPanner::on_expose_event (GdkEventExpose* ev)
134 Glib::RefPtr<Gdk::Window> win (get_window());
135 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
137 cairo_t* cr = gdk_cairo_create (win->gobj());
140 double pos = position_control->get_value (); /* 0..1 */
141 double swidth = width_control->get_value (); /* -1..+1 */
142 double fswidth = fabs (swidth);
147 height = get_height ();
151 } else if (swidth < 0.0) {
157 o = colors[state].outline;
158 f = colors[state].fill;
159 t = colors[state].text;
160 b = colors[state].background;
164 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
165 cairo_rectangle (cr, 0, 0, width, height);
168 /* the usable width is reduced from the real width, because we need space for
169 the two halves of LR boxes that will extend past the actual left/right
170 positions (indicated by the vertical line segment above them).
173 double usable_width = width - lr_box_size;
175 /* compute the centers of the L/R boxes based on the current stereo width */
177 if (fmod (usable_width,2.0) == 0) {
178 /* even width, but we need odd, so that there is an exact center.
179 So, offset cairo by 1, and reduce effective width by 1
182 cairo_translate (cr, 1.0, 0.0);
185 double center = (lr_box_size/2.0) + (usable_width * pos);
186 const double pan_spread = (fswidth * usable_width)/2.0;
187 const double half_lr_box = lr_box_size/2.0;
191 left = center - pan_spread; // center of left box
192 right = center + pan_spread; // center of right box
194 /* compute & draw the line through the box */
196 cairo_set_line_width (cr, 2);
197 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
198 cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
199 cairo_line_to (cr, left, top_step+(pos_box_size/2));
200 cairo_line_to (cr, right, top_step+(pos_box_size/2));
201 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
208 half_lr_box+step_down,
209 lr_box_size, lr_box_size);
210 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
211 cairo_stroke_preserve (cr);
212 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
218 left - half_lr_box + 3,
219 (lr_box_size/2) + step_down + 13);
220 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
223 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
225 cairo_show_text (cr, _("R"));
227 cairo_show_text (cr, _("L"));
235 half_lr_box+step_down,
236 lr_box_size, lr_box_size);
237 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
238 cairo_stroke_preserve (cr);
239 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
245 right - half_lr_box + 3,
246 (lr_box_size/2)+step_down + 13);
247 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
250 cairo_show_text (cr, _("M"));
253 cairo_show_text (cr, _("L"));
255 cairo_show_text (cr, _("R"));
259 /* draw the central box */
261 cairo_set_line_width (cr, 1);
262 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
263 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
264 cairo_stroke_preserve (cr);
265 cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
275 StereoPanner::on_button_press_event (GdkEventButton* ev)
277 drag_start_x = ev->x;
280 dragging_position = false;
281 dragging_left = false;
282 dragging_right = false;
284 accumulated_delta = 0;
287 /* Let the binding proxies get first crack at the press event
291 if (position_binder.button_press_handler (ev)) {
295 if (width_binder.button_press_handler (ev)) {
300 if (ev->button != 1) {
304 if (ev->type == GDK_2BUTTON_PRESS) {
305 int width = get_width();
307 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
308 /* handled by button release */
314 /* upper section: adjusts position, constrained by width */
316 const double w = fabs (width_control->get_value ());
317 const double max_pos = 1.0 - (w/2.0);
318 const double min_pos = w/2.0;
320 if (ev->x <= width/3) {
321 /* left side dbl click */
322 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
323 /* 2ndary-double click on left, collapse to hard left */
324 width_control->set_value (0);
325 position_control->set_value (0);
327 position_control->set_value (min_pos);
329 } else if (ev->x > 2*width/3) {
330 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
331 /* 2ndary-double click on right, collapse to hard right */
332 width_control->set_value (0);
333 position_control->set_value (1.0);
335 position_control->set_value (max_pos);
338 position_control->set_value (0.5);
343 /* lower section: adjusts width, constrained by position */
345 const double p = position_control->get_value ();
346 const double max_width = 2.0 * min ((1.0 - p), p);
348 if (ev->x <= width/3) {
349 /* left side dbl click */
350 width_control->set_value (max_width); // reset width to 100%
351 } else if (ev->x > 2*width/3) {
352 /* right side dbl click */
353 width_control->set_value (-max_width); // reset width to inverted 100%
355 /* center dbl click */
356 width_control->set_value (0); // collapse width to 0%
362 } else if (ev->type == GDK_BUTTON_PRESS) {
364 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
365 /* handled by button release */
370 /* top section of widget is for position drags */
371 dragging_position = true;
372 StartPositionGesture ();
374 /* lower section is for dragging width */
376 double pos = position_control->get_value (); /* 0..1 */
377 double swidth = width_control->get_value (); /* -1..+1 */
378 double fswidth = fabs (swidth);
379 int usable_width = get_width() - lr_box_size;
380 double center = (lr_box_size/2.0) + (usable_width * pos);
381 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
382 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
383 const int half_box = lr_box_size/2;
385 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
387 dragging_right = true;
389 dragging_left = true;
391 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
393 dragging_left = true;
395 dragging_right = true;
398 StartWidthGesture ();
408 StereoPanner::on_button_release_event (GdkEventButton* ev)
410 if (ev->button != 1) {
414 bool dp = dragging_position;
417 dragging_position = false;
418 dragging_left = false;
419 dragging_right = false;
420 accumulated_delta = 0;
423 if (drag_data_window) {
424 drag_data_window->hide ();
427 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
428 /* reset to default */
429 position_control->set_value (0.5);
430 width_control->set_value (1.0);
433 StopPositionGesture ();
443 StereoPanner::on_scroll_event (GdkEventScroll* ev)
445 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
446 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
447 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
450 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
453 step = one_degree * 5.0;
456 switch (ev->direction) {
457 case GDK_SCROLL_LEFT:
459 width_control->set_value (wv);
463 position_control->set_value (pv);
465 case GDK_SCROLL_RIGHT:
467 width_control->set_value (wv);
469 case GDK_SCROLL_DOWN:
471 position_control->set_value (pv);
479 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
485 if (!drag_data_window) {
486 drag_data_window = new Window (WINDOW_POPUP);
487 drag_data_window->set_name (X_("ContrastingPopup"));
488 drag_data_window->set_position (WIN_POS_MOUSE);
489 drag_data_window->set_decorated (false);
491 drag_data_label = manage (new Label);
492 drag_data_label->set_use_markup (true);
494 drag_data_window->set_border_width (6);
495 drag_data_window->add (*drag_data_label);
496 drag_data_label->show ();
498 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
500 drag_data_window->set_transient_for (*toplevel);
504 if (!drag_data_window->is_visible ()) {
505 /* move the popup window vertically down from the panner display */
507 get_window()->get_origin (rx, ry);
508 drag_data_window->move (rx, ry+get_height());
509 drag_data_window->present ();
513 double delta = (ev->x - last_drag_x) / (double) w;
514 double current_width = width_control->get_value ();
520 if (dragging_left || dragging_right) {
522 /* maintain position as invariant as we change the width */
525 /* create a detent close to the center */
527 if (!detented && fabs (current_width) < 0.02) {
530 width_control->set_value (0);
535 accumulated_delta += delta;
537 /* have we pulled far enough to escape ? */
539 if (fabs (accumulated_delta) >= 0.025) {
540 width_control->set_value (current_width + accumulated_delta);
542 accumulated_delta = false;
546 width_control->set_value (current_width + delta);
549 } else if (dragging_position) {
551 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
552 position_control->set_value (pv + delta);
560 StereoPanner::on_key_press_event (GdkEventKey* ev)
562 double one_degree = 1.0/180.0;
563 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
564 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
567 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
570 step = one_degree * 5.0;
573 /* up/down control width because we consider pan position more "important"
574 (and thus having higher "sense" priority) than width.
577 switch (ev->keyval) {
579 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
580 width_control->set_value (1.0);
582 width_control->set_value (wv + step);
586 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
587 width_control->set_value (-1.0);
589 width_control->set_value (wv - step);
594 position_control->set_value (pv);
598 position_control->set_value (pv);
604 width_control->set_value (0.0);
615 StereoPanner::on_key_release_event (GdkEventKey* ev)
621 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
624 Keyboard::magic_widget_grab_focus ();
629 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
631 Keyboard::magic_widget_drop_focus ();
636 StereoPanner::set_colors ()
638 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
639 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
640 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
641 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
642 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
644 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
645 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
646 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
647 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
648 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
650 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
651 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
652 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
653 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
654 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
658 StereoPanner::color_handler ()