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.
117 drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
118 (int) rint (100.0 * (1.0 - pos)),
119 (int) rint (100.0 * pos),
120 (int) floor (100.0 * width_control->get_value())));
124 StereoPanner::value_change ()
131 StereoPanner::on_expose_event (GdkEventExpose* ev)
133 Glib::RefPtr<Gdk::Window> win (get_window());
134 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
136 cairo_t* cr = gdk_cairo_create (win->gobj());
139 double pos = position_control->get_value (); /* 0..1 */
140 double swidth = width_control->get_value (); /* -1..+1 */
141 double fswidth = fabs (swidth);
146 height = get_height ();
150 } else if (swidth < 0.0) {
156 o = colors[state].outline;
157 f = colors[state].fill;
158 t = colors[state].text;
159 b = colors[state].background;
163 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));
164 cairo_rectangle (cr, 0, 0, width, height);
167 /* the usable width is reduced from the real width, because we need space for
168 the two halves of LR boxes that will extend past the actual left/right
169 positions (indicated by the vertical line segment above them).
172 double usable_width = width - lr_box_size;
174 /* compute the centers of the L/R boxes based on the current stereo width */
176 if (fmod (usable_width,2.0) == 0) {
177 /* even width, but we need odd, so that there is an exact center.
178 So, offset cairo by 1, and reduce effective width by 1
181 cairo_translate (cr, 1.0, 0.0);
184 double center = (lr_box_size/2.0) + (usable_width * pos);
185 const double pan_spread = (fswidth * usable_width)/2.0;
186 const double half_lr_box = lr_box_size/2.0;
190 left = center - pan_spread; // center of left box
191 right = center + pan_spread; // center of right box
193 /* compute & draw the line through the box */
195 cairo_set_line_width (cr, 2);
196 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));
197 cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
198 cairo_line_to (cr, left, top_step+(pos_box_size/2));
199 cairo_line_to (cr, right, top_step+(pos_box_size/2));
200 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
207 half_lr_box+step_down,
208 lr_box_size, lr_box_size);
209 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));
210 cairo_stroke_preserve (cr);
211 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));
217 left - half_lr_box + 3,
218 (lr_box_size/2) + step_down + 13);
219 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
222 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));
224 cairo_show_text (cr, _("R"));
226 cairo_show_text (cr, _("L"));
234 half_lr_box+step_down,
235 lr_box_size, lr_box_size);
236 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));
237 cairo_stroke_preserve (cr);
238 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));
244 right - half_lr_box + 3,
245 (lr_box_size/2)+step_down + 13);
246 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));
249 cairo_show_text (cr, _("M"));
252 cairo_show_text (cr, _("L"));
254 cairo_show_text (cr, _("R"));
258 /* draw the central box */
260 cairo_set_line_width (cr, 1);
261 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
262 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));
263 cairo_stroke_preserve (cr);
264 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));
274 StereoPanner::on_button_press_event (GdkEventButton* ev)
276 drag_start_x = ev->x;
279 dragging_position = false;
280 dragging_left = false;
281 dragging_right = false;
283 accumulated_delta = 0;
286 /* Let the binding proxies get first crack at the press event
290 if (position_binder.button_press_handler (ev)) {
294 if (width_binder.button_press_handler (ev)) {
299 if (ev->button != 1) {
303 if (ev->type == GDK_2BUTTON_PRESS) {
304 int width = get_width();
306 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
307 /* handled by button release */
313 /* upper section: adjusts position, constrained by width */
315 const double w = width_control->get_value ();
316 const double max_pos = 1.0 - (w/2.0);
317 const double min_pos = w/2.0;
319 if (ev->x <= width/3) {
320 /* left side dbl click */
321 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
322 /* 2ndary-double click on left, collapse to hard left */
323 width_control->set_value (0);
324 position_control->set_value (0);
326 position_control->set_value (min_pos);
328 } else if (ev->x > 2*width/3) {
329 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
330 /* 2ndary-double click on right, collapse to hard right */
331 width_control->set_value (0);
332 position_control->set_value (1.0);
334 position_control->set_value (max_pos);
337 position_control->set_value (0.5);
342 /* lower section: adjusts width, constrained by position */
344 const double p = position_control->get_value ();
345 const double max_width = 2.0 * min ((1.0 - p), p);
347 if (ev->x <= width/3) {
348 /* left side dbl click */
349 width_control->set_value (max_width); // reset width to 100%
350 } else if (ev->x > 2*width/3) {
351 /* right side dbl click */
352 width_control->set_value (-max_width); // reset width to inverted 100%
354 /* center dbl click */
355 width_control->set_value (0); // collapse width to 0%
361 } else if (ev->type == GDK_BUTTON_PRESS) {
363 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
364 /* handled by button release */
369 /* top section of widget is for position drags */
370 dragging_position = true;
371 StartPositionGesture ();
373 /* lower section is for dragging width */
375 double pos = position_control->get_value (); /* 0..1 */
376 double swidth = width_control->get_value (); /* -1..+1 */
377 double fswidth = fabs (swidth);
378 int usable_width = get_width() - lr_box_size;
379 double center = (lr_box_size/2.0) + (usable_width * pos);
380 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
381 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
382 const int half_box = lr_box_size/2;
384 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
386 dragging_right = true;
388 dragging_left = true;
390 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
392 dragging_left = true;
394 dragging_right = true;
397 StartWidthGesture ();
407 StereoPanner::on_button_release_event (GdkEventButton* ev)
409 if (ev->button != 1) {
413 bool dp = dragging_position;
416 dragging_position = false;
417 dragging_left = false;
418 dragging_right = false;
419 accumulated_delta = 0;
422 if (drag_data_window) {
423 drag_data_window->hide ();
426 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
427 /* reset to default */
428 position_control->set_value (0.5);
429 width_control->set_value (1.0);
432 StopPositionGesture ();
442 StereoPanner::on_scroll_event (GdkEventScroll* ev)
444 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
445 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
446 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
449 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
452 step = one_degree * 5.0;
455 switch (ev->direction) {
456 case GDK_SCROLL_LEFT:
458 width_control->set_value (wv);
462 position_control->set_value (pv);
464 case GDK_SCROLL_RIGHT:
466 width_control->set_value (wv);
468 case GDK_SCROLL_DOWN:
470 position_control->set_value (pv);
478 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
484 if (!drag_data_window) {
485 drag_data_window = new Window (WINDOW_POPUP);
486 drag_data_window->set_name (X_("ContrastingPopup"));
487 drag_data_window->set_position (WIN_POS_MOUSE);
488 drag_data_window->set_decorated (false);
490 drag_data_label = manage (new Label);
491 drag_data_label->set_use_markup (true);
493 drag_data_window->set_border_width (6);
494 drag_data_window->add (*drag_data_label);
495 drag_data_label->show ();
497 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
499 drag_data_window->set_transient_for (*toplevel);
503 if (!drag_data_window->is_visible ()) {
504 /* move the popup window vertically down from the panner display */
506 get_window()->get_origin (rx, ry);
507 drag_data_window->move (rx, ry+get_height());
508 drag_data_window->present ();
512 double delta = (ev->x - last_drag_x) / (double) w;
513 double current_width = width_control->get_value ();
519 if (dragging_left || dragging_right) {
521 /* maintain position as invariant as we change the width */
524 /* create a detent close to the center */
526 if (!detented && fabs (current_width) < 0.02) {
529 width_control->set_value (0);
534 accumulated_delta += delta;
536 /* have we pulled far enough to escape ? */
538 if (fabs (accumulated_delta) >= 0.025) {
539 width_control->set_value (current_width + accumulated_delta);
541 accumulated_delta = false;
545 width_control->set_value (current_width + delta);
548 } else if (dragging_position) {
550 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
551 position_control->set_value (pv + delta);
559 StereoPanner::on_key_press_event (GdkEventKey* ev)
561 double one_degree = 1.0/180.0;
562 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
563 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
566 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
569 step = one_degree * 5.0;
572 /* up/down control width because we consider pan position more "important"
573 (and thus having higher "sense" priority) than width.
576 switch (ev->keyval) {
578 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
579 width_control->set_value (1.0);
581 width_control->set_value (wv + step);
585 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
586 width_control->set_value (-1.0);
588 width_control->set_value (wv - step);
593 position_control->set_value (pv);
597 position_control->set_value (pv);
603 width_control->set_value (0.0);
614 StereoPanner::on_key_release_event (GdkEventKey* ev)
620 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
623 Keyboard::magic_widget_grab_focus ();
628 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
630 Keyboard::magic_widget_drop_focus ();
635 StereoPanner::set_colors ()
637 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
638 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
639 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
640 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
641 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
643 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
644 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
645 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
646 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
647 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
649 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
650 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
651 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
652 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
653 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
657 StereoPanner::color_handler ()