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)
76 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
77 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 color_change.connect (connections, invalidator (*this), boost::bind (&DrawingArea::queue_draw, this), gui_context());
91 StereoPanner::~StereoPanner ()
93 delete drag_data_window;
97 StereoPanner::set_tooltip ()
99 Gtkmm2ext::UI::instance()->set_tip (this,
100 string_compose (_("0 -> set width to zero (mono)\n%1-uparrow -> set width to 100\n%1-downarrow -> set width to -100"),
102 Keyboard::secondary_modifier_name()).c_str());
106 StereoPanner::unset_tooltip ()
108 Gtkmm2ext::UI::instance()->set_tip (this, "");
112 StereoPanner::set_drag_data ()
114 if (!drag_data_label) {
118 double pos = position_control->get_value(); // 0..1
120 /* We show the position of the center of the image relative to the left & right.
121 This is expressed as a pair of percentage values that ranges from (100,0)
122 (hard left) through (50,50) (hard center) to (0,100) (hard right).
124 This is pretty wierd, but its the way audio engineers expect it. Just remember that
125 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
128 drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
129 (int) rint (100.0 * (1.0 - pos)),
130 (int) rint (100.0 * pos),
131 (int) floor (100.0 * width_control->get_value())));
135 StereoPanner::value_change ()
142 StereoPanner::on_expose_event (GdkEventExpose* ev)
144 Glib::RefPtr<Gdk::Window> win (get_window());
145 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
147 cairo_t* cr = gdk_cairo_create (win->gobj());
150 double pos = position_control->get_value (); /* 0..1 */
151 double swidth = width_control->get_value (); /* -1..+1 */
152 double fswidth = fabs (swidth);
157 height = get_height ();
161 } else if (swidth < 0.0) {
167 o = colors[state].outline;
168 f = colors[state].fill;
169 t = colors[state].text;
170 b = colors[state].background;
174 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));
175 cairo_rectangle (cr, 0, 0, width, height);
178 /* the usable width is reduced from the real width, because we need space for
179 the two halves of LR boxes that will extend past the actual left/right
180 positions (indicated by the vertical line segment above them).
183 double usable_width = width - lr_box_size;
185 /* compute the centers of the L/R boxes based on the current stereo width */
187 if (fmod (usable_width,2.0) == 0) {
188 /* even width, but we need odd, so that there is an exact center.
189 So, offset cairo by 1, and reduce effective width by 1
192 cairo_translate (cr, 1.0, 0.0);
195 double center = (lr_box_size/2.0) + (usable_width * pos);
196 const double pan_spread = (fswidth * usable_width)/2.0;
197 const double half_lr_box = lr_box_size/2.0;
201 left = center - pan_spread; // center of left box
202 right = center + pan_spread; // center of right box
204 /* compute & draw the line through the box */
206 cairo_set_line_width (cr, 2);
207 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));
208 cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
209 cairo_line_to (cr, left, top_step+(pos_box_size/2));
210 cairo_line_to (cr, right, top_step+(pos_box_size/2));
211 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
218 half_lr_box+step_down,
219 lr_box_size, lr_box_size);
220 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));
221 cairo_stroke_preserve (cr);
222 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));
228 left - half_lr_box + 3,
229 (lr_box_size/2) + step_down + 13);
230 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
233 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));
235 cairo_show_text (cr, _("R"));
237 cairo_show_text (cr, _("L"));
245 half_lr_box+step_down,
246 lr_box_size, lr_box_size);
247 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));
248 cairo_stroke_preserve (cr);
249 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));
255 right - half_lr_box + 3,
256 (lr_box_size/2)+step_down + 13);
257 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));
260 cairo_show_text (cr, _("M"));
263 cairo_show_text (cr, _("L"));
265 cairo_show_text (cr, _("R"));
269 /* draw the central box */
271 cairo_set_line_width (cr, 1);
272 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
273 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));
274 cairo_stroke_preserve (cr);
275 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));
285 StereoPanner::on_button_press_event (GdkEventButton* ev)
287 drag_start_x = ev->x;
290 dragging_position = false;
291 dragging_left = false;
292 dragging_right = false;
294 accumulated_delta = 0;
297 if (ev->type == GDK_2BUTTON_PRESS) {
298 int width = get_width();
300 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
301 /* handled by button release */
306 /* lower section: adjusts position, constrained by width */
308 if (ev->x >= width/2 - 10 && ev->x <= width/2 + 10) {
309 /* double click near center, reset position to center */
310 position_control->set_value (0.5);
312 if (ev->x < width/2) {
313 /* double click on left, collapse to hard left */
314 width_control->set_value (0);
315 position_control->set_value (0);
317 /* double click on right, collapse to hard right */
318 width_control->set_value (0);
319 position_control->set_value (1.0);
324 /* lower section: adjusts width, constrained by position */
326 if (ev->x <= width/3) {
327 /* left side dbl click */
328 width_control->set_value (1.0); // reset width to 100%
329 } else if (ev->x > 2*width/3) {
330 /* right side dbl click */
331 width_control->set_value (-1.0); // reset width to inverted 100%
333 /* center dbl click */
334 width_control->set_value (0); // collapse width to 0%
340 } else if (ev->type == GDK_BUTTON_PRESS) {
342 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
343 /* handled by button release */
348 /* top section of widget is for position drags */
349 dragging_position = true;
351 /* lower section is for dragging width */
353 double pos = position_control->get_value (); /* 0..1 */
354 double swidth = width_control->get_value (); /* -1..+1 */
355 double fswidth = fabs (swidth);
356 int usable_width = get_width() - lr_box_size;
357 double center = (lr_box_size/2.0) + (usable_width * pos);
358 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
359 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
360 const int half_box = lr_box_size/2;
362 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
363 dragging_left = true;
364 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
365 dragging_right = true;
377 StereoPanner::on_button_release_event (GdkEventButton* ev)
380 dragging_position = false;
381 dragging_left = false;
382 dragging_right = false;
383 accumulated_delta = 0;
386 if (drag_data_window) {
387 drag_data_window->hide ();
390 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
391 /* reset to default */
392 position_control->set_value (0.5);
393 width_control->set_value (1.0);
402 StereoPanner::on_scroll_event (GdkEventScroll* ev)
404 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
405 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
406 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
409 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
412 step = one_degree * 5.0;
415 switch (ev->direction) {
416 case GDK_SCROLL_LEFT:
418 width_control->set_value (wv);
422 position_control->set_value (pv);
424 case GDK_SCROLL_RIGHT:
426 width_control->set_value (wv);
428 case GDK_SCROLL_DOWN:
430 position_control->set_value (pv);
438 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
444 if (!drag_data_window) {
445 drag_data_window = new Window (WINDOW_POPUP);
446 drag_data_window->set_position (WIN_POS_MOUSE);
447 drag_data_window->set_decorated (false);
449 drag_data_label = manage (new Label);
450 drag_data_label->set_use_markup (true);
452 drag_data_window->set_border_width (6);
453 drag_data_window->add (*drag_data_label);
454 drag_data_label->show ();
456 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
458 drag_data_window->set_transient_for (*toplevel);
462 if (!drag_data_window->is_visible ()) {
463 /* move the window a little away from the mouse */
464 drag_data_window->move (ev->x_root+30, ev->y_root+30);
465 drag_data_window->present ();
470 double delta = (ev->x - last_drag_x) / (double) w;
476 if (dragging_left || dragging_right) {
478 /* maintain position as invariant as we change the width */
480 double current_width = width_control->get_value ();
482 /* create a detent close to the center */
484 if (!detented && fabs (current_width) < 0.02) {
487 width_control->set_value (0);
492 accumulated_delta += delta;
494 /* have we pulled far enough to escape ? */
496 if (fabs (accumulated_delta) >= 0.1) {
497 width_control->set_value (current_width + accumulated_delta);
499 accumulated_delta = false;
503 width_control->set_value (current_width + delta);
506 } else if (dragging_position) {
508 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
509 position_control->set_value (pv + delta);
517 StereoPanner::on_key_press_event (GdkEventKey* ev)
519 double one_degree = 1.0/180.0;
520 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
521 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
524 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
527 step = one_degree * 5.0;
530 /* up/down control width because we consider pan position more "important"
531 (and thus having higher "sense" priority) than width.
534 switch (ev->keyval) {
536 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
537 width_control->set_value (1.0);
539 width_control->set_value (wv + step);
543 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
544 width_control->set_value (-1.0);
546 width_control->set_value (wv - step);
551 position_control->set_value (pv);
555 position_control->set_value (pv);
561 width_control->set_value (0.0);
572 StereoPanner::on_key_release_event (GdkEventKey* ev)
578 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
581 Keyboard::magic_widget_grab_focus ();
586 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
588 Keyboard::magic_widget_drop_focus ();
593 StereoPanner::set_colors ()
595 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
596 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
597 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
598 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
600 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
601 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
602 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
603 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
605 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
606 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
607 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
608 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
610 color_change (); /* EMIT SIGNAL */