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