2 Copyright (C) 2002 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/menu.h>
27 #include "gtkmm2ext/gtk_ui.h"
29 #include "pbd/error.h"
30 #include "pbd/cartesian.h"
31 #include "ardour/panner.h"
32 #include "ardour/pannable.h"
33 #include "ardour/speakers.h"
37 #include "gui_thread.h"
39 #include "public_editor.h"
45 using namespace ARDOUR
;
47 using Gtkmm2ext::Keyboard
;
49 static const int large_size_threshold
= 100;
50 static const int large_border_width
= 25;
51 static const int small_border_width
= 8;
53 Panner2d::Target::Target (const AngularVector
& a
, const char *txt
)
60 Panner2d::Target::~Target ()
65 Panner2d::Target::set_text (const char* txt
)
70 Panner2d::Panner2d (boost::shared_ptr
<Panner
> p
, int32_t h
)
72 , position (AngularVector (0.0, 0.0), "")
77 panner
->StateChanged
.connect (connections
, invalidator (*this), boost::bind (&Panner2d::handle_state_change
, this), gui_context());
79 panner
->pannable()->pan_azimuth_control
->Changed
.connect (connections
, invalidator(*this), boost::bind (&Panner2d::handle_position_change
, this), gui_context());
80 panner
->pannable()->pan_width_control
->Changed
.connect (connections
, invalidator(*this), boost::bind (&Panner2d::handle_position_change
, this), gui_context());
83 set_events (Gdk::BUTTON_PRESS_MASK
|Gdk::BUTTON_RELEASE_MASK
|Gdk::POINTER_MOTION_MASK
);
85 handle_position_change ();
90 for (Targets::iterator i
= speakers
.begin(); i
!= speakers
.end(); ++i
) {
96 Panner2d::reset (uint32_t n_inputs
)
98 uint32_t nouts
= panner
->out().n_audio();
102 while (signals
.size() < n_inputs
) {
103 add_signal ("", AngularVector());
106 if (signals
.size() > n_inputs
) {
107 for (uint32_t i
= signals
.size(); i
< n_inputs
; ++i
) {
111 signals
.resize (n_inputs
);
116 for (uint32_t i
= 0; i
< n_inputs
; ++i
) {
117 signals
[i
]->position
= panner
->signal_position (i
);
120 /* add all outputs */
122 while (speakers
.size() < nouts
) {
123 add_speaker (AngularVector());
126 if (speakers
.size() > nouts
) {
127 for (uint32_t i
= nouts
; i
< speakers
.size(); ++i
) {
131 speakers
.resize (nouts
);
134 for (Targets::iterator x
= speakers
.begin(); x
!= speakers
.end(); ++x
) {
135 (*x
)->visible
= false;
138 vector
<Speaker
>& the_speakers (panner
->get_speakers()->speakers());
140 for (uint32_t n
= 0; n
< nouts
; ++n
) {
143 snprintf (buf
, sizeof (buf
), "%d", n
+1);
144 speakers
[n
]->set_text (buf
);
145 speakers
[n
]->position
= the_speakers
[n
].angles();
146 speakers
[n
]->visible
= true;
153 Panner2d::on_size_allocate (Gtk::Allocation
& alloc
)
155 width
= alloc
.get_width();
156 height
= alloc
.get_height();
158 if (height
> large_size_threshold
) {
159 border
= large_border_width
;
161 border
= small_border_width
;
164 radius
= min (width
, height
);
168 hoffset
= max ((double) (width
- height
), border
);
169 voffset
= max ((double) (height
- width
), border
);
174 DrawingArea::on_size_allocate (alloc
);
178 Panner2d::add_signal (const char* text
, const AngularVector
& a
)
180 Target
* signal
= new Target (a
, text
);
181 signals
.push_back (signal
);
182 signal
->visible
= true;
188 Panner2d::add_speaker (const AngularVector
& a
)
190 Target
* speaker
= new Target (a
, "");
191 speakers
.push_back (speaker
);
192 speaker
->visible
= true;
195 return speakers
.size() - 1;
199 Panner2d::handle_state_change ()
205 Panner2d::label_signals ()
207 double w
= panner
->pannable()->pan_width_control
->get_value();
208 uint32_t sz
= signals
.size();
215 signals
[0]->set_text ("");
220 signals
[0]->set_text ("R");
221 signals
[1]->set_text ("L");
223 signals
[0]->set_text ("L");
224 signals
[1]->set_text ("R");
229 for (uint32_t i
= 0; i
< sz
; ++i
) {
232 snprintf (buf
, sizeof (buf
), "%" PRIu32
, i
+ 1);
234 snprintf (buf
, sizeof (buf
), "%" PRIu32
, sz
- i
);
236 signals
[i
]->set_text (buf
);
243 Panner2d::handle_position_change ()
246 double w
= panner
->pannable()->pan_width_control
->get_value();
248 position
.position
= AngularVector (panner
->pannable()->pan_azimuth_control
->get_value() * 360.0, 0.0);
250 for (uint32_t i
= 0; i
< signals
.size(); ++i
) {
251 signals
[i
]->position
= panner
->signal_position (i
);
254 if (w
* last_width
<= 0) {
261 vector
<Speaker
>& the_speakers (panner
->get_speakers()->speakers());
263 for (n
= 0; n
< speakers
.size(); ++n
) {
264 speakers
[n
]->position
= the_speakers
[n
].angles();
271 Panner2d::move_signal (int which
, const AngularVector
& a
)
273 if (which
>= int (speakers
.size())) {
277 speakers
[which
]->position
= a
;
282 Panner2d::find_closest_object (gdouble x
, gdouble y
, bool& is_signal
)
287 float best_distance
= FLT_MAX
;
290 /* start with the position itself
293 position
.position
.cartesian (c
);
295 best_distance
= sqrt ((c
.x
- x
) * (c
.x
- x
) +
296 (c
.y
- y
) * (c
.y
- y
));
299 for (Targets::const_iterator i
= signals
.begin(); i
!= signals
.end(); ++i
) {
302 candidate
->position
.cartesian (c
);
305 distance
= sqrt ((c
.x
- x
) * (c
.x
- x
) +
306 (c
.y
- y
) * (c
.y
- y
));
308 if (distance
< best_distance
) {
310 best_distance
= distance
;
316 if (height
> large_size_threshold
) {
318 if (best_distance
> 30) { // arbitrary
323 if (best_distance
> 10) { // arbitrary
328 /* if we didn't find a signal close by, check the speakers */
331 for (Targets::const_iterator i
= speakers
.begin(); i
!= speakers
.end(); ++i
) {
334 candidate
->position
.cartesian (c
);
337 distance
= sqrt ((c
.x
- x
) * (c
.x
- x
) +
338 (c
.y
- y
) * (c
.y
- y
));
340 if (distance
< best_distance
) {
342 best_distance
= distance
;
346 if (height
> large_size_threshold
) {
348 if (best_distance
< 30) { // arbitrary
355 if (best_distance
< 10) { // arbitrary
367 Panner2d::on_motion_notify_event (GdkEventMotion
*ev
)
370 GdkModifierType state
;
373 gdk_window_get_pointer (ev
->window
, &x
, &y
, &state
);
375 x
= (int) floor (ev
->x
);
376 y
= (int) floor (ev
->y
);
377 state
= (GdkModifierType
) ev
->state
;
380 if (ev
->state
& (GDK_BUTTON1_MASK
|GDK_BUTTON2_MASK
)) {
384 return handle_motion (x
, y
, state
);
388 Panner2d::on_expose_event (GdkEventExpose
*event
)
392 bool small
= (height
<= large_size_threshold
);
393 const double diameter
= radius
*2.0;
395 cr
= gdk_cairo_create (get_window()->gobj());
399 cairo_rectangle (cr
, event
->area
.x
, event
->area
.y
, event
->area
.width
, event
->area
.height
);
400 if (!panner
->bypassed()) {
401 cairo_set_source_rgba (cr
, 0.1, 0.1, 0.1, 1.0);
403 cairo_set_source_rgba (cr
, 0.1, 0.1, 0.1, 0.2);
405 cairo_fill_preserve (cr
);
408 /* offset to give us some border */
410 cairo_translate (cr
, hoffset
, voffset
);
412 cairo_set_line_width (cr
, 1.0);
414 /* horizontal line of "crosshairs" */
416 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 1.0);
417 cairo_move_to (cr
, 0.0, radius
);
418 cairo_line_to (cr
, diameter
, radius
);
421 /* vertical line of "crosshairs" */
423 cairo_move_to (cr
, radius
, 0);
424 cairo_line_to (cr
, radius
, diameter
);
427 /* the circle on which signals live */
429 cairo_set_line_width (cr
, 2.0);
430 cairo_set_source_rgba (cr
, 0.517, 0.772, 0.882, 1.0);
431 cairo_arc (cr
, radius
, radius
, radius
, 0.0, 2.0 * M_PI
);
434 /* 3 other circles of smaller diameter circle on which signals live */
436 cairo_set_line_width (cr
, 1.0);
437 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 1.0);
438 cairo_arc (cr
, radius
, radius
, radius
* 0.75, 0, 2.0 * M_PI
);
440 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 0.85);
441 cairo_arc (cr
, radius
, radius
, radius
* 0.50, 0, 2.0 * M_PI
);
443 cairo_arc (cr
, radius
, radius
, radius
* 0.25, 0, 2.0 * M_PI
);
446 if (signals
.size() > 1) {
447 /* arc to show "diffusion" */
449 double width_angle
= fabs (panner
->pannable()->pan_width_control
->get_value()) * 2 * M_PI
;
450 double position_angle
= (2 * M_PI
) - panner
->pannable()->pan_azimuth_control
->get_value() * 2 * M_PI
;
453 cairo_translate (cr
, radius
, radius
);
454 cairo_rotate (cr
, position_angle
- (width_angle
/2.0));
455 cairo_move_to (cr
, 0, 0);
456 cairo_arc_negative (cr
, 0, 0, radius
, width_angle
, 0.0);
457 cairo_close_path (cr
);
458 if (panner
->pannable()->pan_width_control
->get_value() >= 0.0) {
460 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 0.45);
463 cairo_set_source_rgba (cr
, 1.0, 0.419, 0.419, 0.45);
469 if (!panner
->bypassed()) {
473 cairo_select_font_face (cr
, "sans", CAIRO_FONT_SLANT_NORMAL
, CAIRO_FONT_WEIGHT_NORMAL
);
478 cairo_set_font_size (cr
, 10);
484 if (signals
.size() > 1) {
485 for (Targets::iterator i
= signals
.begin(); i
!= signals
.end(); ++i
) {
488 if (signal
->visible
) {
490 signal
->position
.cartesian (c
);
494 cairo_arc (cr
, c
.x
, c
.y
, arc_radius
, 0, 2.0 * M_PI
);
495 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 0.85);
496 cairo_fill_preserve (cr
);
497 cairo_set_source_rgba (cr
, 0.517, 0.772, 0.882, 1.0);
500 if (!small
&& !signal
->text
.empty()) {
501 cairo_set_source_rgb (cr
, 0.517, 0.772, 0.882);
502 /* the +/- adjustments are a hack to try to center the text in the circle */
504 cairo_move_to (cr
, c
.x
- 1, c
.y
+ 1);
506 cairo_move_to (cr
, c
.x
- 4, c
.y
+ 4);
508 cairo_show_text (cr
, signal
->text
.c_str());
518 for (Targets::iterator i
= speakers
.begin(); i
!= speakers
.end(); ++i
) {
519 Target
*speaker
= *i
;
523 if (speaker
->visible
) {
527 speaker
->position
.cartesian (c
);
530 snprintf (buf
, sizeof (buf
), "%d", n
);
532 /* stroke out a speaker shape */
534 cairo_move_to (cr
, c
.x
, c
.y
);
536 cairo_rotate (cr
, -(speaker
->position
.azi
/360.0) * (2.0 * M_PI
));
538 cairo_scale (cr
, 0.8, 0.8);
540 cairo_scale (cr
, 1.2, 1.2);
542 cairo_rel_line_to (cr
, 4, -2);
543 cairo_rel_line_to (cr
, 0, -7);
544 cairo_rel_line_to (cr
, 5, +5);
545 cairo_rel_line_to (cr
, 5, 0);
546 cairo_rel_line_to (cr
, 0, 5);
547 cairo_rel_line_to (cr
, -5, 0);
548 cairo_rel_line_to (cr
, -5, +5);
549 cairo_rel_line_to (cr
, 0, -7);
550 cairo_close_path (cr
);
551 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 1.0);
556 cairo_set_font_size (cr
, 16);
558 /* move the text in just a bit */
560 AngularVector
textpos (speaker
->position
.azi
, speaker
->position
.ele
, 0.85);
561 textpos
.cartesian (c
);
563 cairo_move_to (cr
, c
.x
, c
.y
);
564 cairo_show_text (cr
, buf
);
572 position
.position
.cartesian (c
);
576 cairo_arc (cr
, c
.x
, c
.y
, arc_radius
, 0, 2.0 * M_PI
);
577 cairo_set_source_rgba (cr
, 1.0, 0.419, 0.419, 0.85);
578 cairo_fill_preserve (cr
);
579 cairo_set_source_rgba (cr
, 1.0, 0.905, 0.905, 0.85);
589 Panner2d::on_button_press_event (GdkEventButton
*ev
)
591 GdkModifierType state
;
596 if (ev
->type
== GDK_2BUTTON_PRESS
&& ev
->button
== 1) {
602 switch (ev
->button
) {
608 if ((drag_target
= find_closest_object (x
, y
, is_signal
)) != 0) {
610 panner
->set_position (drag_target
->position
.azi
/360.0);
613 drag_target
->set_selected (true);
619 state
= (GdkModifierType
) ev
->state
;
621 return handle_motion (drag_x
, drag_y
, state
);
632 Panner2d::on_button_release_event (GdkEventButton
*ev
)
635 GdkModifierType state
;
638 switch (ev
->button
) {
640 x
= (int) floor (ev
->x
);
641 y
= (int) floor (ev
->y
);
642 state
= (GdkModifierType
) ev
->state
;
643 ret
= handle_motion (x
, y
, state
);
648 x
= (int) floor (ev
->x
);
649 y
= (int) floor (ev
->y
);
650 state
= (GdkModifierType
) ev
->state
;
652 if (Keyboard::modifier_state_contains (state
, Keyboard::TertiaryModifier
)) {
656 ret
= handle_motion (x
, y
, state
);
671 Panner2d::handle_motion (gint evx
, gint evy
, GdkModifierType state
)
673 if (drag_target
== 0) {
677 if ((state
& (GDK_BUTTON1_MASK
|GDK_BUTTON2_MASK
)) == 0) {
682 if (state
& GDK_BUTTON1_MASK
&& !(state
& GDK_BUTTON2_MASK
)) {
684 bool need_move
= false;
686 drag_target
->position
.cartesian (c
);
689 if ((evx
!= c
.x
) || (evy
!= c
.y
)) {
694 CartesianVector
cp (evx
, evy
, 0.0);
697 /* canonicalize position and then clamp to the circle */
700 clamp_to_circle (cp
.x
, cp
.y
);
702 /* generate an angular representation of the current mouse position */
706 if (drag_target
== &position
) {
707 double degree_fract
= av
.azi
/ 360.0;
708 panner
->set_position (degree_fract
);
717 Panner2d::on_scroll_event (GdkEventScroll
* ev
)
719 switch (ev
->direction
) {
721 case GDK_SCROLL_RIGHT
:
722 panner
->set_position (panner
->pannable()->pan_azimuth_control
->get_value() - 1.0/360.0);
725 case GDK_SCROLL_DOWN
:
726 case GDK_SCROLL_LEFT
:
727 panner
->set_position (panner
->pannable()->pan_azimuth_control
->get_value() + 1.0/360.0);
734 Panner2d::cart_to_gtk (CartesianVector
& c
) const
736 /* cartesian coordinate space:
738 dimension = 2.0 * 2.0
739 increasing y moves up
740 so max values along each axis are -1..+1
742 GTK uses a coordinate space that is:
744 dimension = (radius*2.0) * (radius*2.0)
745 increasing y moves down
747 const double diameter
= radius
*2.0;
749 c
.x
= diameter
* ((c
.x
+ 1.0) / 2.0);
750 /* extra subtraction inverts the y-axis to match "increasing y moves down" */
751 c
.y
= diameter
- (diameter
* ((c
.y
+ 1.0) / 2.0));
755 Panner2d::gtk_to_cart (CartesianVector
& c
) const
757 const double diameter
= radius
*2.0;
758 c
.x
= ((c
.x
/ diameter
) * 2.0) - 1.0;
759 c
.y
= (((diameter
- c
.y
) / diameter
) * 2.0) - 1.0;
763 Panner2d::clamp_to_circle (double& x
, double& y
)
769 PBD::cartesian_to_spherical (x
, y
, z
, azi
, ele
, l
);
770 PBD::spherical_to_cartesian (azi
, ele
, 1.0, x
, y
, z
);
774 Panner2d::toggle_bypass ()
776 panner
->set_bypassed (!panner
->bypassed());
779 Panner2dWindow::Panner2dWindow (boost::shared_ptr
<Panner
> p
, int32_t h
, uint32_t inputs
)
780 : ArdourDialog (_("Panner (2D)"))
782 , bypass_button (_("Bypass"))
784 widget
.set_name ("MixerPanZone");
786 set_title (_("Panner"));
787 widget
.set_size_request (h
, h
);
789 bypass_button
.signal_toggled().connect (sigc::mem_fun (*this, &Panner2dWindow::bypass_toggled
));
791 button_box
.set_spacing (6);
792 button_box
.pack_start (bypass_button
, false, false);
794 spinner_box
.set_spacing (6);
795 left_side
.set_spacing (6);
797 left_side
.pack_start (button_box
, false, false);
798 left_side
.pack_start (spinner_box
, false, false);
800 bypass_button
.show ();
805 hpacker
.set_spacing (6);
806 hpacker
.set_border_width (12);
807 hpacker
.pack_start (widget
, false, false);
808 hpacker
.pack_start (left_side
, false, false);
811 get_vbox()->pack_start (hpacker
);
817 Panner2dWindow::reset (uint32_t n_inputs
)
819 widget
.reset (n_inputs
);
822 while (spinners
.size() < n_inputs
) {
823 // spinners.push_back (new Gtk::SpinButton (widget.azimuth (spinners.size())));
824 //spinner_box.pack_start (*spinners.back(), false, false);
825 //spinners.back()->set_digits (4);
826 spinners
.back()->show ();
829 while (spinners
.size() > n_inputs
) {
830 spinner_box
.remove (*spinners
.back());
831 delete spinners
.back();
832 spinners
.erase (--spinners
.end());
838 Panner2dWindow::bypass_toggled ()
840 bool view
= bypass_button
.get_active ();
841 bool model
= widget
.get_panner()->bypassed ();
844 widget
.get_panner()->set_bypassed (view
);
849 Panner2dWindow::on_key_press_event (GdkEventKey
* event
)
851 return relay_key_press (event
, &PublicEditor::instance());
855 Panner2dWindow::on_key_release_event (GdkEventKey
*event
)