Fix channel numbering in tooltip.
[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 using dcp::locale_convert;
50
51 #define INDICATOR_SIZE 16
52 #define GRID_SPACING 24
53 #define LEFT_WIDTH (GRID_SPACING * 3)
54 #define TOP_HEIGHT (GRID_SPACING * 2)
55
56 enum {
57         ID_off = 1,
58         ID_full = 2,
59         ID_minus6dB = 3,
60         ID_edit = 4
61 };
62
63 AudioMappingView::AudioMappingView (wxWindow* parent)
64         : wxScrolledWindow (parent, wxID_ANY)
65         , _menu_input (0)
66         , _menu_output (1)
67 {
68         _menu = new wxMenu;
69         _menu->Append (ID_off, _("Off"));
70         _menu->Append (ID_full, _("Full"));
71         _menu->Append (ID_minus6dB, _("-6dB"));
72         _menu->Append (ID_edit, _("Edit..."));
73
74         Bind (wxEVT_PAINT, boost::bind(&AudioMappingView::paint, this));
75         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::off, this), ID_off);
76         Bind (wxEVT_LEFT_DOWN, boost::bind(&AudioMappingView::left_down, this, _1));
77         Bind (wxEVT_RIGHT_DOWN, boost::bind(&AudioMappingView::right_down, this, _1));
78         Bind (wxEVT_MOTION, boost::bind(&AudioMappingView::motion, this, _1));
79         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::full, this), ID_full);
80         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::minus6dB, this), ID_minus6dB);
81         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::edit, this), ID_edit);
82
83         SetScrollRate (GRID_SPACING, GRID_SPACING);
84 }
85
86 void
87 AudioMappingView::paint ()
88 {
89         wxPaintDC dc (this);
90
91         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
92         if (!gc) {
93                 return;
94         }
95
96         int sx, sy;
97         GetViewStart (&sx, &sy);
98         gc->Translate (-sx * GRID_SPACING, -sy * GRID_SPACING);
99         dc.SetLogicalOrigin (sx * GRID_SPACING, sy * GRID_SPACING);
100
101         int const output_channels_width = _output_channels.size() * GRID_SPACING;
102         int const input_channels_height = _input_channels.size() * GRID_SPACING;
103
104         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
105         wxGraphicsPath lines = gc->CreatePath ();
106         dc.SetFont (wxSWISS_FONT->Bold());
107         wxCoord label_width;
108         wxCoord label_height;
109
110         /* DCP label at the top */
111
112         dc.GetTextExtent (_("DCP"), &label_width, &label_height);
113         dc.DrawText (_("DCP"), LEFT_WIDTH + (output_channels_width - label_width) / 2, (GRID_SPACING - label_height) / 2);
114
115         /* Content label on the left */
116
117         dc.GetTextExtent (_("Content"), &label_width, &label_height);
118         dc.DrawRotatedText (
119                 _("Content"),
120                 (GRID_SPACING - label_height) / 2,
121                 TOP_HEIGHT + (input_channels_height + label_width) / 2,
122                 90
123                 );
124
125         dc.SetFont (*wxSWISS_FONT);
126         gc->SetPen (*wxBLACK_PEN);
127
128         /* Column labels and some lines */
129
130         int N = 0;
131         BOOST_FOREACH (string i, _output_channels) {
132                 dc.GetTextExtent (std_to_wx(i), &label_width, &label_height);
133                 dc.DrawText (std_to_wx(i), LEFT_WIDTH + GRID_SPACING * N + (GRID_SPACING - label_width) / 2, GRID_SPACING + (GRID_SPACING - label_height) / 2);
134                 lines.MoveToPoint    (LEFT_WIDTH + GRID_SPACING * N, GRID_SPACING);
135                 lines.AddLineToPoint (LEFT_WIDTH + GRID_SPACING * N, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
136                 ++N;
137         }
138         lines.MoveToPoint    (LEFT_WIDTH + GRID_SPACING * N, GRID_SPACING);
139         lines.AddLineToPoint (LEFT_WIDTH + GRID_SPACING * N, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
140
141         /* Horizontal lines at the top */
142
143         lines.MoveToPoint (LEFT_WIDTH, GRID_SPACING);
144                 lines.AddLineToPoint (LEFT_WIDTH + output_channels_width, GRID_SPACING);
145         lines.MoveToPoint (LEFT_WIDTH, GRID_SPACING * 2);
146                 lines.AddLineToPoint (LEFT_WIDTH + output_channels_width, GRID_SPACING * 2);
147
148         /* Row channel labels */
149
150         N = 0;
151         BOOST_FOREACH (string i, _input_channels) {
152                 dc.GetTextExtent (std_to_wx(i), &label_width, &label_height);
153                 dc.DrawText (std_to_wx(i), GRID_SPACING * 2 + (GRID_SPACING - label_width) / 2, TOP_HEIGHT + GRID_SPACING * N + (GRID_SPACING - label_height) / 2);
154                 lines.MoveToPoint (GRID_SPACING * 2, TOP_HEIGHT + GRID_SPACING * N);
155                 lines.AddLineToPoint (LEFT_WIDTH + output_channels_width, TOP_HEIGHT + GRID_SPACING * N);
156                 ++N;
157         }
158         lines.MoveToPoint (GRID_SPACING * 2, TOP_HEIGHT + GRID_SPACING * N);
159         lines.AddLineToPoint (LEFT_WIDTH + output_channels_width, TOP_HEIGHT + GRID_SPACING * N);
160
161         /* Vertical lines on the left */
162
163         for (int i = 1; i < 3; ++i) {
164                 lines.MoveToPoint    (GRID_SPACING * i, TOP_HEIGHT);
165                 lines.AddLineToPoint (GRID_SPACING * i, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
166         }
167
168         /* Group labels and lines */
169
170         int y = TOP_HEIGHT;
171         BOOST_FOREACH (Group i, _input_groups) {
172                 dc.GetTextExtent (std_to_wx(i.name), &label_width, &label_height);
173                 int const height = (i.to - i.from + 1) * GRID_SPACING;
174                 dc.DrawRotatedText (
175                         std_to_wx(i.name),
176                         GRID_SPACING + (GRID_SPACING - label_height) / 2,
177                         y + (height + label_width) / 2,
178                         90
179                         );
180                 lines.MoveToPoint    (GRID_SPACING,     y);
181                 lines.AddLineToPoint (GRID_SPACING * 2, y);
182                 y += height;
183         }
184
185         lines.MoveToPoint    (GRID_SPACING,     y);
186         lines.AddLineToPoint (GRID_SPACING * 2, y);
187
188         gc->StrokePath (lines);
189
190         /* Indicators */
191
192         for (size_t x = 0; x < _output_channels.size(); ++x) {
193                 for (size_t y = 0; y < _input_channels.size(); ++y) {
194                         dc.SetBrush (*wxWHITE_BRUSH);
195                         dc.DrawRectangle (
196                                 wxRect(
197                                         LEFT_WIDTH + x * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
198                                         TOP_HEIGHT + y * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
199                                         INDICATOR_SIZE, INDICATOR_SIZE
200                                         )
201                                 );
202
203                         float const value_dB = 20 * log10 (_map.get(y, x));
204                         int const range = 18;
205                         int height = 0;
206                         if (value_dB > -range) {
207                                 height = INDICATOR_SIZE * (1 + value_dB / range);
208                         }
209
210                         dc.SetBrush (*wxTheBrushList->FindOrCreateBrush(wxColour (0, 255, 0), wxBRUSHSTYLE_SOLID));
211                         dc.DrawRectangle (
212                                 wxRect(
213                                         LEFT_WIDTH + x * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
214                                         TOP_HEIGHT + y * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2 + INDICATOR_SIZE - height,
215                                         INDICATOR_SIZE, height
216                                         )
217                                 );
218                 }
219         }
220
221         delete gc;
222 }
223
224 optional<pair<int, int> >
225 AudioMappingView::mouse_event_to_channels (wxMouseEvent& ev) const
226 {
227         int sx, sy;
228         GetViewStart (&sx, &sy);
229         int const x = ev.GetX() + sx * GRID_SPACING;
230         int const y = ev.GetY() + sy * GRID_SPACING;
231
232         if (x <= LEFT_WIDTH || y < TOP_HEIGHT) {
233                 return optional<pair<int, int> >();
234         }
235
236         int const input = (y - TOP_HEIGHT) / GRID_SPACING;
237         int const output = (x - LEFT_WIDTH) / GRID_SPACING;
238
239         if (input >= int(_input_channels.size()) || output >= int(_output_channels.size())) {
240                 return optional<pair<int, int> >();
241         }
242
243         return make_pair (input, output);
244 }
245
246 void
247 AudioMappingView::left_down (wxMouseEvent& ev)
248 {
249         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
250         if (!channels) {
251                 return;
252         }
253
254         if (_map.get(channels->first, channels->second) > 0) {
255                 _map.set (channels->first, channels->second, 0);
256         } else {
257                 _map.set (channels->first, channels->second, 1);
258         }
259
260         map_values_changed ();
261 }
262
263 void
264 AudioMappingView::right_down (wxMouseEvent& ev)
265 {
266         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
267         if (!channels) {
268                 return;
269         }
270
271         _menu_input = channels->first;
272         _menu_output = channels->second;
273         PopupMenu (_menu, ev.GetPosition());
274 }
275
276 /** Called when any gain value has changed */
277 void
278 AudioMappingView::map_values_changed ()
279 {
280         Changed (_map);
281         _last_tooltip_channels = optional<pair<int, int> >();
282         Refresh ();
283 }
284
285 void
286 AudioMappingView::off ()
287 {
288         _map.set (_menu_input, _menu_output, 0);
289         map_values_changed ();
290 }
291
292 void
293 AudioMappingView::full ()
294 {
295         _map.set (_menu_input, _menu_output, 1);
296         map_values_changed ();
297 }
298
299 void
300 AudioMappingView::minus6dB ()
301 {
302         _map.set (_menu_input, _menu_output, pow (10, -6.0 / 20));
303         map_values_changed ();
304 }
305
306 void
307 AudioMappingView::edit ()
308 {
309         int const d = _menu_output - 1;
310
311         AudioGainDialog* dialog = new AudioGainDialog (this, _menu_input, _menu_output - 1, _map.get(_menu_input, d));
312         if (dialog->ShowModal() == wxID_OK) {
313                 _map.set (_menu_input, d, dialog->value ());
314                 map_values_changed ();
315         }
316
317         dialog->Destroy ();
318 }
319
320 void
321 AudioMappingView::set_virtual_size ()
322 {
323         SetVirtualSize (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
324 }
325
326 void
327 AudioMappingView::set (AudioMapping map)
328 {
329         _map = map;
330         Refresh ();
331 }
332
333 void
334 AudioMappingView::set_input_channels (vector<string> const & names)
335 {
336         _input_channels = names;
337         set_virtual_size ();
338         Refresh ();
339 }
340
341 void
342 AudioMappingView::set_output_channels (vector<string> const & names)
343 {
344         _output_channels = names;
345         set_virtual_size ();
346         Refresh ();
347 }
348
349 void
350 AudioMappingView::motion (wxMouseEvent& ev)
351 {
352         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
353         if (!channels) {
354                 SetToolTip ("");
355                 _last_tooltip_channels = channels;
356                 return;
357         }
358
359         if (channels != _last_tooltip_channels) {
360                 wxString s;
361                 float const gain = _map.get(channels->first, channels->second);
362                 if (gain == 0) {
363                         s = wxString::Format (_("No audio will be passed from content channel %d to DCP channel %d."), channels->first + 1, channels->second + 1);
364                 } else if (gain == 1) {
365                         s = wxString::Format (_("Audio will be passed from content channel %d to DCP channel %d unaltered."), channels->first + 1, channels->second + 1);
366                 } else {
367                         float const dB = 20 * log10 (gain);
368                         s = wxString::Format (_("Audio will be passed from content channel %d to DCP channel %d with gain %.1fdB."), channels->first + 1, channels->second + 1, dB);
369                 }
370
371                 SetToolTip (s + " " + _("Right click to change gain."));
372                 _last_tooltip_channels = channels;
373         }
374
375         ev.Skip ();
376 }
377
378 void
379 AudioMappingView::set_input_groups (vector<Group> const & groups)
380 {
381         _input_groups = groups;
382 }