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\n%4-uparrow -> set width to 100\n%4-downarrow -> set width to -100"),
100 Keyboard::secondary_modifier_name()).c_str());
104 StereoPanner::unset_tooltip ()
106 Gtkmm2ext::UI::instance()->set_tip (this, "");
110 StereoPanner::set_drag_data ()
112 if (!drag_data_label) {
116 double pos = position_control->get_value(); // 0..1
118 /* We show the position of the center of the image relative to the left & right.
119 This is expressed as a pair of percentage values that ranges from (100,0)
120 (hard left) through (50,50) (hard center) to (0,100) (hard right).
122 This is pretty wierd, but its the way audio engineers expect it. Just remember that
123 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
126 drag_data_label->set_markup (string_compose (_("L:%1 R:%2 Width: %3%%"),
127 (int) rint (100.0 * (1.0 - pos)),
128 (int) rint (100.0 * pos),
129 (int) floor (100.0 * width_control->get_value())));
133 StereoPanner::value_change ()
140 StereoPanner::on_expose_event (GdkEventExpose* ev)
142 Glib::RefPtr<Gdk::Window> win (get_window());
143 Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
145 cairo_t* cr = gdk_cairo_create (win->gobj());
148 double pos = position_control->get_value (); /* 0..1 */
149 double swidth = width_control->get_value (); /* -1..+1 */
150 double fswidth = fabs (swidth);
155 height = get_height ();
159 } else if (swidth < 0.0) {
165 o = colors[state].outline;
166 f = colors[state].fill;
167 t = colors[state].text;
168 b = colors[state].background;
172 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));
173 cairo_rectangle (cr, 0, 0, width, height);
176 double usable_width = width - lr_box_size;
178 /* compute the centers of the L/R boxes based on the current stereo width */
180 if (fmod (usable_width,2.0) == 0) {
181 /* even width, but we need odd, so that there is an exact center.
182 So, offset cairo by 1, and reduce effective width by 1
185 cairo_translate (cr, 1.0, 0.0);
188 double center = (lr_box_size/2.0) + (usable_width * pos);
189 const double pan_spread = (fswidth * usable_width)/2.0;
190 const double half_lr_box = lr_box_size/2.0;
194 left = center - pan_spread; // center of left box
195 right = center + pan_spread; // right of right box
197 /* compute & draw the line through the box */
199 cairo_set_line_width (cr, 2);
200 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));
201 cairo_move_to (cr, left, top_step+(pos_box_size/2)+step_down);
202 cairo_line_to (cr, left, top_step+(pos_box_size/2));
203 cairo_line_to (cr, right, top_step+(pos_box_size/2));
204 cairo_line_to (cr, right, top_step+(pos_box_size/2) + step_down);
211 (lr_box_size/2)+step_down,
212 lr_box_size, lr_box_size);
213 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));
214 cairo_stroke_preserve (cr);
215 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));
221 left - half_lr_box + 3,
222 (lr_box_size/2) + step_down + 13);
223 cairo_select_font_face (cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
226 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));
228 cairo_show_text (cr, _("R"));
230 cairo_show_text (cr, _("L"));
238 (lr_box_size/2)+step_down,
239 lr_box_size, lr_box_size);
240 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));
241 cairo_stroke_preserve (cr);
242 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));
248 right - half_lr_box + 3,
249 (lr_box_size/2)+step_down + 13);
250 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));
253 cairo_show_text (cr, _("M"));
256 cairo_show_text (cr, _("L"));
258 cairo_show_text (cr, _("R"));
262 /* draw the central box */
264 cairo_set_line_width (cr, 1);
265 cairo_rectangle (cr, lrint (center - (pos_box_size/2.0)), top_step, pos_box_size, pos_box_size);
266 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));
267 cairo_stroke_preserve (cr);
268 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));
278 StereoPanner::on_button_press_event (GdkEventButton* ev)
280 drag_start_x = ev->x;
283 dragging_position = false;
284 dragging_left = false;
285 dragging_right = false;
286 accumulated_delta = 0;
289 /* top section of widget is for position drags */
290 dragging_position = true;
292 /* lower section is for dragging width */
294 double pos = position_control->get_value (); /* 0..1 */
295 double swidth = width_control->get_value (); /* -1..+1 */
296 double fswidth = fabs (swidth);
297 int usable_width = get_width() - lr_box_size;
298 double center = (lr_box_size/2.0) + (usable_width * pos);
299 int left = lrint (center - (fswidth * usable_width / 2.0)); // center of leftmost box
300 int right = lrint (center + (fswidth * usable_width / 2.0)); // center of rightmost box
301 const int half_box = lr_box_size/2;
303 if (ev->x >= (left - half_box) && ev->x < (left + half_box)) {
304 dragging_left = true;
305 } else if (ev->x >= (right - half_box) && ev->x < (right + half_box)) {
306 dragging_right = true;
311 if (ev->type == GDK_2BUTTON_PRESS) {
312 if (dragging_position) {
313 int width = get_width();
314 if (ev->x >= width/2 - 10 && ev->x <= width/2 + 10) {
315 /* double click near center, reset position to center */
316 position_control->set_value (0.5);
318 if (ev->x < width/2) {
319 /* double click on left, collapse to hard left */
320 width_control->set_value (0);
321 position_control->set_value (0);
323 /* double click on right, collapse to hard right */
324 width_control->set_value (0);
325 position_control->set_value (1.0);
329 if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
330 width_control->set_value (-1.0); // reset position to reversed full, LR
332 width_control->set_value (1.0); // reset position to full, LR
344 StereoPanner::on_button_release_event (GdkEventButton* ev)
347 dragging_position = false;
348 dragging_left = false;
349 dragging_right = false;
350 accumulated_delta = 0;
352 if (drag_data_window) {
353 drag_data_window->hide ();
362 StereoPanner::on_scroll_event (GdkEventScroll* ev)
364 double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
365 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
366 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
369 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
372 step = one_degree * 5.0;
375 switch (ev->direction) {
376 case GDK_SCROLL_LEFT:
378 width_control->set_value (wv);
382 position_control->set_value (pv);
384 case GDK_SCROLL_RIGHT:
386 width_control->set_value (wv);
388 case GDK_SCROLL_DOWN:
390 position_control->set_value (pv);
398 StereoPanner::on_motion_notify_event (GdkEventMotion* ev)
404 if (!drag_data_window) {
405 drag_data_window = new Window (WINDOW_POPUP);
406 drag_data_window->set_position (WIN_POS_MOUSE);
407 drag_data_window->set_decorated (false);
409 drag_data_label = manage (new Label);
410 drag_data_label->set_use_markup (true);
412 drag_data_window->set_border_width (6);
413 drag_data_window->add (*drag_data_label);
414 drag_data_label->show ();
416 Window* toplevel = dynamic_cast<Window*> (get_toplevel());
418 drag_data_window->set_transient_for (*toplevel);
422 if (!drag_data_window->is_visible ()) {
423 /* move the window a little away from the mouse */
424 drag_data_window->move (ev->x_root+30, ev->y_root+30);
425 drag_data_window->present ();
430 double delta = (ev->x - last_drag_x) / (double) w;
436 if (dragging_left || dragging_right) {
438 /* maintain position as invariant as we change the width */
440 double current_width = width_control->get_value ();
442 if (fabs (current_width) < 0.1) {
443 accumulated_delta += delta;
444 /* in the detent - have we pulled far enough to escape ? */
445 if (fabs (accumulated_delta) >= 0.1) {
446 width_control->set_value (current_width + accumulated_delta);
447 accumulated_delta = 0;
450 width_control->set_value (current_width + delta);
453 } else if (dragging_position) {
455 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
456 position_control->set_value (pv + delta);
464 StereoPanner::on_key_press_event (GdkEventKey* ev)
466 double one_degree = 1.0/180.0;
467 double pv = position_control->get_value(); // 0..1.0 ; 0 = left
468 double wv = width_control->get_value(); // 0..1.0 ; 0 = left
471 if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
474 step = one_degree * 5.0;
477 /* up/down control width because we consider pan position more "important"
478 (and thus having higher "sense" priority) than width.
481 switch (ev->keyval) {
483 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
484 width_control->set_value (1.0);
486 width_control->set_value (wv + step);
490 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
491 width_control->set_value (-1.0);
493 width_control->set_value (wv - step);
498 position_control->set_value (pv);
502 position_control->set_value (pv);
508 width_control->set_value (0.0);
519 StereoPanner::on_key_release_event (GdkEventKey* ev)
525 StereoPanner::on_enter_notify_event (GdkEventCrossing* ev)
528 Keyboard::magic_widget_grab_focus ();
533 StereoPanner::on_leave_notify_event (GdkEventCrossing*)
535 Keyboard::magic_widget_drop_focus ();
540 StereoPanner::set_colors ()
542 colors[Normal].fill = ARDOUR_UI::config()->canvasvar_StereoPannerFill.get();
543 colors[Normal].outline = ARDOUR_UI::config()->canvasvar_StereoPannerOutline.get();
544 colors[Normal].text = ARDOUR_UI::config()->canvasvar_StereoPannerText.get();
545 colors[Normal].background = ARDOUR_UI::config()->canvasvar_StereoPannerBackground.get();
547 colors[Mono].fill = ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill.get();
548 colors[Mono].outline = ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline.get();
549 colors[Mono].text = ARDOUR_UI::config()->canvasvar_StereoPannerMonoText.get();
550 colors[Mono].background = ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground.get();
552 colors[Inverted].fill = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill.get();
553 colors[Inverted].outline = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline.get();
554 colors[Inverted].text = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText.get();
555 colors[Inverted].background = ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground.get();
557 color_change (); /* EMIT SIGNAL */