Transpose midi clips in song editor (#6409)
* Transpose midi clips in song editor
* Fix undo stupidity
* Check boundries when transposing clips
and move transpose function to NoteVector
* Avoid update if nothing has changed
* Make getNoteBounds a separate function
* Rename getNoteBounds to boundsForNotes
* bool operator instead of optional + qobject_cast
* Revert "bool operator instead of optional + qobject_cast"
This reverts commit 98c56a96cf.
* qobject_cast and nullopt
This commit is contained in:
@@ -73,6 +73,7 @@ protected slots:
|
||||
|
||||
void resetName();
|
||||
void changeName();
|
||||
void transposeSelection();
|
||||
|
||||
|
||||
protected:
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#ifndef NOTE_H
|
||||
#define NOTE_H
|
||||
|
||||
#include <optional>
|
||||
#include <QVector>
|
||||
|
||||
#include "volume.h"
|
||||
@@ -252,6 +253,18 @@ private:
|
||||
typedef QVector<Note *> NoteVector;
|
||||
|
||||
|
||||
struct NoteBounds
|
||||
{
|
||||
TimePos start;
|
||||
TimePos end;
|
||||
int lowest;
|
||||
int highest;
|
||||
};
|
||||
|
||||
|
||||
std::optional<NoteBounds> boundsForNotes(const NoteVector& notes);
|
||||
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
#endif
|
||||
|
||||
@@ -237,4 +237,33 @@ bool Note::withinRange(int tickStart, int tickEnd) const
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*! \brief Get the start/end/bottom/top positions of notes in a vector
|
||||
*
|
||||
* Returns no value if there are no notes
|
||||
*/
|
||||
std::optional<NoteBounds> boundsForNotes(const NoteVector& notes)
|
||||
{
|
||||
if (notes.empty()) { return std::nullopt; }
|
||||
|
||||
TimePos start = notes.front()->pos();
|
||||
TimePos end = start;
|
||||
int lower = notes.front()->key();
|
||||
int upper = lower;
|
||||
|
||||
for (const Note* note: notes)
|
||||
{
|
||||
// TODO should we assume that NoteVector is always sorted correctly,
|
||||
// so first() always has the lowest time position?
|
||||
start = std::min(start, note->pos());
|
||||
end = std::max(end, note->endPos());
|
||||
lower = std::min(lower, note->key());
|
||||
upper = std::max(upper, note->key());
|
||||
}
|
||||
|
||||
return NoteBounds{start, end, lower, upper};
|
||||
}
|
||||
|
||||
|
||||
} // namespace lmms
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <QApplication>
|
||||
#include <QInputDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
|
||||
@@ -36,6 +37,7 @@
|
||||
#include "MidiClip.h"
|
||||
#include "PianoRoll.h"
|
||||
#include "RenameDialog.h"
|
||||
#include "TrackView.h"
|
||||
|
||||
namespace lmms::gui
|
||||
{
|
||||
@@ -144,8 +146,66 @@ void MidiClipView::changeName()
|
||||
|
||||
|
||||
|
||||
void MidiClipView::transposeSelection()
|
||||
{
|
||||
const auto selection = getClickedClips();
|
||||
|
||||
// Calculate the key boundries for all clips
|
||||
int highest = 0;
|
||||
int lowest = NumKeys - 1;
|
||||
for (ClipView* clipview: selection)
|
||||
{
|
||||
if (auto mcv = qobject_cast<MidiClipView*>(clipview))
|
||||
{
|
||||
if (auto bounds = boundsForNotes(mcv->getMidiClip()->notes()))
|
||||
{
|
||||
lowest = std::min(bounds->lowest, lowest);
|
||||
highest = std::max(bounds->highest, highest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int semitones = QInputDialog::getInt(this, tr("Transpose"), tr("Semitones to transpose by:"),
|
||||
/*start*/ 0, /*min*/ -lowest, /*max*/ (NumKeys - 1 - highest));
|
||||
|
||||
if (semitones == 0) { return; }
|
||||
|
||||
// TODO make this not crash
|
||||
// Engine::getSong()->addJournalCheckPoint();
|
||||
|
||||
QSet<Track*> m_changedTracks;
|
||||
for (ClipView* clipview: selection)
|
||||
{
|
||||
auto mcv = qobject_cast<MidiClipView*>(clipview);
|
||||
if (!mcv) { continue; }
|
||||
|
||||
auto clip = mcv->getMidiClip();
|
||||
if (clip->notes().empty()) { continue; }
|
||||
|
||||
auto track = clipview->getTrackView()->getTrack();
|
||||
if (!m_changedTracks.contains(track))
|
||||
{
|
||||
track->addJournalCheckPoint();
|
||||
m_changedTracks.insert(track);
|
||||
}
|
||||
|
||||
for (Note* note: clip->notes())
|
||||
{
|
||||
note->setKey(note->key() + semitones);
|
||||
}
|
||||
emit clip->dataChanged();
|
||||
}
|
||||
// At least one clip must have notes to show the transpose dialog, so something *has* changed
|
||||
Engine::getSong()->setModified();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void MidiClipView::constructContextMenu( QMenu * _cm )
|
||||
{
|
||||
bool isBeat = m_clip->type() == MidiClip::BeatClip;
|
||||
|
||||
QAction * a = new QAction( embed::getIconPixmap( "piano" ),
|
||||
tr( "Open in piano-roll" ), _cm );
|
||||
_cm->insertAction( _cm->actions()[0], a );
|
||||
@@ -163,6 +223,10 @@ void MidiClipView::constructContextMenu( QMenu * _cm )
|
||||
|
||||
_cm->addAction( embed::getIconPixmap( "edit_erase" ),
|
||||
tr( "Clear all notes" ), m_clip, SLOT(clear()));
|
||||
if (!isBeat)
|
||||
{
|
||||
_cm->addAction(embed::getIconPixmap("scale"), tr("Transpose"), this, &MidiClipView::transposeSelection);
|
||||
}
|
||||
_cm->addSeparator();
|
||||
|
||||
_cm->addAction( embed::getIconPixmap( "reload" ), tr( "Reset name" ),
|
||||
@@ -171,7 +235,7 @@ void MidiClipView::constructContextMenu( QMenu * _cm )
|
||||
tr( "Change name" ),
|
||||
this, SLOT(changeName()));
|
||||
|
||||
if ( m_clip->type() == MidiClip::BeatClip )
|
||||
if (isBeat)
|
||||
{
|
||||
_cm->addSeparator();
|
||||
|
||||
@@ -622,4 +686,4 @@ void MidiClipView::paintEvent( QPaintEvent * )
|
||||
}
|
||||
|
||||
|
||||
} // namespace lmms::gui
|
||||
} // namespace lmms::gui
|
||||
|
||||
Reference in New Issue
Block a user