Vkeybd: add a mod-wheel
[ardour.git] / gtk2_ardour / speaker_dialog.cc
1 /*
2  * Copyright (C) 2011-2012 Carl Hetherington <carl@carlh.net>
3  * Copyright (C) 2011-2017 Paul Davis <paul@linuxaudiosystems.com>
4  * Copyright (C) 2011 David Robillard <d@drobilla.net>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with this program; if not, write to the Free Software Foundation, Inc.,
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19  */
20
21 #include "pbd/cartesian.h"
22
23 #include "gtkmm2ext/keyboard.h"
24
25 #include "speaker_dialog.h"
26 #include "gui_thread.h"
27
28 #include "pbd/i18n.h"
29
30 using namespace ARDOUR;
31 using namespace PBD;
32 using namespace std;
33 using namespace Gtk;
34 using namespace Gtkmm2ext;
35
36 SpeakerDialog::SpeakerDialog ()
37         : ArdourWindow (_("Speaker Configuration"))
38         , aspect_frame ("", 0.5, 0.5, 1.5, false)
39         , azimuth_adjustment (0, 0.0, 360.0, 10.0, 1.0)
40         , azimuth_spinner (azimuth_adjustment)
41         , add_speaker_button (_("Add Speaker"))
42         , remove_speaker_button (_("Remove Speaker"))
43         /* initialize to 0 so that set_selected works below */
44         , selected_index (0)
45         , ignore_speaker_position_change (false)
46         , ignore_azimuth_change (false)
47 {
48         side_vbox.set_homogeneous (false);
49         side_vbox.set_border_width (6);
50         side_vbox.set_spacing (6);
51         side_vbox.pack_start (add_speaker_button, false, false);
52
53         aspect_frame.set_size_request (300, 200);
54         aspect_frame.set_shadow_type (SHADOW_NONE);
55         aspect_frame.add (darea);
56
57         hbox.set_spacing (6);
58         hbox.set_border_width (6);
59         hbox.pack_start (aspect_frame, true, true);
60         hbox.pack_start (side_vbox, false, false);
61
62         HBox* current_speaker_hbox = manage (new HBox);
63         current_speaker_hbox->set_spacing (4);
64         current_speaker_hbox->pack_start (*manage (new Label (_("Azimuth:"))), false, false);
65         current_speaker_hbox->pack_start (azimuth_spinner, true, true);
66         current_speaker_hbox->pack_start (remove_speaker_button, true, true);
67
68         VBox* vbox = manage (new VBox);
69         vbox->pack_start (hbox);
70         vbox->pack_start (*current_speaker_hbox, true, true);
71         vbox->show_all ();
72         add (*vbox);
73
74         darea.add_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|Gdk::POINTER_MOTION_MASK);
75
76         darea.signal_size_allocate().connect (sigc::mem_fun (*this, &SpeakerDialog::darea_size_allocate));
77         darea.signal_expose_event().connect (sigc::mem_fun (*this, &SpeakerDialog::darea_expose_event));
78         darea.signal_button_press_event().connect (sigc::mem_fun (*this, &SpeakerDialog::darea_button_press_event));
79         darea.signal_button_release_event().connect (sigc::mem_fun (*this, &SpeakerDialog::darea_button_release_event));
80         darea.signal_motion_notify_event().connect (sigc::mem_fun (*this, &SpeakerDialog::darea_motion_notify_event));
81
82         add_speaker_button.signal_clicked().connect (sigc::mem_fun (*this, &SpeakerDialog::add_speaker));
83         remove_speaker_button.signal_clicked().connect (sigc::mem_fun (*this, &SpeakerDialog::remove_speaker));
84         azimuth_adjustment.signal_value_changed().connect (sigc::mem_fun (*this, &SpeakerDialog::azimuth_changed));
85
86         drag_index = -1;
87
88         /* selected index initialised to 0 above; this will set `no selection' and
89            sensitize widgets accordingly.
90         */
91         set_selected (-1);
92 }
93
94 void
95 SpeakerDialog::set_speakers (boost::shared_ptr<Speakers> s)
96 {
97         _speakers = s;
98 }
99
100 boost::shared_ptr<Speakers>
101 SpeakerDialog::get_speakers () const
102 {
103         return _speakers.lock ();
104 }
105
106 bool
107 SpeakerDialog::darea_expose_event (GdkEventExpose* event)
108 {
109         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
110         if (!speakers) {
111                 return false;
112         }
113
114         gint x, y;
115         cairo_t* cr;
116
117         cr = gdk_cairo_create (darea.get_window()->gobj());
118
119         cairo_set_line_width (cr, 1.0);
120
121         cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height);
122         cairo_set_source_rgba (cr, 0.1, 0.1, 0.1, 1.0);
123         cairo_fill_preserve (cr);
124         cairo_clip (cr);
125
126         cairo_translate (cr, x_origin, y_origin);
127
128         /* horizontal line of "crosshairs" */
129
130         cairo_set_source_rgb (cr, 0.0, 0.1, 0.7);
131         cairo_move_to (cr, 0.5, height/2.0+0.5);
132         cairo_line_to (cr, width+0.5, height/2+0.5);
133         cairo_stroke (cr);
134
135         /* vertical line of "crosshairs" */
136
137         cairo_move_to (cr, width/2+0.5, 0.5);
138         cairo_line_to (cr, width/2+0.5, height+0.5);
139         cairo_stroke (cr);
140
141         /* the circle on which signals live */
142
143         cairo_arc (cr, width/2, height/2, height/2, 0, 2.0 * M_PI);
144         cairo_stroke (cr);
145
146         float arc_radius;
147
148         cairo_select_font_face (cr, "sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
149
150         if (height < 100) {
151                 cairo_set_font_size (cr, 10);
152                 arc_radius = 2.0;
153         } else {
154                 cairo_set_font_size (cr, 16);
155                 arc_radius = 4.0;
156         }
157
158         int n = 0;
159         for (vector<Speaker>::iterator i = speakers->speakers().begin(); i != speakers->speakers().end(); ++i) {
160
161                 Speaker& s (*i);
162                 CartesianVector c (s.coords());
163
164                 cart_to_gtk (c);
165
166                 /* We have already moved our plotting origin to x_origin, y_origin,
167                    so compensate for that.
168                 */
169                 c.x -= x_origin;
170                 c.y -= y_origin;
171
172                 x = (gint) floor (c.x);
173                 y = (gint) floor (c.y);
174
175                 /* XXX need to shift circles so that they are centered on the circle */
176
177                 cairo_arc (cr, x, y, arc_radius, 0, 2.0 * M_PI);
178                 if (selected_index == n) {
179                         cairo_set_source_rgb (cr, 0.8, 0.8, 0.2);
180                 } else {
181                         cairo_set_source_rgb (cr, 0.8, 0.2, 0.1);
182                 }
183                 cairo_close_path (cr);
184                 cairo_fill (cr);
185
186                 cairo_move_to (cr, x + 6, y + 6);
187
188                 char buf[256];
189                 if (n == selected_index) {
190                         snprintf (buf, sizeof (buf), "%d:%d", n+1, (int) lrint (s.angles().azi));
191                 } else {
192                         snprintf (buf, sizeof (buf), "%d", n + 1);
193                 }
194                 cairo_show_text (cr, buf);
195                 ++n;
196         }
197
198         cairo_destroy (cr);
199
200         return true;
201
202 }
203
204 void
205 SpeakerDialog::cart_to_gtk (CartesianVector& c) const
206 {
207         /* "c" uses a coordinate space that is:
208
209            center = 0.0
210            dimension = 2.0 * 2.0
211            so max values along each axis are -1..+1
212
213            GTK uses a coordinate space that is:
214
215            top left = 0.0
216            dimension = width * height
217            so max values along each axis are 0,width and
218            0,height
219         */
220
221         c.x = (width / 2) * (c.x + 1) + x_origin;
222         c.y = (height / 2) * (1 - c.y) + y_origin;
223
224         /* XXX z-axis not handled - 2D for now */
225 }
226
227 void
228 SpeakerDialog::gtk_to_cart (CartesianVector& c) const
229 {
230         c.x = ((c.x - x_origin) / (width / 2.0)) - 1.0;
231         c.y = -(((c.y - y_origin) / (height / 2.0)) - 1.0);
232
233         /* XXX z-axis not handled - 2D for now */
234 }
235
236 void
237 SpeakerDialog::clamp_to_circle (double& x, double& y)
238 {
239         double azi, ele;
240         double z = 0.0;
241         double l;
242
243         PBD::cartesian_to_spherical (x, y, z, azi, ele, l);
244         PBD::spherical_to_cartesian (azi, ele, 1.0, x, y, z);
245 }
246
247 void
248 SpeakerDialog::darea_size_allocate (Gtk::Allocation& alloc)
249 {
250         width = alloc.get_width();
251         height = alloc.get_height();
252
253         /* The allocation will (should) be rectangualar, but make the basic
254          * drawing square; space to the right of the square is for over-hanging
255          * text labels.
256          */
257         width = height;
258
259         if (height > 100) {
260                 width -= 20;
261                 height -= 20;
262         }
263
264         /* Put the x origin to the left of the rectangular allocation */
265         x_origin = (alloc.get_width() - width) / 3;
266         y_origin = (alloc.get_height() - height) / 2;
267 }
268
269 bool
270 SpeakerDialog::darea_button_press_event (GdkEventButton *ev)
271 {
272         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
273         if (!speakers) {
274                 return false;
275         }
276
277         GdkModifierType state;
278
279         if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
280                 return false;
281         }
282
283         drag_index = -1;
284
285         switch (ev->button) {
286         case 1:
287         case 2:
288         {
289                 int const index = find_closest_object (ev->x, ev->y);
290                 set_selected (index);
291
292                 drag_index = index;
293                 int const drag_x = (int) floor (ev->x);
294                 int const drag_y = (int) floor (ev->y);
295                 state = (GdkModifierType) ev->state;
296
297                 if (drag_index >= 0) {
298                         CartesianVector c;
299                         speakers->speakers()[drag_index].angles().cartesian (c);
300                         cart_to_gtk (c);
301                         drag_offset_x = drag_x - x_origin - c.x;
302                         drag_offset_y = drag_y - y_origin - c.y;
303                 }
304
305                 return handle_motion (drag_x, drag_y, state);
306                 break;
307         }
308
309         default:
310                 break;
311         }
312
313         return false;
314 }
315
316 bool
317 SpeakerDialog::darea_button_release_event (GdkEventButton *ev)
318 {
319         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
320         if (!speakers) {
321                 return false;
322         }
323
324         gint x, y;
325         GdkModifierType state;
326         bool ret = false;
327
328         switch (ev->button) {
329         case 1:
330                 x = (int) floor (ev->x);
331                 y = (int) floor (ev->y);
332                 state = (GdkModifierType) ev->state;
333
334                 if (Keyboard::modifier_state_contains (state, Keyboard::TertiaryModifier)) {
335
336                         for (vector<Speaker>::iterator i = speakers->speakers().begin(); i != speakers->speakers().end(); ++i) {
337                                 /* XXX DO SOMETHING TO SET SPEAKER BACK TO "normal" */
338                         }
339
340                         queue_draw ();
341                         ret = true;
342
343                 } else {
344                         ret = handle_motion (x, y, state);
345                 }
346
347                 break;
348
349         case 2:
350                 x = (int) floor (ev->x);
351                 y = (int) floor (ev->y);
352                 state = (GdkModifierType) ev->state;
353
354                 ret = handle_motion (x, y, state);
355                 break;
356
357         case 3:
358                 break;
359
360         }
361
362         drag_index = -1;
363
364         return ret;
365 }
366
367 int
368 SpeakerDialog::find_closest_object (gdouble x, gdouble y)
369 {
370         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
371         if (!speakers) {
372                 return -1;
373         }
374
375         float distance;
376         float best_distance = FLT_MAX;
377         int n = 0;
378         int which = -1;
379
380         for (vector<Speaker>::iterator i = speakers->speakers().begin(); i != speakers->speakers().end(); ++i, ++n) {
381
382                 Speaker& candidate (*i);
383                 CartesianVector c;
384
385                 candidate.angles().cartesian (c);
386                 cart_to_gtk (c);
387
388                 distance = sqrt ((c.x - x) * (c.x - x) +
389                                  (c.y - y) * (c.y - y));
390
391
392                 if (distance < best_distance) {
393                         best_distance = distance;
394                         which = n;
395                 }
396         }
397
398         if (best_distance > 20) { // arbitrary
399                 return -1;
400         }
401
402         return which;
403 }
404
405 bool
406 SpeakerDialog::darea_motion_notify_event (GdkEventMotion *ev)
407 {
408         gint x, y;
409         GdkModifierType state;
410
411         if (ev->is_hint) {
412                 gdk_window_get_pointer (ev->window, &x, &y, &state);
413         } else {
414                 x = (int) floor (ev->x);
415                 y = (int) floor (ev->y);
416                 state = (GdkModifierType) ev->state;
417         }
418
419         return handle_motion (x, y, state);
420 }
421
422 bool
423 SpeakerDialog::handle_motion (gint evx, gint evy, GdkModifierType state)
424 {
425         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
426         if (!speakers) {
427                 return false;
428         }
429
430         if (drag_index < 0) {
431                 return false;
432         }
433
434         if ((state & (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK)) == 0) {
435                 return false;
436         }
437
438         /* correct event coordinates to have their origin at the corner of our graphic
439            rather than the corner of our allocation */
440
441         double obx = evx - x_origin;
442         double oby = evy - y_origin;
443
444         /* and compensate for any distance between the mouse pointer and the centre
445            of the object being dragged */
446
447         obx -= drag_offset_x;
448         oby -= drag_offset_y;
449
450         if (state & GDK_BUTTON1_MASK && !(state & GDK_BUTTON2_MASK)) {
451                 CartesianVector c;
452                 bool need_move = false;
453                 Speaker& moving (speakers->speakers()[drag_index]);
454
455                 moving.angles().cartesian (c);
456                 cart_to_gtk (c);
457
458                 if (obx != c.x || oby != c.y) {
459                         need_move = true;
460                 }
461
462                 if (need_move) {
463                         CartesianVector cp (obx, oby, 0.0);
464
465                         /* canonicalize position */
466
467                         gtk_to_cart (cp);
468
469                         /* position actual signal on circle */
470
471                         clamp_to_circle (cp.x, cp.y);
472
473                         /* generate an angular representation and set drag target (GUI) position */
474
475                         AngularVector a;
476
477                         cp.angular (a);
478
479                         moving.move (a);
480
481                         queue_draw ();
482                 }
483         }
484
485         return true;
486 }
487
488 void
489 SpeakerDialog::add_speaker ()
490 {
491         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
492         if (!speakers) {
493                 return;
494         }
495
496         speakers->add_speaker (PBD::AngularVector (0, 0, 0));
497         queue_draw ();
498 }
499
500 void
501 SpeakerDialog::set_selected (int i)
502 {
503         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
504         if (!speakers) {
505                 return;
506         }
507
508         if (i == selected_index) {
509                 return;
510         }
511
512         selected_index = i;
513         queue_draw ();
514
515         selected_speaker_connection.disconnect ();
516
517         azimuth_spinner.set_sensitive (selected_index != -1);
518         remove_speaker_button.set_sensitive (selected_index != -1);
519
520         if (selected_index != -1) {
521                 azimuth_adjustment.set_value (speakers->speakers()[selected_index].angles().azi);
522                 speakers->speakers()[selected_index].PositionChanged.connect (
523                         selected_speaker_connection, MISSING_INVALIDATOR,
524                         boost::bind (&SpeakerDialog::speaker_position_changed, this),
525                         gui_context ()
526                         );
527         }
528 }
529
530 void
531 SpeakerDialog::azimuth_changed ()
532 {
533         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
534         if (!speakers) {
535                 return;
536         }
537
538         assert (selected_index != -1);
539
540         if (ignore_azimuth_change) {
541                 return;
542         }
543
544         ignore_speaker_position_change = true;
545         speakers->move_speaker (speakers->speakers()[selected_index].id, PBD::AngularVector (azimuth_adjustment.get_value (), 0, 0));
546         ignore_speaker_position_change = false;
547
548         queue_draw ();
549 }
550
551 void
552 SpeakerDialog::speaker_position_changed ()
553 {
554         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
555         if (!speakers) {
556                 return;
557         }
558
559         assert (selected_index != -1);
560
561         if (ignore_speaker_position_change) {
562                 return;
563         }
564
565         ignore_azimuth_change = true;
566         azimuth_adjustment.set_value (speakers->speakers()[selected_index].angles().azi);
567         ignore_azimuth_change = false;
568
569         queue_draw ();
570 }
571
572 void
573 SpeakerDialog::remove_speaker ()
574 {
575         boost::shared_ptr<Speakers> speakers = _speakers.lock ();
576         if (!speakers) {
577                 return;
578         }
579
580         assert (selected_index != -1);
581
582         speakers->remove_speaker (speakers->speakers()[selected_index].id);
583         set_selected (-1);
584
585         queue_draw ();
586 }