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;
56 StereoPanner::StereoPanner (boost::shared_ptr<PBD::Controllable> position, boost::shared_ptr<PBD::Controllable> width)
57 : position_control (position)
58 , width_control (width)
60 , dragging_position (false)
61 , dragging_left (false)
62 , dragging_right (false)
65 , accumulated_delta (0)
67 , drag_data_window (0)
69 , position_binder (position)
70 , width_binder (width)
77 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
78 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
80 set_flags (Gtk::CAN_FOCUS);
82 add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
83 Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
84 Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
86 Gdk::POINTER_MOTION_MASK);
88 ColorsChanged.connect (sigc::mem_fun (*this, &StereoPanner::color_handler));
91 StereoPanner::~StereoPanner ()
93 delete drag_data_window;
97 StereoPanner::set_drag_data ()
99 if (!drag_data_label) {
103 double pos = position_control->get_value(); // 0..1
105 /* We show the position of the center of the image relative to the left & right.
106 This is expressed as a pair of percentage values that ranges from (100,0)
107 (hard left) through (50,50) (hard center) to (0,100) (hard right).
109 This is pretty wierd, but its the way audio engineers expect it. Just remember that
110 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
113 drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
114 (int) rint (100.0 * (1.0 - pos)),
115 (int) rint (100.0 * pos),
116 (int) floor (100.0 * width_control->get_value())));
120 StereoPanner::value_change ()
127 StereoPanner::on_expose_event (GdkEventExpose* ev)
129 Glib::RefPtr<Gdk::Window> win (get_window());
130 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
132 cairo_t* cr = gdk_cairo_create (win->gobj());
135 double pos = position_control->get_value (); /* 0..1 */
136 double swidth = width_control->get_value (); /* -1..+1 */
137 double fswidth = fabs (swidth);
142 height = get_height ();
146 } else if (swidth < 0.0) {
152 o = colors[state].outline;
153 f = colors[state].fill;
154 t = colors[state].text;
155 b = colors[state].background;
159 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));
160 cairo_rectangle (cr, 0, 0, width, height);
163 /* the usable width is reduced from the real width, because we need space for
164 the two halves of LR boxes that will extend past the actual left/right
165 positions (indicated by the vertical line segment above them).
168 double usable_width = width - lr_box_size;
170 /* compute the centers of the L/R boxes based on the current stereo width */
172 if (fmod (usable_width,2.0) == 0) {
173 /* even width, but we need odd, so that there is an exact center.
174 So, offset cairo by 1, and reduce effective width by 1
177 cairo_translate (cr, 1.0, 0.0);
180 double center = (lr_box_size/2.0) + (usable_width * pos);
181 const double pan_spread = (fswidth * usable_width)/2.0;
182 const double half_lr_box = lr_box_size/2.0;
186 left = center - pan_spread; // center of left box
187 right = center + pan_spread; // center of right box
189 /* compute & draw the line through the box */
191 cairo_set_line_width (cr, 2);
192 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));
193 cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
194 cairo_line_to (cr, left, top_step+(pos_box_size/2));
195 cairo_line_to (cr, right, top_step+(pos_box_size/2));
196 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
203 half_lr_box+step_down,
204 lr_box_size, lr_box_size);
205 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));
206 cairo_stroke_preserve (cr);
207 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));
213 left - half_lr_box + 3,
214 (lr_box_size/2) + step_down + 13);
215 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
218 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));
220 cairo_show_text (cr, _("R"));
222 cairo_show_text (cr, _("L"));
230 half_lr_box+step_down,
231 lr_box_size, lr_box_size);
232 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));
233 cairo_stroke_preserve (cr);
234 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));
240 right - half_lr_box + 3,
241 (lr_box_size/2)+step_down + 13);
242 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));
245 cairo_show_text (cr, _("M"));
248 cairo_show_text (cr, _("L"));
250 cairo_show_text (cr, _("R"));
254 /* draw the central box */
256 cairo_set_line_width (cr, 1);
257 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
258 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));
259 cairo_stroke_preserve (cr);
260 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));
270 StereoPanner::on_button_press_event (GdkEventButton* ev)
272 drag_start_x = ev->x;
275 dragging_position = false;
276 dragging_left = false;
277 dragging_right = false;
279 accumulated_delta = 0;
282 /* Let the binding proxies get first crack at the press event
286 if (position_binder.button_press_handler (ev)) {
290 if (width_binder.button_press_handler (ev)) {
295 if (ev->button != 1) {
299 if (ev->type == GDK_2BUTTON_PRESS) {
300 int width = get_width();
302 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
303 /* handled by button release */
309 /* upper section: adjusts position, constrained by width */
311 const double w = width_control->get_value ();
312 const double max_pos = 1.0 - (w/2.0);
313 const double min_pos = w/2.0;
315 if (ev->x <= width/3) {
316 /* left side dbl click */
317 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
318 /* 2ndary-double click on left, collapse to hard left */
319 width_control->set_value (0);
320 position_control->set_value (0);
322 position_control->set_value (min_pos);
324 } else if (ev->x > 2*width/3) {
325 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
326 /* 2ndary-double click on right, collapse to hard right */
327 width_control->set_value (0);
328 position_control->set_value (1.0);
330 position_control->set_value (max_pos);
332 position_control->set_value (0.5);
337 /* lower section: adjusts width, constrained by position */
339 const double p = position_control->get_value ();
340 const double max_width = 2.0 * min ((1.0 - p), p);
342 if (ev->x <= width/3) {
343 /* left side dbl click */
344 width_control->set_value (max_width); // reset width to 100%
345 } else if (ev->x > 2*width/3) {
346 /* right side dbl click */
347 width_control->set_value (-max_width); // reset width to inverted 100%
349 /* center dbl click */
350 width_control->set_value (0); // collapse width to 0%
356 } else if (ev->type == GDK_BUTTON_PRESS) {
358 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
359 /* handled by button release */
364 /* top section of widget is for position drags */
365 dragging_position = true;
366 StartPositionGesture ();
368 /* lower section is for dragging width */
370 double pos = position_control->get_value (); /* 0..1 */
371 double swidth = width_control->get_value (); /* -1..+1 */
372 double fswidth = fabs (swidth);
373 int usable_width = get_width() - lr_box_size;
374 double center = (lr_box_size/2.0) + (usable_width * pos);
375 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
376 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
377 const int half_box = lr_box_size/2;
379 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
381 dragging_right = true;
383 dragging_left = true;
385 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
387 dragging_left = true;
389 dragging_right = true;
392 StartWidthGesture ();
402 StereoPanner::on_button_release_event (GdkEventButton* ev)
404 if (ev->button != 1) {
408 bool dp = dragging_position;
411 dragging_position = false;
412 dragging_left = false;
413 dragging_right = false;
414 accumulated_delta = 0;
417 if (drag_data_window) {
418 drag_data_window->hide ();
421 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
422 /* reset to default */
423 position_control->set_value (0.5);
424 width_control->set_value (1.0);
427 StopPositionGesture ();
437 StereoPanner::on_scroll_event (GdkEventScroll* ev)
439 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
440 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
441 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
444 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
447 step = one_degree * 5.0;
450 switch (ev->direction) {
451 case GDK_SCROLL_LEFT:
453 width_control->set_value (wv);
457 position_control->set_value (pv);
459 case GDK_SCROLL_RIGHT:
461 width_control->set_value (wv);
463 case GDK_SCROLL_DOWN:
465 position_control->set_value (pv);
473 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
479 if (!drag_data_window) {
480 drag_data_window = new Window (WINDOW_POPUP);
481 drag_data_window->set_position (WIN_POS_MOUSE);
482 drag_data_window->set_decorated (false);
484 drag_data_label = manage (new Label);
485 drag_data_label->set_use_markup (true);
487 drag_data_window->set_border_width (6);
488 drag_data_window->add (*drag_data_label);
489 drag_data_label->show ();
491 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
493 drag_data_window->set_transient_for (*toplevel);
497 if (!drag_data_window->is_visible ()) {
498 /* move the window a little away from the mouse */
499 drag_data_window->move (ev->x_root+30, ev->y_root+30);
500 drag_data_window->present ();
504 double delta = (ev->x - last_drag_x) / (double) w;
505 double current_width = width_control->get_value ();
511 if (dragging_left || dragging_right) {
513 /* maintain position as invariant as we change the width */
516 /* create a detent close to the center */
518 if (!detented && fabs (current_width) < 0.02) {
521 width_control->set_value (0);
526 accumulated_delta += delta;
528 /* have we pulled far enough to escape ? */
530 if (fabs (accumulated_delta) >= 0.025) {
531 width_control->set_value (current_width + accumulated_delta);
533 accumulated_delta = false;
537 width_control->set_value (current_width + delta);
540 } else if (dragging_position) {
542 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
543 position_control->set_value (pv + delta);
551 StereoPanner::on_key_press_event (GdkEventKey* ev)
553 double one_degree = 1.0/180.0;
554 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
555 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
558 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
561 step = one_degree * 5.0;
564 /* up/down control width because we consider pan position more "important"
565 (and thus having higher "sense" priority) than width.
568 switch (ev->keyval) {
570 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
571 width_control->set_value (1.0);
573 width_control->set_value (wv + step);
577 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
578 width_control->set_value (-1.0);
580 width_control->set_value (wv - step);
585 position_control->set_value (pv);
589 position_control->set_value (pv);
595 width_control->set_value (0.0);
606 StereoPanner::on_key_release_event (GdkEventKey* ev)
612 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
615 Keyboard::magic_widget_grab_focus ();
620 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
622 Keyboard::magic_widget_drop_focus ();
627 StereoPanner::set_colors ()
629 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
630 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
631 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
632 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
633 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
635 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
636 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
637 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
638 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
639 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
641 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
642 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
643 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
644 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
645 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
649 StereoPanner::color_handler ()