1 #include "midi_mapping_dialog.h"
3 #include "midi_mapper.h"
4 #include "midi_mapping.pb.h"
5 #include "post_to_main_thread.h"
6 #include "ui_midi_mapping.h"
10 #include <QMessageBox>
16 using namespace google::protobuf;
19 vector<MIDIMappingDialog::Control> per_bus_controllers = {
20 { "Treble", MIDIMappingBusProto::kTrebleFieldNumber, MIDIMappingProto::kTrebleBankFieldNumber },
21 { "Mid", MIDIMappingBusProto::kMidFieldNumber, MIDIMappingProto::kMidBankFieldNumber },
22 { "Bass", MIDIMappingBusProto::kBassFieldNumber, MIDIMappingProto::kBassBankFieldNumber },
23 { "Gain", MIDIMappingBusProto::kGainFieldNumber, MIDIMappingProto::kGainBankFieldNumber },
24 { "Compressor threshold", MIDIMappingBusProto::kCompressorThresholdFieldNumber,
25 MIDIMappingProto::kCompressorThresholdBankFieldNumber},
26 { "Fader", MIDIMappingBusProto::kFaderFieldNumber, MIDIMappingProto::kFaderBankFieldNumber }
28 vector<MIDIMappingDialog::Control> per_bus_buttons = {
29 { "Toggle locut", MIDIMappingBusProto::kToggleLocutFieldNumber,
30 MIDIMappingProto::kToggleLocutBankFieldNumber },
31 { "Togle auto gain staging", MIDIMappingBusProto::kToggleAutoGainStagingFieldNumber,
32 MIDIMappingProto::kToggleAutoGainStagingBankFieldNumber },
33 { "Togle compressor", MIDIMappingBusProto::kToggleCompressorFieldNumber,
34 MIDIMappingProto::kToggleCompressorBankFieldNumber },
35 { "Clear peak", MIDIMappingBusProto::kClearPeakFieldNumber,
36 MIDIMappingProto::kClearPeakBankFieldNumber }
38 vector<MIDIMappingDialog::Control> global_controllers = {
39 { "Locut cutoff", MIDIMappingBusProto::kLocutFieldNumber, MIDIMappingProto::kLocutBankFieldNumber },
40 { "Limiter threshold", MIDIMappingBusProto::kLimiterThresholdFieldNumber,
41 MIDIMappingProto::kLimiterThresholdBankFieldNumber },
42 { "Makeup gain", MIDIMappingBusProto::kMakeupGainFieldNumber,
43 MIDIMappingProto::kMakeupGainBankFieldNumber }
45 vector<MIDIMappingDialog::Control> global_buttons = {
46 { "Previous bank", MIDIMappingBusProto::kPrevBankFieldNumber, 0 },
47 { "Next bank", MIDIMappingBusProto::kNextBankFieldNumber, 0 },
48 { "Select bank 1", MIDIMappingBusProto::kSelectBank1FieldNumber, 0 },
49 { "Select bank 2", MIDIMappingBusProto::kSelectBank2FieldNumber, 0 },
50 { "Select bank 3", MIDIMappingBusProto::kSelectBank3FieldNumber, 0 },
51 { "Select bank 4", MIDIMappingBusProto::kSelectBank4FieldNumber, 0 },
52 { "Select bank 5", MIDIMappingBusProto::kSelectBank5FieldNumber, 0 }
57 int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value)
59 const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number);
60 const Reflection *reflection = mapping_proto.GetReflection();
61 if (!reflection->HasField(mapping_proto, bank_descriptor)) {
64 return reflection->GetInt32(mapping_proto, bank_descriptor);
67 int get_controller_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
69 if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
73 const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
74 const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
75 const Reflection *bus_reflection = bus_mapping.GetReflection();
76 if (!bus_reflection->HasField(bus_mapping, descriptor)) {
79 const MIDIControllerProto &controller_proto =
80 static_cast<const MIDIControllerProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
81 return controller_proto.controller_number();
84 int get_button_mapping(const MIDIMappingProto &mapping_proto, size_t bus_idx, int field_number, int default_value)
86 if (bus_idx >= size_t(mapping_proto.bus_mapping_size())) {
90 const MIDIMappingBusProto &bus_mapping = mapping_proto.bus_mapping(bus_idx);
91 const FieldDescriptor *descriptor = bus_mapping.GetDescriptor()->FindFieldByNumber(field_number);
92 const Reflection *bus_reflection = bus_mapping.GetReflection();
93 if (!bus_reflection->HasField(bus_mapping, descriptor)) {
96 const MIDIButtonProto &bus_proto =
97 static_cast<const MIDIButtonProto &>(bus_reflection->GetMessage(bus_mapping, descriptor));
98 return bus_proto.note_number();
103 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
104 : ui(new Ui::MIDIMappingDialog),
109 const MIDIMappingProto mapping_proto = mapper->get_current_mapping(); // Take a copy.
110 old_receiver = mapper->set_receiver(this);
114 labels << "Controller bank";
115 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
117 snprintf(buf, sizeof(buf), "Bus %d", bus_idx + 1);
121 ui->treeWidget->setColumnCount(num_buses + 3);
122 ui->treeWidget->setHeaderLabels(labels);
124 add_controls("Per-bus controllers", ControlType::CONTROLLER, mapping_proto, per_bus_controllers);
125 add_controls("Per-bus buttons", ControlType::BUTTON, mapping_proto, per_bus_buttons);
126 add_controls("Global controllers", ControlType::CONTROLLER, mapping_proto, global_controllers);
127 add_controls("Global buttons", ControlType::BUTTON, mapping_proto, global_buttons);
128 fill_controls_from_mapping(mapping_proto);
130 // Auto-resize every column but the last.
131 for (unsigned column_idx = 0; column_idx < num_buses + 3; ++column_idx) {
132 ui->treeWidget->resizeColumnToContents(column_idx);
135 connect(ui->guess_button, &QPushButton::clicked, this, &MIDIMappingDialog::guess_clicked);
136 connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked);
137 connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked);
138 connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked);
139 connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked);
141 update_guess_button_state();
144 MIDIMappingDialog::~MIDIMappingDialog()
146 mapper->set_receiver(old_receiver);
149 bool MIDIMappingDialog::eventFilter(QObject *obj, QEvent *event)
151 if (event->type() == QEvent::FocusIn ||
152 event->type() == QEvent::FocusOut) {
153 // We ignore the guess button itself; it should be allowed
154 // to navigate from a spinner to focus on the button (to click it).
155 if (obj != ui->guess_button) {
156 update_guess_button_state();
162 void MIDIMappingDialog::guess_clicked()
164 int focus_bus_idx = find_focus_bus();
165 if (focus_bus_idx == -1) {
166 // The guess button probably took the focus away from us.
167 focus_bus_idx = last_focus_bus_idx;
169 assert(focus_bus_idx != -1); // The button should have been disabled.
170 pair<int, int> bus_and_offset = guess_offset(focus_bus_idx);
171 const int source_bus_idx = bus_and_offset.first;
172 const int offset = bus_and_offset.second;
173 assert(source_bus_idx != -1); // The button should have been disabled.
175 for (const auto &field_number_and_spinner : spinners[focus_bus_idx]) {
176 int field_number = field_number_and_spinner.first;
177 QSpinBox *spinner = field_number_and_spinner.second;
179 assert(spinners[source_bus_idx].count(field_number));
180 QSpinBox *source_spinner = spinners[source_bus_idx][field_number];
182 if (source_spinner->value() != 0) {
183 spinner->setValue(source_spinner->value() + offset);
188 void MIDIMappingDialog::ok_clicked()
190 unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
191 mapper->set_midi_mapping(*new_mapping);
192 mapper->set_receiver(old_receiver);
196 void MIDIMappingDialog::cancel_clicked()
198 mapper->set_receiver(old_receiver);
202 void MIDIMappingDialog::save_clicked()
204 unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
205 QString filename = QFileDialog::getSaveFileName(this,
206 "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"));
207 if (!filename.endsWith(".midimapping")) {
208 filename += ".midimapping";
210 if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) {
212 box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
217 void MIDIMappingDialog::load_clicked()
219 QString filename = QFileDialog::getOpenFileName(this,
220 "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"));
221 MIDIMappingProto new_mapping;
222 if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) {
224 box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
229 fill_controls_from_mapping(new_mapping);
235 T *get_mutable_bus_message(MIDIMappingProto *mapping_proto, unsigned bus_idx, int field_number)
237 while (size_t(mapping_proto->bus_mapping_size()) <= bus_idx) {
238 mapping_proto->add_bus_mapping();
241 MIDIMappingBusProto *bus_mapping = mapping_proto->mutable_bus_mapping(bus_idx);
242 const FieldDescriptor *descriptor = bus_mapping->GetDescriptor()->FindFieldByNumber(field_number);
243 const Reflection *bus_reflection = bus_mapping->GetReflection();
244 return static_cast<T *>(bus_reflection->MutableMessage(bus_mapping, descriptor));
249 unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
251 unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
252 for (const InstantiatedSpinner &is : controller_spinners) {
253 const int val = is.spinner->value();
258 MIDIControllerProto *controller_proto =
259 get_mutable_bus_message<MIDIControllerProto>(mapping_proto.get(), is.bus_idx, is.field_number);
260 controller_proto->set_controller_number(val);
262 for (const InstantiatedSpinner &is : button_spinners) {
263 const int val = is.spinner->value();
268 MIDIButtonProto *button_proto =
269 get_mutable_bus_message<MIDIButtonProto>(mapping_proto.get(), is.bus_idx, is.field_number);
270 button_proto->set_note_number(val);
272 int highest_bank_used = 0; // 1-indexed.
273 for (const InstantiatedComboBox &ic : bank_combo_boxes) {
274 const int val = ic.combo_box->currentIndex();
275 highest_bank_used = std::max(highest_bank_used, val);
280 const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number);
281 const Reflection *bus_reflection = mapping_proto->GetReflection();
282 bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1);
284 mapping_proto->set_num_controller_banks(highest_bank_used);
285 return mapping_proto;
288 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
290 if (bank_field_number == 0) {
293 QComboBox *bank_selector = new QComboBox(this);
294 bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
295 bank_selector->setAutoFillBackground(true);
297 bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
299 ui->treeWidget->setItemWidget(item, 1, bank_selector);
302 void MIDIMappingDialog::add_controls(const string &heading,
303 MIDIMappingDialog::ControlType control_type,
304 const MIDIMappingProto &mapping_proto,
305 const vector<MIDIMappingDialog::Control> &controls)
307 QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
308 heading_item->setText(0, QString::fromStdString(heading));
309 heading_item->setFirstColumnSpanned(true);
310 heading_item->setExpanded(true);
311 for (const Control &control : controls) {
312 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
313 heading_item->addChild(item);
314 add_bank_selector(item, mapping_proto, control.bank_field_number);
315 item->setText(0, QString::fromStdString(control.label + " "));
317 for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
318 QSpinBox *spinner = new QSpinBox(this);
319 spinner->setRange(0, 127);
320 spinner->setAutoFillBackground(true);
321 spinner->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty).
322 spinner->installEventFilter(this); // So we know when the focus changes.
323 ui->treeWidget->setItemWidget(item, bus_idx + 2, spinner);
325 if (control_type == ControlType::CONTROLLER) {
326 controller_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, control.field_number });
328 assert(control_type == ControlType::BUTTON);
329 button_spinners.push_back(InstantiatedSpinner{ spinner, bus_idx, control.field_number });
331 spinners[bus_idx][control.field_number] = spinner;
332 connect(spinner, static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
333 bind(&MIDIMappingDialog::update_guess_button_state, this));
336 ui->treeWidget->addTopLevelItem(heading_item);
339 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
341 for (const InstantiatedSpinner &is : controller_spinners) {
342 is.spinner->setValue(get_controller_mapping(mapping_proto, is.bus_idx, is.field_number, 0));
344 for (const InstantiatedSpinner &is : button_spinners) {
345 is.spinner->setValue(get_button_mapping(mapping_proto, is.bus_idx, is.field_number, 0));
347 for (const InstantiatedComboBox &ic : bank_combo_boxes) {
348 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
352 void MIDIMappingDialog::controller_changed(unsigned controller)
354 for (const InstantiatedSpinner &is : controller_spinners) {
355 if (is.spinner->hasFocus()) {
356 is.spinner->setValue(controller);
357 is.spinner->selectAll();
362 void MIDIMappingDialog::note_on(unsigned note)
364 for (const InstantiatedSpinner &is : button_spinners) {
365 if (is.spinner->hasFocus()) {
366 is.spinner->setValue(note);
367 is.spinner->selectAll();
372 pair<int, int> MIDIMappingDialog::guess_offset(unsigned bus_idx)
374 constexpr pair<int, int> not_found(-1, 0);
376 if (bus_is_empty(bus_idx)) {
380 // See if we can find a non-empty bus to source from (prefer from the left).
381 unsigned source_bus_idx;
382 if (bus_idx > 0 && !bus_is_empty(bus_idx - 1)) {
383 source_bus_idx = bus_idx - 1;
384 } else if (bus_idx < num_buses - 1 && !bus_is_empty(bus_idx + 1)) {
385 source_bus_idx = bus_idx + 1;
390 // See if we can find a consistent offset.
391 bool found_offset = false;
393 for (const auto &field_number_and_spinner : spinners[bus_idx]) {
394 int field_number = field_number_and_spinner.first;
395 QSpinBox *spinner = field_number_and_spinner.second;
397 if (spinner->value() == 0) {
401 assert(spinners[source_bus_idx].count(field_number));
402 QSpinBox *source_spinner = spinners[source_bus_idx][field_number];
403 if (source_spinner->value() == 0) {
404 // The bus has a controller set that the source bus doesn't set.
408 int candidate_offset = spinner->value() - source_spinner->value();
410 offset = candidate_offset;
412 } else if (candidate_offset != offset) {
418 // Given that the bus wasn't empty, this shouldn't happen.
422 return make_pair(source_bus_idx, offset);
425 bool MIDIMappingDialog::bus_is_empty(unsigned bus_idx)
427 for (const auto &field_number_and_spinner : spinners[bus_idx]) {
428 QSpinBox *spinner = field_number_and_spinner.second;
429 if (spinner->value() != 0) {
436 void MIDIMappingDialog::update_guess_button_state()
438 int focus_bus_idx = find_focus_bus();
439 if (focus_bus_idx < 0) {
442 pair<int, int> bus_and_offset = guess_offset(focus_bus_idx);
443 ui->guess_button->setEnabled(bus_and_offset.first != -1);
444 last_focus_bus_idx = focus_bus_idx;
447 int MIDIMappingDialog::find_focus_bus()
449 for (const InstantiatedSpinner &is : controller_spinners) {
450 if (is.spinner->hasFocus()) {
454 for (const InstantiatedSpinner &is : button_spinners) {
455 if (is.spinner->hasFocus()) {