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/panner_shell.h"
33 #include "ardour/pannable.h"
34 #include "ardour/speakers.h"
38 #include "gui_thread.h"
40 #include "public_editor.h"
46 using namespace ARDOUR
;
48 using Gtkmm2ext::Keyboard
;
50 static const int large_size_threshold
= 100;
51 static const int large_border_width
= 25;
52 static const int small_border_width
= 8;
54 Panner2d::Target::Target (const AngularVector
& a
, const char *txt
)
61 Panner2d::Target::~Target ()
66 Panner2d::Target::set_text (const char* txt
)
71 Panner2d::Panner2d (boost::shared_ptr
<PannerShell
> p
, int32_t h
)
73 , position (AngularVector (0.0, 0.0), "")
78 panner_shell
->Changed
.connect (connections
, invalidator (*this), boost::bind (&Panner2d::handle_state_change
, this), gui_context());
80 panner_shell
->pannable()->pan_azimuth_control
->Changed
.connect (connections
, invalidator(*this), boost::bind (&Panner2d::handle_position_change
, this), gui_context());
81 panner_shell
->pannable()->pan_width_control
->Changed
.connect (connections
, invalidator(*this), boost::bind (&Panner2d::handle_position_change
, this), gui_context());
84 set_events (Gdk::BUTTON_PRESS_MASK
|Gdk::BUTTON_RELEASE_MASK
|Gdk::POINTER_MOTION_MASK
);
86 handle_position_change ();
91 for (Targets::iterator i
= speakers
.begin(); i
!= speakers
.end(); ++i
) {
97 Panner2d::reset (uint32_t n_inputs
)
99 uint32_t nouts
= panner_shell
->panner()->out().n_audio();
103 while (signals
.size() < n_inputs
) {
104 add_signal ("", AngularVector());
107 if (signals
.size() > n_inputs
) {
108 for (uint32_t i
= signals
.size(); i
< n_inputs
; ++i
) {
112 signals
.resize (n_inputs
);
117 for (uint32_t i
= 0; i
< n_inputs
; ++i
) {
118 signals
[i
]->position
= panner_shell
->panner()->signal_position (i
);
121 /* add all outputs */
123 while (speakers
.size() < nouts
) {
124 add_speaker (AngularVector());
127 if (speakers
.size() > nouts
) {
128 for (uint32_t i
= nouts
; i
< speakers
.size(); ++i
) {
132 speakers
.resize (nouts
);
135 for (Targets::iterator x
= speakers
.begin(); x
!= speakers
.end(); ++x
) {
136 (*x
)->visible
= false;
139 vector
<Speaker
>& the_speakers (panner_shell
->panner()->get_speakers()->speakers());
141 for (uint32_t n
= 0; n
< nouts
; ++n
) {
144 snprintf (buf
, sizeof (buf
), "%d", n
+1);
145 speakers
[n
]->set_text (buf
);
146 speakers
[n
]->position
= the_speakers
[n
].angles();
147 speakers
[n
]->visible
= true;
154 Panner2d::on_size_allocate (Gtk::Allocation
& alloc
)
156 width
= alloc
.get_width();
157 height
= alloc
.get_height();
159 if (height
> large_size_threshold
) {
160 border
= large_border_width
;
162 border
= small_border_width
;
165 radius
= min (width
, height
);
169 hoffset
= max ((double) (width
- height
), border
);
170 voffset
= max ((double) (height
- width
), border
);
175 DrawingArea::on_size_allocate (alloc
);
179 Panner2d::add_signal (const char* text
, const AngularVector
& a
)
181 Target
* signal
= new Target (a
, text
);
182 signals
.push_back (signal
);
183 signal
->visible
= true;
189 Panner2d::add_speaker (const AngularVector
& a
)
191 Target
* speaker
= new Target (a
, "");
192 speakers
.push_back (speaker
);
193 speaker
->visible
= true;
196 return speakers
.size() - 1;
200 Panner2d::handle_state_change ()
206 Panner2d::label_signals ()
208 double w
= panner_shell
->pannable()->pan_width_control
->get_value();
209 uint32_t sz
= signals
.size();
216 signals
[0]->set_text ("");
221 signals
[0]->set_text ("R");
222 signals
[1]->set_text ("L");
224 signals
[0]->set_text ("L");
225 signals
[1]->set_text ("R");
230 for (uint32_t i
= 0; i
< sz
; ++i
) {
233 snprintf (buf
, sizeof (buf
), "%" PRIu32
, i
+ 1);
235 snprintf (buf
, sizeof (buf
), "%" PRIu32
, sz
- i
);
237 signals
[i
]->set_text (buf
);
244 Panner2d::handle_position_change ()
247 double w
= panner_shell
->pannable()->pan_width_control
->get_value();
249 position
.position
= AngularVector (panner_shell
->pannable()->pan_azimuth_control
->get_value() * 360.0, 0.0);
251 for (uint32_t i
= 0; i
< signals
.size(); ++i
) {
252 signals
[i
]->position
= panner_shell
->panner()->signal_position (i
);
255 if (w
* last_width
<= 0) {
262 vector
<Speaker
>& the_speakers (panner_shell
->panner()->get_speakers()->speakers());
264 for (n
= 0; n
< speakers
.size(); ++n
) {
265 speakers
[n
]->position
= the_speakers
[n
].angles();
272 Panner2d::move_signal (int which
, const AngularVector
& a
)
274 if (which
>= int (speakers
.size())) {
278 speakers
[which
]->position
= a
;
283 Panner2d::find_closest_object (gdouble x
, gdouble y
, bool& is_signal
)
288 float best_distance
= FLT_MAX
;
291 /* start with the position itself
294 position
.position
.cartesian (c
);
296 best_distance
= sqrt ((c
.x
- x
) * (c
.x
- x
) +
297 (c
.y
- y
) * (c
.y
- y
));
300 for (Targets::const_iterator i
= signals
.begin(); i
!= signals
.end(); ++i
) {
303 candidate
->position
.cartesian (c
);
306 distance
= sqrt ((c
.x
- x
) * (c
.x
- x
) +
307 (c
.y
- y
) * (c
.y
- y
));
309 if (distance
< best_distance
) {
311 best_distance
= distance
;
317 if (height
> large_size_threshold
) {
319 if (best_distance
> 30) { // arbitrary
324 if (best_distance
> 10) { // arbitrary
329 /* if we didn't find a signal close by, check the speakers */
332 for (Targets::const_iterator i
= speakers
.begin(); i
!= speakers
.end(); ++i
) {
335 candidate
->position
.cartesian (c
);
338 distance
= sqrt ((c
.x
- x
) * (c
.x
- x
) +
339 (c
.y
- y
) * (c
.y
- y
));
341 if (distance
< best_distance
) {
343 best_distance
= distance
;
347 if (height
> large_size_threshold
) {
349 if (best_distance
< 30) { // arbitrary
356 if (best_distance
< 10) { // arbitrary
368 Panner2d::on_motion_notify_event (GdkEventMotion
*ev
)
371 GdkModifierType state
;
374 gdk_window_get_pointer (ev
->window
, &x
, &y
, &state
);
376 x
= (int) floor (ev
->x
);
377 y
= (int) floor (ev
->y
);
378 state
= (GdkModifierType
) ev
->state
;
381 if (ev
->state
& (GDK_BUTTON1_MASK
|GDK_BUTTON2_MASK
)) {
385 return handle_motion (x
, y
, state
);
389 Panner2d::on_expose_event (GdkEventExpose
*event
)
393 bool small
= (height
<= large_size_threshold
);
394 const double diameter
= radius
*2.0;
396 cr
= gdk_cairo_create (get_window()->gobj());
400 cairo_rectangle (cr
, event
->area
.x
, event
->area
.y
, event
->area
.width
, event
->area
.height
);
401 if (!panner_shell
->bypassed()) {
402 cairo_set_source_rgba (cr
, 0.1, 0.1, 0.1, 1.0);
404 cairo_set_source_rgba (cr
, 0.1, 0.1, 0.1, 0.2);
406 cairo_fill_preserve (cr
);
409 /* offset to give us some border */
411 cairo_translate (cr
, hoffset
, voffset
);
413 cairo_set_line_width (cr
, 1.0);
415 /* horizontal line of "crosshairs" */
417 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 1.0);
418 cairo_move_to (cr
, 0.0, radius
);
419 cairo_line_to (cr
, diameter
, radius
);
422 /* vertical line of "crosshairs" */
424 cairo_move_to (cr
, radius
, 0);
425 cairo_line_to (cr
, radius
, diameter
);
428 /* the circle on which signals live */
430 cairo_set_line_width (cr
, 2.0);
431 cairo_set_source_rgba (cr
, 0.517, 0.772, 0.882, 1.0);
432 cairo_arc (cr
, radius
, radius
, radius
, 0.0, 2.0 * M_PI
);
435 /* 3 other circles of smaller diameter circle on which signals live */
437 cairo_set_line_width (cr
, 1.0);
438 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 1.0);
439 cairo_arc (cr
, radius
, radius
, radius
* 0.75, 0, 2.0 * M_PI
);
441 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 0.85);
442 cairo_arc (cr
, radius
, radius
, radius
* 0.50, 0, 2.0 * M_PI
);
444 cairo_arc (cr
, radius
, radius
, radius
* 0.25, 0, 2.0 * M_PI
);
447 if (signals
.size() > 1) {
448 /* arc to show "diffusion" */
450 double width_angle
= fabs (panner_shell
->pannable()->pan_width_control
->get_value()) * 2 * M_PI
;
451 double position_angle
= (2 * M_PI
) - panner_shell
->pannable()->pan_azimuth_control
->get_value() * 2 * M_PI
;
454 cairo_translate (cr
, radius
, radius
);
455 cairo_rotate (cr
, position_angle
- (width_angle
/2.0));
456 cairo_move_to (cr
, 0, 0);
457 cairo_arc_negative (cr
, 0, 0, radius
, width_angle
, 0.0);
458 cairo_close_path (cr
);
459 if (panner_shell
->pannable()->pan_width_control
->get_value() >= 0.0) {
461 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 0.45);
464 cairo_set_source_rgba (cr
, 1.0, 0.419, 0.419, 0.45);
470 if (!panner_shell
->bypassed()) {
474 cairo_select_font_face (cr
, "sans", CAIRO_FONT_SLANT_NORMAL
, CAIRO_FONT_WEIGHT_NORMAL
);
479 cairo_set_font_size (cr
, 10);
485 if (signals
.size() > 1) {
486 for (Targets::iterator i
= signals
.begin(); i
!= signals
.end(); ++i
) {
489 if (signal
->visible
) {
491 signal
->position
.cartesian (c
);
495 cairo_arc (cr
, c
.x
, c
.y
, arc_radius
, 0, 2.0 * M_PI
);
496 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 0.85);
497 cairo_fill_preserve (cr
);
498 cairo_set_source_rgba (cr
, 0.517, 0.772, 0.882, 1.0);
501 if (!small
&& !signal
->text
.empty()) {
502 cairo_set_source_rgb (cr
, 0.517, 0.772, 0.882);
503 /* the +/- adjustments are a hack to try to center the text in the circle */
505 cairo_move_to (cr
, c
.x
- 1, c
.y
+ 1);
507 cairo_move_to (cr
, c
.x
- 4, c
.y
+ 4);
509 cairo_show_text (cr
, signal
->text
.c_str());
519 for (Targets::iterator i
= speakers
.begin(); i
!= speakers
.end(); ++i
) {
520 Target
*speaker
= *i
;
524 if (speaker
->visible
) {
528 speaker
->position
.cartesian (c
);
531 snprintf (buf
, sizeof (buf
), "%d", n
);
533 /* stroke out a speaker shape */
535 cairo_move_to (cr
, c
.x
, c
.y
);
537 cairo_rotate (cr
, -(speaker
->position
.azi
/360.0) * (2.0 * M_PI
));
539 cairo_scale (cr
, 0.8, 0.8);
541 cairo_scale (cr
, 1.2, 1.2);
543 cairo_rel_line_to (cr
, 4, -2);
544 cairo_rel_line_to (cr
, 0, -7);
545 cairo_rel_line_to (cr
, 5, +5);
546 cairo_rel_line_to (cr
, 5, 0);
547 cairo_rel_line_to (cr
, 0, 5);
548 cairo_rel_line_to (cr
, -5, 0);
549 cairo_rel_line_to (cr
, -5, +5);
550 cairo_rel_line_to (cr
, 0, -7);
551 cairo_close_path (cr
);
552 cairo_set_source_rgba (cr
, 0.282, 0.517, 0.662, 1.0);
557 cairo_set_font_size (cr
, 16);
559 /* move the text in just a bit */
561 AngularVector
textpos (speaker
->position
.azi
, speaker
->position
.ele
, 0.85);
562 textpos
.cartesian (c
);
564 cairo_move_to (cr
, c
.x
, c
.y
);
565 cairo_show_text (cr
, buf
);
573 position
.position
.cartesian (c
);
577 cairo_arc (cr
, c
.x
, c
.y
, arc_radius
, 0, 2.0 * M_PI
);
578 cairo_set_source_rgba (cr
, 1.0, 0.419, 0.419, 0.85);
579 cairo_fill_preserve (cr
);
580 cairo_set_source_rgba (cr
, 1.0, 0.905, 0.905, 0.85);
590 Panner2d::on_button_press_event (GdkEventButton
*ev
)
592 GdkModifierType state
;
597 if (ev
->type
== GDK_2BUTTON_PRESS
&& ev
->button
== 1) {
603 switch (ev
->button
) {
609 if ((drag_target
= find_closest_object (x
, y
, is_signal
)) != 0) {
611 panner_shell
->panner()->set_position (drag_target
->position
.azi
/360.0);
614 drag_target
->set_selected (true);
620 state
= (GdkModifierType
) ev
->state
;
622 return handle_motion (drag_x
, drag_y
, state
);
633 Panner2d::on_button_release_event (GdkEventButton
*ev
)
636 GdkModifierType state
;
639 switch (ev
->button
) {
641 x
= (int) floor (ev
->x
);
642 y
= (int) floor (ev
->y
);
643 state
= (GdkModifierType
) ev
->state
;
644 ret
= handle_motion (x
, y
, state
);
649 x
= (int) floor (ev
->x
);
650 y
= (int) floor (ev
->y
);
651 state
= (GdkModifierType
) ev
->state
;
653 if (Keyboard::modifier_state_contains (state
, Keyboard::TertiaryModifier
)) {
657 ret
= handle_motion (x
, y
, state
);
672 Panner2d::handle_motion (gint evx
, gint evy
, GdkModifierType state
)
674 if (drag_target
== 0) {
678 if ((state
& (GDK_BUTTON1_MASK
|GDK_BUTTON2_MASK
)) == 0) {
683 if (state
& GDK_BUTTON1_MASK
&& !(state
& GDK_BUTTON2_MASK
)) {
685 bool need_move
= false;
687 drag_target
->position
.cartesian (c
);
690 if ((evx
!= c
.x
) || (evy
!= c
.y
)) {
695 CartesianVector
cp (evx
, evy
, 0.0);
698 /* canonicalize position and then clamp to the circle */
701 clamp_to_circle (cp
.x
, cp
.y
);
703 /* generate an angular representation of the current mouse position */
707 if (drag_target
== &position
) {
708 double degree_fract
= av
.azi
/ 360.0;
709 panner_shell
->panner()->set_position (degree_fract
);
718 Panner2d::on_scroll_event (GdkEventScroll
* ev
)
720 switch (ev
->direction
) {
722 case GDK_SCROLL_RIGHT
:
723 panner_shell
->panner()->set_position (panner_shell
->pannable()->pan_azimuth_control
->get_value() - 1.0/360.0);
726 case GDK_SCROLL_DOWN
:
727 case GDK_SCROLL_LEFT
:
728 panner_shell
->panner()->set_position (panner_shell
->pannable()->pan_azimuth_control
->get_value() + 1.0/360.0);
735 Panner2d::cart_to_gtk (CartesianVector
& c
) const
737 /* cartesian coordinate space:
739 dimension = 2.0 * 2.0
740 increasing y moves up
741 so max values along each axis are -1..+1
743 GTK uses a coordinate space that is:
745 dimension = (radius*2.0) * (radius*2.0)
746 increasing y moves down
748 const double diameter
= radius
*2.0;
750 c
.x
= diameter
* ((c
.x
+ 1.0) / 2.0);
751 /* extra subtraction inverts the y-axis to match "increasing y moves down" */
752 c
.y
= diameter
- (diameter
* ((c
.y
+ 1.0) / 2.0));
756 Panner2d::gtk_to_cart (CartesianVector
& c
) const
758 const double diameter
= radius
*2.0;
759 c
.x
= ((c
.x
/ diameter
) * 2.0) - 1.0;
760 c
.y
= (((diameter
- c
.y
) / diameter
) * 2.0) - 1.0;
764 Panner2d::clamp_to_circle (double& x
, double& y
)
770 PBD::cartesian_to_spherical (x
, y
, z
, azi
, ele
, l
);
771 PBD::spherical_to_cartesian (azi
, ele
, 1.0, x
, y
, z
);
775 Panner2d::toggle_bypass ()
777 panner_shell
->set_bypassed (!panner_shell
->bypassed());
780 Panner2dWindow::Panner2dWindow (boost::shared_ptr
<PannerShell
> p
, int32_t h
, uint32_t inputs
)
781 : ArdourDialog (_("Panner (2D)"))
783 , bypass_button (_("Bypass"))
785 widget
.set_name ("MixerPanZone");
787 set_title (_("Panner"));
788 widget
.set_size_request (h
, h
);
790 bypass_button
.signal_toggled().connect (sigc::mem_fun (*this, &Panner2dWindow::bypass_toggled
));
792 button_box
.set_spacing (6);
793 button_box
.pack_start (bypass_button
, false, false);
795 spinner_box
.set_spacing (6);
796 left_side
.set_spacing (6);
798 left_side
.pack_start (button_box
, false, false);
799 left_side
.pack_start (spinner_box
, false, false);
801 bypass_button
.show ();
806 hpacker
.set_spacing (6);
807 hpacker
.set_border_width (12);
808 hpacker
.pack_start (widget
, false, false);
809 hpacker
.pack_start (left_side
, false, false);
812 get_vbox()->pack_start (hpacker
);
818 Panner2dWindow::reset (uint32_t n_inputs
)
820 widget
.reset (n_inputs
);
823 while (spinners
.size() < n_inputs
) {
824 // spinners.push_back (new Gtk::SpinButton (widget.azimuth (spinners.size())));
825 //spinner_box.pack_start (*spinners.back(), false, false);
826 //spinners.back()->set_digits (4);
827 spinners
.back()->show ();
830 while (spinners
.size() > n_inputs
) {
831 spinner_box
.remove (*spinners
.back());
832 delete spinners
.back();
833 spinners
.erase (--spinners
.end());
839 Panner2dWindow::bypass_toggled ()
841 bool view
= bypass_button
.get_active ();
842 bool model
= widget
.get_panner_shell()->bypassed ();
845 widget
.get_panner_shell()->set_bypassed (view
);
850 Panner2dWindow::on_key_press_event (GdkEventKey
* event
)
852 return relay_key_press (event
, &PublicEditor::instance());
856 Panner2dWindow::on_key_release_event (GdkEventKey
*event
)