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;
367 /* lower section is for dragging width */
369 double pos = position_control->get_value (); /* 0..1 */
370 double swidth = width_control->get_value (); /* -1..+1 */
371 double fswidth = fabs (swidth);
372 int usable_width = get_width() - lr_box_size;
373 double center = (lr_box_size/2.0) + (usable_width * pos);
374 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
375 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
376 const int half_box = lr_box_size/2;
378 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
380 dragging_right = true;
382 dragging_left = true;
384 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
386 dragging_left = true;
388 dragging_right = true;
400 StereoPanner::on_button_release_event (GdkEventButton* ev)
402 if (ev->button != 1) {
407 dragging_position = false;
408 dragging_left = false;
409 dragging_right = false;
410 accumulated_delta = 0;
413 if (drag_data_window) {
414 drag_data_window->hide ();
417 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
418 /* reset to default */
419 position_control->set_value (0.5);
420 width_control->set_value (1.0);
427 StereoPanner::on_scroll_event (GdkEventScroll* ev)
429 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
430 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
431 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
434 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
437 step = one_degree * 5.0;
440 switch (ev->direction) {
441 case GDK_SCROLL_LEFT:
443 width_control->set_value (wv);
447 position_control->set_value (pv);
449 case GDK_SCROLL_RIGHT:
451 width_control->set_value (wv);
453 case GDK_SCROLL_DOWN:
455 position_control->set_value (pv);
463 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
469 if (!drag_data_window) {
470 drag_data_window = new Window (WINDOW_POPUP);
471 drag_data_window->set_position (WIN_POS_MOUSE);
472 drag_data_window->set_decorated (false);
474 drag_data_label = manage (new Label);
475 drag_data_label->set_use_markup (true);
477 drag_data_window->set_border_width (6);
478 drag_data_window->add (*drag_data_label);
479 drag_data_label->show ();
481 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
483 drag_data_window->set_transient_for (*toplevel);
487 if (!drag_data_window->is_visible ()) {
488 /* move the window a little away from the mouse */
489 drag_data_window->move (ev->x_root+30, ev->y_root+30);
490 drag_data_window->present ();
494 double delta = (ev->x - last_drag_x) / (double) w;
495 double current_width = width_control->get_value ();
501 if (dragging_left || dragging_right) {
503 /* maintain position as invariant as we change the width */
506 /* create a detent close to the center */
508 if (!detented && fabs (current_width) < 0.02) {
511 width_control->set_value (0);
516 accumulated_delta += delta;
518 /* have we pulled far enough to escape ? */
520 if (fabs (accumulated_delta) >= 0.025) {
521 width_control->set_value (current_width + accumulated_delta);
523 accumulated_delta = false;
527 width_control->set_value (current_width + delta);
530 } else if (dragging_position) {
532 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
533 position_control->set_value (pv + delta);
541 StereoPanner::on_key_press_event (GdkEventKey* ev)
543 double one_degree = 1.0/180.0;
544 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
545 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
548 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
551 step = one_degree * 5.0;
554 /* up/down control width because we consider pan position more "important"
555 (and thus having higher "sense" priority) than width.
558 switch (ev->keyval) {
560 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
561 width_control->set_value (1.0);
563 width_control->set_value (wv + step);
567 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
568 width_control->set_value (-1.0);
570 width_control->set_value (wv - step);
575 position_control->set_value (pv);
579 position_control->set_value (pv);
585 width_control->set_value (0.0);
596 StereoPanner::on_key_release_event (GdkEventKey* ev)
602 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
605 Keyboard::magic_widget_grab_focus ();
610 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
612 Keyboard::magic_widget_drop_focus ();
617 StereoPanner::set_colors ()
619 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
620 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
621 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
622 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
623 colors[Normal].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
625 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
626 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
627 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
628 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
629 colors[Mono].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
631 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
632 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
633 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
634 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
635 colors[Inverted].rule = ARDOUR_UI::config()->canvasvar_StereoPannerRule.get();
639 StereoPanner::color_handler ()