]> git.sesse.net Git - nageru/blob - futatabi/midi_mapping_dialog.cpp
Add a MIDI mapping editor for Futatabi.
[nageru] / futatabi / 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 "futatabi_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> controllers = {
34         { "Jog",          MIDIMappingProto::kJogFieldNumber,
35                           MIDIMappingProto::kJogBankFieldNumber },
36         { "Master speed", MIDIMappingProto::kMasterSpeedFieldNumber,
37                           MIDIMappingProto::kMasterSpeedBankFieldNumber },
38 };
39 vector<MIDIMappingDialog::Control> controller_lights = {
40         { "Master speed light", MIDIMappingProto::kMasterSpeedLightFieldNumber, 0 },
41 };
42 vector<MIDIMappingDialog::Control> buttons = {
43         { "Preview",      MIDIMappingProto::kPreviewFieldNumber,
44                           MIDIMappingProto::kPreviewBankFieldNumber },
45         { "Queue",        MIDIMappingProto::kQueueFieldNumber,
46                           MIDIMappingProto::kQueueBankFieldNumber },
47         { "Play",         MIDIMappingProto::kPlayFieldNumber,
48                           MIDIMappingProto::kPlayBankFieldNumber },
49         { "Lock master speed", MIDIMappingProto::kToggleLockFieldNumber,
50                           MIDIMappingProto::kToggleLockBankFieldNumber },
51         { "Cue in",       MIDIMappingProto::kCueInFieldNumber,
52                           MIDIMappingProto::kCueInBankFieldNumber },
53         { "Cue out",      MIDIMappingProto::kCueOutFieldNumber,
54                           MIDIMappingProto::kCueOutBankFieldNumber },
55         { "Previous bank", MIDIMappingProto::kPrevBankFieldNumber, 0 },
56         { "Next bank",     MIDIMappingProto::kNextBankFieldNumber, 0 },
57         { "Select bank 1", MIDIMappingProto::kSelectBank1FieldNumber, 0 },
58         { "Select bank 2", MIDIMappingProto::kSelectBank2FieldNumber, 0 },
59         { "Select bank 3", MIDIMappingProto::kSelectBank3FieldNumber, 0 },
60         { "Select bank 4", MIDIMappingProto::kSelectBank4FieldNumber, 0 },
61         { "Select bank 5", MIDIMappingProto::kSelectBank5FieldNumber, 0 },
62 };
63 vector<MIDIMappingDialog::Control> button_lights = {
64         { "Preview playing",      MIDIMappingProto::kPreviewPlayingFieldNumber, 0 },
65         { "Preview ready",        MIDIMappingProto::kPreviewReadyFieldNumber, 0 },
66         { "Queue button enabled", MIDIMappingProto::kQueueEnabledFieldNumber, 0 },
67         { "Playing",              MIDIMappingProto::kPlayingFieldNumber, 0 },
68         { "Play ready",           MIDIMappingProto::kPlayReadyFieldNumber, 0 },
69         { "Master speed locked",  MIDIMappingProto::kLockedFieldNumber, 0 },
70         { "Master speed locked (blinking)",
71                                   MIDIMappingProto::kLockedBlinkingFieldNumber, 0 },
72         { "Cue in enabled",       MIDIMappingProto::kCueInEnabledFieldNumber, 0 },
73         { "Cue out enabled",      MIDIMappingProto::kCueOutEnabledFieldNumber, 0 },
74         { "Bank 1 is selected",   MIDIMappingProto::kBank1IsSelectedFieldNumber, 0 },
75         { "Bank 2 is selected",   MIDIMappingProto::kBank2IsSelectedFieldNumber, 0 },
76         { "Bank 3 is selected",   MIDIMappingProto::kBank3IsSelectedFieldNumber, 0 },
77         { "Bank 4 is selected",   MIDIMappingProto::kBank4IsSelectedFieldNumber, 0 },
78         { "Bank 5 is selected",   MIDIMappingProto::kBank5IsSelectedFieldNumber, 0 },
79 };
80
81 namespace {
82
83 int get_bank(const MIDIMappingProto &mapping_proto, int bank_field_number, int default_value)
84 {
85         const FieldDescriptor *bank_descriptor = mapping_proto.GetDescriptor()->FindFieldByNumber(bank_field_number);
86         const Reflection *reflection = mapping_proto.GetReflection();
87         if (!reflection->HasField(mapping_proto, bank_descriptor)) {
88                 return default_value;
89         }
90         return reflection->GetInt32(mapping_proto, bank_descriptor);
91 }
92
93 }  // namespace
94
95 MIDIMappingDialog::MIDIMappingDialog(MIDIMapper *mapper)
96         : ui(new Ui::MIDIMappingDialog),
97           mapper(mapper)
98 {
99         ui->setupUi(this);
100
101         const MIDIMappingProto mapping_proto = mapper->get_current_mapping();  // Take a copy.
102         old_receiver = mapper->set_receiver(this);
103
104         QStringList labels;
105         labels << "";
106         labels << "Controller bank";
107         labels << "";
108         labels << "";
109         labels << "";
110         labels << "";
111         ui->treeWidget->setColumnCount(6);
112         ui->treeWidget->setHeaderLabels(labels);
113
114         vector<MIDIMappingDialog::Control> camera_select_buttons;
115         vector<MIDIMappingDialog::Control> camera_is_selected_lights;
116         for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) {
117                 char str[256];
118                 snprintf(str, sizeof(str), "Switch to camera %zu", camera_idx + 1);
119                 camera_select_buttons.emplace_back(Control{ str, CameraMIDIMappingProto::kButtonFieldNumber, 0 });
120
121                 snprintf(str, sizeof(str), "Camera %zu is current", camera_idx + 1);
122                 camera_is_selected_lights.emplace_back(Control{ str, CameraMIDIMappingProto::kIsCurrentFieldNumber, 0 });
123         }
124
125         add_controls("Controllers",               ControlType::CONTROLLER,          mapping_proto, controllers);
126         add_controls("Controller lights",         ControlType::CONTROLLER_LIGHT,    mapping_proto, controller_lights);
127         add_controls("Buttons",                   ControlType::BUTTON,              mapping_proto, buttons);
128         add_controls("Button lights",             ControlType::BUTTON_LIGHT,        mapping_proto, button_lights);
129         add_controls("Camera select buttons",     ControlType::CAMERA_BUTTON,       mapping_proto, camera_select_buttons);
130         add_controls("Camera is selected lights", ControlType::CAMERA_BUTTON_LIGHT, mapping_proto, camera_is_selected_lights);
131         fill_controls_from_mapping(mapping_proto);
132
133         // Auto-resize every column but the last.
134         for (unsigned column_idx = 0; column_idx < 5; ++column_idx) {
135                 ui->treeWidget->resizeColumnToContents(column_idx);
136         }
137
138         connect(ui->ok_cancel_buttons, &QDialogButtonBox::accepted, this, &MIDIMappingDialog::ok_clicked);
139         connect(ui->ok_cancel_buttons, &QDialogButtonBox::rejected, this, &MIDIMappingDialog::cancel_clicked);
140         connect(ui->save_button, &QPushButton::clicked, this, &MIDIMappingDialog::save_clicked);
141         connect(ui->load_button, &QPushButton::clicked, this, &MIDIMappingDialog::load_clicked);
142 }
143
144 MIDIMappingDialog::~MIDIMappingDialog()
145 {
146         mapper->set_receiver(old_receiver);
147 }
148
149 void MIDIMappingDialog::ok_clicked()
150 {
151         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
152         mapper->set_midi_mapping(*new_mapping);
153         mapper->set_receiver(old_receiver);
154         accept();
155 }
156
157 void MIDIMappingDialog::cancel_clicked()
158 {
159         mapper->set_receiver(old_receiver);
160         reject();
161 }
162
163 void MIDIMappingDialog::save_clicked()
164 {
165         QFileDialog::Options options;
166         unique_ptr<MIDIMappingProto> new_mapping = construct_mapping_proto_from_ui();
167         QString filename = QFileDialog::getSaveFileName(this,
168                 "Save MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options);
169         if (!filename.endsWith(".midimapping")) {
170                 filename += ".midimapping";
171         }
172         if (!save_midi_mapping_to_file(*new_mapping, filename.toStdString())) {
173                 QMessageBox box;
174                 box.setText("Could not save mapping to '" + filename + "'. Check that you have the right permissions and try again.");
175                 box.exec();
176         }
177 }
178
179 void MIDIMappingDialog::load_clicked()
180 {
181         QFileDialog::Options options;
182         QString filename = QFileDialog::getOpenFileName(this,
183                 "Load MIDI mapping", QString(), tr("Mapping files (*.midimapping)"), /*selectedFilter=*/nullptr, options);
184         MIDIMappingProto new_mapping;
185         if (!load_midi_mapping_from_file(filename.toStdString(), &new_mapping)) {
186                 QMessageBox box;
187                 box.setText("Could not load mapping from '" + filename + "'. Check that the file exists, has the right permissions and is valid.");
188                 box.exec();
189                 return;
190         }
191
192         fill_controls_from_mapping(new_mapping);
193 }
194
195 namespace {
196
197 template<class T, class Proto>
198 T *get_mutable_message(Proto *proto, int field_number)
199 {
200         const FieldDescriptor *descriptor = proto->GetDescriptor()->FindFieldByNumber(field_number);
201         const Reflection *bus_reflection = proto->GetReflection();
202         return static_cast<T *>(bus_reflection->MutableMessage(proto, descriptor));
203 }
204
205 }  // namespace
206
207 unique_ptr<MIDIMappingProto> MIDIMappingDialog::construct_mapping_proto_from_ui()
208 {
209         unique_ptr<MIDIMappingProto> mapping_proto(new MIDIMappingProto);
210         for (const InstantiatedSpinner &is : controller_spinners) {
211                 const int val = is.spinner->value();
212                 if (val == -1) {
213                         continue;
214                 }
215
216                 MIDIControllerProto *controller_proto =
217                         get_mutable_message<MIDIControllerProto>(mapping_proto.get(), is.field_number);
218                 controller_proto->set_controller_number(val);
219         }
220         for (const InstantiatedSpinner &is : controller_light_spinners) {
221                 const int val = is.spinner->value();
222                 if (val == -1) {
223                         continue;
224                 }
225
226                 MIDIControllerProto *controller_proto =
227                         get_mutable_message<MIDIControllerProto>(mapping_proto.get(), is.field_number);
228                 controller_proto->set_controller_number(val);
229
230                 // HACK: We only have one of these right now, so min/max is a given;
231                 // no need to store proto field numbers.
232                 int val2 = is.spinner2->value();
233                 if (val2 != -1) {
234                         mapping_proto->set_master_speed_light_min(val2);
235                 }
236                 int val3 = is.spinner3->value();
237                 if (val3 != -1) {
238                         mapping_proto->set_master_speed_light_max(val3);
239                 }
240         }
241         for (const InstantiatedSpinner &is : button_spinners) {
242                 const int val = is.spinner->value();
243                 if (val == -1) {
244                         continue;
245                 }
246
247                 MIDIButtonProto *button_proto =
248                         get_mutable_message<MIDIButtonProto>(mapping_proto.get(), is.field_number);
249                 button_proto->set_note_number(val);
250         }
251         for (const InstantiatedSpinner &is : button_light_spinners) {
252                 const int val = is.spinner->value();
253                 if (val == -1) {
254                         continue;
255                 }
256
257                 MIDILightProto *light_proto =
258                         get_mutable_message<MIDILightProto>(mapping_proto.get(), is.field_number);
259                 light_proto->set_note_number(val);
260
261                 int val2 = is.spinner2->value();
262                 if (val2 != -1) {
263                         light_proto->set_velocity(val2);
264                 }
265         }
266         int highest_bank_used = 0;  // 1-indexed.
267         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
268                 const int val = ic.combo_box->currentIndex();
269                 highest_bank_used = std::max(highest_bank_used, val);
270                 if (val == 0) {
271                         continue;
272                 }
273
274                 const FieldDescriptor *descriptor = mapping_proto->GetDescriptor()->FindFieldByNumber(ic.field_number);
275                 const Reflection *bus_reflection = mapping_proto->GetReflection();
276                 bus_reflection->SetInt32(mapping_proto.get(), descriptor, val - 1);
277         }
278         mapping_proto->set_num_controller_banks(highest_bank_used);
279
280         size_t num_cameras_used = 0;
281         for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) {
282                 if (camera_button_spinners[camera_idx].spinner->value() != -1) {
283                         num_cameras_used = camera_idx + 1;
284                 } else if (camera_button_light_spinners[camera_idx].spinner->value() != -1) {
285                         num_cameras_used = camera_idx + 1;
286                 }
287         }
288         for (size_t camera_idx = 0; camera_idx < num_cameras_used; ++camera_idx) {
289                 CameraMIDIMappingProto *camera_proto = mapping_proto->add_camera();
290         
291                 {       
292                         const InstantiatedSpinner &is = camera_button_spinners[camera_idx];
293                         MIDIButtonProto *button_proto =
294                                 get_mutable_message<MIDIButtonProto>(camera_proto, is.field_number);
295                         int val = is.spinner->value();
296                         if (val != -1) {
297                                 button_proto->set_note_number(val);
298                         }
299                 }
300                 {       
301                         const InstantiatedSpinner &is = camera_button_light_spinners[camera_idx];
302                         MIDILightProto *light_proto =
303                                 get_mutable_message<MIDILightProto>(camera_proto, is.field_number);
304                         int val = is.spinner->value();
305                         if (val != -1) {
306                                 light_proto->set_note_number(val);
307                         }
308
309                         int val2 = is.spinner2->value();
310                         if (val2 != -1) {
311                                 light_proto->set_velocity(val2);
312                         }
313                 }
314         }
315
316         return mapping_proto;
317 }
318
319 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
320 {
321         if (bank_field_number == 0) {
322                 return;
323         }
324         QComboBox *bank_selector = new QComboBox(this);
325         bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
326         bank_selector->setAutoFillBackground(true);
327
328         bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
329
330         ui->treeWidget->setItemWidget(item, 1, bank_selector);
331 }
332
333 void MIDIMappingDialog::add_controls(const string &heading,
334                                      MIDIMappingDialog::ControlType control_type,
335                                      const MIDIMappingProto &mapping_proto,
336                                      const vector<MIDIMappingDialog::Control> &controls)
337 {
338         QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
339         heading_item->setText(0, QString::fromStdString(heading));
340         if (control_type == ControlType::BUTTON_LIGHT) {
341                 heading_item->setText(3, "Velocity");
342         } else if (control_type == ControlType::CONTROLLER_LIGHT) {
343                 heading_item->setText(3, "Min");
344                 heading_item->setText(4, "Max");
345         } else {
346                 heading_item->setFirstColumnSpanned(true);
347         }
348         heading_item->setExpanded(true);
349         for (const Control &control : controls) {
350                 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
351                 heading_item->addChild(item);
352                 add_bank_selector(item, mapping_proto, control.bank_field_number);
353                 item->setText(0, QString::fromStdString(control.label + "   "));
354
355                 QSpinBox *spinner;
356                 if (control_type == ControlType::CONTROLLER) {
357                         spinner = new ControllerSpinBox(this);
358                         spinner->setRange(-1, 128);  // 128 for pitch bend.
359                 } else {
360                         spinner = new QSpinBox(this);
361                         spinner->setRange(-1, 127);
362                 }
363                 spinner->setAutoFillBackground(true);
364                 spinner->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
365                 ui->treeWidget->setItemWidget(item, 2, spinner);
366
367                 if (control_type == ControlType::CONTROLLER) {
368                         controller_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number });
369                 } else if (control_type == ControlType::CONTROLLER_LIGHT) {
370                         QSpinBox *spinner2 = new QSpinBox(this);
371                         spinner2->setRange(-1, 127);
372                         spinner2->setAutoFillBackground(true);
373                         spinner2->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
374
375                         QSpinBox *spinner3 = new QSpinBox(this);
376                         spinner3->setRange(-1, 127);
377                         spinner3->setAutoFillBackground(true);
378                         spinner3->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
379
380                         ui->treeWidget->setItemWidget(item, 3, spinner2);
381                         ui->treeWidget->setItemWidget(item, 4, spinner3);
382
383                         controller_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, spinner3, control.field_number });
384                 } else if (control_type == ControlType::BUTTON) {
385                         button_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number });
386                 } else if (control_type == ControlType::CAMERA_BUTTON) {
387                         camera_button_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number });
388                 } else {
389                         assert(control_type == ControlType::BUTTON_LIGHT || control_type == ControlType::CAMERA_BUTTON_LIGHT);
390                         QSpinBox *spinner2 = new QSpinBox(this);
391                         spinner2->setRange(-1, 127);
392                         spinner2->setAutoFillBackground(true);
393                         spinner2->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
394                         ui->treeWidget->setItemWidget(item, 3, spinner2);
395                         if (control_type == ControlType::BUTTON_LIGHT) {
396                                 button_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, nullptr, control.field_number });
397                         } else {
398                                 assert(control_type == ControlType::CAMERA_BUTTON_LIGHT);
399                                 camera_button_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, nullptr, control.field_number });
400                         }
401                 }
402                 spinners[control.field_number] = spinner;
403         }
404         ui->treeWidget->addTopLevelItem(heading_item);
405 }
406
407 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
408 {
409         for (const InstantiatedSpinner &is : controller_spinners) {
410                 is.spinner->setValue(get_controller_mapping_helper(mapping_proto, is.field_number, -1));
411         }
412         for (const InstantiatedSpinner &is : controller_light_spinners) {
413                 is.spinner->setValue(get_controller_mapping_helper(mapping_proto, is.field_number, -1));
414
415                 // HACK: We only have one of these right now, so min/max is a given;
416                 // no need to store proto field numbers.
417                 if (mapping_proto.has_master_speed_light_min()) {
418                         is.spinner2->setValue(mapping_proto.master_speed_light_min());
419                 }
420                 if (mapping_proto.has_master_speed_light_max()) {
421                         is.spinner3->setValue(mapping_proto.master_speed_light_max());
422                 }
423         }
424         for (const InstantiatedSpinner &is : button_spinners) {
425                 is.spinner->setValue(get_button_mapping_helper(mapping_proto, is.field_number, -1));
426         }
427         for (const InstantiatedSpinner &is : button_light_spinners) {
428                 MIDILightProto light_proto = get_light_mapping_helper(mapping_proto, is.field_number);
429                 if (light_proto.has_note_number()) {
430                         is.spinner->setValue(light_proto.note_number());
431                 } else {
432                         is.spinner->setValue(-1);
433                 }
434                 if (light_proto.has_velocity()) {
435                         is.spinner2->setValue(light_proto.velocity());
436                 } else {
437                         is.spinner2->setValue(-1);
438                 }
439         }
440         for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) {
441                 CameraMIDIMappingProto camera_proto;
442                 if (camera_idx < size_t(mapping_proto.camera_size())) {
443                         camera_proto = mapping_proto.camera(camera_idx);
444                 }
445                 {
446                         const InstantiatedSpinner &is = camera_button_spinners[camera_idx];
447                         is.spinner->setValue(get_button_mapping_helper(camera_proto, is.field_number, -1));
448                 }
449                 {
450                         const InstantiatedSpinner &is = camera_button_light_spinners[camera_idx];
451                         const MIDILightProto &light_proto = get_light_mapping_helper(camera_proto, is.field_number);
452                         if (light_proto.has_note_number()) {
453                                 is.spinner->setValue(light_proto.note_number());
454                         } else {
455                                 is.spinner->setValue(-1);
456                         }
457                         if (light_proto.has_velocity()) {
458                                 is.spinner2->setValue(light_proto.velocity());
459                         } else {
460                                 is.spinner2->setValue(-1);
461                         }
462                 }
463         }
464         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
465                 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
466         }
467 }
468
469 void MIDIMappingDialog::controller_changed(unsigned controller)
470 {
471         post_to_main_thread([=]{
472                 for (const InstantiatedSpinner &is : controller_spinners) {
473                         if (is.spinner->hasFocus()) {
474                                 is.spinner->setValue(controller);
475                                 is.spinner->selectAll();
476                         }
477                 }
478                 for (const InstantiatedSpinner &is : controller_light_spinners) {
479                         if (is.spinner->hasFocus()) {
480                                 is.spinner->setValue(controller);
481                                 is.spinner->selectAll();
482                         }
483                 }
484         });
485 }
486
487 void MIDIMappingDialog::note_on(unsigned note)
488 {
489         post_to_main_thread([=]{
490                 for (const InstantiatedSpinner &is : button_spinners) {
491                         if (is.spinner->hasFocus()) {
492                                 is.spinner->setValue(note);
493                                 is.spinner->selectAll();
494                         }
495                 }
496                 for (const InstantiatedSpinner &is : button_light_spinners) {
497                         if (is.spinner->hasFocus()) {
498                                 is.spinner->setValue(note);
499                                 is.spinner->selectAll();
500                         }
501                 }
502         });
503 }