MidiRegionView::clear_events() does _selection.clear(). remove the extra one.
[ardour.git] / gtk2_ardour / midi_region_view.cc
index a2aa46f3ff607c354ba200dc2672d3535cd8916f..16569b6dce58f5a270cc36e024458a98f8de84d9 100644 (file)
@@ -122,7 +122,7 @@ MidiRegionView::MidiRegionView (ArdourCanvas::Container*      parent,
        , _last_event_y (0)
        , _grabbed_keyboard (false)
        , _entered (false)
-       , _note_entered (false)
+       , _entered_note (0)
        , _mouse_changed_selection (false)
 {
        CANVAS_DEBUG_NAME (_note_group, string_compose ("note group for %1", get_item_name()));
@@ -165,7 +165,7 @@ MidiRegionView::MidiRegionView (ArdourCanvas::Container*      parent,
        , _last_event_y (0)
        , _grabbed_keyboard (false)
        , _entered (false)
-       , _note_entered (false)
+       , _entered_note (0)
        , _mouse_changed_selection (false)
 {
        CANVAS_DEBUG_NAME (_note_group, string_compose ("note group for %1", get_item_name()));
@@ -213,7 +213,7 @@ MidiRegionView::MidiRegionView (const MidiRegionView& other)
        , _last_event_y (0)
        , _grabbed_keyboard (false)
        , _entered (false)
-       , _note_entered (false)
+       , _entered_note (0)
        , _mouse_changed_selection (false)
 {
        init (false);
@@ -245,7 +245,7 @@ MidiRegionView::MidiRegionView (const MidiRegionView& other, boost::shared_ptr<M
        , _last_event_y (0)
        , _grabbed_keyboard (false)
        , _entered (false)
-       , _note_entered (false)
+       , _entered_note (0)
        , _mouse_changed_selection (false)
 {
        init (true);
@@ -424,12 +424,12 @@ MidiRegionView::mouse_mode_changed ()
                        remove_ghost_note ();
 
                        /* XXX This is problematic as the function is executed for every region
-                       and only for one region _note_entered can be true. Still it's
+                       and only for one region _entered_note can be true. Still it's
                        necessary as to hide the verbose cursor when we're changing from
                        draw mode to internal edit mode. These lines are the reason why
                        in some situations no verbose cursor is shown when we enter internal
                        edit mode over a note. */
-                       if (!_note_entered) {
+                       if (!_entered_note) {
                                hide_verbose_cursor ();
                        }
                }
@@ -464,7 +464,7 @@ MidiRegionView::leave_internal()
 {
        hide_verbose_cursor ();
        remove_ghost_note ();
-       _note_entered = false;
+       _entered_note = 0;
 
        if (_grabbed_keyboard) {
                Keyboard::magic_widget_drop_focus();
@@ -497,7 +497,21 @@ MidiRegionView::button_press (GdkEventButton* ev)
        if (_mouse_state != SelectTouchDragging) {
 
                _pressed_button = ev->button;
-               _mouse_state = Pressed;
+
+               if (m == MouseDraw || (m == MouseContent && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier()))) {
+
+                       if (midi_view()->note_mode() == Percussive) {
+                               editor->drags()->set (new HitCreateDrag (dynamic_cast<Editor *> (editor), group, this), (GdkEvent *) ev);
+                       } else {
+                               editor->drags()->set (new NoteCreateDrag (dynamic_cast<Editor *> (editor), group, this), (GdkEvent *) ev);
+                       }
+
+                       _mouse_state = AddDragging;
+                       remove_ghost_note ();
+                       hide_verbose_cursor ();
+               } else {
+                       _mouse_state = Pressed;
+               }
 
                return true;
        }
@@ -541,29 +555,13 @@ MidiRegionView::button_release (GdkEventButton* ev)
                case MouseTimeFX:
                        {
                                _mouse_changed_selection = true;
-
-                               if (Keyboard::is_insert_note_event(ev)) {
-
-                                       double event_x, event_y;
-
-                                       event_x = ev->x;
-                                       event_y = ev->y;
-                                       group->canvas_to_item (event_x, event_y);
-
-                                       Evoral::Beats beats = get_grid_beats(editor.pixel_to_sample(event_x) + _region->position());
-                                       create_note_at (editor.pixel_to_sample (event_x), event_y, beats, ev->state, true);
-                               } else {
-                                       clear_editor_note_selection ();
-                               }
+                               clear_editor_note_selection ();
 
                                break;
                        }
                case MouseDraw:
-                       {
-                               Evoral::Beats beats = get_grid_beats(editor.pixel_to_sample(event_x) + _region->position());
-                               create_note_at (editor.pixel_to_sample (event_x), event_y, beats, ev->state, true);
-                               break;
-                       }
+                       break;
+
                default:
                        break;
                }
@@ -572,8 +570,8 @@ MidiRegionView::button_release (GdkEventButton* ev)
                break;
 
        case AddDragging:
-               /* Only create a ghost note when we added a note, not when we were drag-selecting. */
-               create_ghost_note (ev->x, ev->y, ev->state);
+               /* Don't a ghost note when we added a note - wait until motion to avoid visual confusion.
+                  we don't want one when we were drag-selecting either. */
        case SelectRectDragging:
                editor.drags()->end_grab ((GdkEvent *) ev);
                _mouse_state = None;
@@ -597,9 +595,14 @@ MidiRegionView::motion (GdkEventMotion* ev)
 {
        PublicEditor& editor = trackview.editor ();
 
-       if (!_note_entered) {
+       if (!_entered_note) {
 
-               if (!_ghost_note && editor.current_mouse_mode() == MouseContent &&
+               if (_mouse_state == AddDragging) {
+                       if (_ghost_note) {
+                               remove_ghost_note ();
+                       }
+
+               } else if (!_ghost_note && editor.current_mouse_mode() == MouseContent &&
                    Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier()) &&
                    _mouse_state != AddDragging) {
 
@@ -639,13 +642,7 @@ MidiRegionView::motion (GdkEventMotion* ev)
 
                        MouseMode m = editor.current_mouse_mode();
 
-                       if (m == MouseDraw || (m == MouseContent && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier()))) {
-                               editor.drags()->set (new NoteCreateDrag (dynamic_cast<Editor *> (&editor), group, this), (GdkEvent *) ev);
-                               _mouse_state = AddDragging;
-                               remove_ghost_note ();
-                               hide_verbose_cursor ();
-                               return true;
-                       } else if (m == MouseContent) {
+                       if (m == MouseContent && !Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier())) {
                                editor.drags()->set (new MidiRubberbandSelectDrag (dynamic_cast<Editor *> (&editor), this), (GdkEvent *) ev);
                                if (!Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
                                        clear_editor_note_selection ();
@@ -723,11 +720,14 @@ MidiRegionView::key_press (GdkEventKey* ev)
           detectable auto-repeat is the name of the game and only sends
           repeated presses, carry out key actions at key press, not release.
        */
-
        bool unmodified = Keyboard::no_modifier_keys_pressed (ev);
 
        if (unmodified && (ev->keyval == GDK_Alt_L || ev->keyval == GDK_Alt_R)) {
-               _mouse_state = SelectTouchDragging;
+
+               if (_mouse_state != AddDragging) {
+                       _mouse_state = SelectTouchDragging;
+               }
+
                return true;
 
        } else if (ev->keyval == GDK_Escape && unmodified) {
@@ -1115,12 +1115,12 @@ MidiRegionView::find_canvas_note (boost::shared_ptr<NoteType> note)
 
 /** This version finds any canvas note matching the supplied note. */
 NoteBase*
-MidiRegionView::find_canvas_note (NoteType note)
+MidiRegionView::find_canvas_note (Evoral::event_id_t id)
 {
        Events::iterator it;
 
        for (it = _events.begin(); it != _events.end(); ++it) {
-               if (*((*it)->note()) == note) {
+               if ((*it)->note()->id() == id) {
                        return *it;
                }
        }
@@ -1200,9 +1200,9 @@ MidiRegionView::redisplay_model()
                                cne = add_note (note, visible);
                        }
 
-                       set<boost::shared_ptr<NoteType> >::iterator it;
+                       set<Evoral::event_id_t>::iterator it;
                        for (it = _pending_note_selection.begin(); it != _pending_note_selection.end(); ++it) {
-                               if (*(*it) == *note) {
+                               if ((*it) == note->id()) {
                                        add_to_selection (cne);
                                }
                        }
@@ -1383,8 +1383,7 @@ MidiRegionView::~MidiRegionView ()
        if (_active_notes) {
                end_write();
        }
-
-       _selection.clear();
+       _entered_note = 0;
        clear_events ();
 
        delete _note_group;
@@ -1480,44 +1479,7 @@ MidiRegionView::apply_note_range (uint8_t min, uint8_t max, bool force)
        _current_range_min = min;
        _current_range_max = max;
 
-       for (Events::const_iterator i = _events.begin(); i != _events.end(); ++i) {
-               NoteBase* event = *i;
-               boost::shared_ptr<NoteType> note (event->note());
-
-               if (note->note() < _current_range_min ||
-                   note->note() > _current_range_max) {
-                       event->hide();
-               } else {
-                       event->show();
-               }
-
-               if (Note* cnote = dynamic_cast<Note*>(event)) {
-
-                       const double y0 = 1. + floor (midi_stream_view()->note_to_y(note->note()));
-                       const double y1 = y0 + std::max(1., floor(midi_stream_view()->note_height()) - 1.);
-
-                       if (y0 < 0 || y1 >= _height) {
-                               /* During DnD, the region uses the 'old/current'
-                                * midi_stream_view()'s range and its position/height calculation.
-                                *
-                                * Ideally DnD would decouple the midi_stream_view() for the
-                                * region(s) being dragged and set it to the target's range
-                                * (or in case of the drop-zone, FullRange).
-                                * but I don't see how this can be done without major rework.
-                                *
-                                * For now, just prevent visual bleeding of events in case
-                                * the target-track is smaller.
-                                */
-                               event->hide();
-                               continue;
-                       }
-                       cnote->set_y0 (y0);
-                       cnote->set_y1 (y1);
-
-               } else if (Hit* chit = dynamic_cast<Hit*>(event)) {
-                       update_hit (chit);
-               }
-       }
+       redisplay_model ();
 }
 
 GhostRegion*
@@ -1540,6 +1502,7 @@ MidiRegionView::add_ghost (TimeAxisView& tv)
                ghost->add_note(*i);
        }
 
+       ghost->set_colors ();
        ghost->set_height ();
        ghost->set_duration (_region->length() / samples_per_pixel);
        ghosts.push_back (ghost);
@@ -1671,11 +1634,13 @@ bool
 MidiRegionView::note_in_region_range (const boost::shared_ptr<NoteType> note, bool& visible) const
 {
        const boost::shared_ptr<ARDOUR::MidiRegion> midi_reg = midi_region();
-       const bool outside = (note->time() < midi_reg->start_beats() ||
-                             note->time() > midi_reg->start_beats() + midi_reg->length_beats());
 
-       visible = (note->note() >= midi_stream_view()->lowest_note()) &&
-               (note->note() <= midi_stream_view()->highest_note());
+       /* must compare double explicitly as Beats::operator< rounds to ppqn */
+       const bool outside = (note->time().to_double() < midi_reg->start_beats() ||
+                             note->time().to_double() >= midi_reg->start_beats() + midi_reg->length_beats());
+
+       visible = (note->note() >= _current_range_min) &&
+               (note->note() <= _current_range_max);
 
        return !outside;
 }
@@ -1702,33 +1667,33 @@ MidiRegionView::update_sustained (Note* ev, bool update_ghost_regions)
        TempoMap& map (trackview.session()->tempo_map());
        const boost::shared_ptr<ARDOUR::MidiRegion> mr = midi_region();
        boost::shared_ptr<NoteType> note = ev->note();
-       const double qn_note_time = note->time().to_double() + ((_region->pulse() * 4.0) - mr->start_beats().to_double());
-       const framepos_t note_start_frames = map.frame_at_quarter_note (qn_note_time) - _region->position();
+
+       const double session_source_start = _region->quarter_note() - mr->start_beats();
+       const framepos_t note_start_frames = map.frame_at_quarter_note (note->time().to_double() + session_source_start) - _region->position();
+
        const double x0 = trackview.editor().sample_to_pixel (note_start_frames);
        double x1;
-       const double y0 = 1 + floor(midi_stream_view()->note_to_y(note->note()));
+       const double y0 = 1 + floor(note_to_y(note->note()));
        double y1;
 
        /* trim note display to not overlap the end of its region */
-       if (note->length() > 0) {
-               Evoral::Beats note_end_time = note->end_time();
+       if (note->length().to_double() > 0.0) {
+               double note_end_time = note->end_time().to_double();
 
-               if (note->end_time() > mr->start_beats() + mr->length_beats()) {
+               if (note_end_time > mr->start_beats() + mr->length_beats()) {
                        note_end_time = mr->start_beats() + mr->length_beats();
                }
-               const double session_qn_start = (_region->pulse() * 4.0) - mr->start_beats().to_double();
-               const double quarter_note_end_time = session_qn_start  + note_end_time.to_double();
 
-               const framepos_t note_end_frames = map.frame_at_quarter_note (quarter_note_end_time) - _region->position();
+               const framepos_t note_end_frames = map.frame_at_quarter_note (session_source_start + note_end_time) - _region->position();
+
                x1 = std::max(1., trackview.editor().sample_to_pixel (note_end_frames)) - 1;
        } else {
                x1 = std::max(1., trackview.editor().sample_to_pixel (_region->length())) - 1;
        }
 
-       y1 = y0 + std::max(1., floor(midi_stream_view()->note_height()) - 1);
+       y1 = y0 + std::max(1., floor(note_height()) - 1);
 
-       ArdourCanvas::Rect rect (x0, y0, x1, y1);
-       ev->set (rect);
+       ev->set (ArdourCanvas::Rect (x0, y0, x1, y1));
 
        if (!note->length()) {
                if (_active_notes && note->note() < 128) {
@@ -1771,12 +1736,12 @@ MidiRegionView::update_hit (Hit* ev, bool update_ghost_regions)
 {
        boost::shared_ptr<NoteType> note = ev->note();
 
-       const double note_time_qn = note->time().to_double() + ((_region->pulse() * 4.0) - midi_region()->start_beats().to_double());
+       const double note_time_qn = note->time().to_double() + (_region->quarter_note() - midi_region()->start_beats());
        const framepos_t note_start_frames = trackview.session()->tempo_map().frame_at_quarter_note (note_time_qn) - _region->position();
 
        const double x = trackview.editor().sample_to_pixel(note_start_frames);
-       const double diamond_size = std::max(1., floor(midi_stream_view()->note_height()) - 2.);
-       const double y = 1.5 + floor(midi_stream_view()->note_to_y(note->note())) + diamond_size * .5;
+       const double diamond_size = std::max(1., floor(note_height()) - 2.);
+       const double y = 1.5 + floor(note_to_y(note->note())) + diamond_size * .5;
 
        // see DnD note in MidiRegionView::apply_note_range() above
        if (y <= 0 || y >= _height) {
@@ -1789,14 +1754,15 @@ MidiRegionView::update_hit (Hit* ev, bool update_ghost_regions)
        ev->set_height (diamond_size);
 
        // Update color in case velocity has changed
-       ev->set_fill_color(ev->base_color());
-       ev->set_outline_color(ev->calculate_outline(ev->base_color(), ev->selected()));
+       const uint32_t base_col = ev->base_color();
+       ev->set_fill_color(base_col);
+       ev->set_outline_color(ev->calculate_outline(base_col, ev->selected()));
 
        if (update_ghost_regions) {
                for (std::vector<GhostRegion*>::iterator i = ghosts.begin(); i != ghosts.end(); ++i) {
                        MidiGhostRegion* gr = dynamic_cast<MidiGhostRegion*> (*i);
                        if (gr) {
-                               gr->update_note (ev);
+                               gr->update_hit (ev);
                        }
                }
        }
@@ -1823,7 +1789,7 @@ MidiRegionView::add_note(const boost::shared_ptr<NoteType> note, bool visible)
 
        } else if (midi_view()->note_mode() == Percussive) {
 
-               const double diamond_size = std::max(1., floor(midi_stream_view()->note_height()) - 2.);
+               const double diamond_size = std::max(1., floor(note_height()) - 2.);
 
                Hit* ev_diamond = new Hit (*this, _note_group, diamond_size, note);
 
@@ -2105,6 +2071,10 @@ MidiRegionView::step_patch (PatchChange& patch, bool bank, int delta)
 void
 MidiRegionView::note_deleted (NoteBase* cne)
 {
+       if (_entered_note && cne == _entered_note) {
+               _entered_note = 0;
+       }
+
        if (_selection.empty()) {
                return;
        }
@@ -2134,6 +2104,7 @@ MidiRegionView::delete_selection()
        _selection.clear();
 
        apply_diff ();
+
        hide_verbose_cursor ();
 }
 
@@ -2227,13 +2198,13 @@ MidiRegionView::invert_selection ()
     The requested notes most likely won't exist in the view until the next model redisplay.
 */
 void
-MidiRegionView::select_notes (list<boost::shared_ptr<NoteType> > notes)
+MidiRegionView::select_notes (list<Evoral::event_id_t> notes)
 {
        NoteBase* cne;
-       list<boost::shared_ptr<NoteType> >::iterator n;
+       list<Evoral::event_id_t>::iterator n;
 
        for (n = notes.begin(); n != notes.end(); ++n) {
-               if ((cne = find_canvas_note(*(*n))) != 0) {
+               if ((cne = find_canvas_note(*n)) != 0) {
                        add_to_selection (cne);
                } else {
                        _pending_note_selection.insert(*n);
@@ -2586,7 +2557,7 @@ MidiRegionView::note_dropped(NoteBase *, frameoffset_t dt, int8_t dnote)
 
        for (Selection::iterator i = _selection.begin(); i != _selection.end() ; ++i) {
 
-               double const start_qn = (_region->pulse() * 4.0) - midi_region()->start_beats().to_double();
+               double const start_qn = _region->quarter_note() - midi_region()->start_beats();
                framepos_t new_frames = map.frame_at_quarter_note (start_qn + (*i)->note()->time().to_double()) + dt;
                Evoral::Beats new_time = Evoral::Beats (map.quarter_note_at_frame (new_frames) - start_qn);
                if (new_time < 0) {
@@ -2912,9 +2883,9 @@ MidiRegionView::commit_resizing (NoteBase* primary, bool at_front, double delta_
                }
 
                /* and then to beats */
-               const double e_baf = tmap.exact_beat_at_frame (current_fr + midi_region()->position(), divisions);
-               const double quarter_note_start_beat = tmap.quarter_note_at_beat (_region->beat() - midi_region()->start_beats().to_double());
-               const Evoral::Beats x_beats = Evoral::Beats (tmap.quarter_note_at_beat (e_baf) - quarter_note_start_beat);
+               const double e_qaf = tmap.exact_qn_at_frame (current_fr + midi_region()->position(), divisions);
+               const double quarter_note_start = _region->quarter_note() - midi_region()->start_beats();
+               const Evoral::Beats x_beats = Evoral::Beats (e_qaf - quarter_note_start);
 
                if (at_front && x_beats < canvas_note->note()->end_time()) {
                        note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::StartTime, x_beats - (sign * snap_delta_beats));
@@ -3330,7 +3301,7 @@ MidiRegionView::change_channel(uint8_t channel)
 void
 MidiRegionView::note_entered(NoteBase* ev)
 {
-       _note_entered = true;
+       _entered_note = ev;
 
        Editor* editor = dynamic_cast<Editor*>(&trackview.editor());
 
@@ -3353,7 +3324,7 @@ MidiRegionView::note_entered(NoteBase* ev)
 void
 MidiRegionView::note_left (NoteBase*)
 {
-       _note_entered = false;
+       _entered_note = 0;
 
        for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
                (*i)->hide_velocity ();
@@ -3385,7 +3356,7 @@ MidiRegionView::patch_left (PatchChange *)
 void
 MidiRegionView::sysex_entered (SysEx* p)
 {
-       ostringstream s;
+       // ostringstream s;
        // CAIROCANVAS
        // need a way to extract text from p->_flag->_text
        // s << p->text();
@@ -3568,14 +3539,14 @@ MidiRegionView::paste_internal (framepos_t pos, unsigned paste_count, float time
        const Evoral::Beats duration      = last_time - first_time;
        const Evoral::Beats snap_duration = duration.snap_to(snap_beats);
        const Evoral::Beats paste_offset  = snap_duration * paste_count;
-       const Evoral::Beats pos_beats     = absolute_frames_to_source_beats(pos) + paste_offset;
+       const Evoral::Beats quarter_note     = absolute_frames_to_source_beats(pos) + paste_offset;
        Evoral::Beats       end_point     = Evoral::Beats();
 
        DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("Paste data spans from %1 to %2 (%3) ; paste pos beats = %4 (based on %5 - %6)\n",
                                                       first_time,
                                                       last_time,
                                                       duration, pos, _region->position(),
-                                                      pos_beats));
+                                                      quarter_note));
 
        clear_editor_note_selection ();
 
@@ -3584,7 +3555,7 @@ MidiRegionView::paste_internal (framepos_t pos, unsigned paste_count, float time
                for (Notes::const_iterator i = mcb.notes().begin(); i != mcb.notes().end(); ++i) {
 
                        boost::shared_ptr<NoteType> copied_note (new NoteType (*((*i).get())));
-                       copied_note->set_time (pos_beats + copied_note->time() - first_time);
+                       copied_note->set_time (quarter_note + copied_note->time() - first_time);
                        copied_note->set_id (Evoral::next_event_id());
 
                        /* make all newly added notes selected */
@@ -3743,15 +3714,31 @@ MidiRegionView::update_ghost_note (double x, double y, uint32_t state)
        framepos_t const unsnapped_frame = editor.pixel_to_sample (x);
 
        const int32_t divisions = editor.get_grid_music_divisions (state);
-       const double snapped_region_qn = snap_frame_to_grid_underneath (unsnapped_frame, divisions, true).to_double();
+       const bool shift_snap = midi_view()->note_mode() != Percussive;
+       const Evoral::Beats snapped_beats = snap_frame_to_grid_underneath (unsnapped_frame, divisions, shift_snap);
+
+       /* prevent Percussive mode from displaying a ghost hit at region end */
+       if (!shift_snap && snapped_beats >= midi_region()->start_beats() + midi_region()->length_beats()) {
+               _ghost_note->hide();
+               hide_verbose_cursor ();
+               return;
+       }
+
+       /* ghost note may have been snapped before region */
+       if (_ghost_note && snapped_beats.to_double() < 0.0) {
+               _ghost_note->hide();
+               return;
+
+       } else if (_ghost_note) {
+               _ghost_note->show();
+       }
 
-       Evoral::Beats snapped_beats = Evoral::Beats (snapped_region_qn);
        /* calculate time in beats relative to start of source */
        const Evoral::Beats length = get_grid_beats(unsnapped_frame + _region->position());
 
        _ghost_note->note()->set_time (snapped_beats);
        _ghost_note->note()->set_length (length);
-       _ghost_note->note()->set_note (midi_stream_view()->y_to_note (y));
+       _ghost_note->note()->set_note (y_to_note (y));
        _ghost_note->note()->set_channel (mtv->get_channel_for_add ());
        _ghost_note->note()->set_velocity (get_velocity_for_add (snapped_beats));
        /* the ghost note does not appear in ghost regions, so pass false in here */
@@ -3817,7 +3804,7 @@ MidiRegionView::maybe_select_by_position (GdkEventButton* ev, double /*x*/, doub
 {
        /* XXX: This is dead code.  What was it for? */
 
-       double note = midi_stream_view()->y_to_note(y);
+       double note = y_to_note(y);
        Events e;
        MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
 
@@ -4128,11 +4115,11 @@ MidiRegionView::snap_frame_to_grid_underneath (framepos_t p, int32_t divisions,
                */
                const Evoral::Beats grid_beats = get_grid_beats (p + _region->position());
                const double rem = eqaf - qaf;
-               if (rem >= 0.0 && eqaf - grid_beats.to_double() > _region->pulse() * 4.0) {
+               if (rem >= 0.0) {
                        eqaf -= grid_beats.to_double();
                }
        }
-       const double session_start_off = (_region->pulse() * 4.0) - midi_region()->start_beats().to_double();
+       const double session_start_off = _region->quarter_note() - midi_region()->start_beats();
 
        return Evoral::Beats (eqaf - session_start_off);
 }
@@ -4163,3 +4150,24 @@ MidiRegionView::get_grid_beats(framepos_t pos) const
        }
        return beats;
 }
+uint8_t
+MidiRegionView::y_to_note (double y) const
+{
+       int const n = ((contents_height() - y) / contents_height() * (double)(_current_range_max - _current_range_min + 1))
+               + _current_range_min;
+
+       if (n < 0) {
+               return 0;
+       } else if (n > 127) {
+               return 127;
+       }
+
+       /* min due to rounding and/or off-by-one errors */
+       return min ((uint8_t) n, _current_range_max);
+}
+
+double
+MidiRegionView::note_to_y(uint8_t note) const
+{
+       return contents_height() - (note + 1 - _current_range_min) * note_height() + 1;
+}