Merge branch 'pt-5-7-x' of https://github.com/zamaudio/ardour
[ardour.git] / gtk2_ardour / panner2d.cc
1 /*
2     Copyright (C) 2002 Paul Davis
3
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.
8
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.
13
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.
17
18 */
19
20 #include <cmath>
21 #include <climits>
22 #include <string.h>
23
24 #include <cairo.h>
25 #include <gtkmm/menu.h>
26
27 #include "gtkmm2ext/gtk_ui.h"
28
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"
35
36 #include "canvas/colors.h"
37
38 #include "ardour_ui.h"
39 #include "global_signals.h"
40 #include "panner2d.h"
41 #include "keyboard.h"
42 #include "gui_thread.h"
43 #include "rgb_macros.h"
44 #include "utils.h"
45 #include "public_editor.h"
46
47 #include "i18n.h"
48
49 using namespace std;
50 using namespace Gtk;
51 using namespace ARDOUR;
52 using namespace ARDOUR_UI_UTILS;
53 using namespace PBD;
54 using Gtkmm2ext::Keyboard;
55
56 Panner2d::ColorScheme Panner2d::colors;
57 bool Panner2d::have_colors = false;
58
59 static const int large_size_threshold = 100;
60 static const int large_border_width = 25;
61 static const int small_border_width = 8;
62
63 Panner2d::Target::Target (const AngularVector& a, const char *txt)
64         : position (a)
65         , text (txt)
66         , _selected (false)
67 {
68 }
69
70 Panner2d::Target::~Target ()
71 {
72 }
73
74 void
75 Panner2d::Target::set_text (const char* txt)
76 {
77         text = txt;
78 }
79
80 Panner2d::Panner2d (boost::shared_ptr<PannerShell> p, int32_t h)
81 : panner_shell (p)
82         , position (AngularVector (0.0, 0.0), "")
83         , width (0)
84         , height (h)
85         , last_width (0)
86         , have_elevation (false)
87         , _send_mode (false)
88 {
89         if (!have_colors) {
90                 set_colors ();
91                 have_colors = true;
92         }
93
94         ColorsChanged.connect (sigc::mem_fun (*this, &Panner2d::color_handler));
95
96         panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&Panner2d::handle_state_change, this), gui_context());
97
98         panner_shell->panner()->SignalPositionChanged.connect (panner_connections, invalidator(*this), boost::bind (&Panner2d::handle_position_change, this), gui_context());
99
100         drag_target = 0;
101         set_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|Gdk::POINTER_MOTION_MASK);
102
103         handle_state_change ();
104         handle_position_change ();
105 }
106
107 Panner2d::~Panner2d()
108 {
109         for (Targets::iterator i = speakers.begin(); i != speakers.end(); ++i) {
110                 delete *i;
111         }
112 }
113
114 void
115 Panner2d::set_colors ()
116 {
117         // TODO get all colors from theme, resolve dups
118         colors.background = ARDOUR_UI::config()->color ("mono panner bg");
119         colors.crosshairs =          0x4884a9ff; // 0.282, 0.517, 0.662, 1.0
120         colors.signalcircle_border = 0x84c5e1ff; // 0.517, 0.772, 0.882, 1.0
121         colors.signalcircle =        0x4884a9ff; // 0.282, 0.517, 0.662, 1.0  // also used with a = 0.9
122         colors.diffusion =           0x4884a973; // 0.282, 0.517, 0.662, 0.45
123         colors.diffusion_inv =       0xff6b6b73; // 1.0,   0.419, 0.419, 0.45
124         colors.pos_outline =         0xffe7e7d9; // 1.0,   0.905, 0.905, 0.85
125         colors.pos_fill =            0xff6b6bd9; // 1.0,   0.419, 0.419, 0.85
126         colors.signal_outline =      0x84c5e1cc; // 0.517, 0.772, 0.882, 0.8
127         colors.signal_fill =         0x4884a9bf; // 0.282, 0.517, 0.662, 0.75
128         colors.speaker_fill =        0x4884a9ff; // 0.282, 0.517, 0.662, 1.0
129         colors.text =                0x84c5e1e6; // 0.517, 0.772, 0.882, 0.9
130 }
131
132 void
133 Panner2d::color_handler ()
134 {
135         set_colors ();
136         queue_draw ();
137 }
138
139 void
140 Panner2d::reset (uint32_t n_inputs)
141 {
142         uint32_t nouts = panner_shell->panner()->out().n_audio();
143
144         /* signals */
145
146         while (signals.size() < n_inputs) {
147                 add_signal ("", AngularVector());
148         }
149
150         if (signals.size() > n_inputs) {
151                 for (uint32_t i = signals.size(); i < n_inputs; ++i) {
152                         delete signals[i];
153                 }
154
155                 signals.resize (n_inputs);
156         }
157
158         label_signals ();
159
160         for (uint32_t i = 0; i < n_inputs; ++i) {
161                 signals[i]->position = panner_shell->panner()->signal_position (i);
162         }
163
164         /* add all outputs */
165
166         while (speakers.size() < nouts) {
167                 add_speaker (AngularVector());
168         }
169
170         if (speakers.size() > nouts) {
171                 for (uint32_t i = nouts; i < speakers.size(); ++i) {
172                         delete speakers[i];
173                 }
174
175                 speakers.resize (nouts);
176         }
177
178         for (Targets::iterator x = speakers.begin(); x != speakers.end(); ++x) {
179                 (*x)->visible = false;
180         }
181
182         vector<Speaker>& the_speakers (panner_shell->panner()->get_speakers()->speakers());
183
184         for (uint32_t n = 0; n < nouts; ++n) {
185                 char buf[16];
186
187                 snprintf (buf, sizeof (buf), "%d", n+1);
188                 speakers[n]->set_text (buf);
189                 speakers[n]->position = the_speakers[n].angles();
190                 speakers[n]->visible = true;
191         }
192
193         queue_draw ();
194 }
195
196 void
197 Panner2d::on_size_allocate (Gtk::Allocation& alloc)
198 {
199         width = alloc.get_width();
200         height = alloc.get_height();
201
202         if (height > large_size_threshold) {
203                 border = large_border_width;
204         } else {
205                 border = small_border_width;
206         }
207
208         radius = min (width, height);
209         radius -= border;
210         radius /= 2;
211         radius = rint(radius) + .5;
212
213         hoffset = max ((double) (width - height), border);
214         voffset = max ((double) (height - width), border);
215
216         hoffset = rint(hoffset / 2.0);
217         voffset = rint(voffset / 2.0);
218
219         DrawingArea::on_size_allocate (alloc);
220 }
221
222 int
223 Panner2d::add_signal (const char* text, const AngularVector& a)
224 {
225         Target* signal = new Target (a, text);
226         signals.push_back (signal);
227         signal->visible = true;
228
229         return 0;
230 }
231
232 int
233 Panner2d::add_speaker (const AngularVector& a)
234 {
235         Target* speaker = new Target (a, "");
236         speakers.push_back (speaker);
237         speaker->visible = true;
238         queue_draw ();
239
240         return speakers.size() - 1;
241 }
242
243 void
244 Panner2d::handle_state_change ()
245 {
246         panner_connections.drop_connections();
247         if (!panner_shell->panner()) {
248                 return;
249         }
250
251         panner_shell->panner()->SignalPositionChanged.connect (panner_connections, invalidator(*this), boost::bind (&Panner2d::handle_position_change, this), gui_context());
252
253         set<Evoral::Parameter> params = panner_shell->panner()->what_can_be_automated();
254         set<Evoral::Parameter>::iterator p = params.find(PanElevationAutomation);
255         bool elev = have_elevation;
256         have_elevation = (p == params.end()) ? false : true;
257         if (elev != have_elevation) {
258                 handle_position_change();
259         }
260         queue_draw ();
261 }
262
263 void
264 Panner2d::label_signals ()
265 {
266         uint32_t sz = signals.size();
267
268         switch (sz) {
269                 case 0:
270                         break;
271
272                 case 1:
273                         signals[0]->set_text ("");
274                         break;
275
276                 case 2:
277                         signals[0]->set_text (S_("Panner|L"));
278                         signals[1]->set_text (S_("Panner|R"));
279                         break;
280
281                 default:
282                         for (uint32_t i = 0; i < sz; ++i) {
283                                 char buf[64];
284                                 snprintf (buf, sizeof (buf), "%" PRIu32, i + 1);
285                                 signals[i]->set_text (buf);
286                         }
287                         break;
288         }
289 }
290
291 void
292 Panner2d::handle_position_change ()
293 {
294         uint32_t n;
295         double w = panner_shell->pannable()->pan_width_control->get_value();
296
297         position.position = AngularVector (panner_shell->pannable()->pan_azimuth_control->get_value() * 360.0,
298                         panner_shell->pannable()->pan_elevation_control->get_value() * 90.0);
299
300         for (uint32_t i = 0; i < signals.size(); ++i) {
301                 signals[i]->position = panner_shell->panner()->signal_position (i);
302         }
303
304         if (w * last_width <= 0) {
305                 /* changed sign */
306                 label_signals ();
307         }
308
309         last_width = w;
310
311         vector<Speaker>& the_speakers (panner_shell->panner()->get_speakers()->speakers());
312
313         for (n = 0; n < speakers.size(); ++n) {
314                 speakers[n]->position = the_speakers[n].angles();
315         }
316
317         queue_draw ();
318 }
319
320 void
321 Panner2d::move_signal (int which, const AngularVector& a)
322 {
323         if (which >= int (speakers.size())) {
324                 return;
325         }
326
327         speakers[which]->position = a;
328         queue_draw ();
329 }
330
331 Panner2d::Target *
332 Panner2d::find_closest_object (gdouble x, gdouble y, bool& is_signal)
333 {
334         Target *closest = 0;
335         Target *candidate;
336         float distance;
337         float best_distance = FLT_MAX;
338         CartesianVector c;
339
340         /* start with the position itself */
341
342         PBD::AngularVector dp = position.position;
343         if (!have_elevation) dp.ele = 0;
344         dp.azi = 270 - dp.azi;
345         dp.cartesian (c);
346
347         cart_to_gtk (c);
348         best_distance = sqrt ((c.x - x) * (c.x - x) +
349                         (c.y - y) * (c.y - y));
350         closest = &position;
351
352 #if 0 // TODO signal grab -> change width, not position
353         for (Targets::const_iterator i = signals.begin(); i != signals.end(); ++i) {
354                 candidate = *i;
355
356                 candidate->position.cartesian (c);
357                 cart_to_gtk (c);
358
359                 distance = sqrt ((c.x - x) * (c.x - x) +
360                                 (c.y - y) * (c.y - y));
361
362                 if (distance < best_distance) {
363                         closest = candidate;
364                         best_distance = distance;
365                 }
366         }
367 #endif
368
369         is_signal = true;
370
371         if (height > large_size_threshold) {
372                 /* "big" */
373                 if (best_distance > 30) { // arbitrary
374                         closest = 0;
375                 }
376         } else {
377                 /* "small" */
378                 if (best_distance > 10) { // arbitrary
379                         closest = 0;
380                 }
381         }
382
383         /* if we didn't find a signal close by, check the speakers */
384
385         if (!closest) {
386                 for (Targets::const_iterator i = speakers.begin(); i != speakers.end(); ++i) {
387                         candidate = *i;
388                         PBD::AngularVector sp = candidate->position;
389                         sp.azi = 270 -sp.azi;
390                         CartesianVector c;
391                         sp.cartesian (c);
392                         cart_to_gtk (c);
393
394                         distance = sqrt ((c.x - x) * (c.x - x) +
395                                         (c.y - y) * (c.y - y));
396
397                         if (distance < best_distance) {
398                                 closest = candidate;
399                                 best_distance = distance;
400                         }
401                 }
402
403                 if (height > large_size_threshold) {
404                         /* "big" */
405                         if (best_distance < 30) { // arbitrary
406                                 is_signal = false;
407                         } else {
408                                 closest = 0;
409                         }
410                 } else {
411                         /* "small" */
412                         if (best_distance < 10) { // arbitrary
413                                 is_signal = false;
414                         } else {
415                                 closest = 0;
416                         }
417                 }
418         }
419
420         return closest;
421 }
422
423 void
424 Panner2d::set_send_drawing_mode (bool onoff)
425 {
426         if (_send_mode != onoff) {
427                 _send_mode = onoff;
428                 queue_draw ();
429         }
430 }
431
432 bool
433 Panner2d::on_motion_notify_event (GdkEventMotion *ev)
434 {
435         gint x, y;
436         GdkModifierType state;
437
438         if (ev->is_hint) {
439                 gdk_window_get_pointer (ev->window, &x, &y, &state);
440         } else {
441                 x = (int) floor (ev->x);
442                 y = (int) floor (ev->y);
443                 state = (GdkModifierType) ev->state;
444         }
445
446         if (ev->state & (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK)) {
447                 did_move = true;
448         }
449
450         return handle_motion (x, y, state);
451 }
452
453 #define CSSRGBA(CL) \
454         cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(CL), UINT_RGBA_G_FLT(CL), UINT_RGBA_B_FLT(CL), UINT_RGBA_A_FLT(CL));
455
456 #define CSSRGB(CL, A) \
457         cairo_set_source_rgba (cr, UINT_RGBA_R_FLT(CL), UINT_RGBA_G_FLT(CL), UINT_RGBA_B_FLT(CL), A);
458 bool
459 Panner2d::on_expose_event (GdkEventExpose *event)
460 {
461         CartesianVector c;
462         cairo_t* cr;
463         bool xsmall = (height <= large_size_threshold);
464         const double diameter = radius*2.0;
465
466         cr = gdk_cairo_create (get_window()->gobj());
467
468         /* background */
469
470         cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height);
471
472         uint32_t bg = colors.background;
473         if (_send_mode) {
474                 bg = ARDOUR_UI::config()->color ("send bg");
475         }
476
477         if (!panner_shell->bypassed()) {
478                 CSSRGBA(bg);
479         } else {
480                 CSSRGB(bg, 0.2);
481         }
482         cairo_fill_preserve (cr);
483         cairo_clip (cr);
484
485         /* offset to give us some border */
486
487         cairo_translate (cr, hoffset, voffset);
488
489         cairo_set_line_width (cr, 1.0);
490
491         /* horizontal line of "crosshairs" */
492
493         CSSRGBA(colors.crosshairs);
494         cairo_move_to (cr, 0.0, radius);
495         cairo_line_to (cr, diameter, radius);
496         cairo_stroke (cr);
497
498         /* vertical line of "crosshairs" */
499
500         cairo_move_to (cr, radius, 0);
501         cairo_line_to (cr, radius, diameter);
502         cairo_stroke (cr);
503
504         /* the circle on which signals live */
505
506         cairo_set_line_width (cr, 1.5);
507         CSSRGBA(colors.signalcircle_border);
508         cairo_arc (cr, radius, radius, radius, 0.0, 2.0 * M_PI);
509         cairo_stroke (cr);
510
511         for (uint32_t rad = 15; rad < 90; rad += 15) {
512                 cairo_set_line_width (cr, .5 + (float)rad / 150.0);
513                 if (rad == 45) {
514                         CSSRGBA(colors.signalcircle);
515                 } else {
516                         CSSRGB(colors.signalcircle, 0.9);
517                 }
518                 cairo_new_path (cr);
519                 cairo_arc (cr, radius, radius, radius * sin(M_PI * (float) rad / 180.0), 0, 2.0 * M_PI);
520                 cairo_stroke (cr);
521         }
522
523         if (!panner_shell->bypassed()) {
524                 /* convention top == front ^= azimuth == .5 (same as stereo/mono panners) */
525
526                 if (signals.size() > 1) {
527                         /* arc to show "diffusion" */
528
529                         double width_angle = fabs (panner_shell->pannable()->pan_width_control->get_value()) * 2 * M_PI;
530                         double position_angle = panner_shell->pannable()->pan_azimuth_control->get_value() * 2 * M_PI;
531
532                         cairo_save (cr);
533                         cairo_translate (cr, radius, radius);
534                         cairo_rotate (cr, M_PI / 2.0);
535                         cairo_rotate (cr, position_angle - (width_angle/2.0));
536                         cairo_move_to (cr, 0, 0);
537                         cairo_arc_negative (cr, 0, 0, radius, width_angle, 0.0);
538                         cairo_close_path (cr);
539                         if (panner_shell->pannable()->pan_width_control->get_value() >= 0.0) {
540                                 /* normal width */
541                                 CSSRGBA(colors.diffusion);
542                         } else {
543                                 /* inverse width */
544                                 CSSRGBA(colors.diffusion_inv);
545                         }
546                         cairo_fill (cr);
547                         cairo_restore (cr);
548                 }
549
550                 double arc_radius;
551
552                 cairo_select_font_face (cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
553
554                 if (xsmall) {
555                         arc_radius = 4.0;
556                 } else {
557                         cairo_set_font_size (cr, 10);
558                         arc_radius = 12.0;
559                 }
560
561                 /* draw position */
562
563                 PBD::AngularVector dp = position.position;
564                 if (!have_elevation) dp.ele = 0;
565                 dp.azi = 270 - dp.azi;
566                 dp.cartesian (c);
567                 cart_to_gtk (c);
568
569                 cairo_new_path (cr);
570                 cairo_arc (cr, c.x, c.y, arc_radius + 1.0, 0, 2.0 * M_PI);
571                 CSSRGBA(colors.pos_fill);
572                 cairo_fill_preserve (cr);
573                 CSSRGBA(colors.pos_outline);
574                 cairo_stroke (cr);
575
576                 /* signals */
577
578                 if (signals.size() > 0) {
579                         for (Targets::iterator i = signals.begin(); i != signals.end(); ++i) {
580                                 Target* signal = *i;
581
582                                 if (signal->visible) {
583
584                                         /* TODO check for overlap - multiple src at same position
585                                          * -> visualize it properly
586                                          */
587                                         PBD::AngularVector sp = signal->position;
588                                         if (!have_elevation) sp.ele = 0;
589                                         sp.azi += 270.0;
590                                         sp.cartesian (c);
591                                         cart_to_gtk (c);
592
593                                         cairo_new_path (cr);
594                                         cairo_arc (cr, c.x, c.y, arc_radius, 0, 2.0 * M_PI);
595                                         CSSRGBA(colors.signal_fill);
596                                         cairo_fill_preserve (cr);
597                                         CSSRGBA(colors.signal_outline);
598                                         cairo_stroke (cr);
599
600                                         if (!xsmall && !signal->text.empty()) {
601                                                 CSSRGBA(colors.text);
602                                                 /* the +/- adjustments are a hack to try to center the text in the circle
603                                                  * TODO use pango get_pixel_size() -- see mono_panner.cc
604                                                  */
605                                                 if (xsmall) {
606                                                         cairo_move_to (cr, c.x - 1, c.y + 1);
607                                                 } else {
608                                                         cairo_move_to (cr, c.x - 4, c.y + 4);
609                                                 }
610                                                 cairo_show_text (cr, signal->text.c_str());
611                                         }
612                                 }
613                         }
614                 }
615
616                 /* speakers */
617
618                 int n = 0;
619
620                 for (Targets::iterator i = speakers.begin(); i != speakers.end(); ++i) {
621                         Target *speaker = *i;
622                         char buf[256];
623                         ++n;
624
625                         if (speaker->visible) {
626
627                                 PBD::AngularVector sp = speaker->position;
628                                 sp.azi += 270.0;
629                                 CartesianVector c;
630                                 sp.cartesian (c);
631                                 cart_to_gtk (c);
632
633                                 snprintf (buf, sizeof (buf), "%d", n);
634
635                                 /* stroke out a speaker shape */
636
637                                 cairo_move_to (cr, c.x, c.y);
638                                 cairo_save (cr);
639                                 cairo_rotate (cr, -(sp.azi/360.0) * (2.0 * M_PI));
640                                 if (xsmall) {
641                                         cairo_scale (cr, 0.8, 0.8);
642                                 } else {
643                                         cairo_scale (cr, 1.2, 1.2);
644                                 }
645                                 cairo_rel_line_to (cr, 4, -2);
646                                 cairo_rel_line_to (cr, 0, -7);
647                                 cairo_rel_line_to (cr, 5, +5);
648                                 cairo_rel_line_to (cr, 5, 0);
649                                 cairo_rel_line_to (cr, 0, 5);
650                                 cairo_rel_line_to (cr, -5, 0);
651                                 cairo_rel_line_to (cr, -5, +5);
652                                 cairo_rel_line_to (cr, 0, -7);
653                                 cairo_close_path (cr);
654                                 CSSRGBA(colors.speaker_fill);
655                                 cairo_fill (cr);
656                                 cairo_restore (cr);
657
658                                 if (!xsmall) {
659                                         cairo_set_font_size (cr, 16);
660
661                                         /* move the text in just a bit */
662
663                                         AngularVector textpos (speaker->position.azi + 270.0, speaker->position.ele, 0.85);
664
665                                         textpos.cartesian (c);
666                                         cart_to_gtk (c);
667                                         cairo_move_to (cr, c.x, c.y);
668                                         cairo_show_text (cr, buf);
669                                 }
670
671                         }
672                 }
673         }
674
675         cairo_destroy (cr);
676
677         return true;
678 }
679
680 bool
681 Panner2d::on_button_press_event (GdkEventButton *ev)
682 {
683         GdkModifierType state;
684         int x;
685         int y;
686         bool is_signal;
687
688         if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
689                 return false;
690         }
691
692         did_move = false;
693
694         switch (ev->button) {
695         case 1:
696         case 2:
697                 x = ev->x - hoffset;
698                 y = ev->y - voffset;
699
700                 if ((drag_target = find_closest_object (x, y, is_signal)) != 0) {
701                         if (!is_signal) {
702                                 panner_shell->panner()->set_position (drag_target->position.azi/360.0);
703                                 drag_target = 0;
704                         } else {
705                                 drag_target->set_selected (true);
706                         }
707                 }
708
709                 state = (GdkModifierType) ev->state;
710                 return handle_motion (ev->x, ev->y, state);
711                 break;
712
713         default:
714                 break;
715         }
716
717         return false;
718 }
719
720 bool
721 Panner2d::on_button_release_event (GdkEventButton *ev)
722 {
723         gint x, y;
724         GdkModifierType state;
725         bool ret = false;
726
727         switch (ev->button) {
728         case 1:
729                 x = (int) floor (ev->x);
730                 y = (int) floor (ev->y);
731                 state = (GdkModifierType) ev->state;
732                 ret = handle_motion (x, y, state);
733                 drag_target = 0;
734                 break;
735
736         case 2:
737                 x = (int) floor (ev->x);
738                 y = (int) floor (ev->y);
739                 state = (GdkModifierType) ev->state;
740
741                 if (Keyboard::modifier_state_contains (state, Keyboard::TertiaryModifier)) {
742                         toggle_bypass ();
743                         ret = true;
744                 } else {
745                         ret = handle_motion (x, y, state);
746                 }
747
748                 drag_target = 0;
749                 break;
750
751         case 3:
752                 break;
753
754         }
755
756         return ret;
757 }
758
759 gint
760 Panner2d::handle_motion (gint evx, gint evy, GdkModifierType state)
761 {
762         if (drag_target == 0) {
763                 return false;
764         }
765
766         if ((state & (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK)) == 0) {
767                 return false;
768         }
769
770         evx -= hoffset;
771         evy -= voffset;
772
773         if (state & GDK_BUTTON1_MASK && !(state & GDK_BUTTON2_MASK)) {
774                 CartesianVector c;
775                 bool need_move = false;
776
777                 drag_target->position.cartesian (c);
778                 cart_to_gtk (c);
779
780                 if ((evx != c.x) || (evy != c.y)) {
781                         need_move = true;
782                 }
783
784                 if (need_move) {
785                         CartesianVector cp (evx, evy, 0.0);
786                         AngularVector av;
787                         gtk_to_cart (cp);
788
789                         if (!have_elevation) {
790                                 clamp_to_circle (cp.x, cp.y);
791                                 cp.angular (av);
792                                 av.azi = fmod(270 - av.azi, 360);
793                                 if (drag_target == &position) {
794                                         double degree_fract = av.azi / 360.0;
795                                         panner_shell->panner()->set_position (degree_fract);
796                                 }
797                         } else {
798                                 /* sphere projection */
799                                 sphere_project (cp.x, cp.y, cp.z);
800
801                                 double r2d = 180.0 / M_PI;
802                                 av.azi = r2d * atan2(cp.y, cp.x);
803                                 av.ele = r2d * asin(cp.z);
804                                 av.azi = fmod(270 - av.azi, 360);
805
806                                 if (drag_target == &position) {
807                                         double azi_fract = av.azi / 360.0;
808                                         double ele_fract = av.ele / 90.0;
809                                         panner_shell->panner()->set_position (azi_fract);
810                                         panner_shell->panner()->set_elevation (ele_fract);
811                                 }
812                         }
813                 }
814         }
815
816         return true;
817 }
818
819 bool
820 Panner2d::on_scroll_event (GdkEventScroll* ev)
821 {
822         switch (ev->direction) {
823         case GDK_SCROLL_UP:
824         case GDK_SCROLL_RIGHT:
825                 panner_shell->panner()->set_position (panner_shell->pannable()->pan_azimuth_control->get_value() - 1.0/360.0);
826                 break;
827
828         case GDK_SCROLL_DOWN:
829         case GDK_SCROLL_LEFT:
830                 panner_shell->panner()->set_position (panner_shell->pannable()->pan_azimuth_control->get_value() + 1.0/360.0);
831                 break;
832         }
833         return true;
834 }
835
836 void
837 Panner2d::cart_to_gtk (CartesianVector& c) const
838 {
839         /* cartesian coordinate space:
840               center = 0.0
841               dimension = 2.0 * 2.0
842               increasing y moves up
843               so max values along each axis are -1..+1
844
845            GTK uses a coordinate space that is:
846               top left = 0.0
847               dimension = (radius*2.0) * (radius*2.0)
848               increasing y moves down
849         */
850         const double diameter = radius*2.0;
851
852         c.x = diameter * ((c.x + 1.0) / 2.0);
853         /* extra subtraction inverts the y-axis to match "increasing y moves down" */
854         c.y = diameter - (diameter * ((c.y + 1.0) / 2.0));
855 }
856
857 void
858 Panner2d::gtk_to_cart (CartesianVector& c) const
859 {
860         const double diameter = radius*2.0;
861         c.x = ((c.x / diameter) * 2.0) - 1.0;
862         c.y = (((diameter - c.y) / diameter) * 2.0) - 1.0;
863 }
864
865 void
866 Panner2d::sphere_project (double& x, double& y, double& z)
867 {
868         double r, r2;
869         r2 = x * x + y * y;
870         if (r2 < 1.0) {
871                 z = sqrt (1.0 - r2);
872         } else {
873                 r = sqrt (r2);
874                 x = x / r;
875                 y = y / r;
876                 z = 0.0;
877         }
878 }
879
880 void
881 Panner2d::clamp_to_circle (double& x, double& y)
882 {
883         double azi, ele;
884         double z = 0.0;
885         double l;
886         PBD::cartesian_to_spherical (x, y, z, azi, ele, l);
887         PBD::spherical_to_cartesian (azi, ele, 1.0, x, y, z);
888 }
889
890 void
891 Panner2d::toggle_bypass ()
892 {
893         panner_shell->set_bypassed (!panner_shell->bypassed());
894 }
895
896 Panner2dWindow::Panner2dWindow (boost::shared_ptr<PannerShell> p, int32_t h, uint32_t inputs)
897         : ArdourWindow (_("Panner (2D)"))
898         , widget (p, h)
899         , bypass_button (_("Bypass"))
900         , width_adjustment (0, -100, 100, 1, 5, 0)
901         , width_spinner (width_adjustment)
902 {
903         widget.set_name ("MixerPanZone");
904
905         set_title (_("Panner"));
906         widget.set_size_request (h, h);
907
908         bypass_button.signal_toggled().connect (sigc::mem_fun (*this, &Panner2dWindow::bypass_toggled));
909         width_spinner.signal_changed().connect (sigc::mem_fun (*this, &Panner2dWindow::width_changed));
910
911         p->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&Panner2dWindow::set_bypassed, this), gui_context());
912         /* needed for the width-spinbox in the main window */
913         p->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&Panner2dWindow::pannable_handler, this), gui_context());
914         p->pannable()->pan_width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&Panner2dWindow::set_width, this), gui_context());
915
916
917         button_box.set_spacing (6);
918         button_box.pack_start (bypass_button, false, false);
919
920         left_side.set_spacing (6);
921
922         left_side.pack_start (button_box, false, false);
923
924         Gtk::Label* l = manage (new Label (
925                                 p->panner()->describe_parameter(PanWidthAutomation),
926                                 Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER, false));
927         spinner_box.pack_start (*l, false, false);
928         spinner_box.pack_start (width_spinner, false, false);
929         left_side.pack_start (spinner_box, false, false);
930
931         l->show ();
932         bypass_button.show ();
933         button_box.show ();
934         width_spinner.show ();
935         spinner_box.show ();
936         left_side.show ();
937
938         hpacker.set_spacing (6);
939         hpacker.set_border_width (12);
940         hpacker.pack_start (widget, false, false);
941         hpacker.pack_start (left_side, false, false);
942         hpacker.show ();
943
944         add (hpacker);
945         reset (inputs);
946         set_width();
947         set_bypassed();
948         widget.show ();
949 }
950
951 void
952 Panner2dWindow::reset (uint32_t n_inputs)
953 {
954         widget.reset (n_inputs);
955 }
956
957 void
958 Panner2dWindow::bypass_toggled ()
959 {
960         bool view = bypass_button.get_active ();
961         bool model = widget.get_panner_shell()->bypassed ();
962
963         if (model != view) {
964                 widget.get_panner_shell()->set_bypassed (view);
965         }
966 }
967 void
968 Panner2dWindow::width_changed ()
969 {
970         float model = widget.get_panner_shell()->pannable()->pan_width_control->get_value();
971         float view  = width_spinner.get_value() / 100.0;
972         if (model != view) {
973                 widget.get_panner_shell()->panner()->set_width (view);
974         }
975 }
976
977 void
978 Panner2dWindow::pannable_handler ()
979 {
980         panvalue_connections.drop_connections();
981         widget.get_panner_shell()->pannable()->pan_width_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&Panner2dWindow::set_width, this), gui_context());
982         set_width();
983 }
984
985 void
986 Panner2dWindow::set_bypassed ()
987 {
988         bool view = bypass_button.get_active ();
989         bool model = widget.get_panner_shell()->bypassed ();
990         if (model != view) {
991                 bypass_button.set_active(model);
992         }
993
994         set<Evoral::Parameter> params = widget.get_panner_shell()->panner()->what_can_be_automated();
995         set<Evoral::Parameter>::iterator p = params.find(PanWidthAutomation);
996         if (p == params.end()) {
997                 spinner_box.set_sensitive(false);
998         } else {
999                 spinner_box.set_sensitive(true);
1000         }
1001 }
1002
1003 void
1004 Panner2dWindow::set_width ()
1005 {
1006         // rounding of spinbox is different from slider -- TODO use slider
1007         float model = (widget.get_panner_shell()->pannable()->pan_width_control->get_value() * 100.0);
1008         float view  = (width_spinner.get_value());
1009         if (model != view) {
1010                 width_spinner.set_value (model);
1011         }
1012 }
1013
1014 bool
1015 Panner2dWindow::on_key_press_event (GdkEventKey* event)
1016 {
1017         return relay_key_press (event, &PublicEditor::instance());
1018 }
1019
1020 bool
1021 Panner2dWindow::on_key_release_event (GdkEventKey*)
1022 {
1023         return true;
1024 }