From c12fd571f543ddc6067014527c0551e90c420a83 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:29:29 -0500 Subject: [PATCH] Allow cutting multiple notes at once in Piano Roll (#7715) Adds the ability to cut multiple notes at once in the Piano Roll. Users can select the Knife tool and create a cut line by holding the mouse and dragging it across the notes that should be cut. This also allows cutting the notes at an angle. When releasing the mouse, the Shift key can be pressed to remove the shorter end of the notes that were cut. If any notes are selected, only they will be considered for the cut, even if the cut line covers more notes. --- include/MidiClip.h | 3 ++ include/PianoRoll.h | 11 +++-- src/gui/editors/PianoRoll.cpp | 76 +++++++++++++++-------------------- src/tracks/MidiClip.cpp | 42 +++++++++++++++++++ 4 files changed, 85 insertions(+), 47 deletions(-) diff --git a/include/MidiClip.h b/include/MidiClip.h index b3ed0d84a20..f3150ba6f24 100644 --- a/include/MidiClip.h +++ b/include/MidiClip.h @@ -82,6 +82,9 @@ class LMMS_EXPORT MidiClip : public Clip // Split the list of notes on the given position void splitNotes(const NoteVector& notes, TimePos pos); + // Split the list of notes along a line + void splitNotesAlongLine(const NoteVector notes, TimePos pos1, int key1, TimePos pos2, int key2, bool deleteShortEnds); + // clip-type stuff inline Type type() const { diff --git a/include/PianoRoll.h b/include/PianoRoll.h index e9939101c3f..a1d045ff452 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -456,9 +456,14 @@ protected slots: // did we start a mouseclick with shift pressed bool m_startedWithShift; - // Variable that holds the position in ticks for the knife action - int m_knifeTickPos; - void updateKnifePos(QMouseEvent* me); + // Variables that hold the start and end position for the knife line + TimePos m_knifeStartTickPos; + int m_knifeStartKey; + TimePos m_knifeEndTickPos; + int m_knifeEndKey; + bool m_knifeDown; + + void updateKnifePos(QMouseEvent* me, bool initial); friend class PianoRollWindow; diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 41b760495b9..f0f54f0ba85 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -1608,19 +1608,8 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) // -- Knife if (m_editMode == EditMode::Knife && me->button() == Qt::LeftButton) { - NoteVector n; - Note* note = noteUnderMouse(); - - if (note) - { - n.push_back(note); - - updateKnifePos(me); - - // Call splitNotes for the note - m_midiClip->splitNotes(n, TimePos(m_knifeTickPos)); - } - + updateKnifePos(me, true); + m_knifeDown = true; update(); return; } @@ -2138,6 +2127,7 @@ void PianoRoll::setKnifeAction() m_knifeMode = m_editMode; m_editMode = EditMode::Knife; m_action = Action::Knife; + m_knifeDown = false; setCursor(Qt::ArrowCursor); update(); } @@ -2147,6 +2137,7 @@ void PianoRoll::cancelKnifeAction() { m_editMode = m_knifeMode; m_action = Action::None; + m_knifeDown = false; update(); } @@ -2275,6 +2266,13 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me ) m_midiClip->rearrangeAllNotes(); } + else if (m_action == Action::Knife && hasValidMidiClip()) + { + bool deleteShortEnds = me->modifiers() & Qt::ShiftModifier; + const NoteVector selectedNotes = getSelectedNotes(); + m_midiClip->splitNotesAlongLine(!selectedNotes.empty() ? selectedNotes : m_midiClip->notes(), TimePos(m_knifeStartTickPos), m_knifeStartKey, TimePos(m_knifeEndTickPos), m_knifeEndKey, deleteShortEnds); + m_knifeDown = false; + } if( m_action == Action::MoveNote || m_action == Action::ResizeNote ) { @@ -2378,7 +2376,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) // Update Knife position if we are on knife mode if (m_editMode == EditMode::Knife) { - updateKnifePos(me); + updateKnifePos(me, false); } if( me->y() > PR_TOP_MARGIN || m_action != Action::None ) @@ -2759,19 +2757,27 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) -void PianoRoll::updateKnifePos(QMouseEvent* me) +void PianoRoll::updateKnifePos(QMouseEvent* me, bool initial) { // Calculate the TimePos from the mouse - int mouseViewportPos = me->x() - m_whiteKeyWidth; - int mouseTickPos = mouseViewportPos * TimePos::ticksPerBar() / m_ppb + m_currentPosition; + int mouseViewportPosX = me->x() - m_whiteKeyWidth; + int mouseViewportPosY = keyAreaBottom() - 1 - me->y(); + int mouseTickPos = mouseViewportPosX * TimePos::ticksPerBar() / m_ppb + m_currentPosition; + int mouseKey = std::round(1.f * mouseViewportPosY / m_keyLineHeight) + m_startKey - 1; // If ctrl is not pressed, quantize the position if (!(me->modifiers() & Qt::ControlModifier)) { - mouseTickPos = floor(mouseTickPos / quantization()) * quantization(); + mouseTickPos = std::round(1.f * mouseTickPos / quantization()) * quantization(); } - m_knifeTickPos = mouseTickPos; + if (initial) + { + m_knifeStartTickPos = mouseTickPos; + m_knifeStartKey = mouseKey; + } + m_knifeEndTickPos = mouseTickPos; + m_knifeEndKey = mouseKey; } @@ -3531,37 +3537,19 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) } // -- Knife tool (draw cut line) - if (m_action == Action::Knife) + if (m_action == Action::Knife && m_knifeDown) { auto xCoordOfTick = [this](int tick) { return m_whiteKeyWidth + ( (tick - m_currentPosition) * m_ppb / TimePos::ticksPerBar()); }; - Note* n = noteUnderMouse(); - if (n) - { - const int key = n->key() - m_startKey + 1; - int y = y_base - key * m_keyLineHeight; + int x1 = xCoordOfTick(m_knifeStartTickPos); + int y1 = y_base - (m_knifeStartKey - m_startKey + 1) * m_keyLineHeight; + int x2 = xCoordOfTick(m_knifeEndTickPos); + int y2 = y_base - (m_knifeEndKey - m_startKey + 1) * m_keyLineHeight; - int x = xCoordOfTick(m_knifeTickPos); - - if (x > xCoordOfTick(n->pos()) && - x < xCoordOfTick(n->pos() + n->length())) - { - p.setPen(QPen(m_knifeCutLineColor, 1)); - p.drawLine(x, y, x, y + m_keyLineHeight); - - setCursor(Qt::BlankCursor); - } - else - { - setCursor(Qt::ArrowCursor); - } - } - else - { - setCursor(Qt::ArrowCursor); - } + p.setPen(QPen(m_knifeCutLineColor, 1)); + p.drawLine(x1, y1, x2, y2); } // -- End knife tool diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index 409fb60aefe..ab532168746 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -344,6 +344,48 @@ void MidiClip::splitNotes(const NoteVector& notes, TimePos pos) } } +void MidiClip::splitNotesAlongLine(const NoteVector notes, TimePos pos1, int key1, TimePos pos2, int key2, bool deleteShortEnds) +{ + if (notes.empty()) { return; } + + // Don't split if the line is horitzontal + if (key1 == key2) { return; } + + addJournalCheckPoint(); + + const auto slope = 1.f * (pos2 - pos1) / (key2 - key1); + const auto& [minKey, maxKey] = std::minmax(key1, key2); + + for (const auto& note : notes) + { + // Skip if the key is <= to minKey, since the line is drawn from the top of minKey to the top of maxKey, but only passes through maxKey - minKey - 1 total keys. + if (note->key() <= minKey || note->key() > maxKey) { continue; } + + // Subtracting 0.5 to get the line's intercept at the "center" of the key, not the top. + const TimePos keyIntercept = slope * (note->key() - 0.5 - key1) + pos1; + if (note->pos() < keyIntercept && note->endPos() > keyIntercept) + { + auto newNote1 = Note{*note}; + newNote1.setLength(keyIntercept - note->pos()); + + auto newNote2 = Note{*note}; + newNote2.setPos(keyIntercept); + newNote2.setLength(note->endPos() - keyIntercept); + + if (deleteShortEnds) + { + addNote(newNote1.length() >= newNote2.length() ? newNote1 : newNote2, false); + } + else + { + addNote(newNote1, false); + addNote(newNote2, false); + } + + removeNote(note); + } + } +}