preparations for a working speaker editing dialog
[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 "pbd/cartesian.h"
29 #include "ardour/panner.h"
30 #include <gtkmm2ext/gtk_ui.h>
31
32 #include "panner2d.h"
33 #include "keyboard.h"
34 #include "gui_thread.h"
35
36 #include "i18n.h"
37
38 using namespace std;
39 using namespace Gtk;
40 using namespace ARDOUR;
41 using namespace PBD;
42 using Gtkmm2ext::Keyboard;
43
44 Panner2d::Target::Target (const AngularVector& a, const char *txt)
45         : position (a)
46         , text (txt)
47         , _selected (false)
48 {
49 }
50
51 Panner2d::Target::~Target ()
52 {
53 }
54
55 void
56 Panner2d::Target::set_text (const char* txt)
57 {
58         text = txt;
59 }
60
61 Panner2d::Panner2d (boost::shared_ptr<Panner> p, int32_t h)
62         : panner (p), width (0), height (h)
63 {
64         panner->StateChanged.connect (state_connection, invalidator (*this), boost::bind (&Panner2d::handle_state_change, this), gui_context());
65         panner->Changed.connect (change_connection, invalidator (*this), boost::bind (&Panner2d::handle_position_change, this), gui_context());
66
67         drag_target = 0;
68         set_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|Gdk::POINTER_MOTION_MASK);
69 }
70
71 Panner2d::~Panner2d()
72 {
73         for (Targets::iterator i = targets.begin(); i != targets.end(); ++i) {
74                 delete *i;
75         }
76 }
77
78 void
79 Panner2d::reset (uint32_t n_inputs)
80 {
81         uint32_t nouts = panner->out().n_audio();
82
83         /* pucks */
84
85         while (pucks.size() < n_inputs) {
86                 add_puck ("", AngularVector());
87         }
88
89         if (pucks.size() > n_inputs) {
90                 for (uint32_t i = pucks.size(); i < n_inputs; ++i) {
91                         delete pucks[i];
92                 }
93
94                 pucks.resize (n_inputs);
95         }
96                                                 
97         for (Targets::iterator x = pucks.begin(); x != pucks.end(); ++x) {
98                 (*x)->visible = false;
99         }
100
101         switch (n_inputs) {
102         case 0:
103                 break;
104
105         case 1:
106                 pucks[0]->set_text ("");
107                 break;
108
109         case 2:
110                 pucks[0]->set_text ("R");
111                 pucks[1]->set_text ("L");
112                 break;
113
114         default:
115                 for (uint32_t i = 0; i < n_inputs; ++i) {
116                         char buf[64];
117                         snprintf (buf, sizeof (buf), "%" PRIu32, i + 1);
118                         pucks[i]->set_text (buf);
119                 }
120                 break;
121         }
122
123 #ifdef PANNER_HACKS
124         for (uint32_t i = existing_pucks; i < n_inputs; ++i) {
125                 pucks[i]->position = panner->streampanner (i).get_position ();
126                 pucks[i]->visible = true;
127         }
128 #endif
129
130         /* add all outputs */
131
132         while (targets.size() < nouts) {
133                 add_target (AngularVector());
134         }
135
136         if (targets.size() > nouts) {
137                 for (uint32_t i = nouts; i < targets.size(); ++i) {
138                         delete targets[i];
139                 }
140
141                 targets.resize (nouts);
142         }
143
144         for (Targets::iterator x = targets.begin(); x != targets.end(); ++x) {
145                 (*x)->visible = false;
146         }
147
148         for (uint32_t n = 0; n < nouts; ++n) {
149                 char buf[16];
150
151                 snprintf (buf, sizeof (buf), "%d", n+1);
152                 targets[n]->set_text (buf);
153 #ifdef PANNER_HACKS
154                 targets[n]->position = panner->output(n).position;
155                 targets[n]->visible = true;
156 #endif
157         }
158
159         queue_draw ();
160 }
161
162 void
163 Panner2d::on_size_allocate (Gtk::Allocation& alloc)
164 {
165         width = alloc.get_width();
166         height = alloc.get_height();
167
168         if (height > 100) {
169                 width -= 20;
170                 height -= 20;
171         }
172
173         DrawingArea::on_size_allocate (alloc);
174 }
175
176 int
177 Panner2d::add_puck (const char* text, const AngularVector& a)
178 {
179         Target* puck = new Target (a, text);
180         pucks.push_back (puck);
181         puck->visible = true;
182
183         return 0;
184 }
185
186 int
187 Panner2d::add_target (const AngularVector& a)
188 {
189         Target* target = new Target (a, "");
190         targets.push_back (target);
191         target->visible = true;
192         queue_draw ();
193
194         return targets.size() - 1;
195 }
196
197 void
198 Panner2d::handle_state_change ()
199 {
200         ENSURE_GUI_THREAD (*this, &Panner2d::handle_state_change)
201
202         queue_draw ();
203 }
204
205 void
206 Panner2d::handle_position_change ()
207 {
208 #ifdef PANNER_HACKS 
209         uint32_t n;
210         ENSURE_GUI_THREAD (*this, &Panner2d::handle_position_change)
211
212         for (n = 0; n < pucks.size(); ++n) {
213                 pucks[n]->position = panner->streampanner(n).get_position ();
214         }
215
216         for (n = 0; n < targets.size(); ++n) {
217                 targets[n]->position = panner->output(n).position;
218         }
219
220         queue_draw ();
221 #endif
222 }
223
224 void
225 Panner2d::move_puck (int which, const AngularVector& a)
226 {
227         if (which >= int (targets.size())) {
228                 return;
229         }
230         
231         targets[which]->position = a;
232         queue_draw ();
233 }
234
235 Panner2d::Target *
236 Panner2d::find_closest_object (gdouble x, gdouble y, int& which) const
237 {
238         Target *closest = 0;
239         Target *candidate;
240         float distance;
241         float best_distance = FLT_MAX;
242         int pwhich;
243
244         which = 0;
245         pwhich = 0;
246
247         cerr << "@ " << x << ", " << y << endl;
248
249         for (Targets::const_iterator i = pucks.begin(); i != pucks.end(); ++i, ++pwhich) {
250                 candidate = *i;
251
252                 CartesianVector c;
253
254                 candidate->position.cartesian (c);
255                 cart_to_gtk (c);
256
257                 distance = sqrt ((c.x - x) * (c.x - x) +
258                                  (c.y - y) * (c.y - y));
259
260                 cerr << "\tConsider candiate " << candidate->text << " @ " << c.x << ", " << c.y << ", " << c.z <<  " distance = " << distance << endl;
261
262                 if (distance < best_distance) {
263                         closest = candidate;
264                         best_distance = distance;
265                         which = pwhich;
266                 }
267         }
268
269
270         if (best_distance > 20) { // arbitrary 
271                 return 0;
272         }
273
274         cerr << "the winner is " << closest->text << endl;
275
276         return closest;
277 }
278
279 bool
280 Panner2d::on_motion_notify_event (GdkEventMotion *ev)
281 {
282         gint x, y;
283         GdkModifierType state;
284
285         if (ev->is_hint) {
286                 gdk_window_get_pointer (ev->window, &x, &y, &state);
287         } else {
288                 x = (int) floor (ev->x);
289                 y = (int) floor (ev->y);
290                 state = (GdkModifierType) ev->state;
291         }
292
293         return handle_motion (x, y, state);
294 }
295 bool
296 Panner2d::on_expose_event (GdkEventExpose *event)
297 {
298         gint x, y;
299         cairo_t* cr;
300
301         cr = gdk_cairo_create (get_window()->gobj());
302
303         cairo_set_line_width (cr, 1.0);
304
305         cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height);
306         if (!panner->bypassed()) {
307                 cairo_set_source_rgba (cr, 0.1, 0.1, 0.1, 1.0);
308         } else {
309                 cairo_set_source_rgba (cr, 0.1, 0.1, 0.1, 0.2);
310         }
311         cairo_fill_preserve (cr);
312         cairo_clip (cr);
313
314         if (height > 100) {
315                 cairo_translate (cr, 10.0, 10.0);
316         }
317
318         /* horizontal line of "crosshairs" */
319
320         cairo_set_source_rgb (cr, 0.0, 0.1, 0.7);
321         cairo_move_to (cr, 0.5, height/2.0+0.5);
322         cairo_line_to (cr, width+0.5, height/2+0.5);
323         cairo_stroke (cr);
324
325         /* vertical line of "crosshairs" */
326         
327         cairo_move_to (cr, width/2+0.5, 0.5);
328         cairo_line_to (cr, width/2+0.5, height+0.5);
329         cairo_stroke (cr);
330
331         /* the circle on which signals live */
332
333         cairo_arc (cr, width/2, height/2, height/2, 0, 2.0 * M_PI);
334         cairo_stroke (cr);
335
336         if (!panner->bypassed()) {
337                 float arc_radius;
338
339                 cairo_select_font_face (cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
340
341                 if (height < 100) {
342                         cairo_set_font_size (cr, 10);
343                         arc_radius = 2.0;
344                 } else {
345                         cairo_set_font_size (cr, 16);
346                         arc_radius = 4.0;
347                 }
348
349                 for (Targets::iterator i = pucks.begin(); i != pucks.end(); ++i) {
350
351                         Target* puck = *i;
352
353                         if (puck->visible) {
354                                 /* redraw puck */
355
356                                 CartesianVector c;
357                                 
358                                 puck->position.cartesian (c);
359                                 cart_to_gtk (c);
360                                 
361                                 x = (gint) floor (c.x);
362                                 y = (gint) floor (c.y);
363
364                                 /* XXX need to shift circles so that they are centered on the circle */
365                                         
366                                 cairo_arc (cr, x, y, arc_radius, 0, 2.0 * M_PI);
367                                 cairo_set_source_rgb (cr, 0.8, 0.2, 0.1);
368                                 cairo_close_path (cr);
369                                 cairo_fill (cr);
370
371                                 cairo_move_to (cr, x + 6, y + 6);
372
373                                 char buf[256];
374                                 snprintf (buf, sizeof (buf), "%s:%d", puck->text.c_str(), (int) lrint (puck->position.azi));
375                                 cairo_show_text (cr, buf);
376                         }
377                 }
378
379                 /* redraw any visible targets */
380
381                 int n = 0;
382
383                 for (Targets::iterator i = targets.begin(); i != targets.end(); ++i) {
384                         Target *target = *i;
385                         char buf[256];
386                         ++n;
387
388                         if (target->visible) {
389
390                                 CartesianVector c;
391                                 
392                                 target->position.cartesian (c);
393                                 cart_to_gtk (c);
394
395                                 x = (int) floor (c.x);
396                                 y = (int) floor (c.y);
397                                 
398                                 snprintf (buf, sizeof (buf), "%d", n);
399
400                                 cairo_set_source_rgb (cr, 0.0, 0.8, 0.1);
401                                 cairo_rectangle (cr, x-2, y-2, 4, 4);
402                                 cairo_fill (cr);
403                                 cairo_move_to (cr, x+6, y+6);
404                                 cairo_show_text (cr, buf);
405
406                         }
407                 }
408         }
409
410         cairo_destroy (cr);
411
412         return true;
413 }
414
415 bool
416 Panner2d::on_button_press_event (GdkEventButton *ev)
417 {
418         GdkModifierType state;
419
420         if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
421                 return false;
422         }
423
424         switch (ev->button) {
425         case 1:
426         case 2:
427                 if ((drag_target = find_closest_object (ev->x, ev->y, drag_index)) != 0) {
428                         drag_target->set_selected (true);
429                 }
430
431                 drag_x = (int) floor (ev->x);
432                 drag_y = (int) floor (ev->y);
433                 state = (GdkModifierType) ev->state;
434
435                 return handle_motion (drag_x, drag_y, state);
436                 break;
437
438         default:
439                 break;
440         }
441
442         return false;
443 }
444
445 bool
446 Panner2d::on_button_release_event (GdkEventButton *ev)
447 {
448         gint x, y;
449         GdkModifierType state;
450         bool ret = false;
451
452         switch (ev->button) {
453         case 1:
454                 x = (int) floor (ev->x);
455                 y = (int) floor (ev->y);
456                 state = (GdkModifierType) ev->state;
457
458                 if (Keyboard::modifier_state_contains (state, Keyboard::TertiaryModifier)) {
459                         
460                         for (Targets::iterator i = pucks.begin(); i != pucks.end(); ++i) {
461                                 //Target* puck = i->second;
462                                 /* XXX DO SOMETHING TO SET PUCK BACK TO "normal" */
463                         }
464
465                         queue_draw ();
466                         PuckMoved (-1);
467                         ret = true;
468
469                 } else {
470                         ret = handle_motion (x, y, state);
471                 }
472
473                 drag_target = 0;
474                 break;
475
476         case 2:
477                 x = (int) floor (ev->x);
478                 y = (int) floor (ev->y);
479                 state = (GdkModifierType) ev->state;
480
481                 if (Keyboard::modifier_state_contains (state, Keyboard::TertiaryModifier)) {
482                         toggle_bypass ();
483                         ret = true;
484                 } else {
485                         ret = handle_motion (x, y, state);
486                 }
487
488                 drag_target = 0;
489                 break;
490
491         case 3:
492                 break;
493
494         }
495
496         return ret;
497 }
498
499 gint
500 Panner2d::handle_motion (gint evx, gint evy, GdkModifierType state)
501 {
502         if (drag_target == 0) {
503                 return false;
504         }
505
506         if ((state & (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK)) == 0) {
507                 return false;
508         }
509
510
511         if (state & GDK_BUTTON1_MASK && !(state & GDK_BUTTON2_MASK)) {
512                 CartesianVector c;
513                 bool need_move = false;
514                 
515                 drag_target->position.cartesian (c);
516                 cart_to_gtk (c);
517
518                 if ((evx != c.x) || (evy != c.y)) {
519                         need_move = true;
520                 }
521
522                 if (need_move) {
523                         CartesianVector cp (evx, evy, 0.0);
524
525                         /* canonicalize position */
526
527                         gtk_to_cart (cp);
528
529                         /* position actual signal on circle */
530
531                         clamp_to_circle (cp.x, cp.y);
532                         
533                         /* generate an angular representation and set drag target (GUI) position */
534
535                         cp.angular (drag_target->position); /* sets drag target position */
536
537 #ifdef PANNER_HACKS
538                         panner->streampanner (drag_index).set_position (drag_target->position);
539 #endif 
540                         
541                         queue_draw ();
542                 }
543         } 
544
545         return true;
546 }
547
548 void
549 Panner2d::cart_to_gtk (CartesianVector& c) const
550 {
551         /* "c" uses a coordinate space that is:
552             
553            center = 0.0
554            dimension = 2.0 * 2.0
555            so max values along each axis are -1..+1
556
557            GTK uses a coordinate space that is:
558
559            top left = 0.0
560            dimension = width * height
561            so max values along each axis are 0,width and
562            0,height
563         */
564
565         c.x = (width / 2) * (c.x + 1);
566         c.y = (height / 2) * (1 - c.y);
567
568         /* XXX z-axis not handled - 2D for now */
569 }
570
571 void
572 Panner2d::gtk_to_cart (CartesianVector& c) const
573 {
574         c.x = (c.x / (width / 2.0)) - 1.0;
575         c.y = -((c.y / (height / 2.0)) - 1.0);
576
577         /* XXX z-axis not handled - 2D for now */
578 }
579
580 void
581 Panner2d::clamp_to_circle (double& x, double& y)
582 {
583         double azi, ele;
584         double z = 0.0;
585         
586         PBD::cart_to_azi_ele (x, y, z, azi, ele);
587         PBD::azi_ele_to_cart (azi, ele, x, y, z);
588 }
589
590 void
591 Panner2d::toggle_bypass ()
592 {
593         panner->set_bypassed (!panner->bypassed());
594 }
595
596 Panner2dWindow::Panner2dWindow (boost::shared_ptr<Panner> p, int32_t h, uint32_t inputs)
597         : widget (p, h)
598         , reset_button (_("Reset"))
599         , bypass_button (_("Bypass"))
600         , mute_button (_("Mute"))
601 {
602         widget.set_name ("MixerPanZone");
603
604         set_title (_("Panner"));
605         widget.set_size_request (h, h);
606
607         button_box.set_spacing (6);
608         button_box.pack_start (reset_button, false, false);
609         button_box.pack_start (bypass_button, false, false);
610         button_box.pack_start (mute_button, false, false);
611
612         spinner_box.set_spacing (6);
613         left_side.set_spacing (6);
614
615         left_side.pack_start (button_box, false, false);
616         left_side.pack_start (spinner_box, false, false);
617
618         reset_button.show ();
619         bypass_button.show ();
620         mute_button.show ();
621         button_box.show ();
622         spinner_box.show ();
623         left_side.show ();
624
625         hpacker.set_spacing (6);
626         hpacker.set_border_width (12);
627         hpacker.pack_start (widget, false, false);
628         hpacker.pack_start (left_side, false, false);
629         hpacker.show ();
630
631         add (hpacker);
632         reset (inputs);
633         widget.show ();
634 }
635
636 void
637 Panner2dWindow::reset (uint32_t n_inputs)
638 {
639         widget.reset (n_inputs);
640
641 #if 0
642         while (spinners.size() < n_inputs) {
643                 // spinners.push_back (new Gtk::SpinButton (widget.azimuth (spinners.size())));
644                 //spinner_box.pack_start (*spinners.back(), false, false);
645                 //spinners.back()->set_digits (4);
646                 spinners.back()->show ();
647         }
648
649         while (spinners.size() > n_inputs) {
650                 spinner_box.remove (*spinners.back());
651                 delete spinners.back();
652                 spinners.erase (--spinners.end());
653         }
654 #endif
655 }