1 #include "midi_mapping_dialog.h"
4 #include <google/protobuf/descriptor.h>
5 #include <google/protobuf/message.h>
7 #include <QDialogButtonBox>
10 #include <QMessageBox>
11 #include <QPushButton>
13 #include <QStringList>
14 #include <QTreeWidget>
25 #include "midi_mapper.h"
26 #include "midi_mapping.pb.h"
27 #include "nageru_midi_mapping.pb.h"
28 #include "shared/controller_spin_box.h"
29 #include "shared/midi_device.h"
30 #include "shared/midi_mapper_util.h"
31 #include "shared/post_to_main_thread.h"
32 #include "ui_midi_mapping.h"
36 using namespace google::protobuf;
39 vector<MIDIMappingDialog::Control> per_bus_controllers = {
40 { "Stereo width", MIDIMappingBusProto::kStereoWidthFieldNumber,
41 MIDIMappingProto::kStereoWidthBankFieldNumber },
42 { "Treble", MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber },
43 { "Mid", MIDIMappingBusProto::kMidFieldNumber, MIDIMappingProto::kMidBankFieldNumber },
44 { "Bass", MIDIMappingBusProto::kBassFieldNumber, MIDIMappingProto::kBassBankFieldNumber },
45 { "Gain", MIDIMappingBusProto::kGainFieldNumber, MIDIMappingProto::kGainBankFieldNumber },
46 { "Compressor threshold", MIDIMappingBusProto::kCompressorThresholdFieldNumber,
47 MIDIMappingProto::kCompressorThresholdBankFieldNumber},
48 { "Fader", MIDIMappingBusProto::kFaderFieldNumber, MIDIMappingProto::kFaderBankFieldNumber }
50 vector<MIDIMappingDialog::Control> per_bus_buttons = {
51 { "Toggle mute", MIDIMappingBusProto::kToggleMuteFieldNumber,
52 MIDIMappingProto::kToggleMuteBankFieldNumber },
53 { "Toggle locut", MIDIMappingBusProto::kToggleLocutFieldNumber,
54 MIDIMappingProto::kToggleLocutBankFieldNumber },
55 { "Togle auto gain staging", MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber,
56 MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber },
57 { "Togle compressor", MIDIMappingBusProto::kToggleCompressorFieldNumber,
58 MIDIMappingProto::kToggleCompressorBankFieldNumber },
59 { "Clear peak", MIDIMappingBusProto::kClearPeakFieldNumber,
60 MIDIMappingProto::kClearPeakBankFieldNumber }
62 vector<MIDIMappingDialog::Control> per_bus_lights = {
63 { "Is muted", MIDIMappingBusProto::kIsMutedFieldNumber, 0 },
64 { "Locut is on", MIDIMappingBusProto::kLocutIsOnFieldNumber, 0 },
65 { "Auto gain staging is on", MIDIMappingBusProto::kAutoGainStagingIsOnFieldNumber, 0 },
66 { "Compressor is on", MIDIMappingBusProto::kCompressorIsOnFieldNumber, 0 },
67 { "Bus has peaked", MIDIMappingBusProto::kHasPeakedFieldNumber, 0 }
69 vector<MIDIMappingDialog::Control> global_controllers = {
70 { "Locut cutoff", MIDIMappingBusProto::kLocutFieldNumber, MIDIMappingProto::kLocutBankFieldNumber },
71 { "Limiter threshold", MIDIMappingBusProto::kLimiterThresholdFieldNumber,
72 MIDIMappingProto::kLimiterThresholdBankFieldNumber },
73 { "Makeup gain", MIDIMappingBusProto::kMakeupGainFieldNumber,
74 MIDIMappingProto::kMakeupGainBankFieldNumber }
76 vector<MIDIMappingDialog::Control> global_buttons = {
77 { "Previous bank", MIDIMappingBusProto::kPrevBankFieldNumber, 0 },
78 { "Next bank", MIDIMappingBusProto::kNextBankFieldNumber, 0 },
79 { "Select bank 1", MIDIMappingBusProto::kSelectBank1FieldNumber, 0 },
80 { "Select bank 2", MIDIMappingBusProto::kSelectBank2FieldNumber, 0 },
81 { "Select bank 3", MIDIMappingBusProto::kSelectBank3FieldNumber, 0 },
82 { "Select bank 4", MIDIMappingBusProto::kSelectBank4FieldNumber, 0 },
83 { "Select bank 5", MIDIMappingBusProto::kSelectBank5FieldNumber, 0 },
84 { "Toggle limiter", MIDIMappingBusProto::kToggleLimiterFieldNumber, MIDIMappingProto::kToggleLimiterBankFieldNumber },
85 { "Toggle auto makeup gain", MIDIMappingBusProto::kToggleAutoMakeupGainFieldNumber, MIDIMappingProto::kToggleAutoMakeupGainBankFieldNumber }
87 vector<MIDIMappingDialog::Control> global_lights = {
88 { "Bank 1 is selected", MIDIMappingBusProto::kBank1IsSelectedFieldNumber, 0 },
89 { "Bank 2 is selected", MIDIMappingBusProto::kBank2IsSelectedFieldNumber, 0 },
90 { "Bank 3 is selected", MIDIMappingBusProto::kBank3IsSelectedFieldNumber, 0 },
91 { "Bank 4 is selected", MIDIMappingBusProto::kBank4IsSelectedFieldNumber, 0 },
92 { "Bank 5 is selected", MIDIMappingBusProto::kBank5IsSelectedFieldNumber, 0 },
93 { "Limiter is on", MIDIMappingBusProto::kLimiterIsOnFieldNumber, 0 },
94 { "Auto makeup gain is on", MIDIMappingBusProto::kAutoMakeupGainIsOnFieldNumber, 0 },
97 vector<MIDIMappingDialog::Control> global_video = {
98 { "Switch video channel", MIDIMappingBusProto::kSwitchVideoChannelFieldNumber, MIDIMappingProto::kSwitchVideoChannelBankFieldNumber },
99 { "Apply transition", MIDIMappingBusProto::kApplyTransitionFieldNumber, MIDIMappingProto::kApplyTransitionBankFieldNumber },
102 vector<MIDIMappingDialog::Control> main_ui = {
103 { "Previous audio view", MIDIMappingBusProto::kPrevAudioViewFieldNumber, MIDIMappingProto::kPrevAudioViewBankFieldNumber },
104 { "Next audio view", MIDIMappingBusProto::kNextAudioViewFieldNumber, MIDIMappingProto::kNextAudioViewBankFieldNumber },
105 { "Begin new video segment", MIDIMappingBusProto::kBeginNewVideoSegmentFieldNumber, MIDIMappingProto::kBeginNewVideoSegmentBankFieldNumber },
106 { "Exit Nageru", MIDIMappingBusProto::kExitFieldNumber, MIDIMappingProto::kExitBankFieldNumber },
111 int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value)
113 const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number);
114 const Reflection *reflection = mapping_proto.GetReflection();
115 if (!reflection->HasField(mapping_proto, bank_descriptor)) {
116 return default_value;
118 return reflection->GetInt32(mapping_proto, bank_descriptor);
121 int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
123 if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
124 return default_value;
126 const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
127 return get_controller_mapping_helper(bus_mapping, field_number, default_value);
130 int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
132 if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
133 return default_value;
136 const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
137 return get_button_mapping_helper(bus_mapping, field_number, default_value);
140 int get_light_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
142 if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
143 return default_value;
146 const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
147 const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
148 const Reflection *bus_reflection = bus_mapping.GetReflection();
149 if (!bus_reflection->HasField(bus_mapping, descriptor)) {
150 return default_value;
152 const MIDILightProto &bus_proto =
153 static_cast<const MIDILightProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
154 return bus_proto.note_number();
159 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
160 : ui(new Ui::MIDIMappingDialog),
165 const MIDIMappingProto mapping_proto = mapper->get_current_mapping(); // Take a copy.
166 old_receiver = mapper->set_receiver(this);
170 labels << "Controller bank";
171 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
173 snprintf(buf, sizeof(buf), "Bus %d", bus_idx + 1);
177 ui->treeWidget->setColumnCount(num_buses + 3);
178 ui->treeWidget->setHeaderLabels(labels);
180 add_controls("Per-bus controllers", ControlType::CONTROLLER, SpinnerGroup::PER_BUS_CONTROLLERS, mapping_proto, per_bus_controllers);
181 add_controls("Per-bus buttons", ControlType::BUTTON, SpinnerGroup::PER_BUS_BUTTONS, mapping_proto, per_bus_buttons);
182 add_controls("Per-bus lights", ControlType::LIGHT, SpinnerGroup::PER_BUS_LIGHTS, mapping_proto, per_bus_lights);
183 add_controls("Video mixing", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, global_video);
184 add_controls("Global controllers", ControlType::CONTROLLER, SpinnerGroup::GLOBAL_CONTROLLERS, mapping_proto, global_controllers);
185 add_controls("Global buttons", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, global_buttons);
186 add_controls("Main UI", ControlType::BUTTON, SpinnerGroup::GLOBAL_BUTTONS, mapping_proto, main_ui);
187 add_controls("Global lights", ControlType::LIGHT, SpinnerGroup::GLOBAL_LIGHTS, mapping_proto, global_lights);
188 fill_controls_from_mapping(mapping_proto);
190 // Auto-resize every column but the last.
191 for (unsigned column_idx = 0; column_idx < num_buses + 3; ++column_idx) {
192 ui->treeWidget->resizeColumnToContents(column_idx);
195 connect(ui->guess_bus_button, &QPushButton::clicked,
196 bind(&MIDIMappingDialog::guess_clicked, this, false));
197 connect(ui->guess_group_button, &QPushButton::clicked,
198 bind(&MIDIMappingDialog::guess_clicked, this, true));
199 connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked);
200 connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked);
201 connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked);
202 connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked);
204 update_guess_button_state();
207 MIDIMappingDialog::~MIDIMappingDialog()
209 mapper->set_receiver(old_receiver);
210 mapper->refresh_highlights();
213 bool MIDIMappingDialog::eventFilter(QObject *obj, QEvent *event)
215 if (event->type() == QEvent::FocusIn ||
216 event->type() == QEvent::FocusOut) {
217 // We ignore the guess buttons themselves; it should be allowed
218 // to navigate from a spinner to focus on a button (to click it).
219 if (obj != ui->guess_bus_button && obj != ui->guess_group_button) {
220 update_guess_button_state();
226 void MIDIMappingDialog::guess_clicked(bool limit_to_group)
228 FocusInfo focus = find_focus();
229 if (focus.bus_idx == -1) {
230 // The guess button probably took the focus away from us.
233 assert(focus.bus_idx != -1); // The button should have been disabled.
234 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, limit_to_group ? focus.spinner_group : SpinnerGroup::ALL_GROUPS);
235 const int source_bus_idx = bus_and_offset.first;
236 const int offset = bus_and_offset.second;
237 assert(source_bus_idx != -1); // The button should have been disabled.
239 for (const auto &field_number_and_spinner : spinners[focus.bus_idx]) {
240 int field_number = field_number_and_spinner.first;
241 QSpinBox *spinner = field_number_and_spinner.second.spinner;
242 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
244 if (limit_to_group && this_spinner_group != focus.spinner_group) {
248 assert(spinners[source_bus_idx].count(field_number));
249 QSpinBox *source_spinner = spinners[source_bus_idx][field_number].spinner;
250 assert(spinners[source_bus_idx][field_number].group == this_spinner_group);
252 if (source_spinner->value() != -1) {
253 spinner->setValue(source_spinner->value() + offset);
257 // See if we can find a “next” bus to move the focus to.
258 const int next_bus_idx = focus.bus_idx + (focus.bus_idx - source_bus_idx); // Note: Could become e.g. -1.
259 for (const InstantiatedSpinner &is : controller_spinners) {
260 if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) {
261 is.spinner->setFocus();
264 for (const InstantiatedSpinner &is : button_spinners) {
265 if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) {
266 is.spinner->setFocus();
269 for (const InstantiatedSpinner &is : light_spinners) {
270 if (int(is.bus_idx) == next_bus_idx && is.field_number == focus.field_number) {
271 is.spinner->setFocus();
276 void MIDIMappingDialog::ok_clicked()
278 unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
279 mapper->set_midi_mapping(*new_mapping);
280 mapper->set_receiver(old_receiver);
284 void MIDIMappingDialog::cancel_clicked()
286 mapper->set_receiver(old_receiver);
290 void MIDIMappingDialog::save_clicked()
293 // The native file dialog uses GTK+, which interferes with CEF's use of the GLib main loop.
294 QFileDialog::Options options(QFileDialog::DontUseNativeDialog);
296 QFileDialog::Options options;
298 unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
299 QString filename = QFileDialog::getSaveFileName(this,
300 "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options);
301 if (!filename.endsWith(".midimapping")) {
302 filename += ".midimapping";
304 if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) {
306 box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
311 void MIDIMappingDialog::load_clicked()
314 // The native file dialog uses GTK+, which interferes with CEF's use of the GLib main loop.
315 QFileDialog::Options options(QFileDialog::DontUseNativeDialog);
317 QFileDialog::Options options;
319 QString filename = QFileDialog::getOpenFileName(this,
320 "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options);
321 MIDIMappingProto new_mapping;
322 if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) {
324 box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
329 fill_controls_from_mapping(new_mapping);
335 T *get_mutable_bus_message(MIDIMappingProto *mapping_proto, unsigned bus_idx, int field_number)
337 while (size_t(mapping_proto->bus_mapping_size()) <= bus_idx) {
338 mapping_proto->add_bus_mapping();
341 MIDIMappingBusProto *bus_mapping = mapping_proto->mutable_bus_mapping(bus_idx);
342 const FieldDescriptor *descriptor = bus_mapping->GetDescriptor()->FindFieldByNumber(field_number);
343 const Reflection *bus_reflection = bus_mapping->GetReflection();
344 return static_cast<T *>(bus_reflection->MutableMessage(bus_mapping, descriptor));
349 unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
351 unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
352 for (const InstantiatedSpinner &is : controller_spinners) {
353 const int val = is.spinner->value();
358 MIDIControllerProto *controller_proto =
359 get_mutable_bus_message<MIDIControllerProto>(mapping_proto.get(), is.bus_idx, is.field_number);
360 controller_proto->set_controller_number(val);
362 for (const InstantiatedSpinner &is : button_spinners) {
363 const int val = is.spinner->value();
368 MIDIButtonProto *button_proto =
369 get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
370 button_proto->set_note_number(val);
372 for (const InstantiatedSpinner &is : light_spinners) {
373 const int val = is.spinner->value();
378 MIDILightProto *light_proto =
379 get_mutable_bus_message<MIDILightProto>(mapping_proto.get(), is.bus_idx, is.field_number);
380 light_proto->set_note_number(val);
382 int highest_bank_used = 0; // 1-indexed.
383 for (const InstantiatedComboBox &ic : bank_combo_boxes) {
384 const int val = ic.combo_box->currentIndex();
385 highest_bank_used = std::max(highest_bank_used, val);
390 const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number);
391 const Reflection *bus_reflection = mapping_proto->GetReflection();
392 bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1);
394 mapping_proto->set_num_controller_banks(highest_bank_used);
395 return mapping_proto;
398 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
400 if (bank_field_number == 0) {
403 QComboBox *bank_selector = new QComboBox(this);
404 bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
405 bank_selector->setAutoFillBackground(true);
407 bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
409 ui->treeWidget->setItemWidget(item, 1, bank_selector);
412 void MIDIMappingDialog::add_controls(const string &heading,
413 MIDIMappingDialog::ControlType control_type,
414 MIDIMappingDialog::SpinnerGroup spinner_group,
415 const MIDIMappingProto &mapping_proto,
416 const vector<MIDIMappingDialog::Control> &controls)
418 QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
419 heading_item->setText(0, QString::fromStdString(heading));
420 heading_item->setFirstColumnSpanned(true);
421 heading_item->setExpanded(true);
422 for (const Control &control : controls) {
423 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
424 heading_item->addChild(item);
425 add_bank_selector(item, mapping_proto, control.bank_field_number);
426 item->setText(0, QString::fromStdString(control.label + " "));
428 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
430 if (control_type == ControlType::CONTROLLER) {
431 spinner = new ControllerSpinBox(this);
432 spinner->setRange(-1, 128); // 128 for pitch bend.
434 spinner = new QSpinBox(this);
435 spinner->setRange(-1, 127);
437 spinner->setAutoFillBackground(true);
438 spinner->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty).
439 spinner->installEventFilter(this); // So we know when the focus changes.
440 ui->treeWidget->setItemWidget(item, bus_idx + 2, spinner);
442 if (control_type == ControlType::CONTROLLER) {
443 controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
444 } else if (control_type == ControlType::BUTTON) {
445 button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
447 assert(control_type == ControlType::LIGHT);
448 light_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, spinner_group, control.field_number });
450 spinners[bus_idx][control.field_number] = SpinnerAndGroup{ spinner, spinner_group };
451 connect(spinner, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
452 bind(&MIDIMappingDialog::update_guess_button_state, this));
455 ui->treeWidget->addTopLevelItem(heading_item);
458 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
460 for (const InstantiatedSpinner &is : controller_spinners) {
461 is.spinner->setValue(get_controller_mapping(mapping_proto, is.bus_idx, is.field_number, -1));
463 for (const InstantiatedSpinner &is : button_spinners) {
464 is.spinner->setValue(get_button_mapping(mapping_proto, is.bus_idx, is.field_number, -1));
466 for (const InstantiatedSpinner &is : light_spinners) {
467 is.spinner->setValue(get_light_mapping(mapping_proto, is.bus_idx, is.field_number, -1));
469 for (const InstantiatedComboBox &ic : bank_combo_boxes) {
470 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
474 void MIDIMappingDialog::controller_changed(unsigned controller)
476 post_to_main_thread([=]{
477 for (const InstantiatedSpinner &is : controller_spinners) {
478 if (is.spinner->hasFocus()) {
479 is.spinner->setValue(controller);
480 is.spinner->selectAll();
486 void MIDIMappingDialog::note_on(unsigned note)
488 post_to_main_thread([=]{
489 for (const InstantiatedSpinner &is : button_spinners) {
490 if (is.spinner->hasFocus()) {
491 is.spinner->setValue(note);
492 is.spinner->selectAll();
495 for (const InstantiatedSpinner &is : light_spinners) {
496 if (is.spinner->hasFocus()) {
497 is.spinner->setValue(note);
498 is.spinner->selectAll();
504 pair<int, int> MIDIMappingDialog::guess_offset(unsigned bus_idx, MIDIMappingDialog::SpinnerGroup spinner_group)
506 constexpr pair<int, int> not_found(-1, 0);
508 if (bus_is_empty(bus_idx, spinner_group)) {
512 // See if we can find a non-empty bus to source from (prefer from the left).
513 unsigned source_bus_idx;
514 if (bus_idx > 0 && !bus_is_empty(bus_idx - 1, spinner_group)) {
515 source_bus_idx = bus_idx - 1;
516 } else if (bus_idx < num_buses - 1 && !bus_is_empty(bus_idx + 1, spinner_group)) {
517 source_bus_idx = bus_idx + 1;
522 // See if we can find a consistent offset.
523 bool found_offset = false;
525 int minimum_allowed_offset = numeric_limits<int>::min();
526 int maximum_allowed_offset = numeric_limits<int>::max();
527 for (const auto &field_number_and_spinner : spinners[bus_idx]) {
528 int field_number = field_number_and_spinner.first;
529 QSpinBox *spinner = field_number_and_spinner.second.spinner;
530 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
531 assert(spinners[source_bus_idx].count(field_number));
532 QSpinBox *source_spinner = spinners[source_bus_idx][field_number].spinner;
533 assert(spinners[source_bus_idx][field_number].group == this_spinner_group);
535 if (spinner_group != SpinnerGroup::ALL_GROUPS &&
536 spinner_group != this_spinner_group) {
539 if (spinner->value() == -1) {
540 if (source_spinner->value() != -1) {
541 // If the source value is e.g. 3, offset can't be less than -2 or larger than 124.
542 // Otherwise, we'd extrapolate values outside [1..127].
543 minimum_allowed_offset = max(minimum_allowed_offset, 1 - source_spinner->value());
544 maximum_allowed_offset = min(maximum_allowed_offset, 127 - source_spinner->value());
548 if (source_spinner->value() == -1) {
549 // The bus has a controller set that the source bus doesn't set.
552 if (source_spinner->value() == MIDIReceiver::PITCH_BEND_CONTROLLER) {
553 // It's impossible to interpolate across the pitch bend.
557 int candidate_offset = spinner->value() - source_spinner->value();
559 offset = candidate_offset;
561 } else if (candidate_offset != offset) {
567 // Given that the bus wasn't empty, this shouldn't happen.
572 if (offset < minimum_allowed_offset || offset > maximum_allowed_offset) {
575 return make_pair(source_bus_idx, offset);
578 bool MIDIMappingDialog::bus_is_empty(unsigned bus_idx, SpinnerGroup spinner_group)
580 for (const auto &field_number_and_spinner : spinners[bus_idx]) {
581 QSpinBox *spinner = field_number_and_spinner.second.spinner;
582 SpinnerGroup this_spinner_group = field_number_and_spinner.second.group;
583 if (spinner_group != SpinnerGroup::ALL_GROUPS &&
584 spinner_group != this_spinner_group) {
587 if (spinner->value() != -1) {
594 void MIDIMappingDialog::update_guess_button_state()
596 FocusInfo focus = find_focus();
597 if (focus.bus_idx < 0) {
601 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, SpinnerGroup::ALL_GROUPS);
602 ui->guess_bus_button->setEnabled(bus_and_offset.first != -1);
605 pair<int, int> bus_and_offset = guess_offset(focus.bus_idx, focus.spinner_group);
606 ui->guess_group_button->setEnabled(bus_and_offset.first != -1);
611 MIDIMappingDialog::FocusInfo MIDIMappingDialog::find_focus() const
613 for (const InstantiatedSpinner &is : controller_spinners) {
614 if (is.spinner->hasFocus()) {
615 return FocusInfo{ int(is.bus_idx), is.spinner_group, is.field_number };
618 for (const InstantiatedSpinner &is : button_spinners) {
619 if (is.spinner->hasFocus()) {
620 return FocusInfo{ int(is.bus_idx), is.spinner_group, is.field_number };
623 for (const InstantiatedSpinner &is : light_spinners) {
624 if (is.spinner->hasFocus()) {
625 return FocusInfo{ int(is.bus_idx), is.spinner_group, is.field_number };
628 return FocusInfo{ -1, SpinnerGroup::ALL_GROUPS, -1 };