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