32f4233804f9a4b1a48652070407b0380777e36f
[dcpomatic.git] / src / wx / audio_mapping_view.cc
1 /*
2     Copyright (C) 2013-2019 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic 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     DCP-o-matic 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
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 /** @file  src/wx/audio_mapping_view.cc
22  *  @brief AudioMappingView class and helpers.
23  */
24
25 #include "audio_mapping_view.h"
26 #include "wx_util.h"
27 #include "audio_gain_dialog.h"
28 #include "lib/audio_mapping.h"
29 #include "lib/util.h"
30 #include <dcp/locale_convert.h>
31 #include <dcp/types.h>
32 #include <wx/wx.h>
33 #include <wx/renderer.h>
34 #include <wx/grid.h>
35 #include <wx/graphics.h>
36 #include <boost/foreach.hpp>
37 #include <iostream>
38
39 using std::cout;
40 using std::list;
41 using std::string;
42 using std::min;
43 using std::max;
44 using std::vector;
45 using std::pair;
46 using std::make_pair;
47 using boost::shared_ptr;
48 using boost::optional;
49 #if BOOST_VERSION >= 106100
50 using namespace boost::placeholders;
51 #endif
52 using dcp::locale_convert;
53
54 #define INDICATOR_SIZE 20
55 #define GRID_SPACING 32
56 #define LEFT_WIDTH (GRID_SPACING * 3)
57 #define TOP_HEIGHT (GRID_SPACING * 2)
58
59 enum {
60         ID_off = 1,
61         ID_full = 2,
62         ID_minus6dB = 3,
63         ID_edit = 4
64 };
65
66 AudioMappingView::AudioMappingView (wxWindow* parent)
67         : wxPanel (parent, wxID_ANY)
68         , _menu_input (0)
69         , _menu_output (1)
70 {
71         _menu = new wxMenu;
72         _menu->Append (ID_off, _("Off"));
73         _menu->Append (ID_full, _("Full"));
74         _menu->Append (ID_minus6dB, _("-6dB"));
75         _menu->Append (ID_edit, _("Edit..."));
76
77         _body = new wxPanel (this, wxID_ANY);
78         _vertical_scroll = new wxScrollBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL);
79         _horizontal_scroll = new wxScrollBar (this, wxID_ANY);
80
81         Bind (wxEVT_SIZE, boost::bind(&AudioMappingView::size, this, _1));
82         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::off, this), ID_off);
83         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::full, this), ID_full);
84         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::minus6dB, this), ID_minus6dB);
85         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::edit, this), ID_edit);
86         Bind (wxEVT_MOUSEWHEEL, boost::bind(&AudioMappingView::mouse_wheel, this, _1));
87         _body->Bind (wxEVT_PAINT, boost::bind(&AudioMappingView::paint, this));
88         _body->Bind (wxEVT_LEFT_DOWN, boost::bind(&AudioMappingView::left_down, this, _1));
89         _body->Bind (wxEVT_RIGHT_DOWN, boost::bind(&AudioMappingView::right_down, this, _1));
90         _body->Bind (wxEVT_MOTION, boost::bind(&AudioMappingView::motion, this, _1));
91         _vertical_scroll->Bind (wxEVT_SCROLL_TOP, boost::bind(&AudioMappingView::scroll, this));
92         _vertical_scroll->Bind (wxEVT_SCROLL_BOTTOM, boost::bind(&AudioMappingView::scroll, this));
93         _vertical_scroll->Bind (wxEVT_SCROLL_LINEUP, boost::bind(&AudioMappingView::scroll, this));
94         _vertical_scroll->Bind (wxEVT_SCROLL_LINEDOWN, boost::bind(&AudioMappingView::scroll, this));
95         _vertical_scroll->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&AudioMappingView::scroll, this));
96         _vertical_scroll->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&AudioMappingView::scroll, this));
97         _vertical_scroll->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&AudioMappingView::scroll, this));
98         _vertical_scroll->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&AudioMappingView::scroll, this));
99         _horizontal_scroll->Bind (wxEVT_SCROLL_TOP, boost::bind(&AudioMappingView::scroll, this));
100         _horizontal_scroll->Bind (wxEVT_SCROLL_BOTTOM, boost::bind(&AudioMappingView::scroll, this));
101         _horizontal_scroll->Bind (wxEVT_SCROLL_LINEUP, boost::bind(&AudioMappingView::scroll, this));
102         _horizontal_scroll->Bind (wxEVT_SCROLL_LINEDOWN, boost::bind(&AudioMappingView::scroll, this));
103         _horizontal_scroll->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&AudioMappingView::scroll, this));
104         _horizontal_scroll->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&AudioMappingView::scroll, this));
105         _horizontal_scroll->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&AudioMappingView::scroll, this));
106         _horizontal_scroll->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&AudioMappingView::scroll, this));
107 }
108
109 void
110 AudioMappingView::size (wxSizeEvent& ev)
111 {
112         setup ();
113         ev.Skip ();
114 }
115
116 void
117 AudioMappingView::setup ()
118 {
119         wxSize const s = GetSize();
120         int const w = _vertical_scroll->GetSize().GetWidth();
121         int const h = _horizontal_scroll->GetSize().GetHeight();
122
123         _vertical_scroll->SetPosition (wxPoint(s.GetWidth() - w, 0));
124         _vertical_scroll->SetSize (wxSize(w, max(0, s.GetHeight() - h)));
125
126         _body->SetSize (wxSize(max(0, s.GetWidth() - w), max(0, s.GetHeight() - h)));
127
128         _horizontal_scroll->SetPosition (wxPoint(0, s.GetHeight() - h));
129         _horizontal_scroll->SetSize (wxSize(max(0, s.GetWidth() - w), h));
130
131         _vertical_scroll->SetScrollbar (
132                 _vertical_scroll->GetThumbPosition(),
133                 s.GetHeight() - h - 8,
134                 GRID_SPACING * (2 + _input_channels.size()),
135                 GRID_SPACING,
136                 true
137                 );
138
139         _horizontal_scroll->SetScrollbar (
140                 _horizontal_scroll->GetThumbPosition(),
141                 s.GetWidth() - w - 8,
142                 GRID_SPACING * (3 + _output_channels.size()),
143                 GRID_SPACING,
144                 true);
145 }
146
147 void
148 AudioMappingView::scroll ()
149 {
150         Refresh ();
151 }
152
153 void
154 AudioMappingView::paint_static (wxDC& dc, wxGraphicsContext* gc)
155 {
156         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
157         dc.SetFont (wxSWISS_FONT->Bold());
158         wxCoord label_width;
159         wxCoord label_height;
160
161         /* DCP label at the top */
162
163         dc.GetTextExtent (_("DCP"), &label_width, &label_height);
164         dc.DrawText (_("DCP"), LEFT_WIDTH + (_output_channels.size() * GRID_SPACING - label_width) / 2, (GRID_SPACING - label_height) / 2);
165
166         /* Content label on the left */
167
168         dc.GetTextExtent (_("Content"), &label_width, &label_height);
169         dc.DrawRotatedText (
170                 _("Content"),
171                 (GRID_SPACING - label_height) / 2,
172                 TOP_HEIGHT + (_input_channels.size() * GRID_SPACING + label_width) / 2,
173                 90
174                 );
175
176         dc.SetFont (*wxSWISS_FONT);
177         gc->SetPen (*wxBLACK_PEN);
178 }
179
180 void
181 AudioMappingView::paint_column_labels (wxDC& dc, wxGraphicsContext* gc)
182 {
183         wxCoord label_width;
184         wxCoord label_height;
185         int N = 0;
186         BOOST_FOREACH (string i, _output_channels) {
187                 dc.GetTextExtent (std_to_wx(i), &label_width, &label_height);
188                 dc.DrawText (std_to_wx(i), LEFT_WIDTH + GRID_SPACING * N + (GRID_SPACING - label_width) / 2, GRID_SPACING + (GRID_SPACING - label_height) / 2);
189                 ++N;
190         }
191
192         wxGraphicsPath lines = gc->CreatePath ();
193         lines.MoveToPoint (LEFT_WIDTH, GRID_SPACING);
194         lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, GRID_SPACING);
195         lines.MoveToPoint (LEFT_WIDTH, GRID_SPACING * 2);
196         lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, GRID_SPACING * 2);
197         gc->StrokePath (lines);
198 }
199
200 void
201 AudioMappingView::paint_column_lines (wxGraphicsContext* gc)
202 {
203         wxGraphicsPath lines = gc->CreatePath ();
204         for (size_t i = 0; i < _output_channels.size(); ++i) {
205                 lines.MoveToPoint    (LEFT_WIDTH + GRID_SPACING * i, GRID_SPACING);
206                 lines.AddLineToPoint (LEFT_WIDTH + GRID_SPACING * i, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
207         }
208         lines.MoveToPoint    (LEFT_WIDTH + GRID_SPACING * _output_channels.size(), GRID_SPACING);
209         lines.AddLineToPoint (LEFT_WIDTH + GRID_SPACING * _output_channels.size(), TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
210         gc->StrokePath (lines);
211 }
212
213 void
214 AudioMappingView::paint_row_labels (wxDC& dc, wxGraphicsContext* gc)
215 {
216         wxCoord label_width;
217         wxCoord label_height;
218         wxGraphicsPath lines = gc->CreatePath ();
219
220         /* Row channel labels */
221
222         int N = 0;
223         BOOST_FOREACH (string i, _input_channels) {
224                 dc.GetTextExtent (std_to_wx(i), &label_width, &label_height);
225                 dc.DrawText (std_to_wx(i), GRID_SPACING * 2 + (GRID_SPACING - label_width) / 2, TOP_HEIGHT + GRID_SPACING * N + (GRID_SPACING - label_height) / 2);
226                 ++N;
227         }
228
229         /* Vertical lines on the left */
230
231         for (int i = 1; i < 3; ++i) {
232                 lines.MoveToPoint    (GRID_SPACING * i, TOP_HEIGHT);
233                 lines.AddLineToPoint (GRID_SPACING * i, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
234         }
235
236         /* Group labels and lines */
237
238         int y = TOP_HEIGHT;
239         BOOST_FOREACH (Group i, _input_groups) {
240                 int const height = (i.to - i.from + 1) * GRID_SPACING;
241                 dc.GetTextExtent (std_to_wx(i.name), &label_width, &label_height);
242                 if (label_width > height) {
243                         label_width = height - 8;
244                 }
245
246                 {
247                         int yp = y;
248                         if ((yp - 2 * GRID_SPACING) < dc.GetLogicalOrigin().y) {
249                                 yp += dc.GetLogicalOrigin().y;
250                         }
251
252                         wxCoord old_x, old_y, old_width, old_height;
253                         dc.GetClippingBox (&old_x, &old_y, &old_width, &old_height);
254                         dc.DestroyClippingRegion ();
255                         dc.SetClippingRegion (GRID_SPACING, yp + 4, GRID_SPACING, height - 8);
256
257                         dc.DrawRotatedText (
258                                 std_to_wx(i.name),
259                                 GRID_SPACING + (GRID_SPACING - label_height) / 2,
260                                 y + (height + label_width) / 2,
261                                 90
262                                 );
263
264                         dc.DestroyClippingRegion ();
265                         dc.SetClippingRegion (old_x, old_y, old_width, old_height);
266                 }
267
268                 lines.MoveToPoint    (GRID_SPACING,     y);
269                 lines.AddLineToPoint (GRID_SPACING * 2, y);
270                 y += height;
271         }
272
273         lines.MoveToPoint    (GRID_SPACING,     y);
274         lines.AddLineToPoint (GRID_SPACING * 2, y);
275
276         gc->StrokePath (lines);
277 }
278
279 void
280 AudioMappingView::paint_row_lines (wxGraphicsContext* gc)
281 {
282         wxGraphicsPath lines = gc->CreatePath ();
283         for (size_t i = 0; i < _input_channels.size(); ++i) {
284                 lines.MoveToPoint (GRID_SPACING * 2, TOP_HEIGHT + GRID_SPACING * i);
285                 lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, TOP_HEIGHT + GRID_SPACING * i);
286         }
287         lines.MoveToPoint (GRID_SPACING * 2, TOP_HEIGHT + GRID_SPACING * _input_channels.size());
288         lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, TOP_HEIGHT + GRID_SPACING * _input_channels.size());
289         gc->StrokePath (lines);
290 }
291
292 void
293 AudioMappingView::paint_indicators (wxDC& dc)
294 {
295         /* _{input,output}_channels and _map may not always be in sync, be careful here */
296         size_t const output = min(_output_channels.size(), size_t(_map.output_channels()));
297         size_t const input = min(_input_channels.size(), size_t(_map.input_channels()));
298
299         for (size_t x = 0; x < output; ++x) {
300                 for (size_t y = 0; y < input; ++y) {
301                         dc.SetBrush (*wxWHITE_BRUSH);
302                         dc.DrawRectangle (
303                                 wxRect(
304                                         LEFT_WIDTH + x * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
305                                         TOP_HEIGHT + y * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
306                                         INDICATOR_SIZE, INDICATOR_SIZE
307                                         )
308                                 );
309
310                         float const value_dB = 20 * log10 (_map.get(y, x));
311                         int const range = 18;
312                         int height = 0;
313                         if (value_dB > -range) {
314                                 height = INDICATOR_SIZE * (1 + value_dB / range);
315                         }
316
317                         dc.SetBrush (*wxTheBrushList->FindOrCreateBrush(wxColour (0, 255, 0), wxBRUSHSTYLE_SOLID));
318                         dc.DrawRectangle (
319                                 wxRect(
320                                         LEFT_WIDTH + x * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
321                                         TOP_HEIGHT + y * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2 + INDICATOR_SIZE - height,
322                                         INDICATOR_SIZE, height
323                                         )
324                                 );
325                 }
326         }
327 }
328
329 static
330 void clip (wxDC& dc, wxGraphicsContext* gc, int x, int y, int w, int h)
331 {
332         dc.SetClippingRegion (x, y, w, h);
333         gc->Clip (x, y, w, h);
334 }
335
336 static
337 void translate (wxDC& dc, wxGraphicsContext* gc, int x, int y)
338 {
339         gc->PushState ();
340         gc->Translate (-x, -y);
341         dc.SetLogicalOrigin (x, y);
342 }
343
344 static
345 void restore (wxDC& dc, wxGraphicsContext* gc)
346 {
347         dc.SetLogicalOrigin (0, 0);
348         gc->PopState ();
349         dc.DestroyClippingRegion ();
350         gc->ResetClip ();
351 }
352
353 void
354 AudioMappingView::paint ()
355 {
356         wxPaintDC dc (_body);
357
358         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
359         if (!gc) {
360                 return;
361         }
362
363         int const hs = _horizontal_scroll->GetThumbPosition ();
364         int const vs = _vertical_scroll->GetThumbPosition ();
365
366         paint_static (dc, gc);
367
368         clip (dc, gc, LEFT_WIDTH, 0, GRID_SPACING * _output_channels.size(), GRID_SPACING * (2 + _input_channels.size()));
369         translate (dc, gc, hs, 0);
370         paint_column_labels (dc, gc);
371         restore (dc, gc);
372
373         clip (dc, gc, 0, TOP_HEIGHT, GRID_SPACING * (3 + _output_channels.size()), GRID_SPACING * _input_channels.size() + 1);
374         translate (dc, gc, 0, vs);
375         paint_row_labels (dc, gc);
376         restore (dc, gc);
377
378         clip (dc, gc, GRID_SPACING * 2, TOP_HEIGHT, GRID_SPACING * (1 + _output_channels.size()), GRID_SPACING * _input_channels.size() + 1);
379         translate (dc, gc, hs, vs);
380         paint_row_lines (gc);
381         restore (dc, gc);
382
383         clip (dc, gc, LEFT_WIDTH, GRID_SPACING, GRID_SPACING * (1 + _output_channels.size()), GRID_SPACING * (1 + _input_channels.size()));
384         translate (dc, gc, hs, vs);
385         paint_column_lines (gc);
386         restore (dc, gc);
387
388         clip (dc, gc, LEFT_WIDTH, TOP_HEIGHT, GRID_SPACING * _output_channels.size(), GRID_SPACING * _input_channels.size());
389         translate (dc, gc, hs, vs);
390         paint_indicators (dc);
391         restore (dc, gc);
392
393         delete gc;
394 }
395
396 optional<pair<int, int> >
397 AudioMappingView::mouse_event_to_channels (wxMouseEvent& ev) const
398 {
399         int const x = ev.GetX() + _horizontal_scroll->GetThumbPosition();
400         int const y = ev.GetY() + _vertical_scroll->GetThumbPosition();
401
402         if (x <= LEFT_WIDTH || y < TOP_HEIGHT) {
403                 return optional<pair<int, int> >();
404         }
405
406         int const input = (y - TOP_HEIGHT) / GRID_SPACING;
407         int const output = (x - LEFT_WIDTH) / GRID_SPACING;
408
409         if (input >= int(_input_channels.size()) || output >= int(_output_channels.size())) {
410                 return optional<pair<int, int> >();
411         }
412
413         return make_pair (input, output);
414 }
415
416 optional<string>
417 AudioMappingView::mouse_event_to_input_group_name (wxMouseEvent& ev) const
418 {
419         int const x = ev.GetX() + _horizontal_scroll->GetThumbPosition();
420         if (x < GRID_SPACING || x > (2 * GRID_SPACING)) {
421                 return optional<string>();
422         }
423
424         int y = (ev.GetY() + _vertical_scroll->GetThumbPosition() - (GRID_SPACING * 2)) / GRID_SPACING;
425         BOOST_FOREACH (Group i, _input_groups) {
426                 if (i.from <= y && y <= i.to) {
427                         return i.name;
428                 }
429         }
430
431         return optional<string>();
432 }
433
434 void
435 AudioMappingView::left_down (wxMouseEvent& ev)
436 {
437         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
438         if (!channels) {
439                 return;
440         }
441
442         if (_map.get(channels->first, channels->second) > 0) {
443                 _map.set (channels->first, channels->second, 0);
444         } else {
445                 _map.set (channels->first, channels->second, 1);
446         }
447
448         map_values_changed ();
449 }
450
451 void
452 AudioMappingView::right_down (wxMouseEvent& ev)
453 {
454         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
455         if (!channels) {
456                 return;
457         }
458
459         _menu_input = channels->first;
460         _menu_output = channels->second;
461         PopupMenu (_menu, ev.GetPosition());
462 }
463
464 void
465 AudioMappingView::mouse_wheel (wxMouseEvent& ev)
466 {
467         if (ev.ShiftDown()) {
468                 _horizontal_scroll->SetThumbPosition (
469                         _horizontal_scroll->GetThumbPosition() + (ev.GetWheelRotation() > 0 ? -GRID_SPACING : GRID_SPACING)
470                         );
471
472         } else {
473                 _vertical_scroll->SetThumbPosition (
474                         _vertical_scroll->GetThumbPosition() + (ev.GetWheelRotation() > 0 ? -GRID_SPACING : GRID_SPACING)
475                         );
476         }
477         Refresh ();
478 }
479
480 /** Called when any gain value has changed */
481 void
482 AudioMappingView::map_values_changed ()
483 {
484         Changed (_map);
485         _last_tooltip_channels = optional<pair<int, int> >();
486         Refresh ();
487 }
488
489 void
490 AudioMappingView::off ()
491 {
492         _map.set (_menu_input, _menu_output, 0);
493         map_values_changed ();
494 }
495
496 void
497 AudioMappingView::full ()
498 {
499         _map.set (_menu_input, _menu_output, 1);
500         map_values_changed ();
501 }
502
503 void
504 AudioMappingView::minus6dB ()
505 {
506         _map.set (_menu_input, _menu_output, pow (10, -6.0 / 20));
507         map_values_changed ();
508 }
509
510 void
511 AudioMappingView::edit ()
512 {
513         AudioGainDialog* dialog = new AudioGainDialog (this, _menu_input, _menu_output, _map.get(_menu_input, _menu_output));
514         if (dialog->ShowModal() == wxID_OK) {
515                 _map.set (_menu_input, _menu_output, dialog->value ());
516                 map_values_changed ();
517         }
518
519         dialog->Destroy ();
520 }
521
522 void
523 AudioMappingView::set (AudioMapping map)
524 {
525         _map = map;
526         Refresh ();
527 }
528
529 void
530 AudioMappingView::set_input_channels (vector<string> const & names)
531 {
532         _input_channels = names;
533         setup ();
534         Refresh ();
535 }
536
537 void
538 AudioMappingView::set_output_channels (vector<string> const & names)
539 {
540         _output_channels = names;
541         setup ();
542         Refresh ();
543 }
544
545 void
546 AudioMappingView::motion (wxMouseEvent& ev)
547 {
548         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
549         if (channels) {
550                 if (channels != _last_tooltip_channels) {
551                         wxString s;
552                         float const gain = _map.get(channels->first, channels->second);
553                         if (gain == 0) {
554                                 s = wxString::Format (
555                                         _("No audio will be passed from content channel %d to DCP channel %d."),
556                                         channels->first + 1, channels->second + 1
557                                         );
558                         } else if (gain == 1) {
559                                 s = wxString::Format (
560                                         _("Audio will be passed from content channel %d to DCP channel %d unaltered."),
561                                         channels->first + 1, channels->second + 1
562                                         );
563                         } else {
564                                 float const dB = 20 * log10 (gain);
565                                 s = wxString::Format (
566                                         _("Audio will be passed from content channel %d to DCP channel %d with gain %.1fdB."),
567                                         channels->first + 1, channels->second + 1, dB
568                                         );
569                         }
570
571                         SetToolTip (s + " " + _("Right click to change gain."));
572                 }
573         } else {
574                 optional<string> group = mouse_event_to_input_group_name (ev);
575                 if (group) {
576                         SetToolTip (std_to_wx(*group));
577                 } else {
578                         SetToolTip ("");
579                 }
580         }
581
582         _last_tooltip_channels = channels;
583         ev.Skip ();
584 }
585
586 void
587 AudioMappingView::set_input_groups (vector<Group> const & groups)
588 {
589         _input_groups = groups;
590 }