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