]> git.sesse.net Git - nageru/blob - nageru/midi_mapping_dialog.cpp
Fix a dangling reference (found by GCC 14).
[nageru] / nageru / midi_mapping_dialog.cpp
1 #include "midi_mapping_dialog.h"
2
3 #include <assert.h>
4 #include <google/protobuf/descriptor.h>
5 #include <google/protobuf/message.h>
6 #include <QComboBox>
7 #include <QDialogButtonBox>
8 #include <QFileDialog>
9 #include <QList>
10 #include <QMessageBox>
11 #include <QPushButton>
12 #include <QSpinBox>
13 #include <QStringList>
14 #include <QTreeWidget>
15 #include <memory>
16 #include <stdio.h>
17 #include <algorithm>
18 #include <cstddef>
19 #include <functional>
20 #include <limits>
21 #include <string>
22 #include <utility>
23 #include <vector>
24
25 #include "midi_mapper.h"
26 #include "midi_mapping.pb.h"
27 #include "nageru_midi_mapping.pb.h"
28 #include "shared/controller_spin_box.h"
29 #include "shared/midi_device.h"
30 #include "shared/midi_mapper_util.h"
31 #include "shared/post_to_main_thread.h"
32 #include "ui_midi_mapping.h"
33
34 class QObject;
35
36 using namespace google::protobuf;
37 using namespace std;
38
39 vector<MIDIMappingDialog::Control> per_bus_controllers = {
40         { "Stereo width",             MIDIMappingBusProto::kStereoWidthFieldNumber,
41                                       MIDIMappingProto::kStereoWidthBankFieldNumber },
42         { "Treble",                   MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber },
43         { "Mid",                      MIDIMappingBusProto::kMidFieldNumber,    MIDIMappingProto::kMidBankFieldNumber },
44         { "Bass",                     MIDIMappingBusProto::kBassFieldNumber,   MIDIMappingProto::kBassBankFieldNumber },
45         { "Gain",                     MIDIMappingBusProto::kGainFieldNumber,   MIDIMappingProto::kGainBankFieldNumber },
46         { "Compressor threshold",     MIDIMappingBusProto::kCompressorThresholdFieldNumber,
47                                       MIDIMappingProto::kCompressorThresholdBankFieldNumber},
48         { "Fader",                    MIDIMappingBusProto::kFaderFieldNumber,  MIDIMappingProto::kFaderBankFieldNumber }
49 };
50 vector<MIDIMappingDialog::Control> per_bus_buttons = {
51         { "Toggle mute",              MIDIMappingBusProto::kToggleMuteFieldNumber,
52                                       MIDIMappingProto::kToggleMuteBankFieldNumber },
53         { "Toggle locut",             MIDIMappingBusProto::kToggleLocutFieldNumber,
54                                       MIDIMappingProto::kToggleLocutBankFieldNumber },
55         { "Togle auto gain staging",  MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber,
56                                       MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber },
57         { "Togle compressor",         MIDIMappingBusProto::kToggleCompressorFieldNumber,
58                                       MIDIMappingProto::kToggleCompressorBankFieldNumber },
59         { "Clear peak",               MIDIMappingBusProto::kClearPeakFieldNumber,
60                                       MIDIMappingProto::kClearPeakBankFieldNumber }
61 };
62 vector<MIDIMappingDialog::Control> per_bus_lights = {
63         { "Is muted",                 MIDIMappingBusProto::kIsMutedFieldNumber, 0 },
64         { "Locut is on",              MIDIMappingBusProto::kLocutIsOnFieldNumber, 0 },
65         { "Auto gain staging is on",  MIDIMappingBusProto::kAutoGainStagingIsOnFieldNumber, 0 },
66         { "Compressor is on",         MIDIMappingBusProto::kCompressorIsOnFieldNumber, 0 },
67         { "Bus has peaked",           MIDIMappingBusProto::kHasPeakedFieldNumber, 0 }
68 };
69 vector<MIDIMappingDialog::Control> global_controllers = {
70         { "Locut cutoff",             MIDIMappingBusProto::kLocutFieldNumber,  MIDIMappingProto::kLocutBankFieldNumber },
71         { "Limiter threshold",        MIDIMappingBusProto::kLimiterThresholdFieldNumber,
72                                       MIDIMappingProto::kLimiterThresholdBankFieldNumber },
73         { "Makeup gain",              MIDIMappingBusProto::kMakeupGainFieldNumber,
74                                       MIDIMappingProto::kMakeupGainBankFieldNumber }
75 };
76 vector<MIDIMappingDialog::Control> global_buttons = {
77         { "Previous bank",            MIDIMappingBusProto::kPrevBankFieldNumber, 0 },
78         { "Next bank",                MIDIMappingBusProto::kNextBankFieldNumber, 0 },
79         { "Select bank 1",            MIDIMappingBusProto::kSelectBank1FieldNumber, 0 },
80         { "Select bank 2",            MIDIMappingBusProto::kSelectBank2FieldNumber, 0 },
81         { "Select bank 3",            MIDIMappingBusProto::kSelectBank3FieldNumber, 0 },
82         { "Select bank 4",            MIDIMappingBusProto::kSelectBank4FieldNumber, 0 },
83         { "Select bank 5",            MIDIMappingBusProto::kSelectBank5FieldNumber, 0 },
84         { "Toggle limiter",           MIDIMappingBusProto::kToggleLimiterFieldNumber, MIDIMappingProto::kToggleLimiterBankFieldNumber },
85         { "Toggle auto makeup gain",  MIDIMappingBusProto::kToggleAutoMakeupGainFieldNumber, MIDIMappingProto::kToggleAutoMakeupGainBankFieldNumber }
86 };
87 vector<MIDIMappingDialog::Control> global_lights = {
88         { "Bank 1 is selected",       MIDIMappingBusProto::kBank1IsSelectedFieldNumber, 0 },
89         { "Bank 2 is selected",       MIDIMappingBusProto::kBank2IsSelectedFieldNumber, 0 },
90         { "Bank 3 is selected",       MIDIMappingBusProto::kBank3IsSelectedFieldNumber, 0 },
91         { "Bank 4 is selected",       MIDIMappingBusProto::kBank4IsSelectedFieldNumber, 0 },
92         { "Bank 5 is selected",       MIDIMappingBusProto::kBank5IsSelectedFieldNumber, 0 },
93         { "Limiter is on",            MIDIMappingBusProto::kLimiterIsOnFieldNumber, 0 },
94         { "Auto makeup gain is on",   MIDIMappingBusProto::kAutoMakeupGainIsOnFieldNumber, 0 },
95 };
96
97 vector<MIDIMappingDialog::Control> global_video = {
98         { "Switch video channel",     MIDIMappingBusProto::kSwitchVideoChannelFieldNumber, MIDIMappingProto::kSwitchVideoChannelBankFieldNumber },
99         { "Apply transition",         MIDIMappingBusProto::kApplyTransitionFieldNumber, MIDIMappingProto::kApplyTransitionBankFieldNumber },
100 };
101
102 vector<MIDIMappingDialog::Control> main_ui = {
103         { "Previous audio view",       MIDIMappingBusProto::kPrevAudioViewFieldNumber, MIDIMappingProto::kPrevAudioViewBankFieldNumber },
104         { "Next audio view",           MIDIMappingBusProto::kNextAudioViewFieldNumber, MIDIMappingProto::kNextAudioViewBankFieldNumber },
105         { "Begin new video segment",   MIDIMappingBusProto::kBeginNewVideoSegmentFieldNumber, MIDIMappingProto::kBeginNewVideoSegmentBankFieldNumber },
106         { "Exit Nageru",               MIDIMappingBusProto::kExitFieldNumber, MIDIMappingProto::kExitBankFieldNumber },
107 };
108
109 namespace {
110
111 int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value)
112 {
113         const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number);
114         const Reflection *reflection = mapping_proto.GetReflection();
115         if (!reflection->HasField(mapping_proto, bank_descriptor)) {
116                 return default_value;
117         }
118         return reflection->GetInt32(mapping_proto, bank_descriptor);
119 }
120
121 int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
122 {
123         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
124                 return default_value;
125         }
126         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
127         return get_controller_mapping_helper(bus_mapping, field_number, default_value);
128 }
129
130 int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
131 {
132         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
133                 return default_value;
134         }
135
136         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
137         return get_button_mapping_helper(bus_mapping, field_number, default_value);
138 }
139
140 int get_light_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
141 {
142         if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
143                 return default_value;
144         }
145
146         const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
147         const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
148         const Reflection *bus_reflection = bus_mapping.GetReflection();
149         if (!bus_reflection->HasField(bus_mapping, descriptor)) {
150                 return default_value;
151         }
152         const MIDILightProto &bus_proto =
153                 static_cast<const MIDILightProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
154         return bus_proto.note_number();
155 }
156
157 }  // namespace
158
159 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
160         : ui(new Ui::MIDIMappingDialog),
161           mapper(mapper)
162 {
163         ui->setupUi(this);
164
165         const MIDIMappingProto mapping_proto = mapper->get_current_mapping();  // Take a copy.
166         old_receiver = mapper->set_receiver(this);
167
168         QStringList labels;
169         labels << "";
170         labels << "Controller bank";
171         for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
172                 char buf[256];
173                 snprintf(buf, sizeof(buf), "Bus %d", bus_idx + 1);
174                 labels << buf;
175         }
176         labels << "";
177         ui->treeWidget->setColumnCount(num_buses + 3);
178         ui->treeWidget->setHeaderLabels(labels);
179
180         add_controls("Per-bus controllers", ControlType::CONTROLLER, SpinnerGroup::PER_BUS_CONTROLLERS, mapping_proto, per_bus_controllers);
181         add_controls("Per-bus buttons", ControlType::BUTTON, SpinnerGroup::PER_BUS_BUTTONS, mapping_proto, per_bus_buttons);
182         add_controls("Per-bus lights", ControlType::LIGHT, SpinnerGroup::PER_BUS_LIGHTS, mapping_proto, per_bus_lights);
183         add_controls("Video mixing", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, global_video);
184         add_controls("Global controllers", ControlType::CONTROLLER, SpinnerGroup::GLOBAL_CONTROLLERS, mapping_proto, global_controllers);
185         add_controls("Global buttons", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, global_buttons);
186         add_controls("Main UI", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, main_ui);
187         add_controls("Global lights", ControlType::LIGHT, SpinnerGroup::GLOBAL_LIGHTS, mapping_proto, global_lights);
188         fill_controls_from_mapping(mapping_proto);
189
190         // Auto-resize every column but the last.
191         for (unsigned column_idx = 0; column_idx < num_buses + 3; ++column_idx) {
192                 ui->treeWidget->resizeColumnToContents(column_idx);
193         }
194
195         connect(ui->guess_bus_button, &QPushButton::clicked,
196                 bind(&MIDIMappingDialog::guess_clicked, this, false));
197         connect(ui->guess_group_button, &QPushButton::clicked,
198                 bind(&MIDIMappingDialog::guess_clicked, this, true));
199         connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked);
200         connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked);
201         connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked);
202         connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked);
203
204         update_guess_button_state();
205 }
206
207 MIDIMappingDialog::~MIDIMappingDialog()
208 {
209         mapper->set_receiver(old_receiver);
210         mapper->refresh_highlights();
211 }
212
213 bool MIDIMappingDialog::eventFilter(QObject *obj, QEvent *event)
214 {
215         if (event->type() == QEvent::FocusIn ||
216             event->type() == QEvent::FocusOut) {
217                 // We ignore the guess buttons themselves; it should be allowed
218                 // to navigate from a spinner to focus on a button (to click it).
219                 if (obj != ui->guess_bus_button && obj != ui->guess_group_button) {
220                         update_guess_button_state();
221                 }
222         }
223         return false;
224 }
225
226 void MIDIMappingDialog::guess_clicked(bool limit_to_group)
227 {
228         FocusInfo focus = find_focus();
229         if (focus.bus_idx == -1) {
230                 // The guess button probably took the focus away from us.
231                 focus = last_focus;
232         }
233         assert(focus.bus_idx != -1);  // The button should have been disabled.
234         pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, limit_to_group ? focus.spinner_group : SpinnerGroup::ALL_GROUPS);
235         const int source_bus_idx = bus_and_offset.first;
236         const int offset = bus_and_offset.second;
237         assert(source_bus_idx != -1);  // The button should have been disabled.
238
239         for (const auto &field_number_and_spinner : spinners[focus.bus_idx]) {
240                 int field_number = field_number_and_spinner.first;
241                 QSpinBox *spinner = field_number_and_spinner.second.spinner;
242                 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
243
244                 if (limit_to_group && this_spinner_group != focus.spinner_group) {
245                         continue;
246                 }
247
248                 assert(spinners[source_bus_idx].count(field_number));
249                 QSpinBox *source_spinner = spinners[source_bus_idx][field_number].spinner;
250                 assert(spinners[source_bus_idx][field_number].group == this_spinner_group);
251
252                 if (source_spinner->value() != -1) {
253                         spinner->setValue(source_spinner->value() + offset);
254                 }
255         }
256
257         // See if we can find a “next” bus to move the focus to.
258         const int next_bus_idx = focus.bus_idx + (focus.bus_idx - source_bus_idx);  // Note: Could become e.g. -1.
259         for (const InstantiatedSpinner &is : controller_spinners) {
260                 if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) {
261                         is.spinner->setFocus();
262                 }
263         }
264         for (const InstantiatedSpinner &is : button_spinners) {
265                 if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) {
266                         is.spinner->setFocus();
267                 }
268         }
269         for (const InstantiatedSpinner &is : light_spinners) {
270                 if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) {
271                         is.spinner->setFocus();
272                 }
273         }
274 }
275
276 void MIDIMappingDialog::ok_clicked()
277 {
278         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
279         mapper->set_midi_mapping(*new_mapping);
280         mapper->set_receiver(old_receiver);
281         accept();
282 }
283
284 void MIDIMappingDialog::cancel_clicked()
285 {
286         mapper->set_receiver(old_receiver);
287         reject();
288 }
289
290 void MIDIMappingDialog::save_clicked()
291 {
292 #if HAVE_CEF
293         // The native file dialog uses GTK+, which interferes with CEF's use of the GLib main loop.
294         QFileDialog::Options options(QFileDialog::DontUseNativeDialog);
295 #else
296         QFileDialog::Options options;
297 #endif
298         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
299         QString filename = QFileDialog::getSaveFileName(this,
300                 "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options);
301         if (!filename.endsWith(".midimapping")) {
302                 filename += ".midimapping";
303         }
304         if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) {
305                 QMessageBox box;
306                 box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
307                 box.exec();
308         }
309 }
310
311 void MIDIMappingDialog::load_clicked()
312 {
313 #if HAVE_CEF
314         // The native file dialog uses GTK+, which interferes with CEF's use of the GLib main loop.
315         QFileDialog::Options options(QFileDialog::DontUseNativeDialog);
316 #else
317         QFileDialog::Options options;
318 #endif
319         QString filename = QFileDialog::getOpenFileName(this,
320                 "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options);
321         MIDIMappingProto new_mapping;
322         if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) {
323                 QMessageBox box;
324                 box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
325                 box.exec();
326                 return;
327         }
328
329         fill_controls_from_mapping(new_mapping);
330 }
331
332 namespace {
333
334 template<class T>
335 T *get_mutable_bus_message(MIDIMappingProto *mapping_proto, unsigned bus_idx, int field_number)
336 {
337         while (size_t(mapping_proto->bus_mapping_size()) <= bus_idx) {
338                 mapping_proto->add_bus_mapping();
339         }
340
341         MIDIMappingBusProto *bus_mapping = mapping_proto->mutable_bus_mapping(bus_idx);
342         const FieldDescriptor *descriptor = bus_mapping->GetDescriptor()->FindFieldByNumber(field_number);
343         const Reflection *bus_reflection = bus_mapping->GetReflection();
344         return static_cast<T *>(bus_reflection->MutableMessage(bus_mapping, descriptor));
345 }
346
347 }  // namespace
348
349 unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
350 {
351         unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
352         for (const InstantiatedSpinner &is : controller_spinners) {
353                 const int val = is.spinner->value();
354                 if (val == -1) {
355                         continue;
356                 }
357
358                 MIDIControllerProto *controller_proto =
359                         get_mutable_bus_message<MIDIControllerProto>(mapping_proto.get(), is.bus_idx, is.field_number);
360                 controller_proto->set_controller_number(val);
361         }
362         for (const InstantiatedSpinner &is : button_spinners) {
363                 const int val = is.spinner->value();
364                 if (val == -1) {
365                         continue;
366                 }
367
368                 MIDIButtonProto *button_proto =
369                         get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
370                 button_proto->set_note_number(val);
371         }
372         for (const InstantiatedSpinner &is : light_spinners) {
373                 const int val = is.spinner->value();
374                 if (val == -1) {
375                         continue;
376                 }
377
378                 MIDILightProto *light_proto =
379                         get_mutable_bus_message<MIDILightProto>(mapping_proto.get(), is.bus_idx, is.field_number);
380                 light_proto->set_note_number(val);
381         }
382         int highest_bank_used = 0;  // 1-indexed.
383         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
384                 const int val = ic.combo_box->currentIndex();
385                 highest_bank_used = std::max(highest_bank_used, val);
386                 if (val == 0) {
387                         continue;
388                 }
389
390                 const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number);
391                 const Reflection *bus_reflection = mapping_proto->GetReflection();
392                 bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1);
393         }
394         mapping_proto->set_num_controller_banks(highest_bank_used);
395         return mapping_proto;
396 }
397
398 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
399 {
400         if (bank_field_number == 0) {
401                 return;
402         }
403         QComboBox *bank_selector = new QComboBox(this);
404         bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
405         bank_selector->setAutoFillBackground(true);
406
407         bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
408
409         ui->treeWidget->setItemWidget(item, 1, bank_selector);
410 }
411
412 void MIDIMappingDialog::add_controls(const string &heading,
413                                      MIDIMappingDialog::ControlType control_type,
414                                      MIDIMappingDialog::SpinnerGroup spinner_group,
415                                      const MIDIMappingProto &mapping_proto,
416                                      const vector<MIDIMappingDialog::Control> &controls)
417 {
418         QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
419         heading_item->setText(0, QString::fromStdString(heading));
420         heading_item->setFirstColumnSpanned(true);
421         heading_item->setExpanded(true);
422         for (const Control &control : controls) {
423                 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
424                 heading_item->addChild(item);
425                 add_bank_selector(item, mapping_proto, control.bank_field_number);
426                 item->setText(0, QString::fromStdString(control.label + "   "));
427
428                 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
429                         QSpinBox *spinner;
430                         if (control_type == ControlType::CONTROLLER) {
431                                 spinner = new ControllerSpinBox(this);
432                                 spinner->setRange(-1, 128);  // 128 for pitch bend.
433                         } else {
434                                 spinner = new QSpinBox(this);
435                                 spinner->setRange(-1, 127);
436                         }
437                         spinner->setAutoFillBackground(true);
438                         spinner->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
439                         spinner->installEventFilter(this);  // So we know when the focus changes.
440                         ui->treeWidget->setItemWidget(item, bus_idx + 2, spinner);
441
442                         if (control_type == ControlType::CONTROLLER) {
443                                 controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
444                         } else if (control_type == ControlType::BUTTON) {
445                                 button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
446                         } else {
447                                 assert(control_type == ControlType::LIGHT);
448                                 light_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
449                         }
450                         spinners[bus_idx][control.field_number] = SpinnerAndGroup{ spinner, spinner_group };
451                         connect(spinner, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
452                                 bind(&MIDIMappingDialog::update_guess_button_state, this));
453                 }
454         }
455         ui->treeWidget->addTopLevelItem(heading_item);
456 }
457
458 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
459 {
460         for (const InstantiatedSpinner &is : controller_spinners) {
461                 is.spinner->setValue(get_controller_mapping(mapping_proto, is.bus_idx, is.field_number, -1));
462         }
463         for (const InstantiatedSpinner &is : button_spinners) {
464                 is.spinner->setValue(get_button_mapping(mapping_proto, is.bus_idx, is.field_number, -1));
465         }
466         for (const InstantiatedSpinner &is : light_spinners) {
467                 is.spinner->setValue(get_light_mapping(mapping_proto, is.bus_idx, is.field_number, -1));
468         }
469         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
470                 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
471         }
472 }
473
474 void MIDIMappingDialog::controller_changed(unsigned controller)
475 {
476         post_to_main_thread([=]{
477                 for (const InstantiatedSpinner &is : controller_spinners) {
478                         if (is.spinner->hasFocus()) {
479                                 is.spinner->setValue(controller);
480                                 is.spinner->selectAll();
481                         }
482                 }
483         });
484 }
485
486 void MIDIMappingDialog::note_on(unsigned note)
487 {
488         post_to_main_thread([=]{
489                 for (const InstantiatedSpinner &is : button_spinners) {
490                         if (is.spinner->hasFocus()) {
491                                 is.spinner->setValue(note);
492                                 is.spinner->selectAll();
493                         }
494                 }
495                 for (const InstantiatedSpinner &is : light_spinners) {
496                         if (is.spinner->hasFocus()) {
497                                 is.spinner->setValue(note);
498                                 is.spinner->selectAll();
499                         }
500                 }
501         });
502 }
503
504 pair<int, int> MIDIMappingDialog::guess_offset(unsigned bus_idx, MIDIMappingDialog::SpinnerGroup spinner_group)
505 {
506         constexpr pair<int, int> not_found(-1, 0);
507
508         if (bus_is_empty(bus_idx, spinner_group)) {
509                 return not_found;
510         }
511
512         // See if we can find a non-empty bus to source from (prefer from the left).
513         unsigned source_bus_idx;
514         if (bus_idx > 0 && !bus_is_empty(bus_idx - 1, spinner_group)) {
515                 source_bus_idx = bus_idx - 1;
516         } else if (bus_idx < num_buses - 1 && !bus_is_empty(bus_idx + 1, spinner_group)) {
517                 source_bus_idx = bus_idx + 1;
518         } else {
519                 return not_found;
520         }
521
522         // See if we can find a consistent offset.
523         bool found_offset = false;
524         int offset = 0;
525         int minimum_allowed_offset = numeric_limits<int>::min();
526         int maximum_allowed_offset = numeric_limits<int>::max();
527         for (const auto &field_number_and_spinner : spinners[bus_idx]) {
528                 int field_number = field_number_and_spinner.first;
529                 QSpinBox *spinner = field_number_and_spinner.second.spinner;
530                 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
531                 assert(spinners[source_bus_idx].count(field_number));
532                 QSpinBox *source_spinner = spinners[source_bus_idx][field_number].spinner;
533                 assert(spinners[source_bus_idx][field_number].group == this_spinner_group);
534
535                 if (spinner_group != SpinnerGroup::ALL_GROUPS &&
536                     spinner_group != this_spinner_group) {
537                         continue;
538                 }
539                 if (spinner->value() == -1) {
540                         if (source_spinner->value() != -1) {
541                                 // If the source value is e.g. 3, offset can't be less than -2 or larger than 124.
542                                 // Otherwise, we'd extrapolate values outside [1..127].
543                                 minimum_allowed_offset = max(minimum_allowed_offset, 1 - source_spinner->value());
544                                 maximum_allowed_offset = min(maximum_allowed_offset, 127 - source_spinner->value());
545                         }
546                         continue;
547                 }
548                 if (source_spinner->value() == -1) {
549                         // The bus has a controller set that the source bus doesn't set.
550                         return not_found;
551                 }
552                 if (source_spinner->value() == MIDIReceiver::PITCH_BEND_CONTROLLER) {
553                         // It's impossible to interpolate across the pitch bend.
554                         return not_found;
555                 }
556
557                 int candidate_offset = spinner->value() - source_spinner->value();
558                 if (!found_offset) {
559                         offset = candidate_offset;
560                         found_offset = true;
561                 } else if (candidate_offset != offset) {
562                         return not_found;
563                 }
564         }
565
566         if (!found_offset) {
567                 // Given that the bus wasn't empty, this shouldn't happen.
568                 assert(false);
569                 return not_found;
570         }
571
572         if (offset < minimum_allowed_offset || offset > maximum_allowed_offset) {
573                 return not_found;
574         }
575         return make_pair(source_bus_idx, offset);
576 }
577
578 bool MIDIMappingDialog::bus_is_empty(unsigned bus_idx, SpinnerGroup spinner_group)
579 {
580         for (const auto &field_number_and_spinner : spinners[bus_idx]) {
581                 QSpinBox *spinner = field_number_and_spinner.second.spinner;
582                 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
583                 if (spinner_group != SpinnerGroup::ALL_GROUPS &&
584                     spinner_group != this_spinner_group) {
585                         continue;
586                 }
587                 if (spinner->value() != -1) {
588                         return false;
589                 }
590         }
591         return true;
592 }
593
594 void MIDIMappingDialog::update_guess_button_state()
595 {
596         FocusInfo focus = find_focus();
597         if (focus.bus_idx < 0) {
598                 return;
599         }
600         {
601                 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, SpinnerGroup::ALL_GROUPS);
602                 ui->guess_bus_button->setEnabled(bus_and_offset.first != -1);
603         }
604         {
605                 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, focus.spinner_group);
606                 ui->guess_group_button->setEnabled(bus_and_offset.first != -1);
607         }
608         last_focus = focus;
609 }
610
611 MIDIMappingDialog::FocusInfo MIDIMappingDialog::find_focus() const
612 {
613         for (const InstantiatedSpinner &is : controller_spinners) {
614                 if (is.spinner->hasFocus()) {
615                         return FocusInfo{ int(is.bus_idx), is.spinner_group, is.field_number };
616                 }
617         }
618         for (const InstantiatedSpinner &is : button_spinners) {
619                 if (is.spinner->hasFocus()) {
620                         return FocusInfo{ int(is.bus_idx), is.spinner_group, is.field_number };
621                 }
622         }
623         for (const InstantiatedSpinner &is : light_spinners) {
624                 if (is.spinner->hasFocus()) {
625                         return FocusInfo{ int(is.bus_idx), is.spinner_group, is.field_number };
626                 }
627         }
628         return FocusInfo{ -1, SpinnerGroup::ALL_GROUPS, -1 };
629 }