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