]> git.sesse.net Git - nageru/blob - futatabi/midi_mapping_dialog.cpp
Add support for aborting playing a clip early.
[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                         MIDIButtonProto *button_proto =
297                                 get_mutable_message<MIDIButtonProto>(camera_proto, is.field_number);
298                         int val = is.spinner->value();
299                         if (val != -1) {
300                                 button_proto->set_note_number(val);
301                         }
302                 }
303                 {       
304                         const InstantiatedSpinner &is = camera_button_light_spinners[camera_idx];
305                         MIDILightProto *light_proto =
306                                 get_mutable_message<MIDILightProto>(camera_proto, is.field_number);
307                         int val = is.spinner->value();
308                         if (val != -1) {
309                                 light_proto->set_note_number(val);
310                         }
311
312                         int val2 = is.spinner2->value();
313                         if (val2 != -1) {
314                                 light_proto->set_velocity(val2);
315                         }
316                 }
317         }
318
319         return mapping_proto;
320 }
321
322 void MIDIMappingDialog::add_bank_selector(QTreeWidgetItem *item, const MIDIMappingProto &mapping_proto, int bank_field_number)
323 {
324         if (bank_field_number == 0) {
325                 return;
326         }
327         QComboBox *bank_selector = new QComboBox(this);
328         bank_selector->addItems(QStringList() << "" << "Bank 1" << "Bank 2" << "Bank 3" << "Bank 4" << "Bank 5");
329         bank_selector->setAutoFillBackground(true);
330
331         bank_combo_boxes.push_back(InstantiatedComboBox{ bank_selector, bank_field_number });
332
333         ui->treeWidget->setItemWidget(item, 1, bank_selector);
334 }
335
336 void MIDIMappingDialog::add_controls(const string &heading,
337                                      MIDIMappingDialog::ControlType control_type,
338                                      const MIDIMappingProto &mapping_proto,
339                                      const vector<MIDIMappingDialog::Control> &controls)
340 {
341         QTreeWidgetItem *heading_item = new QTreeWidgetItem(ui->treeWidget);
342         heading_item->setText(0, QString::fromStdString(heading));
343         if (control_type == ControlType::BUTTON_LIGHT) {
344                 heading_item->setText(3, "Velocity");
345         } else if (control_type == ControlType::CONTROLLER_LIGHT) {
346                 heading_item->setText(3, "Min");
347                 heading_item->setText(4, "Max");
348         } else {
349                 heading_item->setFirstColumnSpanned(true);
350         }
351         heading_item->setExpanded(true);
352         for (const Control &control : controls) {
353                 QTreeWidgetItem *item = new QTreeWidgetItem(heading_item);
354                 heading_item->addChild(item);
355                 add_bank_selector(item, mapping_proto, control.bank_field_number);
356                 item->setText(0, QString::fromStdString(control.label + "   "));
357
358                 QSpinBox *spinner;
359                 if (control_type == ControlType::CONTROLLER) {
360                         spinner = new ControllerSpinBox(this);
361                         spinner->setRange(-1, 128);  // 128 for pitch bend.
362                 } else {
363                         spinner = new QSpinBox(this);
364                         spinner->setRange(-1, 127);
365                 }
366                 spinner->setAutoFillBackground(true);
367                 spinner->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
368                 ui->treeWidget->setItemWidget(item, 2, spinner);
369
370                 if (control_type == ControlType::CONTROLLER) {
371                         controller_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number });
372                 } else if (control_type == ControlType::CONTROLLER_LIGHT) {
373                         QSpinBox *spinner2 = new QSpinBox(this);
374                         spinner2->setRange(-1, 127);
375                         spinner2->setAutoFillBackground(true);
376                         spinner2->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
377
378                         QSpinBox *spinner3 = new QSpinBox(this);
379                         spinner3->setRange(-1, 127);
380                         spinner3->setAutoFillBackground(true);
381                         spinner3->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
382
383                         ui->treeWidget->setItemWidget(item, 3, spinner2);
384                         ui->treeWidget->setItemWidget(item, 4, spinner3);
385
386                         controller_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, spinner3, control.field_number });
387                 } else if (control_type == ControlType::BUTTON) {
388                         button_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number });
389                 } else if (control_type == ControlType::CAMERA_BUTTON) {
390                         camera_button_spinners.push_back(InstantiatedSpinner{ spinner, nullptr, nullptr, control.field_number });
391                 } else {
392                         assert(control_type == ControlType::BUTTON_LIGHT || control_type == ControlType::CAMERA_BUTTON_LIGHT);
393                         QSpinBox *spinner2 = new QSpinBox(this);
394                         spinner2->setRange(-1, 127);
395                         spinner2->setAutoFillBackground(true);
396                         spinner2->setSpecialValueText("\u200d");  // Zero-width joiner (ie., empty).
397                         ui->treeWidget->setItemWidget(item, 3, spinner2);
398                         if (control_type == ControlType::BUTTON_LIGHT) {
399                                 button_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, nullptr, control.field_number });
400                         } else {
401                                 assert(control_type == ControlType::CAMERA_BUTTON_LIGHT);
402                                 camera_button_light_spinners.push_back(InstantiatedSpinner{ spinner, spinner2, nullptr, control.field_number });
403                         }
404                 }
405                 spinners[control.field_number] = spinner;
406         }
407         ui->treeWidget->addTopLevelItem(heading_item);
408 }
409
410 void MIDIMappingDialog::fill_controls_from_mapping(const MIDIMappingProto &mapping_proto)
411 {
412         for (const InstantiatedSpinner &is : controller_spinners) {
413                 is.spinner->setValue(get_controller_mapping_helper(mapping_proto, is.field_number, -1));
414         }
415         for (const InstantiatedSpinner &is : controller_light_spinners) {
416                 is.spinner->setValue(get_controller_mapping_helper(mapping_proto, is.field_number, -1));
417
418                 // HACK: We only have one of these right now, so min/max is a given;
419                 // no need to store proto field numbers.
420                 if (mapping_proto.has_master_speed_light_min()) {
421                         is.spinner2->setValue(mapping_proto.master_speed_light_min());
422                 }
423                 if (mapping_proto.has_master_speed_light_max()) {
424                         is.spinner3->setValue(mapping_proto.master_speed_light_max());
425                 }
426         }
427         for (const InstantiatedSpinner &is : button_spinners) {
428                 is.spinner->setValue(get_button_mapping_helper(mapping_proto, is.field_number, -1));
429         }
430         for (const InstantiatedSpinner &is : button_light_spinners) {
431                 MIDILightProto light_proto = get_light_mapping_helper(mapping_proto, is.field_number);
432                 if (light_proto.has_note_number()) {
433                         is.spinner->setValue(light_proto.note_number());
434                 } else {
435                         is.spinner->setValue(-1);
436                 }
437                 if (light_proto.has_velocity()) {
438                         is.spinner2->setValue(light_proto.velocity());
439                 } else {
440                         is.spinner2->setValue(-1);
441                 }
442         }
443         for (size_t camera_idx = 0; camera_idx < MAX_STREAMS; ++camera_idx) {
444                 CameraMIDIMappingProto camera_proto;
445                 if (camera_idx < size_t(mapping_proto.camera_size())) {
446                         camera_proto = mapping_proto.camera(camera_idx);
447                 }
448                 {
449                         const InstantiatedSpinner &is = camera_button_spinners[camera_idx];
450                         is.spinner->setValue(get_button_mapping_helper(camera_proto, is.field_number, -1));
451                 }
452                 {
453                         const InstantiatedSpinner &is = camera_button_light_spinners[camera_idx];
454                         const MIDILightProto &light_proto = get_light_mapping_helper(camera_proto, is.field_number);
455                         if (light_proto.has_note_number()) {
456                                 is.spinner->setValue(light_proto.note_number());
457                         } else {
458                                 is.spinner->setValue(-1);
459                         }
460                         if (light_proto.has_velocity()) {
461                                 is.spinner2->setValue(light_proto.velocity());
462                         } else {
463                                 is.spinner2->setValue(-1);
464                         }
465                 }
466         }
467         for (const InstantiatedComboBox &ic : bank_combo_boxes) {
468                 ic.combo_box->setCurrentIndex(get_bank(mapping_proto, ic.field_number, -1) + 1);
469         }
470 }
471
472 void MIDIMappingDialog::controller_changed(unsigned controller)
473 {
474         post_to_main_thread([=]{
475                 for (const InstantiatedSpinner &is : controller_spinners) {
476                         if (is.spinner->hasFocus()) {
477                                 is.spinner->setValue(controller);
478                                 is.spinner->selectAll();
479                         }
480                 }
481                 for (const InstantiatedSpinner &is : controller_light_spinners) {
482                         if (is.spinner->hasFocus()) {
483                                 is.spinner->setValue(controller);
484                                 is.spinner->selectAll();
485                         }
486                 }
487         });
488 }
489
490 void MIDIMappingDialog::note_on(unsigned note)
491 {
492         post_to_main_thread([=]{
493                 for (const InstantiatedSpinner &is : button_spinners) {
494                         if (is.spinner->hasFocus()) {
495                                 is.spinner->setValue(note);
496                                 is.spinner->selectAll();
497                         }
498                 }
499                 for (const InstantiatedSpinner &is : button_light_spinners) {
500                         if (is.spinner->hasFocus()) {
501                                 is.spinner->setValue(note);
502                                 is.spinner->selectAll();
503                         }
504                 }
505         });
506 }