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/panner.h"
36 #include "ardour_ui.h"
37 #include "global_signals.h"
38 #include "stereo_panner.h"
39 #include "rgb_macros.h"
46 using namespace Gtkmm2ext;
48 static const int pos_box_size = 10;
49 static const int lr_box_size = 15;
50 static const int step_down = 10;
51 static const int top_step = 2;
53 StereoPanner::ColorScheme StereoPanner::colors[3];
54 bool StereoPanner::have_colors = false;
55 PBD::Signal0<void> StereoPanner::color_change;
57 StereoPanner::StereoPanner (boost::shared_ptr<PBD::Controllable> position, boost::shared_ptr<PBD::Controllable> width)
58 : position_control (position)
59 , width_control (width)
61 , dragging_position (false)
62 , dragging_left (false)
63 , dragging_right (false)
66 , accumulated_delta (0)
68 , drag_data_window (0)
70 , position_binder (position)
71 , width_binder (width)
78 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
79 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
82 set_flags (Gtk::CAN_FOCUS);
84 add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
85 Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
86 Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
88 Gdk::POINTER_MOTION_MASK);
90 color_change.connect (connections, invalidator (*this), boost::bind (&DrawingArea::queue_draw, this), gui_context());
93 StereoPanner::~StereoPanner ()
95 delete drag_data_window;
99 StereoPanner::set_tooltip ()
101 Gtkmm2ext::UI::instance()->set_tip (this,
102 string_compose (_("0 -> set width to zero (mono)\n%1-uparrow -> set width to 100\n%1-downarrow -> set width to -100"),
104 Keyboard::secondary_modifier_name()).c_str());
108 StereoPanner::unset_tooltip ()
110 Gtkmm2ext::UI::instance()->set_tip (this, "");
114 StereoPanner::set_drag_data ()
116 if (!drag_data_label) {
120 double pos = position_control->get_value(); // 0..1
122 /* We show the position of the center of the image relative to the left & right.
123 This is expressed as a pair of percentage values that ranges from (100,0)
124 (hard left) through (50,50) (hard center) to (0,100) (hard right).
126 This is pretty wierd, but its the way audio engineers expect it. Just remember that
127 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
130 drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
131 (int) rint (100.0 * (1.0 - pos)),
132 (int) rint (100.0 * pos),
133 (int) floor (100.0 * width_control->get_value())));
137 StereoPanner::value_change ()
144 StereoPanner::on_expose_event (GdkEventExpose* ev)
146 Glib::RefPtr<Gdk::Window> win (get_window());
147 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
149 cairo_t* cr = gdk_cairo_create (win->gobj());
152 double pos = position_control->get_value (); /* 0..1 */
153 double swidth = width_control->get_value (); /* -1..+1 */
154 double fswidth = fabs (swidth);
159 height = get_height ();
163 } else if (swidth < 0.0) {
169 o = colors[state].outline;
170 f = colors[state].fill;
171 t = colors[state].text;
172 b = colors[state].background;
176 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));
177 cairo_rectangle (cr, 0, 0, width, height);
180 /* the usable width is reduced from the real width, because we need space for
181 the two halves of LR boxes that will extend past the actual left/right
182 positions (indicated by the vertical line segment above them).
185 double usable_width = width - lr_box_size;
187 /* compute the centers of the L/R boxes based on the current stereo width */
189 if (fmod (usable_width,2.0) == 0) {
190 /* even width, but we need odd, so that there is an exact center.
191 So, offset cairo by 1, and reduce effective width by 1
194 cairo_translate (cr, 1.0, 0.0);
197 double center = (lr_box_size/2.0) + (usable_width * pos);
198 const double pan_spread = (fswidth * usable_width)/2.0;
199 const double half_lr_box = lr_box_size/2.0;
203 left = center - pan_spread; // center of left box
204 right = center + pan_spread; // center of right box
206 /* compute & draw the line through the box */
208 cairo_set_line_width (cr, 2);
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_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
211 cairo_line_to (cr, left, top_step+(pos_box_size/2));
212 cairo_line_to (cr, right, top_step+(pos_box_size/2));
213 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
220 half_lr_box+step_down,
221 lr_box_size, lr_box_size);
222 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));
223 cairo_stroke_preserve (cr);
224 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));
230 left - half_lr_box + 3,
231 (lr_box_size/2) + step_down + 13);
232 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
235 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));
237 cairo_show_text (cr, _("R"));
239 cairo_show_text (cr, _("L"));
247 half_lr_box+step_down,
248 lr_box_size, lr_box_size);
249 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));
250 cairo_stroke_preserve (cr);
251 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));
257 right - half_lr_box + 3,
258 (lr_box_size/2)+step_down + 13);
259 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));
262 cairo_show_text (cr, _("M"));
265 cairo_show_text (cr, _("L"));
267 cairo_show_text (cr, _("R"));
271 /* draw the central box */
273 cairo_set_line_width (cr, 1);
274 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
275 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));
276 cairo_stroke_preserve (cr);
277 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));
287 StereoPanner::on_button_press_event (GdkEventButton* ev)
289 drag_start_x = ev->x;
292 dragging_position = false;
293 dragging_left = false;
294 dragging_right = false;
296 accumulated_delta = 0;
299 /* Let the binding proxies get first crack at the press event
303 if (position_binder.button_press_handler (ev)) {
307 if (width_binder.button_press_handler (ev)) {
312 if (ev->button != 1) {
316 if (ev->type == GDK_2BUTTON_PRESS) {
317 int width = get_width();
319 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
320 /* handled by button release */
326 /* upper section: adjusts position, constrained by width */
328 const double w = width_control->get_value ();
329 const double max_pos = 1.0 - (w/2.0);
330 const double min_pos = w/2.0;
332 if (ev->x <= width/3) {
333 /* left side dbl click */
334 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
335 /* 2ndary-double click on left, collapse to hard left */
336 width_control->set_value (0);
337 position_control->set_value (0);
339 position_control->set_value (min_pos);
341 } else if (ev->x > 2*width/3) {
342 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
343 /* 2ndary-double click on right, collapse to hard right */
344 width_control->set_value (0);
345 position_control->set_value (1.0);
347 position_control->set_value (max_pos);
349 position_control->set_value (0.5);
354 /* lower section: adjusts width, constrained by position */
356 const double p = position_control->get_value ();
357 const double max_width = 2.0 * min ((1.0 - p), p);
359 if (ev->x <= width/3) {
360 /* left side dbl click */
361 width_control->set_value (max_width); // reset width to 100%
362 } else if (ev->x > 2*width/3) {
363 /* right side dbl click */
364 width_control->set_value (-max_width); // reset width to inverted 100%
366 /* center dbl click */
367 width_control->set_value (0); // collapse width to 0%
373 } else if (ev->type == GDK_BUTTON_PRESS) {
375 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
376 /* handled by button release */
381 /* top section of widget is for position drags */
382 dragging_position = true;
384 /* lower section is for dragging width */
386 double pos = position_control->get_value (); /* 0..1 */
387 double swidth = width_control->get_value (); /* -1..+1 */
388 double fswidth = fabs (swidth);
389 int usable_width = get_width() - lr_box_size;
390 double center = (lr_box_size/2.0) + (usable_width * pos);
391 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
392 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
393 const int half_box = lr_box_size/2;
395 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
397 dragging_right = true;
399 dragging_left = true;
401 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
403 dragging_left = true;
405 dragging_right = true;
417 StereoPanner::on_button_release_event (GdkEventButton* ev)
419 if (ev->button != 1) {
424 dragging_position = false;
425 dragging_left = false;
426 dragging_right = false;
427 accumulated_delta = 0;
430 if (drag_data_window) {
431 drag_data_window->hide ();
434 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
435 /* reset to default */
436 position_control->set_value (0.5);
437 width_control->set_value (1.0);
446 StereoPanner::on_scroll_event (GdkEventScroll* ev)
448 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
449 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
450 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
453 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
456 step = one_degree * 5.0;
459 switch (ev->direction) {
460 case GDK_SCROLL_LEFT:
462 width_control->set_value (wv);
466 position_control->set_value (pv);
468 case GDK_SCROLL_RIGHT:
470 width_control->set_value (wv);
472 case GDK_SCROLL_DOWN:
474 position_control->set_value (pv);
482 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
488 if (!drag_data_window) {
489 drag_data_window = new Window (WINDOW_POPUP);
490 drag_data_window->set_position (WIN_POS_MOUSE);
491 drag_data_window->set_decorated (false);
493 drag_data_label = manage (new Label);
494 drag_data_label->set_use_markup (true);
496 drag_data_window->set_border_width (6);
497 drag_data_window->add (*drag_data_label);
498 drag_data_label->show ();
500 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
502 drag_data_window->set_transient_for (*toplevel);
506 if (!drag_data_window->is_visible ()) {
507 /* move the window a little away from the mouse */
508 drag_data_window->move (ev->x_root+30, ev->y_root+30);
509 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();
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();
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();
654 color_change (); /* EMIT SIGNAL */