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