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