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