new 2d panner GUI, implemented originally in a branch of 2.0-ongoing at LAC2009;...
[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 <pbd/error.h>
28 #include <ardour/panner.h>
29 #include <gtkmm2ext/gtk_ui.h>
30
31 #include "panner2d.h"
32 #include "keyboard.h"
33 #include "gui_thread.h"
34
35 #include "i18n.h"
36
37 using namespace std;
38 using namespace Gtk;
39 using namespace sigc;
40 using namespace ARDOUR;
41 using namespace PBD;
42
43 Panner2d::Target::Target (float xa, float ya, const char *txt)
44         : x (xa, 0.0, 1.0, 0.01, 0.1)
45         , y (ya, 0.0, 1.0, 0.01, 0.1)
46         , azimuth (M_PI/2.0, 0.0, 2.0 * M_PI, 0.1, 0.5)
47         , text (txt ? strdup (txt) : 0)
48 {
49         azimuth.set_value ((random() / (double) INT_MAX) * (2.0 * M_PI));
50 }
51
52 Panner2d::Target::~Target ()
53
54         if (text) {
55                 free (text);
56         }
57 }
58
59 void
60 Panner2d::Target::set_text (const char* txt)
61 {
62         if (text) {
63                 free (text);
64         }
65         text = strdup (txt);
66 }
67
68 Panner2d::Panner2d (Panner& p, int32_t h)
69         : panner (p), width (0), height (h)
70 {
71         allow_x = false;
72         allow_y = false;
73         allow_target = false;
74
75         panner.StateChanged.connect (mem_fun(*this, &Panner2d::handle_state_change));
76         panner.Changed.connect (mem_fun(*this, &Panner2d::handle_position_change));
77         
78         drag_target = 0;
79         set_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|Gdk::POINTER_MOTION_MASK);
80 }
81
82 Panner2d::~Panner2d()
83 {
84         for (Targets::iterator i = targets.begin(); i != targets.end(); ++i) {
85                 delete i->second;
86         }
87 }
88
89 void
90 Panner2d::reset (uint32_t n_inputs)
91 {
92         Targets::size_type existing_pucks = pucks.size();
93
94         /* pucks */
95
96         while (pucks.size() < n_inputs) {
97                 add_puck ("", 0.0, 0.0);
98         }
99         
100         while (pucks.size() > n_inputs) {
101                 pucks.erase (pucks.begin());
102         }
103
104         for (Targets::iterator x = pucks.begin(); x != pucks.end(); ++x) {
105                 (*x).second->visible = false;
106         }
107
108         switch (n_inputs) {
109         case 0:
110                 break;
111                 
112         case 1:
113                 pucks[0]->set_text ("");
114                 pucks[0]->x.set_value (0.0);
115                 pucks[0]->y.set_value (0.5);
116                 pucks[0]->visible = true;
117                 break;
118                 
119         case 2:
120                 pucks[0]->set_text ("R");
121                 if (existing_pucks < 0) {
122                         pucks[0]->x.set_value (0.5f);
123                         pucks[1]->y.set_value (0.25f);
124                 }
125                 pucks[0]->visible = true;
126                 pucks[1]->set_text ("L");
127                 if (existing_pucks < 2) {
128                         pucks[1]->x.set_value (0.25f);
129                         pucks[1]->y.set_value (0.5f);
130                 }
131                 pucks[1]->visible = true;
132                 break;
133                 
134         default:
135                 for (uint32_t i = 0; i < n_inputs; ++i) {
136                         char buf[64];
137                         snprintf (buf, sizeof (buf), "%" PRIu32, i);
138                         pucks[i]->set_text (buf);
139                         
140                         if (existing_pucks < i) {
141                                 float x, y;
142                                 panner.streampanner (i).get_position (x, y);
143                                 pucks[i]->x.set_value (x);
144                                 pucks[i]->y.set_value (y);
145                         }
146
147                         pucks[i]->visible = true;
148                 }
149                 break;
150         }
151         
152         /* add all outputs */
153         
154         while (targets.size() < panner.nouts()) {
155                 add_target (0.0, 0.0);
156         }
157         
158         while (targets.size() > panner.nouts()) {
159                 targets.erase (targets.begin());
160         }
161
162         for (Targets::iterator x = targets.begin(); x != targets.end(); ++x) {
163                 (*x).second->visible = false;
164         }
165
166         for (uint32_t n = 0; n < panner.nouts(); ++n) {
167                 char buf[16];
168
169                 snprintf (buf, sizeof (buf), "%d", n+1);
170                 targets[n]->set_text (buf);
171                 targets[n]->x.set_value (panner.output(n).x);
172                 targets[n]->y.set_value (panner.output(n).y);
173                 targets[n]->visible = true;
174         }
175         
176         allow_x_motion (true);
177         allow_y_motion (true);
178         allow_target_motion (true);
179
180         queue_draw ();
181 }
182
183 Gtk::Adjustment&
184 Panner2d::azimuth (uint32_t which)
185 {
186         assert (which < pucks.size());
187         return pucks[which]->azimuth;
188 }
189
190 void
191 Panner2d::on_size_allocate (Gtk::Allocation& alloc)
192 {
193         width = alloc.get_width();
194         height = alloc.get_height();
195
196         if (height > 100) {
197                 width -= 20;
198                 height -= 20;
199         }
200
201         DrawingArea::on_size_allocate (alloc);
202 }
203
204 int
205 Panner2d::add_puck (const char* text, float x, float y)
206 {
207         Target* puck = new Target (x, y, text);
208
209         pair<int,Target *> newpair;
210         newpair.first = pucks.size();
211         newpair.second = puck;
212
213         pucks.insert (newpair);
214         puck->visible = true;
215         
216         return 0;
217 }
218
219 int
220 Panner2d::add_target (float x, float y)
221 {
222         Target *target = new Target (x, y, "");
223
224         pair<int,Target *> newpair;
225         newpair.first = targets.size();
226         newpair.second = target;
227
228         targets.insert (newpair);
229         target->visible = true;
230         queue_draw ();
231
232         return newpair.first;
233 }
234
235 void
236 Panner2d::drop_targets ()
237 {
238         for (Targets::iterator i = targets.begin(); i != targets.end(); ) {
239
240                 Targets::iterator tmp;
241
242                 tmp = i;
243                 ++tmp;
244
245                 delete i->second;
246                 targets.erase (i);
247
248                 i = tmp;
249         }
250
251         queue_draw ();
252 }
253
254 void
255 Panner2d::remove_target (int which)
256 {
257         Targets::iterator i = targets.find (which);
258
259         if (i != targets.end()) {
260                 delete i->second;
261                 targets.erase (i);
262                 queue_draw ();
263         }
264 }               
265
266 void
267 Panner2d::handle_state_change ()
268 {
269         ENSURE_GUI_THREAD(mem_fun(*this, &Panner2d::handle_state_change));
270
271         queue_draw ();
272 }
273
274 void
275 Panner2d::handle_position_change ()
276 {
277         uint32_t n;
278         ENSURE_GUI_THREAD(mem_fun(*this, &Panner2d::handle_position_change));
279
280         for (n = 0; n < pucks.size(); ++n) {
281                 float x, y;
282                 panner.streampanner(n).get_position (x, y);
283                 pucks[n]->x.set_value (x);
284                 pucks[n]->y.set_value (y);
285         }
286
287         for (n = 0; n < targets.size(); ++n) {
288                 targets[n]->x.set_value (panner.output(n).x);
289                 targets[n]->y.set_value (panner.output(n).y);
290         }
291
292         queue_draw ();
293 }
294
295 void
296 Panner2d::move_target (int which, float x, float y)
297 {
298         Targets::iterator i = targets.find (which);
299         Target *target;
300
301         if (!allow_target) {
302                 return;
303         }
304
305         if (i != targets.end()) {
306                 target = i->second;
307                 target->x.set_value (x);
308                 target->y.set_value (y);
309                 
310                 queue_draw ();
311         }
312 }               
313
314 void
315 Panner2d::move_puck (int which, float x, float y)
316 {
317         Targets::iterator i = pucks.find (which);
318         Target *target;
319
320         if (i != pucks.end()) {
321                 target = i->second;
322                 target->x.set_value (x);
323                 target->y.set_value (y);
324                 
325                 queue_draw ();
326         }
327 }               
328
329 void
330 Panner2d::show_puck (int which)
331 {
332         Targets::iterator i = pucks.find (which);
333
334         if (i != pucks.end()) {
335                 Target* puck = i->second;
336                 if (!puck->visible) {
337                         puck->visible = true;
338                         queue_draw ();
339                 }
340         }
341 }
342
343 void
344 Panner2d::hide_puck (int which)
345 {
346         Targets::iterator i = pucks.find (which);
347
348         if (i != pucks.end()) {
349                 Target* puck = i->second;
350                 if (!puck->visible) {
351                         puck->visible = false;
352                         queue_draw ();
353                 }
354         }
355 }
356
357 void
358 Panner2d::show_target (int which)
359 {
360         Targets::iterator i = targets.find (which);
361         if (i != targets.end()) {
362                 if (!i->second->visible) {
363                         i->second->visible = true;
364                         queue_draw ();
365                 }
366         }
367 }
368
369 void
370 Panner2d::hide_target (int which)
371 {
372         Targets::iterator i = targets.find (which);
373         if (i != targets.end()) {
374                 if (i->second->visible) {
375                         i->second->visible = false;
376                         queue_draw ();
377                 }
378         }
379 }
380
381 Panner2d::Target *
382 Panner2d::find_closest_object (gdouble x, gdouble y, int& which, bool& is_puck) const
383 {
384         gdouble efx, efy;
385         gdouble cx, cy;
386         Target *closest = 0;
387         Target *candidate;
388         float distance;
389         float best_distance = FLT_MAX;
390         int pwhich;
391
392         efx = x/width;
393         efy = y/height;
394         which = 0;
395         pwhich = 0;
396         is_puck = false;
397
398         for (Targets::const_iterator i = targets.begin(); i != targets.end(); ++i, ++which) {
399                 candidate = i->second;
400
401                 cx = candidate->x.get_value();
402                 cy = candidate->y.get_value();
403
404                 distance = sqrt ((cx - efx) * (cx - efx) +
405                                  (cy - efy) * (cy - efy));
406
407                 if (distance < best_distance) {
408                         closest = candidate;
409                         best_distance = distance;
410                 }
411         }
412
413         for (Targets::const_iterator i = pucks.begin(); i != pucks.end(); ++i, ++pwhich) {
414                 candidate = i->second;
415
416                 cx = candidate->x.get_value();
417                 cy = candidate->y.get_value();
418
419                 distance = sqrt ((cx - efx) * (cx - efx) +
420                                  (cy - efy) * (cy - efy));
421
422                 if (distance < best_distance) {
423                         closest = candidate;
424                         best_distance = distance;
425                         is_puck = true;
426                         which = pwhich;
427                 }
428         }
429         
430         return closest;
431 }               
432
433 bool
434 Panner2d::on_motion_notify_event (GdkEventMotion *ev)
435 {
436         gint x, y;
437         GdkModifierType state;
438
439         if (ev->is_hint) {
440                 gdk_window_get_pointer (ev->window, &x, &y, &state);
441         } else {
442                 x = (int) floor (ev->x);
443                 y = (int) floor (ev->y);
444                 state = (GdkModifierType) ev->state;
445         }
446
447         return handle_motion (x, y, state);
448 }
449 bool
450 Panner2d::on_expose_event (GdkEventExpose *event)
451 {
452         gint x, y;
453         float fx, fy;
454         cairo_t* cr;
455
456         cr = gdk_cairo_create (get_window()->gobj());
457
458         cairo_set_line_width (cr, 1.0);
459
460         cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height);
461         if (!panner.bypassed()) {
462                 cairo_set_source_rgba (cr, 0.1, 0.1, 0.1, 1.0);
463         } else {
464                 cairo_set_source_rgba (cr, 0.1, 0.1, 0.1, 0.2);
465         }
466         cairo_fill_preserve (cr);
467         cairo_clip (cr);
468
469         if (height > 100) {
470                 cairo_translate (cr, 10.0, 10.0);
471         }
472
473         cairo_set_source_rgb (cr, 0.0, 0.1, 0.7);
474         cairo_move_to (cr, 0.5, height/2.0+0.5);
475         cairo_line_to (cr, height+0.5, height/2+0.5);
476         cairo_stroke (cr);
477
478         cairo_move_to (cr, height/2+0.5, 0.5);
479         cairo_line_to (cr, height/2+0.5, height+0.5);
480         cairo_stroke (cr);
481
482         cairo_arc (cr, height/2, height/2, height/2, 0, 2.0 * M_PI);
483         cairo_stroke (cr);
484
485         if (!panner.bypassed()) {
486                 float arc_radius;
487
488                 cairo_select_font_face (cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
489
490                 if (height < 100) {
491                         cairo_set_font_size (cr, 10);
492                         arc_radius = 2.0;
493                 } else {
494                         cairo_set_font_size (cr, 16);
495                         arc_radius = 4.0;
496                 }
497
498                 for (Targets::iterator i = pucks.begin(); i != pucks.end(); ++i) {
499
500                         Target* puck = i->second;
501
502                         if (puck->visible) {
503                                 /* redraw puck */
504                                 
505                                 fx = min (puck->x.get_value(), 1.0);
506                                 fx = max (fx, -1.0f);
507                                 x = (gint) floor (width * fx - 4);
508                                 
509                                 fy = min (puck->y.get_value(), 1.0);
510                                 fy = max (fy, -1.0f);
511                                 y = (gint) floor (height * fy - 4);
512                                 
513                                 cairo_arc (cr, x, y, arc_radius, 0, 2.0 * M_PI);
514                                 cairo_set_source_rgb (cr, 0.8, 0.2, 0.1);
515                                 cairo_close_path (cr);
516                                 cairo_fill (cr);
517
518                                 /* arrow */
519                                 
520                                 if (height > 100.0f) {
521
522                                         float endx, endy;
523                                         endx = x;
524                                         endy = y;
525                                         
526                                         cairo_save (cr);
527                                         cairo_translate (cr, x, y);
528                                         cairo_rotate (cr, puck->azimuth.get_value());
529
530                                         /* horizontal left-to-right line (rotation will rotate it, duh) */
531
532                                         endx = 30.0;
533                                         endy = 0.0;
534
535                                         /* stem */
536                                         cairo_set_line_width (cr, 4.0);
537                                         cairo_move_to (cr, 0.0, 0.0);
538                                         cairo_line_to (cr, endx, endy);
539                                         cairo_stroke (cr);
540                                         
541                                         /* arrow head */
542                                         
543                                         cairo_move_to (cr, endx - 10.0, endy + 10.0);
544                                         cairo_line_to (cr, endx, endy);
545                                         cairo_line_to (cr, endx - 10.0, endy - 10.0);
546                                         cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
547                                         cairo_stroke (cr);
548
549                                         cairo_restore (cr);
550                                 }
551
552                                 cairo_move_to (cr, x + 6, y + 6);
553                                 cairo_show_text (cr, puck->text);
554                         }
555                 }
556
557                 /* redraw any visible targets */
558                 
559                 int n = 0;
560
561                 for (Targets::iterator i = targets.begin(); i != targets.end(); ++i) {
562                         Target *target = i->second;
563                         char buf[256];
564                         ++n;
565
566                         if (target->visible) {
567                                 
568                                 fx = min (target->x.get_value(), 1.0);
569                                 fx = max (fx, -1.0f);
570                                 x = (gint) floor (width  * fx);
571                         
572                                 fy = min (target->y.get_value(), 1.0);
573                                 fy = max (fy, -1.0f);
574                                 y = (gint) floor (height * fy);
575
576                                 snprintf (buf, sizeof (buf), "%d", n);
577
578                                 cairo_set_source_rgb (cr, 0.0, 0.8, 0.1);
579                                 cairo_rectangle (cr, x-2, y-2, 4, 4);
580                                 cairo_fill (cr);
581                                 cairo_move_to (cr, x+6, y+6);
582                                 cairo_show_text (cr, buf);
583                         }
584                 }
585         }
586
587         cairo_destroy (cr);
588
589         return TRUE;
590 }
591
592 bool
593 Panner2d::on_button_press_event (GdkEventButton *ev)
594 {
595         GdkModifierType state;
596
597         if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
598                 return false;
599         }
600
601         switch (ev->button) {
602         case 1:
603         case 2:
604                 drag_target = find_closest_object (ev->x, ev->y, drag_index, drag_is_puck);
605                 drag_x = (int) floor (ev->x);
606                 drag_y = (int) floor (ev->y);
607                 state = (GdkModifierType) ev->state;
608
609                 return handle_motion (drag_x, drag_y, state);
610                 break;
611
612         default:
613                 break;
614         }
615         
616         return FALSE;
617 }
618
619 bool
620 Panner2d::on_button_release_event (GdkEventButton *ev)
621 {
622         gint x, y;
623         GdkModifierType state;
624         bool ret = false;
625
626         switch (ev->button) {
627         case 1:
628                 x = (int) floor (ev->x);
629                 y = (int) floor (ev->y);
630                 state = (GdkModifierType) ev->state;
631
632                 if (drag_is_puck && (Keyboard::modifier_state_contains (state, Keyboard::TertiaryModifier))) {
633
634
635                         for (Targets::iterator i = pucks.begin(); i != pucks.end(); ++i) {
636                                 Target* puck = i->second;
637
638                                 /* XXX DO SOMETHING TO SET PUCK BACK TO "normal" */
639                         }
640
641                         queue_draw ();
642                         PuckMoved (-1);
643                         ret = true;
644
645                 } else {
646                         ret = handle_motion (x, y, state);
647                 }
648                 
649                 drag_target = 0;
650                 break;
651
652         case 2:
653                 x = (int) floor (ev->x);
654                 y = (int) floor (ev->y);
655                 state = (GdkModifierType) ev->state;
656
657                 if (drag_is_puck && (Keyboard::modifier_state_contains (state, Keyboard::TertiaryModifier))) {
658                         toggle_bypass ();
659                         ret = true;
660                 } else {
661                         ret = handle_motion (x, y, state);
662                 }
663                 
664                 drag_target = 0;
665                 break;
666
667         case 3:
668                 break;
669
670         }
671
672         return ret;
673 }
674
675 gint
676 Panner2d::handle_motion (gint evx, gint evy, GdkModifierType state)
677 {
678         if (drag_target == 0) {
679                 return false;
680         }
681
682         if ((state & (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK)) == 0) {
683                 return false;
684         }
685
686         int x, y;
687         bool need_move = false;
688
689         if (!drag_is_puck && !allow_target) {
690                 cerr << "dip = " << drag_is_puck << " at = " << allow_target << endl;
691                 return true;
692         }
693
694         if (state & GDK_BUTTON1_MASK && !(state & GDK_BUTTON2_MASK)) {
695
696                 if (allow_x || !drag_is_puck) {
697                         float new_x;
698                         x = min (evx, width - 1);
699                         x = max (x, 0);
700                         new_x = (float) x / (width - 1);
701                         if (new_x != drag_target->x.get_value()) {
702                                 drag_target->x.set_value (new_x);
703                                 need_move = true;
704                         }
705                 }
706                 
707                 if (allow_y || drag_is_puck) {
708                         float new_y;
709                         y = min (evy, height - 1);
710                         y = max (y, 0);
711                         new_y = (float) y / (height - 1);
712                         if (new_y != drag_target->y.get_value()) {
713                                 drag_target->y.set_value (new_y);
714                                 need_move = true;
715                         }
716                 }
717                 
718                 if (need_move) {
719                         
720                         if (drag_is_puck) {
721                                 
722                                 panner.streampanner(drag_index).set_position (drag_target->x.get_value(), drag_target->y.get_value(), false);
723                                 
724                         } else {
725                                 
726                                 TargetMoved (drag_index);
727                         }
728
729                         queue_draw ();
730                 }
731                 
732
733         } else if ((state & GDK_BUTTON2_MASK) && !(state & GDK_BUTTON1_MASK)) {
734
735                 if (!drag_is_puck) {
736                         return false;
737                 }
738
739                 int xdelta = drag_x - evx;
740                 int ydelta = drag_x - evy;
741                 
742                 drag_target->azimuth.set_value (drag_target->azimuth.get_value() + (2 * M_PI) * ((float)ydelta)/height * ((float) -xdelta)/height);
743                 queue_draw ();
744         }
745
746         return true;
747 }
748
749 void
750 Panner2d::toggle_bypass ()
751 {
752         panner.set_bypassed (!panner.bypassed());
753 }
754
755 void
756 Panner2d::allow_x_motion (bool yn)
757 {
758         allow_x = yn;
759 }
760
761 void
762 Panner2d::allow_target_motion (bool yn)
763 {
764         allow_target = yn;
765 }
766
767 void
768 Panner2d::allow_y_motion (bool yn)
769 {
770         allow_y = yn;
771 }
772
773 Panner2dWindow::Panner2dWindow (Panner&p, int32_t h, uint32_t inputs)
774         : widget (p, h)
775         , reset_button (_("Reset"))
776         , bypass_button (_("Bypass"))
777         , mute_button (_("Mute"))
778 {
779         widget.set_name ("MixerPanZone");
780
781         set_title (_("Panner"));
782         widget.set_size_request (h, h);
783         
784         button_box.set_spacing (6);
785         button_box.pack_start (reset_button, false, false);
786         button_box.pack_start (bypass_button, false, false);
787         button_box.pack_start (mute_button, false, false);
788
789         spinner_box.set_spacing (6);
790         left_side.set_spacing (6);
791
792         left_side.pack_start (button_box, false, false);
793         left_side.pack_start (spinner_box, false, false);
794
795         reset_button.show ();
796         bypass_button.show ();
797         mute_button.show ();
798         button_box.show ();
799         spinner_box.show ();
800         left_side.show ();
801
802         hpacker.set_spacing (6);
803         hpacker.set_border_width (12);
804         hpacker.pack_start (widget, false, false);
805         hpacker.pack_start (left_side, false, false);
806         hpacker.show ();
807         
808         add (hpacker);
809         reset (inputs);
810         widget.show ();
811 }
812
813 void
814 Panner2dWindow::reset (uint32_t n_inputs)
815 {
816         widget.reset (n_inputs);
817
818         while (spinners.size() < n_inputs) {
819                 spinners.push_back (new Gtk::SpinButton (widget.azimuth (spinners.size())));
820                 spinner_box.pack_start (*spinners.back(), false, false);
821                 spinners.back()->set_digits (4);
822                 spinners.back()->show ();
823         }
824
825         while (spinners.size() > n_inputs) {
826                 spinner_box.remove (*spinners.back());
827                 delete spinners.back();
828                 spinners.erase (--spinners.end());
829         }
830 }