Skip to content

Commit c12fd57

Browse files
authored
Allow cutting multiple notes at once in Piano Roll (LMMS#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.
1 parent 5fa01e7 commit c12fd57

File tree

4 files changed

+85
-47
lines changed

4 files changed

+85
-47
lines changed

include/MidiClip.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ class LMMS_EXPORT MidiClip : public Clip
8282
// Split the list of notes on the given position
8383
void splitNotes(const NoteVector& notes, TimePos pos);
8484

85+
// Split the list of notes along a line
86+
void splitNotesAlongLine(const NoteVector notes, TimePos pos1, int key1, TimePos pos2, int key2, bool deleteShortEnds);
87+
8588
// clip-type stuff
8689
inline Type type() const
8790
{

include/PianoRoll.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,14 @@ protected slots:
456456
// did we start a mouseclick with shift pressed
457457
bool m_startedWithShift;
458458

459-
// Variable that holds the position in ticks for the knife action
460-
int m_knifeTickPos;
461-
void updateKnifePos(QMouseEvent* me);
459+
// Variables that hold the start and end position for the knife line
460+
TimePos m_knifeStartTickPos;
461+
int m_knifeStartKey;
462+
TimePos m_knifeEndTickPos;
463+
int m_knifeEndKey;
464+
bool m_knifeDown;
465+
466+
void updateKnifePos(QMouseEvent* me, bool initial);
462467

463468
friend class PianoRollWindow;
464469

src/gui/editors/PianoRoll.cpp

Lines changed: 32 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1608,19 +1608,8 @@ void PianoRoll::mousePressEvent(QMouseEvent * me )
16081608
// -- Knife
16091609
if (m_editMode == EditMode::Knife && me->button() == Qt::LeftButton)
16101610
{
1611-
NoteVector n;
1612-
Note* note = noteUnderMouse();
1613-
1614-
if (note)
1615-
{
1616-
n.push_back(note);
1617-
1618-
updateKnifePos(me);
1619-
1620-
// Call splitNotes for the note
1621-
m_midiClip->splitNotes(n, TimePos(m_knifeTickPos));
1622-
}
1623-
1611+
updateKnifePos(me, true);
1612+
m_knifeDown = true;
16241613
update();
16251614
return;
16261615
}
@@ -2138,6 +2127,7 @@ void PianoRoll::setKnifeAction()
21382127
m_knifeMode = m_editMode;
21392128
m_editMode = EditMode::Knife;
21402129
m_action = Action::Knife;
2130+
m_knifeDown = false;
21412131
setCursor(Qt::ArrowCursor);
21422132
update();
21432133
}
@@ -2147,6 +2137,7 @@ void PianoRoll::cancelKnifeAction()
21472137
{
21482138
m_editMode = m_knifeMode;
21492139
m_action = Action::None;
2140+
m_knifeDown = false;
21502141
update();
21512142
}
21522143

@@ -2275,6 +2266,13 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me )
22752266
m_midiClip->rearrangeAllNotes();
22762267

22772268
}
2269+
else if (m_action == Action::Knife && hasValidMidiClip())
2270+
{
2271+
bool deleteShortEnds = me->modifiers() & Qt::ShiftModifier;
2272+
const NoteVector selectedNotes = getSelectedNotes();
2273+
m_midiClip->splitNotesAlongLine(!selectedNotes.empty() ? selectedNotes : m_midiClip->notes(), TimePos(m_knifeStartTickPos), m_knifeStartKey, TimePos(m_knifeEndTickPos), m_knifeEndKey, deleteShortEnds);
2274+
m_knifeDown = false;
2275+
}
22782276

22792277
if( m_action == Action::MoveNote || m_action == Action::ResizeNote )
22802278
{
@@ -2378,7 +2376,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me )
23782376
// Update Knife position if we are on knife mode
23792377
if (m_editMode == EditMode::Knife)
23802378
{
2381-
updateKnifePos(me);
2379+
updateKnifePos(me, false);
23822380
}
23832381

23842382
if( me->y() > PR_TOP_MARGIN || m_action != Action::None )
@@ -2759,19 +2757,27 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me )
27592757

27602758

27612759

2762-
void PianoRoll::updateKnifePos(QMouseEvent* me)
2760+
void PianoRoll::updateKnifePos(QMouseEvent* me, bool initial)
27632761
{
27642762
// Calculate the TimePos from the mouse
2765-
int mouseViewportPos = me->x() - m_whiteKeyWidth;
2766-
int mouseTickPos = mouseViewportPos * TimePos::ticksPerBar() / m_ppb + m_currentPosition;
2763+
int mouseViewportPosX = me->x() - m_whiteKeyWidth;
2764+
int mouseViewportPosY = keyAreaBottom() - 1 - me->y();
2765+
int mouseTickPos = mouseViewportPosX * TimePos::ticksPerBar() / m_ppb + m_currentPosition;
2766+
int mouseKey = std::round(1.f * mouseViewportPosY / m_keyLineHeight) + m_startKey - 1;
27672767

27682768
// If ctrl is not pressed, quantize the position
27692769
if (!(me->modifiers() & Qt::ControlModifier))
27702770
{
2771-
mouseTickPos = floor(mouseTickPos / quantization()) * quantization();
2771+
mouseTickPos = std::round(1.f * mouseTickPos / quantization()) * quantization();
27722772
}
27732773

2774-
m_knifeTickPos = mouseTickPos;
2774+
if (initial)
2775+
{
2776+
m_knifeStartTickPos = mouseTickPos;
2777+
m_knifeStartKey = mouseKey;
2778+
}
2779+
m_knifeEndTickPos = mouseTickPos;
2780+
m_knifeEndKey = mouseKey;
27752781
}
27762782

27772783

@@ -3531,37 +3537,19 @@ void PianoRoll::paintEvent(QPaintEvent * pe )
35313537
}
35323538

35333539
// -- Knife tool (draw cut line)
3534-
if (m_action == Action::Knife)
3540+
if (m_action == Action::Knife && m_knifeDown)
35353541
{
35363542
auto xCoordOfTick = [this](int tick) {
35373543
return m_whiteKeyWidth + (
35383544
(tick - m_currentPosition) * m_ppb / TimePos::ticksPerBar());
35393545
};
3540-
Note* n = noteUnderMouse();
3541-
if (n)
3542-
{
3543-
const int key = n->key() - m_startKey + 1;
3544-
int y = y_base - key * m_keyLineHeight;
3546+
int x1 = xCoordOfTick(m_knifeStartTickPos);
3547+
int y1 = y_base - (m_knifeStartKey - m_startKey + 1) * m_keyLineHeight;
3548+
int x2 = xCoordOfTick(m_knifeEndTickPos);
3549+
int y2 = y_base - (m_knifeEndKey - m_startKey + 1) * m_keyLineHeight;
35453550

3546-
int x = xCoordOfTick(m_knifeTickPos);
3547-
3548-
if (x > xCoordOfTick(n->pos()) &&
3549-
x < xCoordOfTick(n->pos() + n->length()))
3550-
{
3551-
p.setPen(QPen(m_knifeCutLineColor, 1));
3552-
p.drawLine(x, y, x, y + m_keyLineHeight);
3553-
3554-
setCursor(Qt::BlankCursor);
3555-
}
3556-
else
3557-
{
3558-
setCursor(Qt::ArrowCursor);
3559-
}
3560-
}
3561-
else
3562-
{
3563-
setCursor(Qt::ArrowCursor);
3564-
}
3551+
p.setPen(QPen(m_knifeCutLineColor, 1));
3552+
p.drawLine(x1, y1, x2, y2);
35653553
}
35663554
// -- End knife tool
35673555

src/tracks/MidiClip.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,48 @@ void MidiClip::splitNotes(const NoteVector& notes, TimePos pos)
344344
}
345345
}
346346

347+
void MidiClip::splitNotesAlongLine(const NoteVector notes, TimePos pos1, int key1, TimePos pos2, int key2, bool deleteShortEnds)
348+
{
349+
if (notes.empty()) { return; }
350+
351+
// Don't split if the line is horitzontal
352+
if (key1 == key2) { return; }
353+
354+
addJournalCheckPoint();
355+
356+
const auto slope = 1.f * (pos2 - pos1) / (key2 - key1);
357+
const auto& [minKey, maxKey] = std::minmax(key1, key2);
358+
359+
for (const auto& note : notes)
360+
{
361+
// 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.
362+
if (note->key() <= minKey || note->key() > maxKey) { continue; }
363+
364+
// Subtracting 0.5 to get the line's intercept at the "center" of the key, not the top.
365+
const TimePos keyIntercept = slope * (note->key() - 0.5 - key1) + pos1;
366+
if (note->pos() < keyIntercept && note->endPos() > keyIntercept)
367+
{
368+
auto newNote1 = Note{*note};
369+
newNote1.setLength(keyIntercept - note->pos());
370+
371+
auto newNote2 = Note{*note};
372+
newNote2.setPos(keyIntercept);
373+
newNote2.setLength(note->endPos() - keyIntercept);
374+
375+
if (deleteShortEnds)
376+
{
377+
addNote(newNote1.length() >= newNote2.length() ? newNote1 : newNote2, false);
378+
}
379+
else
380+
{
381+
addNote(newNote1, false);
382+
addNote(newNote2, false);
383+
}
384+
385+
removeNote(note);
386+
}
387+
}
388+
}
347389

348390

349391

0 commit comments

Comments
 (0)