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