diff --git a/data/themes/classic/edit_knife.png b/data/themes/classic/edit_knife.png new file mode 100644 index 00000000000..70b15113d1a Binary files /dev/null and b/data/themes/classic/edit_knife.png differ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 860aa0da1ea..cb136eede3b 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -155,6 +155,7 @@ PianoRoll { qproperty-ghostNoteBorders: true; qproperty-barColor: #4afd85; qproperty-markedSemitoneColor: rgba( 0, 255, 200, 60 ); + qproperty-knifeCutLine: rgba(255, 0, 0, 255); /* Piano keys */ qproperty-whiteKeyWidth: 64; qproperty-whiteKeyActiveTextColor: #000; diff --git a/data/themes/default/edit_knife.png b/data/themes/default/edit_knife.png new file mode 100644 index 00000000000..70b15113d1a Binary files /dev/null and b/data/themes/default/edit_knife.png differ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index ce476f5a9ce..9cc39563ea9 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -187,6 +187,7 @@ PianoRoll { qproperty-ghostNoteBorders: false; qproperty-barColor: #078f3a; qproperty-markedSemitoneColor: rgba(255, 255, 255, 30); + qproperty-knifeCutLine: rgba(255, 0, 0, 255); /* Piano keys */ qproperty-whiteKeyWidth: 64; qproperty-whiteKeyActiveTextColor: #000; diff --git a/include/Pattern.h b/include/Pattern.h index 0a73376c024..082583bfb98 100644 --- a/include/Pattern.h +++ b/include/Pattern.h @@ -76,6 +76,9 @@ class LMMS_EXPORT Pattern : public TrackContentObject Note * addStepNote( int step ); void setStep( int step, bool enabled ); + // Split the list of notes on the given position + void splitNotes(NoteVector notes, TimePos pos); + // pattern-type stuff inline PatternTypes type() const { diff --git a/include/PianoRoll.h b/include/PianoRoll.h index 5e0ea0762c9..77d372bcccc 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -70,6 +70,7 @@ class PianoRoll : public QWidget Q_PROPERTY(QColor textColorLight MEMBER m_textColorLight) Q_PROPERTY(QColor textShadow MEMBER m_textShadow) Q_PROPERTY(QColor markedSemitoneColor MEMBER m_markedSemitoneColor) + Q_PROPERTY(QColor knifeCutLine MEMBER m_knifeCutLineColor) Q_PROPERTY(int noteOpacity MEMBER m_noteOpacity) Q_PROPERTY(bool noteBorders MEMBER m_noteBorders) Q_PROPERTY(int ghostNoteOpacity MEMBER m_ghostNoteOpacity) @@ -95,6 +96,7 @@ class PianoRoll : public QWidget ModeErase, ModeSelect, ModeEditDetuning, + ModeEditKnife }; /*! \brief Resets settings to default when e.g. creating a new project */ @@ -226,7 +228,8 @@ protected slots: ActionResizeNote, ActionSelectNotes, ActionChangeNoteProperty, - ActionResizeNoteEditArea + ActionResizeNoteEditArea, + ActionKnife }; enum NoteEditMode @@ -282,6 +285,9 @@ protected slots: void playChordNotes(int key, int velocity=-1); void pauseChordNotes(int key); + void setKnifeAction(); + void cancelKnifeAction(); + void updateScrollbars(); void updatePositionLineHeight(); @@ -304,6 +310,7 @@ protected slots: static QPixmap * s_toolSelect; static QPixmap * s_toolMove; static QPixmap * s_toolOpen; + static QPixmap* s_toolKnife; static PianoRollKeyTypes prKeyOrder[]; @@ -389,6 +396,7 @@ protected slots: EditModes m_editMode; EditModes m_ctrlMode; // mode they were in before they hit ctrl + EditModes m_knifeMode; // mode they where in before entering knife mode bool m_mouseDownRight; //true if right click is being held down @@ -408,6 +416,10 @@ 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); + friend class PianoRollWindow; StepRecorderWidget m_stepRecorderWidget; @@ -428,6 +440,7 @@ protected slots: QColor m_textColorLight; QColor m_textShadow; QColor m_markedSemitoneColor; + QColor m_knifeCutLineColor; int m_noteOpacity; int m_ghostNoteOpacity; bool m_noteBorders; diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index cee6870b7a3..92d2ab0e3bd 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -115,6 +115,7 @@ QPixmap * PianoRoll::s_toolErase = NULL; QPixmap * PianoRoll::s_toolSelect = NULL; QPixmap * PianoRoll::s_toolMove = NULL; QPixmap * PianoRoll::s_toolOpen = NULL; +QPixmap* PianoRoll::s_toolKnife = nullptr; TextFloat * PianoRoll::s_textFloat = NULL; @@ -200,6 +201,7 @@ PianoRoll::PianoRoll() : m_textColorLight( 0, 0, 0 ), m_textShadow( 0, 0, 0 ), m_markedSemitoneColor( 0, 0, 0 ), + m_knifeCutLineColor(0, 0, 0), m_noteOpacity( 255 ), m_ghostNoteOpacity( 255 ), m_noteBorders( true ), @@ -271,6 +273,10 @@ PianoRoll::PianoRoll() : { s_toolOpen = new QPixmap( embed::getIconPixmap( "automation" ) ); } + if (s_toolKnife == nullptr) + { + s_toolKnife = new QPixmap(embed::getIconPixmap("edit_knife")); + } // init text-float if( s_textFloat == NULL ) @@ -1268,8 +1274,16 @@ void PianoRoll::keyPressEvent(QKeyEvent* ke) break; case Qt::Key_Escape: - // Same as Ctrl + Shift + A - clearSelectedNotes(); + // On the Knife mode, ESC cancels it + if (m_editMode == ModeEditKnife) + { + cancelKnifeAction(); + } + else + { + // Same as Ctrl + Shift + A + clearSelectedNotes(); + } break; case Qt::Key_Backspace: @@ -1314,6 +1328,12 @@ void PianoRoll::keyPressEvent(QKeyEvent* ke) } case Qt::Key_Control: + // Ctrl will not enter selection mode if we are + // in Knife mode, but unquantize it + if (m_editMode == ModeEditKnife) + { + break; + } // Enter selection mode if: // -> 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 ||