VKeybd: Pass on primary (Ctrl/Cmd) shortcuts
[ardour.git] / gtk2_ardour / midi_list_editor.cc
1 /*
2  * Copyright (C) 2009-2015 David Robillard <d@drobilla.net>
3  * Copyright (C) 2009-2017 Paul Davis <paul@linuxaudiosystems.com>
4  * Copyright (C) 2014-2016 Robin Gareus <robin@gareus.org>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License along
17  * with this program; if not, write to the Free Software Foundation, Inc.,
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19  */
20
21 #include <cmath>
22 #include <map>
23
24 #include <gtkmm/cellrenderercombo.h>
25
26 #include "evoral/midi_util.h"
27 #include "evoral/Note.h"
28
29 #include "ardour/beats_samples_converter.h"
30 #include "ardour/midi_model.h"
31 #include "ardour/midi_region.h"
32 #include "ardour/midi_source.h"
33 #include "ardour/session.h"
34 #include "ardour/tempo.h"
35
36 #include "gtkmm2ext/gui_thread.h"
37 #include "gtkmm2ext/keyboard.h"
38 #include "gtkmm2ext/actions.h"
39
40 #include "midi_list_editor.h"
41 #include "note_player.h"
42 #include "ui_config.h"
43
44 #include "pbd/i18n.h"
45
46 using namespace std;
47 using namespace Gtk;
48 using namespace Gtkmm2ext;
49 using namespace Glib;
50 using namespace ARDOUR;
51 using Timecode::BBT_Time;
52
53 static map<int,std::string> note_length_map;
54
55 static void
56 fill_note_length_map ()
57 {
58         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat, _("Whole")));
59         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/2, _("Half")));
60         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/3, _("Triplet")));
61         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/4, _("Quarter")));
62         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/8, _("Eighth")));
63         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/16, _("Sixteenth")));
64         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/32, _("Thirty-second")));
65         note_length_map.insert (make_pair<int,string> (BBT_Time::ticks_per_beat/64, _("Sixty-fourth")));
66 }
67
68 MidiListEditor::MidiListEditor (Session* s, boost::shared_ptr<MidiRegion> r, boost::shared_ptr<MidiTrack> tr)
69         : ArdourWindow (r->name())
70         , buttons (1, 1)
71         , region (r)
72         , track (tr)
73 {
74         if (note_length_map.empty()) {
75                 fill_note_length_map ();
76         }
77
78         /* We do not handle nested sources/regions. Caller should have tackled this */
79
80         if (r->max_source_level() > 0) {
81                 throw failed_constructor();
82         }
83
84         set_session (s);
85
86         edit_column = -1;
87         editing_renderer = 0;
88         editing_editable = 0;
89
90         model = ListStore::create (columns);
91         view.set_model (model);
92
93         note_length_model = ListStore::create (note_length_columns);
94         TreeModel::Row row;
95
96         for (std::map<int,string>::iterator i = note_length_map.begin(); i != note_length_map.end(); ++i) {
97                 row = *(note_length_model->append());
98                 row[note_length_columns.ticks] = i->first;
99                 row[note_length_columns.name] = i->second;
100         }
101
102         view.signal_key_press_event().connect (sigc::mem_fun (*this, &MidiListEditor::key_press), false);
103         view.signal_key_release_event().connect (sigc::mem_fun (*this, &MidiListEditor::key_release), false);
104         view.signal_scroll_event().connect (sigc::mem_fun (*this, &MidiListEditor::scroll_event), false);
105
106         view.append_column (_("Start"), columns.start);
107         view.append_column (_("Channel"), columns.channel);
108         view.append_column (_("Num"), columns.note);
109         view.append_column (_("Name"), columns.note_name);
110         view.append_column (_("Vel"), columns.velocity);
111
112         /* use a combo renderer for length, so that we can offer a selection
113            of pre-defined note lengths. we still allow edited values with
114            arbitrary length (in ticks).
115          */
116
117         Gtk::TreeViewColumn* lenCol = Gtk::manage (new Gtk::TreeViewColumn (_("Length")));
118         Gtk::CellRendererCombo* comboCell = Gtk::manage(new Gtk::CellRendererCombo);
119         lenCol->pack_start(*comboCell);
120         lenCol->add_attribute (comboCell->property_text(), columns.length);
121
122         comboCell->property_model() = note_length_model;
123         comboCell->property_text_column() = 1;
124         comboCell->property_has_entry() = false;
125
126         view.append_column (*lenCol);
127
128         view.set_headers_visible (true);
129         view.set_rules_hint (true);
130         view.get_selection()->set_mode (SELECTION_MULTIPLE);
131         view.get_selection()->signal_changed().connect (sigc::mem_fun (*this, &MidiListEditor::selection_changed));
132
133         for (int i = 0; i < 6; ++i) {
134                 CellRendererText* renderer = dynamic_cast<CellRendererText*>(view.get_column_cell_renderer (i));
135
136                 TreeViewColumn* col = view.get_column (i);
137                 col->set_data (X_("colnum"), GUINT_TO_POINTER(i));
138
139                 renderer->property_editable() = true;
140
141                 renderer->signal_editing_started().connect (sigc::bind (sigc::mem_fun (*this, &MidiListEditor::editing_started), i));
142                 renderer->signal_editing_canceled().connect (sigc::mem_fun (*this, &MidiListEditor::editing_canceled));
143                 renderer->signal_edited().connect (sigc::mem_fun (*this, &MidiListEditor::edited));
144         }
145
146         scroller.add (view);
147         scroller.set_policy (POLICY_NEVER, POLICY_AUTOMATIC);
148
149         redisplay_model ();
150
151         region->midi_source(0)->model()->ContentsChanged.connect (content_connection, invalidator (*this),
152                                                                   boost::bind (&MidiListEditor::redisplay_model, this), gui_context());
153
154         buttons.attach (sound_notes_button, 0, 1, 0, 1);
155         Glib::RefPtr<Gtk::Action> act = ActionManager::get_action ("Editor", "sound-midi-notes");
156         if (act) {
157                 gtk_activatable_set_related_action (GTK_ACTIVATABLE (sound_notes_button.gobj()), act->gobj());
158         }
159
160         view.show ();
161         scroller.show ();
162         buttons.show ();
163         vbox.show ();
164         sound_notes_button.show ();
165
166         vbox.set_spacing (6);
167         vbox.set_border_width (6);
168         vbox.pack_start (buttons, false, false);
169         vbox.pack_start (scroller, true, true);
170
171         add (vbox);
172         set_size_request (-1, 400);
173 }
174
175 MidiListEditor::~MidiListEditor ()
176 {
177 }
178
179 bool
180 MidiListEditor::scroll_event (GdkEventScroll* ev)
181 {
182         TreeModel::Path path;
183         TreeViewColumn* col;
184         int cellx;
185         int celly;
186         int idelta = 0;
187         double fdelta = 0;
188         MidiModel::NoteDiffCommand::Property prop (MidiModel::NoteDiffCommand::NoteNumber);
189         bool apply = false;
190         bool was_selected = false;
191         char const * opname;
192
193         if (!view.get_path_at_pos (ev->x, ev->y, path, col, cellx, celly)) {
194                 return false;
195         }
196
197         if (view.get_selection()->count_selected_rows() == 0) {
198                 was_selected = false;
199         } else if (view.get_selection()->is_selected (path)) {
200                 was_selected = true;
201         } else {
202                 was_selected = false;
203         }
204
205         int colnum = GPOINTER_TO_UINT (col->get_data (X_("colnum")));
206
207         switch (colnum) {
208         case 0:
209                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
210                         fdelta = 1/64.0;
211                 } else {
212                         fdelta = 1/4.0;
213                 }
214                 if (ev->direction == GDK_SCROLL_DOWN || ev->direction == GDK_SCROLL_LEFT) {
215                         fdelta = -fdelta;
216                 }
217                 prop = MidiModel::NoteDiffCommand::StartTime;
218                 opname = _("edit note start");
219                 apply = true;
220                 break;
221         case 1:
222                 idelta = 1;
223                 if (ev->direction == GDK_SCROLL_DOWN || ev->direction == GDK_SCROLL_LEFT) {
224                         idelta = -idelta;
225                 }
226                 prop = MidiModel::NoteDiffCommand::Channel;
227                 opname = _("edit note channel");
228                 apply = true;
229                 break;
230         case 2:
231         case 3:
232                 idelta = 1;
233                 if (ev->direction == GDK_SCROLL_DOWN || ev->direction == GDK_SCROLL_LEFT) {
234                         idelta = -idelta;
235                 }
236                 prop = MidiModel::NoteDiffCommand::NoteNumber;
237                 opname = _("edit note number");
238                 apply = true;
239                 break;
240
241         case 4:
242                 idelta = 1;
243                 if (ev->direction == GDK_SCROLL_DOWN || ev->direction == GDK_SCROLL_LEFT) {
244                         idelta = -idelta;
245                 }
246                 prop = MidiModel::NoteDiffCommand::Velocity;
247                 opname = _("edit note velocity");
248                 apply = true;
249                 break;
250
251         case 5:
252                 if (Keyboard::modifier_state_equals (ev->state, Keyboard::SecondaryModifier)) {
253                         fdelta = 1/64.0;
254                 } else {
255                         fdelta = 1/4.0;
256                 }
257                 if (ev->direction == GDK_SCROLL_DOWN || ev->direction == GDK_SCROLL_LEFT) {
258                         fdelta = -fdelta;
259                 }
260                 prop = MidiModel::NoteDiffCommand::Length;
261                 opname = _("edit note length");
262                 apply = true;
263                 break;
264
265         default:
266                 break;
267         }
268
269
270         if (apply) {
271
272                 boost::shared_ptr<MidiModel> m (region->midi_source(0)->model());
273                 MidiModel::NoteDiffCommand* cmd = m->new_note_diff_command (opname);
274                 vector<TreeModel::Path> previous_selection;
275
276                 if (was_selected) {
277
278                         /* use selection */
279
280                         TreeView::Selection::ListHandle_Path rows = view.get_selection()->get_selected_rows ();
281                         TreeModel::iterator iter;
282                         boost::shared_ptr<NoteType> note;
283
284                         for (TreeView::Selection::ListHandle_Path::iterator i = rows.begin(); i != rows.end(); ++i) {
285
286                                 previous_selection.push_back (*i);
287
288                                 if ((iter = model->get_iter (*i))) {
289
290                                         note = (*iter)[columns._note];
291
292                                         switch (prop) {
293                                         case MidiModel::NoteDiffCommand::StartTime:
294                                                 if (note->time() + fdelta >= 0) {
295                                                         cmd->change (note, prop, note->time() + fdelta);
296                                                 } else {
297                                                         cmd->change (note, prop, Temporal::Beats());
298                                                 }
299                                                 break;
300                                         case MidiModel::NoteDiffCommand::Velocity:
301                                                 cmd->change (note, prop, (uint8_t) (note->velocity() + idelta));
302                                                 break;
303                                         case MidiModel::NoteDiffCommand::Length:
304                                                 if (note->length().to_double() + fdelta >=
305                                                     Temporal::Beats::tick().to_double()) {
306                                                         cmd->change (note, prop, note->length() + fdelta);
307                                                 } else {
308                                                         cmd->change (note, prop, Temporal::Beats::tick());
309                                                 }
310                                                 break;
311                                         case MidiModel::NoteDiffCommand::Channel:
312                                                 cmd->change (note, prop, (uint8_t) (note->channel() + idelta));
313                                                 break;
314                                         case MidiModel::NoteDiffCommand::NoteNumber:
315                                                 cmd->change (note, prop, (uint8_t) (note->note() + idelta));
316                                                 break;
317                                         default:
318                                                 continue;
319                                         }
320                                 }
321                         }
322
323                 } else {
324
325                         /* just this row */
326
327                         TreeModel::iterator iter;
328                         iter = model->get_iter (path);
329
330                         previous_selection.push_back (path);
331
332                         if (iter) {
333                                 boost::shared_ptr<NoteType> note = (*iter)[columns._note];
334
335                                 switch (prop) {
336                                 case MidiModel::NoteDiffCommand::StartTime:
337                                         if (note->time() + fdelta >= 0) {
338                                                 cmd->change (note, prop, note->time() + fdelta);
339                                         } else {
340                                                 cmd->change (note, prop, Temporal::Beats());
341                                         }
342                                         break;
343                                 case MidiModel::NoteDiffCommand::Velocity:
344                                         cmd->change (note, prop, (uint8_t) (note->velocity() + idelta));
345                                         break;
346                                 case MidiModel::NoteDiffCommand::Length:
347                                         if (note->length() + fdelta >=
348                                             Temporal::Beats::tick().to_double()) {
349                                                 cmd->change (note, prop, note->length() + fdelta);
350                                         } else {
351                                                 cmd->change (note, prop, Temporal::Beats::tick());
352                                         }
353                                         break;
354                                 case MidiModel::NoteDiffCommand::Channel:
355                                         cmd->change (note, prop, (uint8_t) (note->channel() + idelta));
356                                         break;
357                                 case MidiModel::NoteDiffCommand::NoteNumber:
358                                         cmd->change (note, prop, (uint8_t) (note->note() + idelta));
359                                         break;
360                                 default:
361                                         break;
362                                 }
363                         }
364                 }
365
366                 m->apply_command (*_session, cmd);
367
368                 /* reset selection to be as it was before we rebuilt */
369
370                 for (vector<TreeModel::Path>::iterator i = previous_selection.begin(); i != previous_selection.end(); ++i) {
371                         view.get_selection()->select (*i);
372                 }
373         }
374
375         return true;
376 }
377
378 bool
379 MidiListEditor::key_press (GdkEventKey* ev)
380 {
381         bool ret = false;
382         TreeModel::Path path;
383         TreeViewColumn* col;
384         int colnum;
385
386         switch (ev->keyval) {
387         case GDK_Tab:
388                 if (edit_column > 0) {
389                         colnum = edit_column;
390                         path = edit_path;
391                         if (editing_editable) {
392                                 editing_editable->editing_done ();
393                         }
394                         if (colnum >= 5) {
395                                 /* wrap to next line */
396                                 colnum = 0;
397                                 path.next();
398                         } else {
399                                 colnum++;
400                         }
401                         col = view.get_column (colnum);
402                         view.set_cursor (path, *col, true);
403                         ret = true;
404                 }
405                 break;
406
407         case GDK_Up:
408         case GDK_uparrow:
409                 if (edit_column > 0) {
410                         colnum = edit_column;
411                         path = edit_path;
412                         if (editing_editable) {
413                                 editing_editable->editing_done ();
414                         }
415                         path.prev ();
416                         col = view.get_column (colnum);
417                         view.set_cursor (path, *col, true);
418                         ret = true;
419                 }
420                 break;
421
422         case GDK_Down:
423         case GDK_downarrow:
424                 if (edit_column > 0) {
425                         colnum = edit_column;
426                         path = edit_path;
427                         if (editing_editable) {
428                                 editing_editable->editing_done ();
429                         }
430                         path.next ();
431                         col = view.get_column (colnum);
432                         view.set_cursor (path, *col, true);
433                         ret = true;
434                 }
435                 break;
436
437         case GDK_Escape:
438                 stop_editing (true);
439                 break;
440
441         }
442
443         return ret;
444 }
445
446 bool
447 MidiListEditor::key_release (GdkEventKey* ev)
448 {
449         bool ret = false;
450         TreeModel::Path path;
451         TreeViewColumn* col;
452         TreeModel::iterator iter;
453         MidiModel::NoteDiffCommand* cmd;
454         boost::shared_ptr<MidiModel> m (region->midi_source(0)->model());
455         boost::shared_ptr<NoteType> note;
456         boost::shared_ptr<NoteType> copy;
457
458         switch (ev->keyval) {
459         case GDK_Insert:
460                 /* add a new note to the model, based on the note at the cursor
461                  * pos
462                  */
463                 view.get_cursor (path, col);
464                 iter = model->get_iter (path);
465                 cmd = m->new_note_diff_command (_("insert new note"));
466                 note = (*iter)[columns._note];
467                 copy.reset (new NoteType (*note.get()));
468                 cmd->add (copy);
469                 m->apply_command (*_session, cmd);
470                 /* model has been redisplayed by now */
471                 path.next ();
472                 /* select, start editing column 2 (note) */
473                 col = view.get_column (2);
474                 view.set_cursor (path, *col, true);
475                 break;
476
477         case GDK_Delete:
478         case GDK_BackSpace:
479                 if (edit_column < 0) {
480                         delete_selected_note ();
481                 }
482                 ret = true;
483                 break;
484
485         case GDK_z:
486                 if (_session && Gtkmm2ext::Keyboard::modifier_state_contains (ev->state, Gtkmm2ext::Keyboard::PrimaryModifier)) {
487                         _session->undo (1);
488                         ret = true;
489                 }
490                 break;
491
492         case GDK_r:
493                 if (_session && Gtkmm2ext::Keyboard::modifier_state_contains (ev->state, Gtkmm2ext::Keyboard::PrimaryModifier)) {
494                         _session->redo (1);
495                         ret = true;
496                 }
497                 break;
498
499         default:
500                 break;
501         }
502
503         return ret;
504 }
505
506 void
507 MidiListEditor::delete_selected_note ()
508 {
509         Glib::RefPtr<TreeSelection> selection = view.get_selection();
510         TreeView::Selection::ListHandle_Path rows = selection->get_selected_rows ();
511
512         if (rows.empty()) {
513                 return;
514         }
515
516         typedef vector<boost::shared_ptr<NoteType> > Notes;
517         Notes to_delete;
518
519         for (TreeView::Selection::ListHandle_Path::iterator i = rows.begin(); i != rows.end(); ++i) {
520                 TreeIter iter;
521
522                 if ((iter = model->get_iter (*i))) {
523                         boost::shared_ptr<NoteType> note = (*iter)[columns._note];
524                         to_delete.push_back (note);
525                 }
526         }
527
528         boost::shared_ptr<MidiModel> m (region->midi_source(0)->model());
529         MidiModel::NoteDiffCommand* cmd = m->new_note_diff_command (_("delete notes (from list)"));
530
531         for (Notes::iterator i = to_delete.begin(); i != to_delete.end(); ++i) {
532                 cmd->remove (*i);
533         }
534
535         m->apply_command (*_session, cmd);
536 }
537
538 void
539 MidiListEditor::stop_editing (bool cancelled)
540 {
541         if (!cancelled) {
542                 if (editing_editable) {
543                         editing_editable->editing_done ();
544                 }
545         } else {
546                 if (editing_renderer) {
547                         editing_renderer->stop_editing (cancelled);
548                 }
549         }
550 }
551
552 void
553 MidiListEditor::editing_started (CellEditable* ed, const string& path, int colno)
554 {
555         edit_path = TreePath (path);
556         edit_column = colno;
557         editing_renderer = dynamic_cast<CellRendererText*>(view.get_column_cell_renderer (colno));
558         editing_editable = ed;
559
560         if (ed) {
561                 Gtk::Entry *e = dynamic_cast<Gtk::Entry*> (ed);
562                 if (e) {
563                         e->signal_key_press_event().connect (sigc::mem_fun (*this, &MidiListEditor::key_press), false);
564                         e->signal_key_release_event().connect (sigc::mem_fun (*this, &MidiListEditor::key_release), false);
565                 }
566         }
567 }
568
569 void
570 MidiListEditor::editing_canceled ()
571 {
572         edit_path.clear ();
573         edit_column = -1;
574         editing_renderer = 0;
575         editing_editable = 0;
576 }
577
578 void
579 MidiListEditor::edited (const std::string& path, const std::string& text)
580 {
581         TreeModel::iterator iter = model->get_iter (path);
582
583         if (!iter || text.empty()) {
584                 return;
585         }
586
587         boost::shared_ptr<NoteType> note = (*iter)[columns._note];
588         MidiModel::NoteDiffCommand::Property prop (MidiModel::NoteDiffCommand::NoteNumber);
589
590         double fval;
591         int    ival;
592         bool   apply = false;
593         int    idelta = 0;
594         double fdelta = 0;
595         char const * opname;
596         switch (edit_column) {
597         case 0: // start
598                 break;
599         case 1: // channel
600                 // correct ival for zero-based counting after scan
601                 if (sscanf (text.c_str(), "%d", &ival) == 1 && --ival != note->channel()) {
602                         idelta = ival - note->channel();
603                         prop = MidiModel::NoteDiffCommand::Channel;
604                         opname = _("change note channel");
605                         apply = true;
606                 }
607                 break;
608         case 2: // note
609                 if (sscanf (text.c_str(), "%d", &ival) == 1 && ival != note->note()) {
610                         idelta = ival - note->note();
611                         prop = MidiModel::NoteDiffCommand::NoteNumber;
612                         opname = _("change note number");
613                         apply = true;
614                 }
615                 break;
616         case 3: // name
617                 ival = ParameterDescriptor::midi_note_num (text);
618                 if (ival < 128) {
619                         idelta = ival - note->note();
620                         prop = MidiModel::NoteDiffCommand::NoteNumber;
621                         opname = _("change note number");
622                         apply = true;
623                 }
624                 break;
625         case 4: // velocity
626                 if (sscanf (text.c_str(), "%d", &ival) == 1 && ival != note->velocity()) {
627                         idelta = ival - note->velocity();
628                         prop = MidiModel::NoteDiffCommand::Velocity;
629                         opname = _("change note velocity");
630                         apply = true;
631                 }
632                 break;
633         case 5: // length
634
635                 if (sscanf (text.c_str(), "%lf", &fval) == 1) {
636
637                         /* numeric value entered */
638
639                         if (text.find ('.') == string::npos && text.find (',') == string::npos) {
640                                 /* integral => units are ticks */
641                                 fval = fval / BBT_Time::ticks_per_beat;
642                         } else {
643                                 /* non-integral => beats, so use as-is */
644                         }
645
646                 } else {
647
648                         /* assume its text from the combo. look for the map
649                          * entry for the actual note ticks
650                          */
651
652                         uint64_t len_ticks = note->length().to_ticks();
653                         std::map<int,string>::iterator x = note_length_map.find (len_ticks);
654
655                         if (x == note_length_map.end()) {
656
657                                 /* tick length not in map - was
658                                  * displaying numeric value ... use new value
659                                  * from note length map, and convert to beats.
660                                  */
661
662                                 for (x = note_length_map.begin(); x != note_length_map.end(); ++x) {
663                                         if (x->second == text) {
664                                                 break;
665                                         }
666                                 }
667
668                                 if (x != note_length_map.end()) {
669                                         fval = x->first / BBT_Time::ticks_per_beat;
670                                 }
671
672                         } else {
673
674                                 fval = -1.0;
675
676                                 if (text != x->second) {
677
678                                         /* get ticks for the newly selected
679                                          * note length
680                                          */
681
682                                         for (x = note_length_map.begin(); x != note_length_map.end(); ++x) {
683                                                 if (x->second == text) {
684                                                         break;
685                                                 }
686                                         }
687
688                                         if (x != note_length_map.end()) {
689                                                 /* convert to beats */
690                                                 fval = (double) x->first / BBT_Time::ticks_per_beat;
691                                         }
692                                 }
693                         }
694                 }
695
696                 if (fval > 0.0) {
697                         fdelta = fval - note->length().to_double();
698                         prop = MidiModel::NoteDiffCommand::Length;
699                         opname = _("change note length");
700                         apply = true;
701                 }
702                 break;
703
704         default:
705                 break;
706         }
707
708         if (apply) {
709
710                 boost::shared_ptr<MidiModel> m (region->midi_source(0)->model());
711                 MidiModel::NoteDiffCommand* cmd = m->new_note_diff_command (opname);
712
713                 TreeView::Selection::ListHandle_Path rows = view.get_selection()->get_selected_rows ();
714
715                 for (TreeView::Selection::ListHandle_Path::iterator i = rows.begin(); i != rows.end(); ++i) {
716                         if ((iter = model->get_iter (*i))) {
717
718                                 note = (*iter)[columns._note];
719
720                                 switch (prop) {
721                                 case MidiModel::NoteDiffCommand::Velocity:
722                                         cmd->change (note, prop, (uint8_t) (note->velocity() + idelta));
723                                         break;
724                                 case MidiModel::NoteDiffCommand::Length:
725                                         cmd->change (note, prop, note->length() + fdelta);
726                                         break;
727                                 case MidiModel::NoteDiffCommand::Channel:
728                                         cmd->change (note, prop, (uint8_t) (note->channel() + idelta));
729                                         break;
730                                 case MidiModel::NoteDiffCommand::NoteNumber:
731                                         cmd->change (note, prop, (uint8_t) (note->note() + idelta));
732                                         break;
733                                 default:
734                                         continue;
735                                 }
736                         }
737                 }
738
739                 m->apply_command (*_session, cmd);
740
741                 /* model has been redisplayed by now */
742                 /* keep selected row(s), move cursor there, don't continue editing */
743
744                 TreeViewColumn* col = view.get_column (edit_column);
745                 view.set_cursor (edit_path, *col, 0);
746
747                 /* reset edit info, since we're done */
748
749                 edit_path.clear ();
750                 edit_column = -1;
751                 editing_renderer = 0;
752                 editing_editable = 0;
753         }
754 }
755
756 void
757 MidiListEditor::redisplay_model ()
758 {
759         view.set_model (Glib::RefPtr<Gtk::ListStore>(0));
760         model->clear ();
761
762         if (_session) {
763
764                 BeatsSamplesConverter conv (_session->tempo_map(), region->position());
765                 MidiModel::Notes notes = region->midi_source(0)->model()->notes();
766                 TreeModel::Row row;
767                 stringstream ss;
768
769                 for (MidiModel::Notes::iterator i = notes.begin(); i != notes.end(); ++i) {
770                         row = *(model->append());
771                         row[columns.channel] = (*i)->channel() + 1;
772                         row[columns.note_name] = ParameterDescriptor::midi_note_name ((*i)->note());
773                         row[columns.note] = (*i)->note();
774                         row[columns.velocity] = (*i)->velocity();
775
776                         Timecode::BBT_Time bbt (_session->tempo_map().bbt_at_sample (region->position() + conv.to ((*i)->time())));
777
778                         ss.str ("");
779                         ss << bbt;
780                         row[columns.start] = ss.str();
781
782                         bbt.bars = 0;
783                         const Temporal::Beats dur = (*i)->end_time() - (*i)->time();
784                         bbt.beats = dur.get_beats ();
785                         bbt.ticks = dur.get_ticks ();
786
787                         uint64_t len_ticks = (*i)->length().to_ticks();
788                         std::map<int,string>::iterator x = note_length_map.find (len_ticks);
789
790                         if (x != note_length_map.end()) {
791                                 row[columns.length] = x->second;
792                         } else {
793                                 ss.str ("");
794                                 ss << len_ticks;
795                                 row[columns.length] = ss.str();
796                         }
797
798                         row[columns._note] = (*i);
799                 }
800         }
801
802         view.set_model (model);
803 }
804
805 void
806 MidiListEditor::selection_changed ()
807 {
808         if (!UIConfiguration::instance().get_sound_midi_notes()) {
809                 return;
810         }
811
812         TreeModel::Path path;
813         TreeModel::iterator iter;
814         boost::shared_ptr<NoteType> note;
815         TreeView::Selection::ListHandle_Path rows = view.get_selection()->get_selected_rows ();
816
817         NotePlayer* player = new NotePlayer (track);
818
819         for (TreeView::Selection::ListHandle_Path::iterator i = rows.begin(); i != rows.end(); ++i) {
820                 if ((iter = model->get_iter (*i))) {
821                         note = (*iter)[columns._note];
822                         player->add (note);
823                 }
824         }
825
826         player->play ();
827 }