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)
67 , drag_data_window (0)
75 position_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
76 width_control->Changed.connect (connections, invalidator(*this), boost::bind (&StereoPanner::value_change, this), gui_context());
79 set_flags (Gtk::CAN_FOCUS);
81 add_events (Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK|
82 Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK|
83 Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|
85 Gdk::POINTER_MOTION_MASK);
87 color_change.connect (connections, invalidator (*this), boost::bind (&DrawingArea::queue_draw, this), gui_context());
90 StereoPanner::~StereoPanner ()
92 delete drag_data_window;
96 StereoPanner::set_tooltip ()
98 Gtkmm2ext::UI::instance()->set_tip (this,
99 string_compose (_("0 -> set width to zero (mono)\n%1-uparrow -> set width to 100\n%1-downarrow -> set width to -100"),
101 Keyboard::secondary_modifier_name()).c_str());
105 StereoPanner::unset_tooltip ()
107 Gtkmm2ext::UI::instance()->set_tip (this, "");
111 StereoPanner::set_drag_data ()
113 if (!drag_data_label) {
117 double pos = position_control->get_value(); // 0..1
119 /* We show the position of the center of the image relative to the left & right.
120 This is expressed as a pair of percentage values that ranges from (100,0)
121 (hard left) through (50,50) (hard center) to (0,100) (hard right).
123 This is pretty wierd, but its the way audio engineers expect it. Just remember that
124 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
127 drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
128 (int) rint (100.0 * (1.0 - pos)),
129 (int) rint (100.0 * pos),
130 (int) floor (100.0 * width_control->get_value())));
134 StereoPanner::value_change ()
141 StereoPanner::on_expose_event (GdkEventExpose* ev)
143 Glib::RefPtr<Gdk::Window> win (get_window());
144 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
146 cairo_t* cr = gdk_cairo_create (win->gobj());
149 double pos = position_control->get_value (); /* 0..1 */
150 double swidth = width_control->get_value (); /* -1..+1 */
151 double fswidth = fabs (swidth);
156 height = get_height ();
160 } else if (swidth < 0.0) {
166 o = colors[state].outline;
167 f = colors[state].fill;
168 t = colors[state].text;
169 b = colors[state].background;
173 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));
174 cairo_rectangle (cr, 0, 0, width, height);
177 /* the usable width is reduced from the real width, because we need space for
178 the two halves of LR boxes that will extend past the actual left/right
179 positions (indicated by the vertical line segment above them).
182 double usable_width = width - lr_box_size;
184 /* compute the centers of the L/R boxes based on the current stereo width */
186 if (fmod (usable_width,2.0) == 0) {
187 /* even width, but we need odd, so that there is an exact center.
188 So, offset cairo by 1, and reduce effective width by 1
191 cairo_translate (cr, 1.0, 0.0);
194 double center = (lr_box_size/2.0) + (usable_width * pos);
195 const double pan_spread = (fswidth * usable_width)/2.0;
196 const double half_lr_box = lr_box_size/2.0;
200 left = center - pan_spread; // center of left box
201 right = center + pan_spread; // center of right box
203 /* compute & draw the line through the box */
205 cairo_set_line_width (cr, 2);
206 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));
207 cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
208 cairo_line_to (cr, left, top_step+(pos_box_size/2));
209 cairo_line_to (cr, right, top_step+(pos_box_size/2));
210 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
217 half_lr_box+step_down,
218 lr_box_size, lr_box_size);
219 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));
220 cairo_stroke_preserve (cr);
221 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));
227 left - half_lr_box + 3,
228 (lr_box_size/2) + step_down + 13);
229 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
232 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));
234 cairo_show_text (cr, _("R"));
236 cairo_show_text (cr, _("L"));
244 half_lr_box+step_down,
245 lr_box_size, lr_box_size);
246 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));
247 cairo_stroke_preserve (cr);
248 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));
254 right - half_lr_box + 3,
255 (lr_box_size/2)+step_down + 13);
256 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));
259 cairo_show_text (cr, _("M"));
262 cairo_show_text (cr, _("L"));
264 cairo_show_text (cr, _("R"));
268 /* draw the central box */
270 cairo_set_line_width (cr, 1);
271 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
272 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));
273 cairo_stroke_preserve (cr);
274 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));
284 StereoPanner::on_button_press_event (GdkEventButton* ev)
286 drag_start_x = ev->x;
289 dragging_position = false;
290 dragging_left = false;
291 dragging_right = false;
292 accumulated_delta = 0;
295 /* top section of widget is for position drags */
296 dragging_position = true;
298 /* lower section is for dragging width */
300 double pos = position_control->get_value (); /* 0..1 */
301 double swidth = width_control->get_value (); /* -1..+1 */
302 double fswidth = fabs (swidth);
303 int usable_width = get_width() - lr_box_size;
304 double center = (lr_box_size/2.0) + (usable_width * pos);
305 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
306 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
307 const int half_box = lr_box_size/2;
309 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
310 dragging_left = true;
311 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
312 dragging_right = true;
317 if (ev->type == GDK_2BUTTON_PRESS) {
318 if (dragging_position) {
319 int width = get_width();
320 if (ev->x >= width/2 - 10 && ev->x <= width/2 + 10) {
321 /* double click near center, reset position to center */
322 position_control->set_value (0.5);
324 if (ev->x < width/2) {
325 /* double click on left, collapse to hard left */
326 width_control->set_value (0);
327 position_control->set_value (0);
329 /* double click on right, collapse to hard right */
330 width_control->set_value (0);
331 position_control->set_value (1.0);
336 width_control->set_value (1.0); // reset width to 100%
337 } else if (dragging_right) {
338 width_control->set_value (-1.0); // reset width to inverted 100%
340 width_control->set_value (0); // collapse width to 0%
355 StereoPanner::on_button_release_event (GdkEventButton* ev)
358 dragging_position = false;
359 dragging_left = false;
360 dragging_right = false;
361 accumulated_delta = 0;
363 if (drag_data_window) {
364 drag_data_window->hide ();
373 StereoPanner::on_scroll_event (GdkEventScroll* ev)
375 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
376 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
377 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
380 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
383 step = one_degree * 5.0;
386 switch (ev->direction) {
387 case GDK_SCROLL_LEFT:
389 width_control->set_value (wv);
393 position_control->set_value (pv);
395 case GDK_SCROLL_RIGHT:
397 width_control->set_value (wv);
399 case GDK_SCROLL_DOWN:
401 position_control->set_value (pv);
409 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
415 if (!drag_data_window) {
416 drag_data_window = new Window (WINDOW_POPUP);
417 drag_data_window->set_position (WIN_POS_MOUSE);
418 drag_data_window->set_decorated (false);
420 drag_data_label = manage (new Label);
421 drag_data_label->set_use_markup (true);
423 drag_data_window->set_border_width (6);
424 drag_data_window->add (*drag_data_label);
425 drag_data_label->show ();
427 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
429 drag_data_window->set_transient_for (*toplevel);
433 if (!drag_data_window->is_visible ()) {
434 /* move the window a little away from the mouse */
435 drag_data_window->move (ev->x_root+30, ev->y_root+30);
436 drag_data_window->present ();
441 double delta = (ev->x - last_drag_x) / (double) w;
447 if (dragging_left || dragging_right) {
449 /* maintain position as invariant as we change the width */
451 double current_width = width_control->get_value ();
453 /* create a detent close to the center */
455 if (fabs (current_width) < 0.1) {
456 accumulated_delta += delta;
457 /* in the detent - have we pulled far enough to escape ? */
458 if (fabs (accumulated_delta) >= 0.1) {
459 width_control->set_value (current_width + accumulated_delta);
460 accumulated_delta = 0;
463 width_control->set_value (0);
466 width_control->set_value (current_width + delta);
469 } else if (dragging_position) {
471 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
472 position_control->set_value (pv + delta);
480 StereoPanner::on_key_press_event (GdkEventKey* ev)
482 double one_degree = 1.0/180.0;
483 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
484 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
487 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
490 step = one_degree * 5.0;
493 /* up/down control width because we consider pan position more "important"
494 (and thus having higher "sense" priority) than width.
497 switch (ev->keyval) {
499 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
500 width_control->set_value (1.0);
502 width_control->set_value (wv + step);
506 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
507 width_control->set_value (-1.0);
509 width_control->set_value (wv - step);
514 position_control->set_value (pv);
518 position_control->set_value (pv);
524 width_control->set_value (0.0);
535 StereoPanner::on_key_release_event (GdkEventKey* ev)
541 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
544 Keyboard::magic_widget_grab_focus ();
549 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
551 Keyboard::magic_widget_drop_focus ();
556 StereoPanner::set_colors ()
558 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
559 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
560 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
561 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
563 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
564 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
565 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
566 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
568 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
569 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
570 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
571 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
573 color_change (); /* EMIT SIGNAL */