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 <cyberdevil@notabug.org> Co-authored-by: Ian Caio <iancaio_dev@hotmail.com>
This commit is contained in:
BIN
data/themes/classic/edit_knife.png
Normal file
BIN
data/themes/classic/edit_knife.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 B |
@@ -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;
|
||||
|
||||
BIN
data/themes/default/edit_knife.png
Normal file
BIN
data/themes/default/edit_knife.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 B |
@@ -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;
|
||||
|
||||
@@ -76,6 +76,9 @@ public:
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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 @@ public:
|
||||
ModeErase,
|
||||
ModeSelect,
|
||||
ModeEditDetuning,
|
||||
ModeEditKnife
|
||||
};
|
||||
|
||||
/*! \brief Resets settings to default when e.g. creating a new project */
|
||||
@@ -226,7 +228,8 @@ private:
|
||||
ActionResizeNote,
|
||||
ActionSelectNotes,
|
||||
ActionChangeNoteProperty,
|
||||
ActionResizeNoteEditArea
|
||||
ActionResizeNoteEditArea,
|
||||
ActionKnife
|
||||
};
|
||||
|
||||
enum NoteEditMode
|
||||
@@ -282,6 +285,9 @@ private:
|
||||
void playChordNotes(int key, int velocity=-1);
|
||||
void pauseChordNotes(int key);
|
||||
|
||||
void setKnifeAction();
|
||||
void cancelKnifeAction();
|
||||
|
||||
void updateScrollbars();
|
||||
void updatePositionLineHeight();
|
||||
|
||||
@@ -304,6 +310,7 @@ private:
|
||||
static QPixmap * s_toolSelect;
|
||||
static QPixmap * s_toolMove;
|
||||
static QPixmap * s_toolOpen;
|
||||
static QPixmap* s_toolKnife;
|
||||
|
||||
static PianoRollKeyTypes prKeyOrder[];
|
||||
|
||||
@@ -389,6 +396,7 @@ private:
|
||||
|
||||
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 @@ private:
|
||||
// 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 @@ private:
|
||||
QColor m_textColorLight;
|
||||
QColor m_textShadow;
|
||||
QColor m_markedSemitoneColor;
|
||||
QColor m_knifeCutLineColor;
|
||||
int m_noteOpacity;
|
||||
int m_ghostNoteOpacity;
|
||||
bool m_noteBorders;
|
||||
|
||||
@@ -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<AutomationPattern> 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);
|
||||
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
Reference in New Issue
Block a user