Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New context menu automation items #7317

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions include/AutomatableModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ class LMMS_EXPORT AutomatableModel : public Model, public JournallingObject
return castValue<T>( m_value );
}

inline float getTrueValue(int frameOffset = 0) const
{
if (!m_controllerConnection && !hasLinkedModels())
{
return m_value;
}

return controllerValue(frameOffset);
}

float controllerValue( int frameOffset ) const;

//! @brief Function that returns sample-exact data as a ValueBuffer
Expand Down
24 changes: 23 additions & 1 deletion include/AutomatableModelView.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@

#include "ModelView.h"
#include "AutomatableModel.h"
#include "AutomationTrack.h"
#include "AutomationClip.h"
#include "SongEditor.h"
#include "Song.h"

class QMenu;
class QMouseEvent;
class Song;

namespace lmms::gui
{
Expand Down Expand Up @@ -97,10 +102,28 @@ class AutomatableModelViewSlots : public QObject
public slots:
void execConnectionDialog();
void removeConnection();
void addSongAutomationNode();
void addSongAutomationNodeAndClip();
void updateSongNearestAutomationNode();
void removeSongNearestAutomationNode();
void editSongGlobalAutomation();
void unlinkAllModels();
void removeSongGlobalAutomation();

private:
// gets the automationTrack with the most amount of clips connected to it
// if this track doesn't exists and "canAddNewTrack", then add a new one else return nullptr
// "clips" = clips that are connected to this model
AutomationTrack* getCurrentAutomationTrack(std::vector<AutomationClip*>* clips, bool canAddNewTrack);
// gets the clip that start before or after the song time position (playback pos)
// if this clip doesn't exists and "canAddNewClip", then add a new one else return nullptr
AutomationClip* getCurrentAutomationClip(AutomationTrack* track, bool canAddNewClip, bool searchAfter);
// gets the automationNode closest to the song time position (playback pos)
// "clipOut" is the clip that has the node
// can return nullptr on "clipOut"
const TimePos getNearestAutomationNode(AutomationTrack* track, AutomationClip** clipOut);
// makes new clip and connects it to this model
AutomationClip* makeNewClip(AutomationTrack* track, TimePos position, bool canSnap);
private slots:
/// Copy the model's value to the clipboard.
void copyToClipboard();
Expand All @@ -109,7 +132,6 @@ private slots:

protected:
AutomatableModelView* m_amv;

szeli1 marked this conversation as resolved.
Show resolved Hide resolved
} ;


Expand Down
251 changes: 251 additions & 0 deletions src/gui/AutomatableModelView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@

#include "AutomatableModelView.h"
#include "AutomationClip.h"
#include "AutomationNode.h"
#include "ControllerConnectionDialog.h"
#include "ControllerConnection.h"
#include "embed.h"
#include "GuiApplication.h"
#include "MainWindow.h"
#include "StringPairDrag.h"
#include "Clipboard.h"
#include "Engine.h"

#include "AutomationEditor.h"

Expand Down Expand Up @@ -83,6 +85,25 @@ void AutomatableModelView::addDefaultActions( QMenu* menu )

menu->addSeparator();

menu->addAction(QPixmap(),
AutomatableModel::tr("add automation node"),
amvSlots,
&AutomatableModelViewSlots::addSongAutomationNode);
menu->addAction(QPixmap(),
AutomatableModel::tr("add automation node to new clip"),
amvSlots,
&AutomatableModelViewSlots::addSongAutomationNodeAndClip);
menu->addAction(QPixmap(),
AutomatableModel::tr("update closest automation node"),
amvSlots,
&AutomatableModelViewSlots::updateSongNearestAutomationNode);
menu->addAction(QPixmap(),
AutomatableModel::tr("remove closest automation node"),
amvSlots,
&AutomatableModelViewSlots::removeSongNearestAutomationNode);

menu->addSeparator();

menu->addAction( embed::getIconPixmap( "automation" ),
AutomatableModel::tr( "Edit song-global automation" ),
amvSlots,
Expand Down Expand Up @@ -258,7 +279,237 @@ void AutomatableModelViewSlots::removeConnection()
}


void AutomatableModelViewSlots::addSongAutomationNode()
{
// getting all the clips that have this model
std::vector<AutomationClip*> clips = AutomationClip::clipsForModel(m_amv->modelUntyped());
// selecting the track with the most clips connected to this model
AutomationTrack* track = getCurrentAutomationTrack(&clips, true);
// getting the clip before the current song time position
AutomationClip* clip = getCurrentAutomationClip(track, true, false);

// getting global song time
TimePos timePos = static_cast<TimePos>(Engine::getSong()->getPlayPos());
// account for the node's relative position inside clip
timePos -= clip->startPosition();
bool autoResize = clip->getAutoResize();

clip->setAutoResize(true);
// adding model value
clip->recordValue(timePos, m_amv->modelUntyped()->getTrueValue());
clip->setAutoResize(autoResize);
}
szeli1 marked this conversation as resolved.
Show resolved Hide resolved

void AutomatableModelViewSlots::addSongAutomationNodeAndClip()
{
std::vector<AutomationClip*> clips = AutomationClip::clipsForModel(m_amv->modelUntyped());
AutomationTrack* track = getCurrentAutomationTrack(&clips, true);
AutomationClip* clip = getCurrentAutomationClip(track, false, false);

TimePos timePos = static_cast<TimePos>(Engine::getSong()->getPlayPos());

if (clip && clip->endPosition().getTicks() < timePos.getTicks())
{
AutomationClip* newClip = makeNewClip(track, timePos, true);
// copying the progressionType of the clip before
newClip->setProgressionType(clip->progressionType());
timePos -= newClip->startPosition();
bool autoResize = newClip->getAutoResize();

newClip->setAutoResize(true);
newClip->recordValue(timePos, m_amv->modelUntyped()->getTrueValue());
newClip->setAutoResize(autoResize);
}
else
{
addSongAutomationNode();
}
}

void AutomatableModelViewSlots::updateSongNearestAutomationNode()
{
std::vector<AutomationClip*> clips = AutomationClip::clipsForModel(m_amv->modelUntyped());
// getting the track without adding a new one if no track was found
AutomationTrack* track = getCurrentAutomationTrack(&clips, false);
// this needs to be checked because getCurrentAutomationTrack might give
// a nullptr if it can not find and add a track
if (!track) { return; }

// getting nearest node position
AutomationClip* nodeClip = nullptr;
TimePos nodePos = getNearestAutomationNode(track, &nodeClip);
if (nodeClip)
{
// modifying its value
nodeClip->recordValue(nodePos, m_amv->modelUntyped()->getTrueValue());
}
}

void AutomatableModelViewSlots::removeSongNearestAutomationNode()
{
std::vector<AutomationClip*> clips = AutomationClip::clipsForModel(m_amv->modelUntyped());
// getting the track without adding a new one if no track was found
AutomationTrack* track = getCurrentAutomationTrack(&clips, false);

// this needs to be checked because getCurrentAutomationTrack might give
// a nullptr if it can not find and add a track
if (!track) { return; }

AutomationClip* nodeClip = nullptr;
TimePos nodePos = getNearestAutomationNode(track, &nodeClip);
if (nodeClip)
{
nodeClip->removeNode(nodePos);
// if there is no node left, the automationClip will be deleted
if (nodeClip->hasAutomation() == false)
{
delete nodeClip;
}
}
}

AutomationTrack* AutomatableModelViewSlots::getCurrentAutomationTrack(std::vector<AutomationClip*>* clips, bool canAddNewTrack)
{
AutomationTrack* output = nullptr;
if (clips->size() > 0)
{
// selecting the track with the most amount of clips
// connected to this model
AutomationTrack* maxTrack = dynamic_cast<AutomationTrack*>((*clips)[0]->getTrack());
int maxTrackCount = 1;
for (size_t i = 1; i < clips->size(); i++)
{
int currentCount = 0;
for (AutomationClip* j : (*clips))
{
if ((*clips)[i]->getTrack() == j->getTrack())
{
currentCount++;
}
}
if (maxTrackCount < currentCount)
{
maxTrackCount = currentCount;
maxTrack = dynamic_cast<AutomationTrack*>((*clips)[i]->getTrack());;
}
}
output = maxTrack;
}
else if (canAddNewTrack)
{
// adding new track
output = new AutomationTrack(getGUI()->songEditor()->m_editor->model(), false);
}
return output;
}

AutomationClip* AutomatableModelViewSlots::getCurrentAutomationClip(AutomationTrack* track, bool canAddNewClip, bool searchAfter)
{
AutomationClip* output = nullptr;
const std::vector<Clip*>& trackClips = track->getClips();
TimePos timePos = static_cast<TimePos>(Engine::getSong()->getPlayPos());

bool tryAdding = false;
if (trackClips.size() > 0)
{
Rossmaxx marked this conversation as resolved.
Show resolved Hide resolved
// getting the closest clip that start before or after the global time position
tick_t closestTime = -1;
Clip* closestClip = nullptr;
for (Clip* currentClip : trackClips)
{
tick_t currentTime = currentClip->startPosition().getTicks();
bool smallerCheck = currentTime > closestTime || closestTime < 0;
bool biggerCheck = currentTime < closestTime || closestTime < 0;
bool searchBeforeCheck = !searchAfter && smallerCheck && timePos.getTicks() > currentTime;
bool searchAfterCheck = searchAfter && biggerCheck && timePos.getTicks() <= currentTime;

if (searchBeforeCheck || searchAfterCheck)
{
closestTime = currentTime;
closestClip = currentClip;
}
}

// in some cases there could be no clips before or after the global time position
// if this is the case, try adding a new one
// (if this fails, return nullptr)
if (!closestClip)
{
tryAdding = true;
}
else
{
output = dynamic_cast<AutomationClip*>(closestClip);
}
}
else
{
tryAdding = true;
}
if (tryAdding && canAddNewClip)
{
// adding a new clip
output = makeNewClip(track, timePos, true);
}
return output;
}

const TimePos AutomatableModelViewSlots::getNearestAutomationNode(AutomationTrack* track, AutomationClip** clipOut)
{
TimePos output;
AutomationClip* minClip = nullptr;
int minDistance = -1;

TimePos timePos = static_cast<TimePos>(Engine::getSong()->getPlayPos());
// getting the clips before and after the global time position
AutomationClip* clipBefore = getCurrentAutomationClip(track, false, false);
AutomationClip* clipAfter = getCurrentAutomationClip(track, false, true);

if (clipBefore && clipBefore->hasAutomation())
{
// getting nearest node
// in the clip that starts before this
for (auto it = clipBefore->getTimeMap().begin(); it != clipBefore->getTimeMap().end(); ++it)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Range based loop here.

Copy link
Contributor Author

@szeli1 szeli1 Jun 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop can not be replaced, because it would be a pointer to a typename Tnode instead of a key.
Qt provides a key() function that finds a key for a given T, but that is not optimized.

Qt documentation:

This function can be slow linear time, because QMap's internal data structure is optimized for fast lookup by key, not by value.

{
int curDistance = std::abs(static_cast<int>(POS(it) + clipBefore->startPosition().getTicks()) - static_cast<int>(timePos.getTicks()));
if (curDistance < minDistance || minDistance < 0)
{
minDistance = curDistance;
output = TimePos(POS(it));
minClip = clipBefore;
}
}
}
if (clipAfter && clipAfter->hasAutomation())
{
// getting the nearest node
// in the clip that starts after this
int curDistance = static_cast<int>(POS(clipAfter->getTimeMap().begin()) + clipAfter->startPosition().getTicks()) - static_cast<int>(timePos.getTicks());
if (curDistance < minDistance || minDistance < 0)
{
minDistance = curDistance;
output = TimePos(POS(clipAfter->getTimeMap().begin()));
minClip = clipAfter;
}
}

*clipOut = minClip;

return output;
}

AutomationClip* AutomatableModelViewSlots::makeNewClip(AutomationTrack* track, TimePos position, bool canSnap)
{
if (canSnap)
{
// snapping to the bar before
position.setTicks(position.getTicks() - position.getTickWithinBar(TimeSig(Engine::getSong()->getTimeSigModel())));
}
AutomationClip* output = dynamic_cast<AutomationClip*>(track->createClip(position));
// connect to model
output->addObject(m_amv->modelUntyped(), true);
return output;
}

void AutomatableModelViewSlots::editSongGlobalAutomation()
{
Expand Down
Loading