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"
33 #include "gtkmm2ext/utils.h"
35 #include "ardour/pannable.h"
36 #include "ardour/panner.h"
38 #include "ardour_ui.h"
39 #include "global_signals.h"
40 #include "stereo_panner.h"
41 #include "rgb_macros.h"
48 using namespace Gtkmm2ext
;
50 static const int pos_box_size
= 8;
51 static const int lr_box_size
= 15;
52 static const int step_down
= 10;
53 static const int top_step
= 2;
55 StereoPanner::ColorScheme
StereoPanner::colors
[3];
56 bool StereoPanner::have_colors
= false;
58 using namespace ARDOUR
;
60 StereoPanner::StereoPanner (boost::shared_ptr
<Panner
> panner
)
62 , position_control (_panner
->pannable()->pan_azimuth_control
)
63 , width_control (_panner
->pannable()->pan_width_control
)
65 , dragging_position (false)
66 , dragging_left (false)
67 , dragging_right (false)
70 , accumulated_delta (0)
72 , drag_data_window (0)
74 , position_binder (position_control
)
75 , width_binder (width_control
)
82 position_control
->Changed
.connect (connections
, invalidator(*this), boost::bind (&StereoPanner::value_change
, this), gui_context());
83 width_control
->Changed
.connect (connections
, invalidator(*this), boost::bind (&StereoPanner::value_change
, this), gui_context());
85 set_flags (Gtk::CAN_FOCUS
);
87 add_events (Gdk::ENTER_NOTIFY_MASK
|Gdk::LEAVE_NOTIFY_MASK
|
88 Gdk::KEY_PRESS_MASK
|Gdk::KEY_RELEASE_MASK
|
89 Gdk::BUTTON_PRESS_MASK
|Gdk::BUTTON_RELEASE_MASK
|
91 Gdk::POINTER_MOTION_MASK
);
93 ColorsChanged
.connect (sigc::mem_fun (*this, &StereoPanner::color_handler
));
96 StereoPanner::~StereoPanner ()
98 delete drag_data_window
;
102 StereoPanner::set_drag_data ()
104 if (!drag_data_label
) {
108 double pos
= position_control
->get_value(); // 0..1
110 /* We show the position of the center of the image relative to the left & right.
111 This is expressed as a pair of percentage values that ranges from (100,0)
112 (hard left) through (50,50) (hard center) to (0,100) (hard right).
114 This is pretty wierd, but its the way audio engineers expect it. Just remember that
115 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
119 snprintf (buf
, sizeof (buf
), "L:%3d R:%3d Width:%d%%", (int) rint (100.0 * (1.0 - pos
)),
120 (int) rint (100.0 * pos
),
121 (int) floor (100.0 * width_control
->get_value()));
122 drag_data_label
->set_markup (buf
);
126 StereoPanner::value_change ()
133 StereoPanner::on_expose_event (GdkEventExpose
* ev
)
135 Glib::RefPtr
<Gdk::Window
> win (get_window());
136 Glib::RefPtr
<Gdk::GC
> gc (get_style()->get_base_gc (get_state()));
137 Cairo::RefPtr
<Cairo::Context
> context
= get_window()->create_cairo_context();
140 double pos
= position_control
->get_value (); /* 0..1 */
141 double swidth
= width_control
->get_value (); /* -1..+1 */
142 double fswidth
= fabs (swidth
);
143 uint32_t o
, f
, t
, b
, r
;
145 const double corner_radius
= 5.0;
148 height
= get_height ();
152 } else if (swidth
< 0.0) {
158 o
= colors
[state
].outline
;
159 f
= colors
[state
].fill
;
160 t
= colors
[state
].text
;
161 b
= colors
[state
].background
;
162 r
= colors
[state
].rule
;
166 context
->set_source_rgba (UINT_RGBA_R_FLT(b
), UINT_RGBA_G_FLT(b
), UINT_RGBA_B_FLT(b
), UINT_RGBA_A_FLT(b
));
167 rounded_rectangle (context
, 0, 0, width
, height
, corner_radius
);
170 /* the usable width is reduced from the real width, because we need space for
171 the two halves of LR boxes that will extend past the actual left/right
172 positions (indicated by the vertical line segment above them).
175 double usable_width
= width
- lr_box_size
;
177 /* compute the centers of the L/R boxes based on the current stereo width */
179 if (fmod (usable_width
,2.0) == 0) {
180 /* even width, but we need odd, so that there is an exact center.
181 So, offset cairo by 1, and reduce effective width by 1
184 context
->translate (1.0, 0.0);
187 double center
= (lr_box_size
/2.0) + (usable_width
* pos
);
188 const double pan_spread
= (fswidth
* usable_width
)/2.0;
189 const double half_lr_box
= lr_box_size
/2.0;
193 left
= center
- pan_spread
; // center of left box
194 right
= center
+ pan_spread
; // center of right box
198 context
->set_line_width (1.0);
199 context
->move_to ((usable_width
+ lr_box_size
)/2.0, 0);
200 context
->rel_line_to (0, height
);
201 context
->set_source_rgba (UINT_RGBA_R_FLT(r
), UINT_RGBA_G_FLT(r
), UINT_RGBA_B_FLT(r
), UINT_RGBA_A_FLT(r
));
204 /* compute & draw the line through the box */
206 context
->set_line_width (2);
207 context
->set_source_rgba (UINT_RGBA_R_FLT(o
), UINT_RGBA_G_FLT(o
), UINT_RGBA_B_FLT(o
), UINT_RGBA_A_FLT(o
));
208 context
->move_to (left
, top_step
+(pos_box_size
/2.0)+step_down
);
209 context
->line_to (left
, top_step
+(pos_box_size
/2.0));
210 context
->line_to (right
, top_step
+(pos_box_size
/2.0));
211 context
->line_to (right
, top_step
+(pos_box_size
/2.0) + step_down
);
216 rounded_rectangle (context
, left
- half_lr_box
,
217 half_lr_box
+step_down
,
218 lr_box_size
, lr_box_size
, corner_radius
);
219 context
->set_source_rgba (UINT_RGBA_R_FLT(o
), UINT_RGBA_G_FLT(o
), UINT_RGBA_B_FLT(o
), UINT_RGBA_A_FLT(o
));
220 context
->stroke_preserve ();
221 context
->set_source_rgba (UINT_RGBA_R_FLT(f
), UINT_RGBA_G_FLT(f
), UINT_RGBA_B_FLT(f
), UINT_RGBA_A_FLT(f
));
226 context
->move_to (left
- half_lr_box
+ 3,
227 (lr_box_size
/2) + step_down
+ 13);
228 context
->select_font_face ("sans-serif", Cairo::FONT_SLANT_NORMAL
, Cairo::FONT_WEIGHT_BOLD
);
231 context
->set_source_rgba (UINT_RGBA_R_FLT(t
), UINT_RGBA_G_FLT(t
), UINT_RGBA_B_FLT(t
), UINT_RGBA_A_FLT(t
));
233 context
->show_text (_("R"));
235 context
->show_text (_("L"));
241 rounded_rectangle (context
, right
- half_lr_box
,
242 half_lr_box
+step_down
,
243 lr_box_size
, lr_box_size
, corner_radius
);
244 context
->set_source_rgba (UINT_RGBA_R_FLT(o
), UINT_RGBA_G_FLT(o
), UINT_RGBA_B_FLT(o
), UINT_RGBA_A_FLT(o
));
245 context
->stroke_preserve ();
246 context
->set_source_rgba (UINT_RGBA_R_FLT(f
), UINT_RGBA_G_FLT(f
), UINT_RGBA_B_FLT(f
), UINT_RGBA_A_FLT(f
));
251 context
->move_to (right
- half_lr_box
+ 3, (lr_box_size
/2)+step_down
+ 13);
252 context
->set_source_rgba (UINT_RGBA_R_FLT(t
), UINT_RGBA_G_FLT(t
), UINT_RGBA_B_FLT(t
), UINT_RGBA_A_FLT(t
));
255 context
->show_text (_("M"));
258 context
->show_text (_("L"));
260 context
->show_text (_("R"));
264 /* draw the central box */
266 context
->set_line_width (2.0);
267 context
->move_to (center
+ (pos_box_size
/2.0), top_step
); /* top right */
268 context
->rel_line_to (0.0, pos_box_size
); /* lower right */
269 context
->rel_line_to (-pos_box_size
/2.0, 4.0); /* bottom point */
270 context
->rel_line_to (-pos_box_size
/2.0, -4.0); /* lower left */
271 context
->rel_line_to (0.0, -pos_box_size
); /* upper left */
272 context
->close_path ();
274 context
->set_source_rgba (UINT_RGBA_R_FLT(o
), UINT_RGBA_G_FLT(o
), UINT_RGBA_B_FLT(o
), UINT_RGBA_A_FLT(o
));
275 context
->stroke_preserve ();
276 context
->set_source_rgba (UINT_RGBA_R_FLT(f
), UINT_RGBA_G_FLT(f
), UINT_RGBA_B_FLT(f
), UINT_RGBA_A_FLT(f
));
283 StereoPanner::on_button_press_event (GdkEventButton
* ev
)
285 drag_start_x
= ev
->x
;
288 dragging_position
= false;
289 dragging_left
= false;
290 dragging_right
= false;
292 accumulated_delta
= 0;
295 /* Let the binding proxies get first crack at the press event
299 if (position_binder
.button_press_handler (ev
)) {
303 if (width_binder
.button_press_handler (ev
)) {
308 if (ev
->button
!= 1) {
312 if (ev
->type
== GDK_2BUTTON_PRESS
) {
313 int width
= get_width();
315 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::TertiaryModifier
)) {
316 /* handled by button release */
322 /* upper section: adjusts position, constrained by width */
324 const double w
= fabs (width_control
->get_value ());
325 const double max_pos
= 1.0 - (w
/2.0);
326 const double min_pos
= w
/2.0;
328 if (ev
->x
<= width
/3) {
329 /* left side dbl click */
330 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::SecondaryModifier
)) {
331 /* 2ndary-double click on left, collapse to hard left */
332 width_control
->set_value (0);
333 position_control
->set_value (0);
335 position_control
->set_value (min_pos
);
337 } else if (ev
->x
> 2*width
/3) {
338 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::SecondaryModifier
)) {
339 /* 2ndary-double click on right, collapse to hard right */
340 width_control
->set_value (0);
341 position_control
->set_value (1.0);
343 position_control
->set_value (max_pos
);
346 position_control
->set_value (0.5);
351 /* lower section: adjusts width, constrained by position */
353 const double p
= position_control
->get_value ();
354 const double max_width
= 2.0 * min ((1.0 - p
), p
);
356 if (ev
->x
<= width
/3) {
357 /* left side dbl click */
358 width_control
->set_value (max_width
); // reset width to 100%
359 } else if (ev
->x
> 2*width
/3) {
360 /* right side dbl click */
361 width_control
->set_value (-max_width
); // reset width to inverted 100%
363 /* center dbl click */
364 width_control
->set_value (0); // collapse width to 0%
370 } else if (ev
->type
== GDK_BUTTON_PRESS
) {
372 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::TertiaryModifier
)) {
373 /* handled by button release */
378 /* top section of widget is for position drags */
379 dragging_position
= true;
380 StartPositionGesture ();
382 /* lower section is for dragging width */
384 double pos
= position_control
->get_value (); /* 0..1 */
385 double swidth
= width_control
->get_value (); /* -1..+1 */
386 double fswidth
= fabs (swidth
);
387 int usable_width
= get_width() - lr_box_size
;
388 double center
= (lr_box_size
/2.0) + (usable_width
* pos
);
389 int left
= lrint (center
- (fswidth
* usable_width
/ 2.0)); // center of leftmost box
390 int right
= lrint (center
+ (fswidth
* usable_width
/ 2.0)); // center of rightmost box
391 const int half_box
= lr_box_size
/2;
393 if (ev
->x
>= (left
- half_box
) && ev
->x
< (left
+ half_box
)) {
395 dragging_right
= true;
397 dragging_left
= true;
399 } else if (ev
->x
>= (right
- half_box
) && ev
->x
< (right
+ half_box
)) {
401 dragging_left
= true;
403 dragging_right
= true;
406 StartWidthGesture ();
416 StereoPanner::on_button_release_event (GdkEventButton
* ev
)
418 if (ev
->button
!= 1) {
422 bool dp
= dragging_position
;
425 dragging_position
= false;
426 dragging_left
= false;
427 dragging_right
= false;
428 accumulated_delta
= 0;
431 if (drag_data_window
) {
432 drag_data_window
->hide ();
435 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::TertiaryModifier
)) {
436 /* reset to default */
437 position_control
->set_value (0.5);
438 width_control
->set_value (1.0);
441 StopPositionGesture ();
451 StereoPanner::on_scroll_event (GdkEventScroll
* ev
)
453 double one_degree
= 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
454 double pv
= position_control
->get_value(); // 0..1.0 ; 0 = left
455 double wv
= width_control
->get_value(); // 0..1.0 ; 0 = left
458 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::PrimaryModifier
)) {
461 step
= one_degree
* 5.0;
464 switch (ev
->direction
) {
465 case GDK_SCROLL_LEFT
:
467 width_control
->set_value (wv
);
471 position_control
->set_value (pv
);
473 case GDK_SCROLL_RIGHT
:
475 width_control
->set_value (wv
);
477 case GDK_SCROLL_DOWN
:
479 position_control
->set_value (pv
);
487 StereoPanner::on_motion_notify_event (GdkEventMotion
* ev
)
493 if (!drag_data_window
) {
494 drag_data_window
= new Window (WINDOW_POPUP
);
495 drag_data_window
->set_name (X_("ContrastingPopup"));
496 drag_data_window
->set_position (WIN_POS_MOUSE
);
497 drag_data_window
->set_decorated (false);
499 drag_data_label
= manage (new Label
);
500 drag_data_label
->set_use_markup (true);
502 drag_data_window
->set_border_width (6);
503 drag_data_window
->add (*drag_data_label
);
504 drag_data_label
->show ();
506 Window
* toplevel
= dynamic_cast<Window
*> (get_toplevel());
508 drag_data_window
->set_transient_for (*toplevel
);
512 if (!drag_data_window
->is_visible ()) {
513 /* move the popup window vertically down from the panner display */
515 get_window()->get_origin (rx
, ry
);
516 drag_data_window
->move (rx
, ry
+get_height());
517 drag_data_window
->present ();
521 double delta
= (ev
->x
- last_drag_x
) / (double) w
;
522 double current_width
= width_control
->get_value ();
528 if (dragging_left
|| dragging_right
) {
530 /* maintain position as invariant as we change the width */
533 /* create a detent close to the center */
535 if (!detented
&& fabs (current_width
) < 0.02) {
538 width_control
->set_value (0);
543 accumulated_delta
+= delta
;
545 /* have we pulled far enough to escape ? */
547 if (fabs (accumulated_delta
) >= 0.025) {
548 width_control
->set_value (current_width
+ accumulated_delta
);
550 accumulated_delta
= false;
554 width_control
->set_value (current_width
+ delta
);
557 } else if (dragging_position
) {
559 double pv
= position_control
->get_value(); // 0..1.0 ; 0 = left
560 position_control
->set_value (pv
+ delta
);
568 StereoPanner::on_key_press_event (GdkEventKey
* ev
)
570 double one_degree
= 1.0/180.0;
571 double pv
= position_control
->get_value(); // 0..1.0 ; 0 = left
572 double wv
= width_control
->get_value(); // 0..1.0 ; 0 = left
575 if (Keyboard::modifier_state_contains (ev
->state
, Keyboard::PrimaryModifier
)) {
578 step
= one_degree
* 5.0;
581 /* up/down control width because we consider pan position more "important"
582 (and thus having higher "sense" priority) than width.
585 switch (ev
->keyval
) {
587 if (Keyboard::modifier_state_equals (ev
->state
, Keyboard::SecondaryModifier
)) {
588 width_control
->set_value (1.0);
590 width_control
->set_value (wv
+ step
);
594 if (Keyboard::modifier_state_equals (ev
->state
, Keyboard::SecondaryModifier
)) {
595 width_control
->set_value (-1.0);
597 width_control
->set_value (wv
- step
);
602 position_control
->set_value (pv
);
606 position_control
->set_value (pv
);
612 width_control
->set_value (0.0);
623 StereoPanner::on_key_release_event (GdkEventKey
* ev
)
629 StereoPanner::on_enter_notify_event (GdkEventCrossing
* ev
)
632 Keyboard::magic_widget_grab_focus ();
637 StereoPanner::on_leave_notify_event (GdkEventCrossing
*)
639 Keyboard::magic_widget_drop_focus ();
644 StereoPanner::set_colors ()
646 colors
[Normal
].fill
= ARDOUR_UI::config()->canvasvar_StereoPannerFill
.get();
647 colors
[Normal
].outline
= ARDOUR_UI::config()->canvasvar_StereoPannerOutline
.get();
648 colors
[Normal
].text
= ARDOUR_UI::config()->canvasvar_StereoPannerText
.get();
649 colors
[Normal
].background
= ARDOUR_UI::config()->canvasvar_StereoPannerBackground
.get();
650 colors
[Normal
].rule
= ARDOUR_UI::config()->canvasvar_StereoPannerRule
.get();
652 colors
[Mono
].fill
= ARDOUR_UI::config()->canvasvar_StereoPannerMonoFill
.get();
653 colors
[Mono
].outline
= ARDOUR_UI::config()->canvasvar_StereoPannerMonoOutline
.get();
654 colors
[Mono
].text
= ARDOUR_UI::config()->canvasvar_StereoPannerMonoText
.get();
655 colors
[Mono
].background
= ARDOUR_UI::config()->canvasvar_StereoPannerMonoBackground
.get();
656 colors
[Mono
].rule
= ARDOUR_UI::config()->canvasvar_StereoPannerRule
.get();
658 colors
[Inverted
].fill
= ARDOUR_UI::config()->canvasvar_StereoPannerInvertedFill
.get();
659 colors
[Inverted
].outline
= ARDOUR_UI::config()->canvasvar_StereoPannerInvertedOutline
.get();
660 colors
[Inverted
].text
= ARDOUR_UI::config()->canvasvar_StereoPannerInvertedText
.get();
661 colors
[Inverted
].background
= ARDOUR_UI::config()->canvasvar_StereoPannerInvertedBackground
.get();
662 colors
[Inverted
].rule
= ARDOUR_UI::config()->canvasvar_StereoPannerRule
.get();
666 StereoPanner::color_handler ()