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->button != 1) {
301 if (ev->type == GDK_2BUTTON_PRESS) {
302 int width = get_width();
304 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
305 /* handled by button release */
311 /* upper section: adjusts position, constrained by width */
313 const double w = width_control->get_value ();
314 const double max_pos = 1.0 - (w/2.0);
315 const double min_pos = w/2.0;
317 if (ev->x <= width/3) {
318 /* left side dbl click */
319 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
320 /* 2ndary-double click on left, collapse to hard left */
321 width_control->set_value (0);
322 position_control->set_value (0);
324 position_control->set_value (min_pos);
326 } else if (ev->x > 2*width/3) {
327 if (Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier)) {
328 /* 2ndary-double click on right, collapse to hard right */
329 width_control->set_value (0);
330 position_control->set_value (1.0);
332 position_control->set_value (max_pos);
334 position_control->set_value (0.5);
339 /* lower section: adjusts width, constrained by position */
341 const double p = position_control->get_value ();
342 const double max_width = 2.0 * min ((1.0 - p), p);
344 if (ev->x <= width/3) {
345 /* left side dbl click */
346 width_control->set_value (max_width); // reset width to 100%
347 } else if (ev->x > 2*width/3) {
348 /* right side dbl click */
349 width_control->set_value (-max_width); // reset width to inverted 100%
351 /* center dbl click */
352 width_control->set_value (0); // collapse width to 0%
358 } else if (ev->type == GDK_BUTTON_PRESS) {
360 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
361 /* handled by button release */
366 /* top section of widget is for position drags */
367 dragging_position = true;
369 /* lower section is for dragging width */
371 double pos = position_control->get_value (); /* 0..1 */
372 double swidth = width_control->get_value (); /* -1..+1 */
373 double fswidth = fabs (swidth);
374 int usable_width = get_width() - lr_box_size;
375 double center = (lr_box_size/2.0) + (usable_width * pos);
376 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
377 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
378 const int half_box = lr_box_size/2;
380 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
381 dragging_left = true;
382 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
383 dragging_right = true;
395 StereoPanner::on_button_release_event (GdkEventButton* ev)
397 if (ev->button != 1) {
402 dragging_position = false;
403 dragging_left = false;
404 dragging_right = false;
405 accumulated_delta = 0;
408 if (drag_data_window) {
409 drag_data_window->hide ();
412 if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
413 /* reset to default */
414 position_control->set_value (0.5);
415 width_control->set_value (1.0);
424 StereoPanner::on_scroll_event (GdkEventScroll* ev)
426 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
427 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
428 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
431 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
434 step = one_degree * 5.0;
437 switch (ev->direction) {
438 case GDK_SCROLL_LEFT:
440 width_control->set_value (wv);
444 position_control->set_value (pv);
446 case GDK_SCROLL_RIGHT:
448 width_control->set_value (wv);
450 case GDK_SCROLL_DOWN:
452 position_control->set_value (pv);
460 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
466 if (!drag_data_window) {
467 drag_data_window = new Window (WINDOW_POPUP);
468 drag_data_window->set_position (WIN_POS_MOUSE);
469 drag_data_window->set_decorated (false);
471 drag_data_label = manage (new Label);
472 drag_data_label->set_use_markup (true);
474 drag_data_window->set_border_width (6);
475 drag_data_window->add (*drag_data_label);
476 drag_data_label->show ();
478 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
480 drag_data_window->set_transient_for (*toplevel);
484 if (!drag_data_window->is_visible ()) {
485 /* move the window a little away from the mouse */
486 drag_data_window->move (ev->x_root+30, ev->y_root+30);
487 drag_data_window->present ();
492 double delta = (ev->x - last_drag_x) / (double) w;
498 if (dragging_left || dragging_right) {
500 /* maintain position as invariant as we change the width */
502 double current_width = width_control->get_value ();
504 /* create a detent close to the center */
506 if (!detented && fabs (current_width) < 0.02) {
509 width_control->set_value (0);
514 accumulated_delta += delta;
516 /* have we pulled far enough to escape ? */
518 if (fabs (accumulated_delta) >= 0.025) {
519 width_control->set_value (current_width + accumulated_delta);
521 accumulated_delta = false;
525 width_control->set_value (current_width + delta);
528 } else if (dragging_position) {
530 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
531 position_control->set_value (pv + delta);
539 StereoPanner::on_key_press_event (GdkEventKey* ev)
541 double one_degree = 1.0/180.0;
542 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
543 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
546 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
549 step = one_degree * 5.0;
552 /* up/down control width because we consider pan position more "important"
553 (and thus having higher "sense" priority) than width.
556 switch (ev->keyval) {
558 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
559 width_control->set_value (1.0);
561 width_control->set_value (wv + step);
565 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
566 width_control->set_value (-1.0);
568 width_control->set_value (wv - step);
573 position_control->set_value (pv);
577 position_control->set_value (pv);
583 width_control->set_value (0.0);
594 StereoPanner::on_key_release_event (GdkEventKey* ev)
600 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
603 Keyboard::magic_widget_grab_focus ();
608 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
610 Keyboard::magic_widget_drop_focus ();
615 StereoPanner::set_colors ()
617 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
618 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
619 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
620 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
622 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
623 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
624 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
625 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
627 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
628 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
629 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
630 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
632 color_change (); /* EMIT SIGNAL */