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