Add some convenient public editor methods (for lua-bindings)
[ardour.git] / gtk2_ardour / midi_streamview.cc
1 /*
2     Copyright (C) 2001-2007 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 */
18
19 #include <cmath>
20 #include <utility>
21
22 #include <gtkmm.h>
23
24 #include <gtkmm2ext/gtk_ui.h>
25
26 #include "canvas/line_set.h"
27 #include "canvas/rectangle.h"
28
29 #include "ardour/midi_region.h"
30 #include "ardour/midi_source.h"
31 #include "ardour/midi_track.h"
32 #include "ardour/operations.h"
33 #include "ardour/region_factory.h"
34 #include "ardour/session.h"
35 #include "ardour/smf_source.h"
36
37 #include "gui_thread.h"
38 #include "midi_region_view.h"
39 #include "midi_streamview.h"
40 #include "midi_time_axis.h"
41 #include "midi_util.h"
42 #include "public_editor.h"
43 #include "region_selection.h"
44 #include "region_view.h"
45 #include "rgb_macros.h"
46 #include "selection.h"
47 #include "ui_config.h"
48 #include "utils.h"
49
50 #include "pbd/i18n.h"
51
52 using namespace std;
53 using namespace ARDOUR;
54 using namespace ARDOUR_UI_UTILS;
55 using namespace PBD;
56 using namespace Editing;
57
58 MidiStreamView::MidiStreamView (MidiTimeAxisView& tv)
59         : StreamView (tv)
60         , note_range_adjustment(0.0f, 0.0f, 0.0f)
61         , _range_dirty(false)
62         , _range_sum_cache(-1.0)
63         , _lowest_note(60)
64         , _highest_note(71)
65         , _data_note_min(60)
66         , _data_note_max(71)
67         , _note_lines (0)
68         , _updates_suspended (false)
69 {
70         /* use a group dedicated to MIDI underlays. Audio underlays are not in this group. */
71         midi_underlay_group = new ArdourCanvas::Container (_canvas_group);
72         midi_underlay_group->lower_to_bottom();
73
74         /* put the note lines in the timeaxisview's group, so it
75            can be put below ghost regions from MIDI underlays
76         */
77         _note_lines = new ArdourCanvas::LineSet (_canvas_group, ArdourCanvas::LineSet::Horizontal);
78
79         _note_lines->Event.connect(
80                 sigc::bind(sigc::mem_fun(_trackview.editor(),
81                                          &PublicEditor::canvas_stream_view_event),
82                            _note_lines, &_trackview));
83
84         _note_lines->lower_to_bottom();
85
86         color_handler ();
87
88         UIConfiguration::instance().ColorsChanged.connect(sigc::mem_fun(*this, &MidiStreamView::color_handler));
89
90         note_range_adjustment.set_page_size(_highest_note - _lowest_note);
91         note_range_adjustment.set_value(_lowest_note);
92
93         note_range_adjustment.signal_value_changed().connect(
94                 sigc::mem_fun(*this, &MidiStreamView::note_range_adjustment_changed));
95 }
96
97 MidiStreamView::~MidiStreamView ()
98 {
99 }
100
101 RegionView*
102 MidiStreamView::create_region_view (boost::shared_ptr<Region> r, bool /*wfd*/, bool recording)
103 {
104         boost::shared_ptr<MidiRegion> region = boost::dynamic_pointer_cast<MidiRegion> (r);
105
106         if (region == 0) {
107                 return 0;
108         }
109
110         RegionView* region_view = NULL;
111         if (recording) {
112                 region_view = new MidiRegionView (
113                         _canvas_group, _trackview, region,
114                         _samples_per_pixel, region_color, recording,
115                         TimeAxisViewItem::Visibility(TimeAxisViewItem::ShowFrame));
116         } else {
117                 region_view = new MidiRegionView (_canvas_group, _trackview, region,
118                                                   _samples_per_pixel, region_color);
119         }
120
121         region_view->init (false);
122
123         return region_view;
124 }
125
126 RegionView*
127 MidiStreamView::add_region_view_internal (boost::shared_ptr<Region> r, bool wait_for_data, bool recording)
128 {
129         boost::shared_ptr<MidiRegion> region = boost::dynamic_pointer_cast<MidiRegion> (r);
130
131         if (!region) {
132                 return 0;
133         }
134
135         for (list<RegionView*>::iterator i = region_views.begin(); i != region_views.end(); ++i) {
136                 if ((*i)->region() == r) {
137
138                         /* great. we already have a MidiRegionView for this Region. use it again. */
139
140                         (*i)->set_valid (true);
141
142                         display_region(dynamic_cast<MidiRegionView*>(*i), wait_for_data);
143
144                         return 0;
145                 }
146         }
147
148         MidiRegionView* region_view = dynamic_cast<MidiRegionView*> (create_region_view (r, wait_for_data, recording));
149         if (region_view == 0) {
150                 return 0;
151         }
152
153         region_views.push_front (region_view);
154
155         /* display events and find note range */
156         display_region (region_view, wait_for_data);
157
158         /* fit note range if we are importing */
159         if (_trackview.session()->operation_in_progress (Operations::insert_file)) {
160                 set_note_range (ContentsRange);
161         }
162
163         /* catch regionview going away */
164         boost::weak_ptr<Region> wr (region); // make this explicit
165         region->DropReferences.connect (*this, invalidator (*this), boost::bind (&MidiStreamView::remove_region_view, this, wr), gui_context());
166
167         RegionViewAdded (region_view);
168
169         return region_view;
170 }
171
172 void
173 MidiStreamView::display_region(MidiRegionView* region_view, bool load_model)
174 {
175         if (!region_view) {
176                 return;
177         }
178
179         region_view->enable_display (true);
180         region_view->set_height (child_height());
181
182         boost::shared_ptr<MidiSource> source(region_view->midi_region()->midi_source(0));
183         if (!source) {
184                 error << _("attempt to display MIDI region with no source") << endmsg;
185                 return;
186         }
187
188         if (load_model) {
189                 Glib::Threads::Mutex::Lock lm(source->mutex());
190                 source->load_model(lm);
191         }
192
193         if (!source->model()) {
194                 error << _("attempt to display MIDI region with no model") << endmsg;
195                 return;
196         }
197
198         _range_dirty = update_data_note_range(
199                 source->model()->lowest_note(),
200                 source->model()->highest_note());
201
202         // Display region contents
203         region_view->display_model(source->model());
204 }
205
206
207 void
208 MidiStreamView::display_track (boost::shared_ptr<Track> tr)
209 {
210         StreamView::display_track (tr);
211
212         draw_note_lines();
213
214         NoteRangeChanged();
215 }
216
217 void
218 MidiStreamView::update_contents_metrics(boost::shared_ptr<Region> r)
219 {
220         boost::shared_ptr<MidiRegion> mr = boost::dynamic_pointer_cast<MidiRegion>(r);
221         if (mr) {
222                 Glib::Threads::Mutex::Lock lm(mr->midi_source(0)->mutex());
223                 mr->midi_source(0)->load_model(lm);
224                 _range_dirty = update_data_note_range(
225                         mr->model()->lowest_note(),
226                         mr->model()->highest_note());
227         }
228 }
229
230 bool
231 MidiStreamView::update_data_note_range(uint8_t min, uint8_t max)
232 {
233         bool dirty = false;
234         if (min < _data_note_min) {
235                 _data_note_min = min;
236                 dirty = true;
237         }
238         if (max > _data_note_max) {
239                 _data_note_max = max;
240                 dirty = true;
241         }
242         return dirty;
243 }
244
245 void
246 MidiStreamView::set_layer_display (LayerDisplay d)
247 {
248
249 //revert this change for now.  Although stacked view is weirdly implemented wrt the "scroomer", it is still necessary to manage layered midi regions.
250 //      if (d != Overlaid) {
251 //              return;
252 //      }
253
254         StreamView::set_layer_display (d);
255 }
256
257 void
258 MidiStreamView::redisplay_track ()
259 {
260         if (!_trackview.is_midi_track()) {
261                 return;
262         }
263
264         list<RegionView*>::iterator i;
265
266         // Load models if necessary, and find note range of all our contents
267         _range_dirty = false;
268         _data_note_min = 127;
269         _data_note_max = 0;
270         _trackview.track()->playlist()->foreach_region(
271                 sigc::mem_fun (*this, &StreamView::update_contents_metrics));
272
273         // No notes, use default range
274         if (!_range_dirty) {
275                 _data_note_min = 60;
276                 _data_note_max = 71;
277         }
278
279         // Flag region views as invalid and disable drawing
280         for (i = region_views.begin(); i != region_views.end(); ++i) {
281                 (*i)->set_valid(false);
282                 (*i)->enable_display(false);
283         }
284
285         // Add and display region views, and flag them as valid
286         _trackview.track()->playlist()->foreach_region(
287                 sigc::hide_return (sigc::mem_fun (*this, &StreamView::add_region_view)));
288
289         // Stack regions by layer, and remove invalid regions
290         layer_regions();
291
292         // Update note range (not regions which are correct) and draw note lines
293         apply_note_range(_lowest_note, _highest_note, false);
294 }
295
296
297 void
298 MidiStreamView::update_contents_height ()
299 {
300         StreamView::update_contents_height();
301
302         _note_lines->set_extent (ArdourCanvas::COORD_MAX);
303
304         apply_note_range (lowest_note(), highest_note(), true);
305 }
306
307 void
308 MidiStreamView::draw_note_lines()
309 {
310         if (!_note_lines || _updates_suspended) {
311                 return;
312         }
313
314         double y;
315         double prev_y = .5;
316         uint32_t color;
317
318         _note_lines->clear();
319
320         if (child_height() < 140 || note_height() < 3) {
321                 /* track is too small for note lines, or there are too many */
322                 return;
323         }
324
325         /* do this is order of highest ... lowest since that matches the
326          * coordinate system in which y=0 is at the top
327          */
328
329         for (int i = highest_note() + 1; i >= lowest_note(); --i) {
330
331                 y = floor(note_to_y (i)) + .5;
332
333                 /* this is the line actually corresponding to the division
334                  * between notes
335                  */
336
337                 if (i <= highest_note()) {
338                         _note_lines->add (y, 1.0, UIConfiguration::instance().color ("piano roll black outline"));
339                 }
340
341                 /* now add a thicker line/bar which covers the entire vertical
342                  * height of this note.
343                  */
344
345                 switch (i % 12) {
346                 case 1:
347                 case 3:
348                 case 6:
349                 case 8:
350                 case 10:
351                         color = UIConfiguration::instance().color_mod ("piano roll black", "piano roll black");
352                         break;
353                 default:
354                         color = UIConfiguration::instance().color_mod ("piano roll white", "piano roll white");
355                         break;
356                 }
357
358                 double h = y - prev_y;
359                 double mid = y + (h/2.0);
360
361                 if (height > 1.0) { // XXX ? should that not be h >= 1 ?
362                         _note_lines->add (mid, h, color);
363                 }
364
365                 prev_y = y;
366         }
367 }
368
369 void
370 MidiStreamView::set_note_range(VisibleNoteRange r)
371 {
372         if (r == FullRange) {
373                 _lowest_note = 0;
374                 _highest_note = 127;
375         } else {
376                 _lowest_note = _data_note_min;
377                 _highest_note = _data_note_max;
378         }
379
380         apply_note_range(_lowest_note, _highest_note, true);
381 }
382
383 void
384 MidiStreamView::apply_note_range(uint8_t lowest, uint8_t highest, bool to_region_views)
385 {
386         _highest_note = highest;
387         _lowest_note = lowest;
388
389         int const max_note_height = 20;  // This should probably be based on text size...
390         int const range = _highest_note - _lowest_note;
391         int const pixels_per_note = floor (child_height () / range);
392
393         /* do not grow note height beyond 10 pixels */
394         if (pixels_per_note > max_note_height) {
395
396                 int const available_note_range = floor (child_height() / max_note_height);
397                 int additional_notes = available_note_range - range;
398
399                 /* distribute additional notes to higher and lower ranges, clamp at 0 and 127 */
400                 for (int i = 0; i < additional_notes; i++){
401
402                         if (i % 2 && _highest_note < 127){
403                                 _highest_note++;
404                         }
405                         else if (i % 2) {
406                                 _lowest_note--;
407                         }
408                         else if (_lowest_note > 0){
409                                 _lowest_note--;
410                         }
411                         else {
412                                 _highest_note++;
413                         }
414                 }
415         }
416
417         note_range_adjustment.set_page_size(_highest_note - _lowest_note);
418         note_range_adjustment.set_value(_lowest_note);
419
420         draw_note_lines();
421
422         if (to_region_views) {
423                 apply_note_range_to_regions ();
424         }
425
426         NoteRangeChanged();
427 }
428
429 void
430 MidiStreamView::apply_note_range_to_regions ()
431 {
432         if (!_updates_suspended) {
433                 for (list<RegionView*>::iterator i = region_views.begin(); i != region_views.end(); ++i) {
434                         ((MidiRegionView*)(*i))->apply_note_range(_lowest_note, _highest_note);
435                 }
436         }
437 }
438
439 void
440 MidiStreamView::update_note_range(uint8_t note_num)
441 {
442         _data_note_min = min(_data_note_min, note_num);
443         _data_note_max = max(_data_note_max, note_num);
444 }
445
446 void
447 MidiStreamView::setup_rec_box ()
448 {
449         // cerr << _trackview.name() << " streamview SRB\n";
450
451         if (_trackview.session()->transport_rolling()) {
452
453                 if (!rec_active &&
454                     _trackview.session()->record_status() == Session::Recording &&
455                     _trackview.track()->rec_enable_control()->get_value()) {
456
457                         if (UIConfiguration::instance().get_show_waveforms_while_recording() && rec_regions.size() == rec_rects.size()) {
458
459                                 /* add a new region, but don't bother if they set show-waveforms-while-recording mid-record */
460
461                                 MidiRegion::SourceList sources;
462
463                                 rec_data_ready_connections.drop_connections ();
464
465                                 sources.push_back (_trackview.midi_track()->write_source());
466
467                                 // handle multi
468
469                                 framepos_t start = 0;
470                                 if (rec_regions.size() > 0) {
471                                         start = rec_regions.back().first->start()
472                                                 + _trackview.track()->get_captured_frames(rec_regions.size()-1);
473                                 }
474
475                                 if (!rec_regions.empty()) {
476                                         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rec_regions.back().second);
477                                         mrv->end_write ();
478                                 }
479
480                                 PropertyList plist;
481
482                                 plist.add (ARDOUR::Properties::start, start);
483                                 plist.add (ARDOUR::Properties::length, 1);
484                                 /* Just above we're setting this nascent region's length to 1.  I think this
485                                    is so that the RegionView gets created with a non-zero width, as apparently
486                                    creating a RegionView with a zero width causes it never to be displayed
487                                    (there is a warning in TimeAxisViewItem::init about this).  However, we
488                                    must also set length_beats to something non-zero, otherwise the frame length
489                                    of 1 causes length_beats to be set to some small quantity << 1.  Then
490                                    when the position is set up below, this length_beats is used to recompute
491                                    length using BeatsFramesConverter::to, which is slightly innacurate for small
492                                    beats values because it converts floating point beats to bars, beats and
493                                    integer ticks.  The upshot of which being that length gets set back to 0,
494                                    meaning no region view is ever seen, meaning no MIDI notes during record (#3820).
495                                 */
496                                 plist.add (ARDOUR::Properties::length_beats, 1);
497                                 plist.add (ARDOUR::Properties::name, string());
498                                 plist.add (ARDOUR::Properties::layer, 0);
499
500                                 boost::shared_ptr<MidiRegion> region (boost::dynamic_pointer_cast<MidiRegion>
501                                                                       (RegionFactory::create (sources, plist, false)));
502                                 if (region) {
503                                         region->set_start (_trackview.track()->current_capture_start()
504                                                            - _trackview.track()->get_capture_start_frame (0));
505                                         region->set_position (_trackview.session()->transport_frame());
506
507                                         RegionView* rv = add_region_view_internal (region, false, true);
508                                         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rv);
509                                         mrv->begin_write ();
510
511                                         /* rec region will be destroyed in setup_rec_box */
512                                         rec_regions.push_back (make_pair (region, rv));
513
514                                         /* we add the region later */
515                                         setup_new_rec_layer_time (region);
516                                 } else {
517                                         error << _("failed to create MIDI region") << endmsg;
518                                 }
519                         }
520
521                         /* start a new rec box */
522
523                         create_rec_box(_trackview.midi_track()->current_capture_start(), 0);
524
525                 } else if (rec_active &&
526                            (_trackview.session()->record_status() != Session::Recording ||
527                             !_trackview.track()->rec_enable_control()->get_value())) {
528                         screen_update_connection.disconnect();
529                         rec_active = false;
530                         rec_updating = false;
531                 }
532
533         } else {
534
535                 // cerr << "\tNOT rolling, rec_rects = " << rec_rects.size() << " rec_regions = " << rec_regions.size() << endl;
536
537                 if (!rec_rects.empty() || !rec_regions.empty()) {
538
539                         /* disconnect rapid update */
540                         screen_update_connection.disconnect();
541                         rec_data_ready_connections.drop_connections ();
542                         rec_updating = false;
543                         rec_active = false;
544
545                         /* remove temp regions */
546
547                         for (list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator iter = rec_regions.begin(); iter != rec_regions.end();) {
548                                 list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator tmp;
549
550                                 tmp = iter;
551                                 ++tmp;
552
553                                 (*iter).first->drop_references ();
554
555                                 iter = tmp;
556                         }
557
558                         rec_regions.clear();
559
560                         // cerr << "\tclear " << rec_rects.size() << " rec rects\n";
561
562                         /* transport stopped, clear boxes */
563                         for (vector<RecBoxInfo>::iterator iter=rec_rects.begin(); iter != rec_rects.end(); ++iter) {
564                                 RecBoxInfo &rect = (*iter);
565                                 delete rect.rectangle;
566                         }
567
568                         rec_rects.clear();
569
570                 }
571         }
572 }
573
574 void
575 MidiStreamView::color_handler ()
576 {
577         draw_note_lines ();
578
579         if (_trackview.is_midi_track()) {
580                 canvas_rect->set_fill_color (UIConfiguration::instance().color_mod ("midi track base", "midi track base"));
581         } else {
582                 canvas_rect->set_fill_color (UIConfiguration::instance().color ("midi bus base"));
583         }
584 }
585
586 void
587 MidiStreamView::note_range_adjustment_changed()
588 {
589         double sum = note_range_adjustment.get_value() + note_range_adjustment.get_page_size();
590         int lowest = (int) floor(note_range_adjustment.get_value());
591         int highest;
592
593         if (sum == _range_sum_cache) {
594                 //cerr << "cached" << endl;
595                 highest = (int) floor(sum);
596         } else {
597                 //cerr << "recalc" << endl;
598                 highest = lowest + (int) floor(note_range_adjustment.get_page_size());
599                 _range_sum_cache = sum;
600         }
601
602         if (lowest == _lowest_note && highest == _highest_note) {
603                 return;
604         }
605
606         //cerr << "note range adjustment changed: " << lowest << " " << highest << endl;
607         //cerr << "  val=" << v_zoom_adjustment.get_value() << " page=" << v_zoom_adjustment.get_page_size() << " sum=" << v_zoom_adjustment.get_value() + v_zoom_adjustment.get_page_size() << endl;
608
609         _lowest_note = lowest;
610         _highest_note = highest;
611         apply_note_range(lowest, highest, true);
612 }
613
614 void
615 MidiStreamView::update_rec_box ()
616 {
617         StreamView::update_rec_box ();
618
619         if (rec_regions.empty()) {
620                 return;
621         }
622
623         /* Update the region being recorded to reflect where we currently are */
624         boost::shared_ptr<ARDOUR::Region> region = rec_regions.back().first;
625         region->set_length (_trackview.track()->current_capture_end () - _trackview.track()->current_capture_start(), 0);
626
627         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rec_regions.back().second);
628         mrv->extend_active_notes ();
629 }
630
631 uint8_t
632 MidiStreamView::y_to_note (double y) const
633 {
634         int const n = ((contents_height() - y) / contents_height() * (double)contents_note_range())
635                 + lowest_note();
636
637         if (n < 0) {
638                 return 0;
639         } else if (n > 127) {
640                 return 127;
641         }
642
643         /* min due to rounding and/or off-by-one errors */
644         return min ((uint8_t) n, highest_note());
645 }
646
647 /** Suspend updates to the regions' note ranges and our
648  *  note lines until resume_updates() is called.
649  */
650 void
651 MidiStreamView::suspend_updates ()
652 {
653         _updates_suspended = true;
654 }
655
656 /** Resume updates to region note ranges and note lines,
657  *  and update them now.
658  */
659 void
660 MidiStreamView::resume_updates ()
661 {
662         _updates_suspended = false;
663
664         draw_note_lines ();
665         apply_note_range_to_regions ();
666
667         _canvas_group->redraw ();
668 }
669
670 struct RegionPositionSorter {
671         bool operator() (RegionView* a, RegionView* b) {
672                 return a->region()->position() < b->region()->position();
673         }
674 };
675
676 bool
677 MidiStreamView::paste (ARDOUR::framepos_t pos, const Selection& selection, PasteContext& ctx, const int32_t sub_num)
678 {
679         /* Paste into the first region which starts on or before pos.  Only called when
680            using an internal editing tool. */
681
682         if (region_views.empty()) {
683                 return false;
684         }
685
686         region_views.sort (RegionView::PositionOrder());
687
688         list<RegionView*>::const_iterator prev = region_views.begin ();
689
690         for (list<RegionView*>::const_iterator i = region_views.begin(); i != region_views.end(); ++i) {
691                 if ((*i)->region()->position() > pos) {
692                         break;
693                 }
694                 prev = i;
695         }
696
697         boost::shared_ptr<Region> r = (*prev)->region ();
698
699         /* If *prev doesn't cover pos, it's no good */
700         if (r->position() > pos || ((r->position() + r->length()) < pos)) {
701                 return false;
702         }
703
704         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (*prev);
705         return mrv ? mrv->paste(pos, selection, ctx, sub_num) : false;
706 }