From 05de59c085183cd5a2ad8fe5935181423f5a140f Mon Sep 17 00:00:00 2001 From: cyber-bridge <75862279+cyber-bridge@users.noreply.github.com> Date: Fri, 26 Feb 2021 12:47:16 +0100 Subject: [PATCH] Feature: PianoRoll Knife (#5845) * Initial PianoRoll razor feature * Restore PianoRoll edit mode after focusOut and in razor mode. * Show changes directly after cut. * Fix hanging note after adjusting vol/pan with razor action. * Extract the split action to a separate method This PR addresses some suggestions from a review, the most important ones being: - Extracting the note split action to a separate method, called Pattern::splitNotes - Removing getMouseTickPos method - Adding a variable that holds the current razor position and a method to update it (quantizing if CTRL is not pressed) - Using [this] to capture "this" on the lambda function instead of [=], since the latter doesn't work as intended from C++20 forward - Fixing some code style and adding comments * Removes an extra call to noteUnderMouse By removing "&& noteUnderMouse()" from the mousePressEvent conditional, we avoid an extra call to noteUnderMouse. The only difference in the behavior of the tool is that now clicking on a place that doesn't have a note will exit Razor mode. * Style change suggested by @russiankumar * Cancel razor action on SHIFT release. * Make razor cut-line (color) themable. * Add razor cut-line color to classic theme style.css * Rename razor to knife. * Change pixmap from razor to knife (from https://github.com/LMMS/lmms/pull/5524) * Remove SHIFT behavior. * Change knife shortcut to SHIFT+K Co-authored-by: CYBERDEViL Co-authored-by: Ian Caio --- data/themes/classic/edit_knife.png | Bin 0 -> 287 bytes data/themes/classic/style.css | 1 + data/themes/default/edit_knife.png | Bin 0 -> 287 bytes data/themes/default/style.css | 1 + include/Pattern.h | 3 + include/PianoRoll.h | 15 ++- src/gui/editors/PianoRoll.cpp | 156 ++++++++++++++++++++++++++++- src/tracks/Pattern.cpp | 34 +++++++ 8 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 data/themes/classic/edit_knife.png create mode 100644 data/themes/default/edit_knife.png diff --git a/data/themes/classic/edit_knife.png b/data/themes/classic/edit_knife.png new file mode 100644 index 0000000000000000000000000000000000000000..70b15113d1ab47359d90e3381cb87d2d25224532 GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc0wmQNuC@Uw&H|6fVg?4j!ywFfJby(BP*AeO zHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IXprSRNE{-7@=X=j? this window is active // -> shift is not pressed @@ -1353,6 +1373,10 @@ void PianoRoll::keyReleaseEvent(QKeyEvent* ke ) switch( ke->key() ) { case Qt::Key_Control: + if (m_editMode == ModeEditKnife) + { + break; + } computeSelectedNotes( ke->modifiers() & Qt::ShiftModifier); m_editMode = m_ctrlMode; update(); @@ -1441,6 +1465,26 @@ void PianoRoll::mousePressEvent(QMouseEvent * me ) return; } + // -- Knife + if (m_editMode == ModeEditKnife && me->button() == Qt::LeftButton) + { + NoteVector n; + Note* note = noteUnderMouse(); + + if (note) + { + n.append(note); + + updateKnifePos(me); + + // Call splitNotes for the note + m_pattern->splitNotes(n, TimePos(m_knifeTickPos)); + } + + update(); + return; + } + if( m_editMode == ModeEditDetuning && noteUnderMouse() ) { static QPointer detuningPattern = nullptr; @@ -1946,6 +1990,24 @@ void PianoRoll::pauseChordNotes(int key) } } +void PianoRoll::setKnifeAction() +{ + if (m_editMode != ModeEditKnife) + { + m_knifeMode = m_editMode; + m_editMode = ModeEditKnife; + m_action = ActionKnife; + setCursor(Qt::ArrowCursor); + update(); + } +} + +void PianoRoll::cancelKnifeAction() +{ + m_editMode = m_knifeMode; + m_action = ActionNone; + update(); +} @@ -2047,6 +2109,12 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me ) s_textFloat->hide(); + // Quit knife mode if we pressed and released the right mouse button + if (m_editMode == ModeEditKnife && me->button() == Qt::RightButton) + { + cancelKnifeAction(); + } + if( me->button() & Qt::LeftButton ) { mustRepaint = true; @@ -2105,7 +2173,11 @@ void PianoRoll::mouseReleaseEvent( QMouseEvent * me ) } m_currentNote = NULL; - m_action = ActionNone; + + if (m_action != ActionKnife) + { + m_action = ActionNone; + } if( m_editMode == ModeDraw ) { @@ -2131,6 +2203,8 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) if( m_action == ActionNone && me->buttons() == 0 ) { + // When cursor is between note editing area and volume/panning + // area show vertical size cursor. if( me->y() > keyAreaBottom() && me->y() < noteEditTop() ) { setCursor( Qt::SizeVerCursor ); @@ -2160,6 +2234,12 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) return; } + // Update Knife position if we are on knife mode + if (m_editMode == ModeEditKnife) + { + updateKnifePos(me); + } + if( me->y() > PR_TOP_MARGIN || m_action != ActionNone ) { bool edit_note = ( me->y() > noteEditTop() ) @@ -2439,7 +2519,7 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) } } } - else if (me->buttons() == Qt::NoButton && m_editMode != ModeDraw) + else if (me->buttons() == Qt::NoButton && m_editMode != ModeDraw && m_editMode != ModeEditKnife) { // Is needed to restore cursor when it previously was set to // Qt::SizeVerCursor (between keyAreaBottom and noteEditTop) @@ -2535,6 +2615,24 @@ void PianoRoll::mouseMoveEvent( QMouseEvent * me ) +void PianoRoll::updateKnifePos(QMouseEvent* me) +{ + // Calculate the TimePos from the mouse + int mouseViewportPos = me->x() - m_whiteKeyWidth; + int mouseTickPos = mouseViewportPos / (m_ppb / TimePos::ticksPerBar()) + m_currentPosition; + + // If ctrl is not pressed, quantize the position + if (!(me->modifiers() & Qt::ControlModifier)) + { + mouseTickPos = floor(mouseTickPos / quantization()) * quantization(); + } + + m_knifeTickPos = mouseTickPos; +} + + + + void PianoRoll::dragNotes( int x, int y, bool alt, bool shift, bool ctrl ) { // dragging one or more notes around @@ -3232,6 +3330,41 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) } } + // -- Knife tool (draw cut line) + if (m_action == ActionKnife) + { + 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 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); + } + } + // -- End knife tool + //draw current step recording notes for( const Note *note : m_stepRecorder.getCurStepNotes() ) { @@ -3353,6 +3486,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) case ModeErase: cursor = s_toolErase; break; case ModeSelect: cursor = s_toolSelect; break; case ModeEditDetuning: cursor = s_toolOpen; break; + case ModeEditKnife: cursor = s_toolKnife; break; } QPoint mousePosition = mapFromGlobal( QCursor::pos() ); if( cursor != NULL && mousePosition.y() > keyAreaTop() && mousePosition.x() > noteEditLeft()) @@ -3560,7 +3694,12 @@ void PianoRoll::focusOutEvent( QFocusEvent * ) m_pattern->instrumentTrack()->pianoModel()->setKeyState( i, false ); } } - m_editMode = m_ctrlMode; + if (m_editMode == ModeEditKnife) { + m_editMode = m_knifeMode; + m_action = ActionNone; + } else { + m_editMode = m_ctrlMode; + } update(); } @@ -4443,7 +4582,14 @@ PianoRollWindow::PianoRollWindow() : connect(glueAction, SIGNAL(triggered()), m_editor, SLOT(glueNotes())); glueAction->setShortcut( Qt::SHIFT | Qt::Key_G ); + // Knife + QAction * knifeAction = new QAction(embed::getIconPixmap("edit_knife"), + tr("Knife"), noteToolsButton); + connect(knifeAction, &QAction::triggered, m_editor, &PianoRoll::setKnifeAction); + knifeAction->setShortcut( Qt::SHIFT | Qt::Key_K ); + noteToolsButton->addAction(glueAction); + noteToolsButton->addAction(knifeAction); notesActionsToolBar->addWidget(noteToolsButton); diff --git a/src/tracks/Pattern.cpp b/src/tracks/Pattern.cpp index 659a1919c19..97bc93bc5ed 100644 --- a/src/tracks/Pattern.cpp +++ b/src/tracks/Pattern.cpp @@ -324,6 +324,40 @@ void Pattern::setStep( int step, bool enabled ) +void Pattern::splitNotes(NoteVector notes, TimePos pos) +{ + if (notes.empty()) { return; } + + addJournalCheckPoint(); + + for (int i = 0; i < notes.size(); ++i) + { + Note* note = notes.at(i); + + int leftLength = pos.getTicks() - note->pos(); + int rightLength = note->length() - leftLength; + + // Split out of bounds + if (leftLength <= 0 || rightLength <= 0) + { + continue; + } + + // Reduce note length + note->setLength(leftLength); + + // Add new note with the remaining length + Note newNote = Note(*note); + newNote.setLength(rightLength); + newNote.setPos(note->pos() + leftLength); + + addNote(newNote, false); + } +} + + + + void Pattern::setType( PatternTypes _new_pattern_type ) { if( _new_pattern_type == BeatPattern ||