Vkeybd: use ArdourWidgets for all GUI elements
[ardour.git] / gtk2_ardour / transform_dialog.cc
1 /*
2  * Copyright (C) 2014-2019 David Robillard <d@drobilla.net>
3  * Copyright (C) 2015-2017 Robin Gareus <robin@gareus.org>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19
20 #include <gtkmm/box.h>
21 #include <gtkmm/label.h>
22 #include <gtkmm/stock.h>
23
24 #include "transform_dialog.h"
25
26 #include "pbd/i18n.h"
27
28 using namespace ARDOUR;
29
30 TransformDialog::Model::Model()
31         : source_list(Gtk::ListStore::create(source_cols))
32         , property_list(Gtk::ListStore::create(property_cols))
33         , operator_list(Gtk::ListStore::create(operator_cols))
34 {
35         static const char* source_labels[] = {
36                 /* no NOTHING */
37                 _("this note's"),
38                 _("the previous note's"),
39                 _("this note's index"),
40                 _("the number of notes"),
41                 _("exactly"),
42                 _("a random number from"),
43                 NULL
44         };
45         for (int s = 0; source_labels[s]; ++s) {
46                 Gtk::TreeModel::Row row = *(source_list->append());
47                 row[source_cols.source] = (Source)(s + 1);  // Skip NOTHING
48                 row[source_cols.label]  = source_labels[s];
49         }
50         // Special row for ramp, which doesn't correspond to a source
51         Gtk::TreeModel::Row row = *(source_list->append());
52         row[source_cols.source] = Value::NOWHERE;
53         row[source_cols.label]  = _("equal steps from");
54
55         static const char* property_labels[] = {
56                 _("note number"),
57                 _("velocity"),
58                 _("start time"),
59                 _("length"),
60                 _("channel"),
61                 NULL
62         };
63         for (int p = 0; property_labels[p]; ++p) {
64                 Gtk::TreeModel::Row row = *(property_list->append());
65                 row[property_cols.property] = (Property)p;
66                 row[property_cols.label]    = property_labels[p];
67         }
68
69         static const char* operator_labels[] = {
70                 /* no PUSH */ "+", "-", "*", "/", "mod", NULL
71         };
72         for (int o = 0; operator_labels[o]; ++o) {
73                 Gtk::TreeModel::Row row = *(operator_list->append());
74                 row[operator_cols.op]    = (Operator)(o + 1);  // Skip PUSH
75                 row[operator_cols.label] = operator_labels[o];
76         }
77 }
78
79 TransformDialog::TransformDialog()
80         : ArdourDialog(_("Transform"), false, false)
81 {
82         _property_combo.set_model(_model.property_list);
83         _property_combo.pack_start(_model.property_cols.label);
84         _property_combo.set_active(1);
85         _property_combo.signal_changed().connect(
86                 sigc::mem_fun(this, &TransformDialog::property_changed));
87
88         Gtk::HBox* property_hbox = Gtk::manage(new Gtk::HBox);
89         property_hbox->pack_start(*Gtk::manage(new Gtk::Label(_("Set "))), false, false);
90         property_hbox->pack_start(_property_combo, false, false);
91         property_hbox->pack_start(*Gtk::manage(new Gtk::Label(_(" to "))), false, false);
92
93         _seed_chooser = Gtk::manage(new ValueChooser(_model));
94         _seed_chooser->set_target_property(MidiModel::NoteDiffCommand::Velocity);
95         _seed_chooser->source_combo.set_active(0);
96         property_hbox->pack_start(*_seed_chooser, false, false);
97
98         Gtk::HBox* add_hbox = Gtk::manage(new Gtk::HBox);
99         _add_button.add(
100                 *Gtk::manage(new Gtk::Image(Gtk::Stock::ADD, Gtk::ICON_SIZE_BUTTON)));
101         add_hbox->pack_start(_add_button, false, false);
102         _add_button.signal_clicked().connect(
103                 sigc::mem_fun(*this, &TransformDialog::add_clicked));
104
105         get_vbox()->set_spacing(6);
106         get_vbox()->pack_start(*property_hbox, false, false);
107         get_vbox()->pack_start(_operations_box, false, false);
108         get_vbox()->pack_start(*add_hbox, false, false);
109
110         add_button(Gtk::Stock::CANCEL, Gtk::RESPONSE_CANCEL);
111         add_button(_("Transform"), Gtk::RESPONSE_OK);
112
113         show_all();
114         _seed_chooser->value_spinner.hide();
115         _seed_chooser->source_changed();
116 }
117
118 TransformDialog::ValueChooser::ValueChooser(const Model& model)
119         : model(model)
120         , target_property((Property)1)
121         , to_label(" to ")
122 {
123         source_combo.set_model(model.source_list);
124         source_combo.pack_start(model.source_cols.label);
125         source_combo.signal_changed().connect(
126                 sigc::mem_fun(this, &TransformDialog::ValueChooser::source_changed));
127
128         property_combo.set_model(model.property_list);
129         property_combo.pack_start(model.property_cols.label);
130
131         set_spacing(4);
132         pack_start(source_combo, false, false);
133         pack_start(property_combo, false, false);
134         pack_start(value_spinner, false, false);
135         pack_start(to_label, false, false);
136         pack_start(max_spinner, false, false);
137         show_all();
138
139         source_combo.set_active(4);
140         property_combo.set_active(1);
141         set_target_property(MidiModel::NoteDiffCommand::Velocity);
142         max_spinner.set_value(127);
143         source_changed();
144 }
145
146 static void
147 set_spinner_for(Gtk::SpinButton&                     spinner,
148                 MidiModel::NoteDiffCommand::Property prop)
149 {
150         switch (prop) {
151         case MidiModel::NoteDiffCommand::NoteNumber:
152         case MidiModel::NoteDiffCommand::Velocity:
153                 spinner.get_adjustment()->set_lower(1); // no 0, note off
154                 spinner.get_adjustment()->set_upper(127);
155                 spinner.get_adjustment()->set_step_increment(1);
156                 spinner.get_adjustment()->set_page_increment(10);
157                 spinner.set_digits(0);
158                 break;
159         case MidiModel::NoteDiffCommand::StartTime:
160                 spinner.get_adjustment()->set_lower(0);
161                 spinner.get_adjustment()->set_upper(1024);
162                 spinner.get_adjustment()->set_step_increment(0.125);
163                 spinner.get_adjustment()->set_page_increment(1.0);
164                 spinner.set_digits(2);
165                 break;
166         case MidiModel::NoteDiffCommand::Length:
167                 spinner.get_adjustment()->set_lower(1.0 / 64.0);
168                 spinner.get_adjustment()->set_upper(32);
169                 spinner.get_adjustment()->set_step_increment(1.0 / 64.0);
170                 spinner.get_adjustment()->set_page_increment(1.0);
171                 spinner.set_digits(2);
172                 break;
173         case MidiModel::NoteDiffCommand::Channel:
174                 spinner.get_adjustment()->set_lower(1);
175                 spinner.get_adjustment()->set_upper(16);
176                 spinner.get_adjustment()->set_step_increment(1);
177                 spinner.get_adjustment()->set_page_increment(10);
178                 spinner.set_digits(0);
179                 break;
180         }
181         spinner.set_value(
182                 std::min(spinner.get_adjustment()->get_upper(),
183                          std::max(spinner.get_adjustment()->get_lower(), spinner.get_value())));
184 }
185
186 void
187 TransformDialog::ValueChooser::set_target_property(Property prop)
188 {
189         target_property = prop;
190         set_spinner_for(value_spinner, prop);
191         set_spinner_for(max_spinner, prop);
192 }
193
194 void
195 TransformDialog::ValueChooser::source_changed()
196 {
197         Gtk::TreeModel::const_iterator s      = source_combo.get_active();
198         const Source                   source = (*s)[model.source_cols.source];
199
200         value_spinner.hide();
201         to_label.hide();
202         max_spinner.hide();
203         if (source == Value::LITERAL) {
204                 value_spinner.show();
205                 property_combo.hide();
206         } else if (source == Value::RANDOM) {
207                 value_spinner.show();
208                 to_label.show();
209                 max_spinner.show();
210                 property_combo.hide();
211         } else if (source == Value::NOWHERE) {
212                 /* Bit of a kludge, hijack this for ramps since it's the only thing
213                    that doesn't correspond to a source.  When we add more fancy
214                    code-generating value chooser options, the column model will need to
215                    be changed a bit to reflect this. */
216                 value_spinner.show();
217                 to_label.show();
218                 max_spinner.show();
219                 property_combo.hide();
220         } else if (source == Value::INDEX || source == Value::N_NOTES) {
221                 value_spinner.hide();
222                 property_combo.hide();
223         } else {
224                 value_spinner.hide();
225                 property_combo.show();
226         }
227 }
228
229 double
230 TransformDialog::ValueChooser::get_value() const
231 {
232         const bool is_channel = target_property == MidiModel::NoteDiffCommand::Channel;
233         return value_spinner.get_value() + (is_channel ? -1.0 : 0.0);
234 }
235
236 double
237 TransformDialog::ValueChooser::get_max() const
238 {
239         const bool is_channel = target_property == MidiModel::NoteDiffCommand::Channel;
240         return max_spinner.get_value() + (is_channel ? -1.0 : 0.0);
241 }
242
243 void
244 TransformDialog::ValueChooser::get(std::list<Operation>& ops)
245 {
246         Gtk::TreeModel::const_iterator s      = source_combo.get_active();
247         const Source                   source = (*s)[model.source_cols.source];
248
249         if (source == Transform::Value::RANDOM) {
250                 /* Special case: a RANDOM value is always 0..1, so here we produce some
251                    code to produce a random number in a range: "rand value *". */
252                 const double a     = get_value();
253                 const double b     = get_max();
254                 const double min   = std::min(a, b);
255                 const double max   = std::max(a, b);
256                 const double range = max - min;
257
258                 // "rand range * min +" ((rand * range) + min)
259                 ops.push_back(Operation(Operation::PUSH, Value(Value::RANDOM)));
260                 ops.push_back(Operation(Operation::PUSH, Value(range)));
261                 ops.push_back(Operation(Operation::MULT));
262                 ops.push_back(Operation(Operation::PUSH, Value(min)));
263                 ops.push_back(Operation(Operation::ADD));
264                 return;
265         } else if (source == Transform::Value::NOWHERE) {
266                 /* Special case: hijack NOWHERE for ramps (see above).  The language
267                    knows nothing of ramps, we generate code to calculate the
268                    appropriate value here. */
269                 const double first = get_value();
270                 const double last  = get_max();
271                 const double rise  = last - first;
272
273                 // "index rise * n_notes 1 - / first +" (index * rise / (n_notes - 1) + first)
274                 ops.push_back(Operation(Operation::PUSH, Value(Value::INDEX)));
275                 ops.push_back(Operation(Operation::PUSH, Value(rise)));
276                 ops.push_back(Operation(Operation::MULT));
277                 ops.push_back(Operation(Operation::PUSH, Value(Value::N_NOTES)));
278                 ops.push_back(Operation(Operation::PUSH, Value(1)));
279                 ops.push_back(Operation(Operation::SUB));
280                 ops.push_back(Operation(Operation::DIV));
281                 ops.push_back(Operation(Operation::PUSH, Value(first)));
282                 ops.push_back(Operation(Operation::ADD));
283                 return;
284         }
285
286         // Produce a simple Value
287         Value val((*s)[model.source_cols.source]);
288         if (val.source == Transform::Value::THIS_NOTE ||
289             val.source == Transform::Value::PREV_NOTE) {
290                 Gtk::TreeModel::const_iterator p = property_combo.get_active();
291                 val.prop = (*p)[model.property_cols.property];
292         } else if (val.source == Transform::Value::LITERAL) {
293                 val.value = Variant(
294                         MidiModel::NoteDiffCommand::value_type(target_property),
295                         get_value());
296         }
297         ops.push_back(Operation(Operation::PUSH, val));
298 }
299
300 TransformDialog::OperationChooser::OperationChooser(const Model& model)
301         : model(model)
302         , value_chooser(model)
303 {
304         operator_combo.set_model(model.operator_list);
305         operator_combo.pack_start(model.operator_cols.label);
306         operator_combo.set_active(0);
307
308         pack_start(operator_combo, false, false);
309         pack_start(value_chooser, false, false);
310         pack_start(*Gtk::manage(new Gtk::Label(" ")), true, true);
311         pack_start(remove_button, false, false);
312
313         remove_button.add(
314                 *Gtk::manage(new Gtk::Image(Gtk::Stock::REMOVE, Gtk::ICON_SIZE_BUTTON)));
315
316         remove_button.signal_clicked().connect(
317                 sigc::mem_fun(*this, &TransformDialog::OperationChooser::remove_clicked));
318
319         value_chooser.source_combo.set_active(0);
320
321         show_all();
322         value_chooser.property_combo.hide();
323         value_chooser.value_spinner.set_value(1);
324         value_chooser.source_changed();
325 }
326
327 void
328 TransformDialog::OperationChooser::get(std::list<Operation>& ops)
329 {
330         Gtk::TreeModel::const_iterator o = operator_combo.get_active();
331
332         value_chooser.get(ops);
333         ops.push_back(Operation((*o)[model.operator_cols.op]));
334 }
335
336 void
337 TransformDialog::OperationChooser::remove_clicked()
338 {
339         delete this;
340 }
341
342 Transform::Program
343 TransformDialog::get()
344 {
345         Transform::Program prog;
346
347         // Set target property
348         prog.prop = (*_property_combo.get_active())[_model.property_cols.property];
349
350         // Append code to push seed to stack
351         _seed_chooser->get(prog.ops);
352
353         // Append all operations' code to program
354         const std::vector<Gtk::Widget*>& choosers = _operations_box.get_children();
355         for (std::vector<Gtk::Widget*>::const_iterator o = choosers.begin();
356              o != choosers.end(); ++o) {
357                 OperationChooser* chooser = dynamic_cast<OperationChooser*>(*o);
358                 if (chooser) {
359                         chooser->get(prog.ops);
360                 }
361         }
362
363         return prog;
364 }
365
366 void
367 TransformDialog::property_changed()
368 {
369         Gtk::TreeModel::const_iterator i = _property_combo.get_active();
370         _seed_chooser->set_target_property((*i)[_model.property_cols.property]);
371 }
372
373 void
374 TransformDialog::add_clicked()
375 {
376         _operations_box.pack_start(
377                 *Gtk::manage(new OperationChooser(_model)), false, false);
378 }