1669 lines
45 KiB
C++
Executable File
1669 lines
45 KiB
C++
Executable File
#include "track_content_widget.h"
|
|
#include "track_entry.h"
|
|
#include "tools.h"
|
|
#include "track_entry_painters.h"
|
|
#include "track_widget.h"
|
|
#include "script_timeline.h"
|
|
#include "filter_background_widget.h"
|
|
#include "track_drag_handler.h"
|
|
#include "track_select_handler.h"
|
|
#include "track_resize_handler.h"
|
|
#include "temporary_pointer_vector.h"
|
|
|
|
#include <LtsWidgets/lts_message_dialog.h>
|
|
|
|
#include <QPainter>
|
|
#include <QBrush>
|
|
#include <QColor>
|
|
#include <QByteArray>
|
|
#include <QDataStream>
|
|
#include <QKeyEvent>
|
|
#include <QJsonDocument>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonParseError>
|
|
#include <QJsonValue>
|
|
#include <QGuiApplication>
|
|
#include <QApplication>
|
|
|
|
#include <cassert>
|
|
|
|
// Todo: header
|
|
#include <QtCore/qglobal.h>
|
|
|
|
|
|
namespace LtsScriptTimeline
|
|
{
|
|
|
|
QDataStream &operator<<(QDataStream &ds, const TrackEntry &te)
|
|
{
|
|
const QByteArray jsonContent = QJsonDocument(te.m_detailData).toBinaryData();
|
|
ds << te.m_topicId
|
|
<< te.m_startTime
|
|
<< te.m_length
|
|
<< static_cast<quint8>(te.m_type)
|
|
<< te.isSet
|
|
<< te.index
|
|
<< jsonContent
|
|
<< te.x << te.y << te.h << te.w
|
|
<< te.m_channelName
|
|
<< te.m_description;
|
|
return ds;
|
|
}
|
|
|
|
|
|
QDataStream &operator>>(QDataStream &ds, TrackEntry &te)
|
|
{
|
|
QByteArray jsonContent;
|
|
quint8 type;
|
|
ds >> te.m_topicId
|
|
>> te.m_startTime
|
|
>> te.m_length
|
|
>> type // (quint8&)te.m_type
|
|
>> te.isSet
|
|
>> te.index
|
|
>> jsonContent
|
|
>> te.x >> te.y >> te.h >> te.w
|
|
>> te.m_channelName
|
|
>> te.m_description;
|
|
|
|
te.m_type = static_cast<TrackEntryType>(type);
|
|
te.m_detailData = QJsonDocument::fromBinaryData(jsonContent).object();
|
|
return ds;
|
|
}
|
|
|
|
|
|
bool isCollision(int start_s, int end_s, int toCompare_start_s, int toCompare_end_s)
|
|
{
|
|
// Check 2 collision possibilites:
|
|
|
|
//qWarning() << "start" << start_s << "end" << end_s << "start2" << toCompare_start_s << "end2" << toCompare_end_s;
|
|
|
|
// < end, > start
|
|
// > start, < ende
|
|
|
|
// if (toCompare_start_s < end_s
|
|
// && toCompare_end_s > start_s)
|
|
// {
|
|
// return true;
|
|
// }
|
|
|
|
// if (toCompare_start_s > start_s
|
|
// && toCompare_end_s < end_s)
|
|
// {
|
|
// return true;
|
|
// }
|
|
|
|
// 1. Start time equal or bigger than current TrackEntry time...
|
|
if (start_s >= toCompare_start_s &&
|
|
// ... AND start time is smaller than current TrackEntry end time? -> Collision
|
|
start_s < toCompare_end_s)
|
|
{
|
|
//qWarning() << "COL1";
|
|
return true;
|
|
}
|
|
|
|
// 2. Start time smaller than current TrackEntry time...
|
|
if (start_s < toCompare_start_s &&
|
|
// ... AND endtime is bigger than the current TrackEntry start time? -> Collision
|
|
end_s > toCompare_start_s)
|
|
{
|
|
//qWarning() << "COL2";
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool isCollision2(int startPreview_s, int endPreview_s, const TrackEntry *pTrackEntry)
|
|
{
|
|
return isCollision(startPreview_s, endPreview_s, pTrackEntry->m_startTime, pTrackEntry->m_startTime + pTrackEntry->m_length);
|
|
}
|
|
|
|
|
|
/*
|
|
* Sort preview entries chronologically from left to right. This point is very important
|
|
* for all collision calculations if several topics have been selected!
|
|
*/
|
|
void sortTrackEntriesChronologically(QVector<TrackEntry *> &vecTrackEntries)
|
|
{
|
|
std::sort(vecTrackEntries.begin(), vecTrackEntries.end(),
|
|
[](const TrackEntry *a, const TrackEntry *b) -> bool
|
|
{
|
|
return a->m_startTime < b->m_startTime;
|
|
});
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::checkForCollisionsSimple(const TrackEntry &previewEntry) const
|
|
{
|
|
// Verify all remaining TrackEntries against Preview TrackEntry
|
|
const int startPreview = previewEntry.m_startTime;
|
|
if (startPreview < 0)
|
|
return false;
|
|
const int endPreview = startPreview + previewEntry.m_length;
|
|
|
|
const QVector<TrackEntry *> trackEntries = getTrackEntries();
|
|
for (const TrackEntry *checkTe : trackEntries)
|
|
{
|
|
if (checkTe->m_topicId == previewEntry.m_topicId)
|
|
continue; // don't check collision on itself :)
|
|
|
|
if (LtsScriptTimeline::isCollision2(startPreview, endPreview, checkTe))
|
|
return false; // fail on any collision
|
|
}
|
|
|
|
return true; // not collision found until here => no collision
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::checkForCollisions(const TrackContentWidget *tcw, const QVector<TrackEntry *> &preview, int dropTime_s, QVector<TrackEntry *> &vecCollisions, bool ignoreSelected, bool ignoreSelfCheck)
|
|
{
|
|
QVector<TrackEntry *> _preview = preview;
|
|
sortTrackEntriesChronologically(_preview);
|
|
|
|
vecCollisions.clear();
|
|
|
|
QVector<TrackEntry *> vecEntriesToCheck;
|
|
const QVector<TrackEntry *> &trackEntries = tcw->getTrackEntries();
|
|
|
|
if (ignoreSelected)
|
|
{
|
|
// Exclude all selected TrackEntries (=preview TrackEntries) from collision detection
|
|
const QVector<TrackEntry *> &selectedEntries = tcw->getSelectedEntries();
|
|
for (TrackEntry *te : qAsConst(trackEntries))
|
|
{
|
|
if (!selectedEntries.contains(te))
|
|
vecEntriesToCheck.append(te);
|
|
}
|
|
}
|
|
else
|
|
vecEntriesToCheck = trackEntries;
|
|
|
|
|
|
auto leftMostI = std::min_element(_preview.constBegin(), _preview.constEnd(), [](const TrackEntry * a, const TrackEntry * b)
|
|
{
|
|
return a->m_startTime < b->m_startTime;
|
|
});
|
|
|
|
if (leftMostI == _preview.constEnd())
|
|
return false;
|
|
|
|
int startTimeMostLeft_s = (*leftMostI)->m_startTime;
|
|
int startOffset_s = dropTime_s - startTimeMostLeft_s;
|
|
|
|
// Verify all remaining TrackEntries against Preview TrackEntries
|
|
for (TrackEntry *previewEntry : qAsConst(_preview))
|
|
{
|
|
int startPreview_s = previewEntry->m_startTime + startOffset_s;
|
|
|
|
if (startPreview_s < 0)
|
|
{
|
|
vecCollisions.append(previewEntry);
|
|
continue;
|
|
}
|
|
|
|
int endPreview_s = startPreview_s + previewEntry->m_length;
|
|
|
|
for (const TrackEntry *checkTe : qAsConst(vecEntriesToCheck))
|
|
{
|
|
if (checkTe->m_topicId == previewEntry->m_topicId && !ignoreSelfCheck)
|
|
{
|
|
continue; // don't check collision on itself :)
|
|
}
|
|
|
|
if (LtsScriptTimeline::isCollision2(startPreview_s, endPreview_s, checkTe))
|
|
{
|
|
vecCollisions.append(previewEntry);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return !vecCollisions.isEmpty();
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::checkForCollisions(const TrackContentWidget *pTCW, const QVector<TrackEntry *> &vecPreviewTrackEntries, int dropTime_s)
|
|
{
|
|
QVector<TrackEntry *> dummy_vecCollidingTopics;
|
|
return TrackContentWidget::checkForCollisions(pTCW, vecPreviewTrackEntries, dropTime_s, dummy_vecCollidingTopics, true);
|
|
}
|
|
|
|
|
|
QString TrackContentWidget::parallelError()
|
|
{
|
|
return tr("At least one topic of type Situation, Building, Audio or Video has been selected\nwhich can not be played parallel on both screens. Placement not possible.");
|
|
}
|
|
|
|
|
|
// Assumes that 'vecPreviewTrackEntries' is sorted chronologically from left to right
|
|
bool TrackContentWidget::checkForParallelTopics(const TrackContentWidget *tcw, const QVector<TrackEntry *> &preview, int dropTime_s, QVector<TrackEntry *> &vecParallelTopics, bool ignoreSelectedTopics, bool hack)
|
|
{
|
|
// check for topic type x parallel to type topic type x
|
|
{
|
|
QVector<TrackEntryType> vecParallelForbiddenFilter = { TrackEntryType::Building, TrackEntryType::Audio, TrackEntryType::Video, TrackEntryType::SituationMap, TrackEntryType::Situation };
|
|
QVector<TrackEntry *> previewCopy(preview);
|
|
sortTrackEntriesChronologically(previewCopy);
|
|
QVector<TrackEntry *> vecTrackEntries(tcw->getTrackEntries());
|
|
|
|
// Remove all non-relevant PreviewTrackEntries
|
|
auto itE = std::remove_if(previewCopy.begin(), previewCopy.end(),
|
|
[vecParallelForbiddenFilter](TrackEntry *pTE)
|
|
{
|
|
return !vecParallelForbiddenFilter.contains(pTE->m_type);
|
|
});
|
|
previewCopy.erase(itE, previewCopy.end());
|
|
|
|
|
|
if (ignoreSelectedTopics)
|
|
{
|
|
// remove all selected topics
|
|
itE = std::remove_if(previewCopy.begin(), previewCopy.end(),
|
|
[tcw](TrackEntry *pTE)
|
|
{
|
|
return tcw->getSelectedEntries().contains(pTE);
|
|
});
|
|
previewCopy.erase(itE, previewCopy.end());
|
|
}
|
|
|
|
|
|
// Remove all non-relevant TrackEntries
|
|
itE = std::remove_if(vecTrackEntries.begin(), vecTrackEntries.end(),
|
|
[vecParallelForbiddenFilter](TrackEntry *pTE)
|
|
{
|
|
return !vecParallelForbiddenFilter.contains(pTE->m_type);
|
|
});
|
|
vecTrackEntries.erase(itE, vecTrackEntries.end());
|
|
|
|
|
|
// Exclude all selected TrackEntries (=preview TrackEntries) from collision detection
|
|
itE = std::remove_if(vecTrackEntries.begin(), vecTrackEntries.end(),
|
|
[tcw](TrackEntry *pTE)
|
|
{
|
|
return tcw->getSelectedEntries().contains(pTE);
|
|
});
|
|
vecTrackEntries.erase(itE, vecTrackEntries.end());
|
|
|
|
|
|
// Releveant topics left to verify against each other?
|
|
if (!previewCopy.isEmpty() && !vecTrackEntries.isEmpty())
|
|
{
|
|
auto leftMostI = std::min_element(previewCopy.constBegin(), previewCopy.constEnd(), [](const TrackEntry *a, const TrackEntry *b)
|
|
{
|
|
return a->m_startTime < b->m_startTime;
|
|
});
|
|
|
|
if (leftMostI != previewCopy.constEnd())
|
|
{
|
|
int startTimeMostLeft_s = (*leftMostI)->m_startTime;
|
|
//qWarning() << "startTimeMostLeft_s" << startTimeMostLeft_s;
|
|
|
|
|
|
// Verify all remaining TrackEntries against Preview TrackEntries
|
|
for (TrackEntry *previewEntry : qAsConst(previewCopy))
|
|
{
|
|
//qWarning() << "#1 checking topic #" << previewEntry->m_topicId;
|
|
|
|
int startOffset_s = previewEntry->m_startTime;
|
|
|
|
if (!hack)
|
|
startOffset_s -= startTimeMostLeft_s;
|
|
|
|
//qWarning() << "old start time" << previewEntry->m_startTime << "new start time" << startOffset_s;
|
|
|
|
for (TrackEntry *pTrackEntry : qAsConst(vecTrackEntries))
|
|
{
|
|
//qWarning() << "#1 ... against topic #" << pTrackEntry->m_topicId;
|
|
// Only parallel topics of same type are forbidden
|
|
if (previewEntry->m_type != pTrackEntry->m_type)
|
|
{
|
|
//qWarning() << "#1 ... but type is different";
|
|
continue;
|
|
}
|
|
|
|
// Exclude currently selected topics from collision detection
|
|
if (previewEntry->m_topicId == pTrackEntry->m_topicId)
|
|
{
|
|
//qWarning() << "#1 ... but id is same";
|
|
continue;
|
|
}
|
|
|
|
int startPreview_s = startOffset_s;
|
|
|
|
if (!hack)
|
|
startPreview_s += dropTime_s;
|
|
|
|
int endPreview_s = startPreview_s + previewEntry->m_length;
|
|
|
|
if (LtsScriptTimeline::isCollision2(startPreview_s, endPreview_s, pTrackEntry))
|
|
{
|
|
//qWarning() << "#1 ... collides!";
|
|
vecParallelTopics.append(previewEntry);
|
|
break;
|
|
}
|
|
|
|
//qWarning() << "#1 ... is okay";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//qWarning() << "#1 ... no topics to check against";
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check for topic type x parallel to type y
|
|
{
|
|
// check for different topic types that cannot be parallel to each other
|
|
QVector<TrackEntry *> previewCopy(preview);
|
|
sortTrackEntriesChronologically(previewCopy);
|
|
QVector<QPair<TrackEntryType, TrackEntryType> > differentParallelForbidden =
|
|
{
|
|
{ TrackEntryType::Situation, TrackEntryType::Snapshot /* Situation and Snapshot contain a replay */ },
|
|
{ TrackEntryType::Situation, TrackEntryType::SituationReplay /* Situation contains a replay */ },
|
|
{ TrackEntryType::Situation, TrackEntryType::SituationMap /* Situation contains a map */ },
|
|
};
|
|
QVector<TrackEntry *> vecTrackEntries(tcw->getTrackEntries());
|
|
|
|
|
|
int step = 1;
|
|
auto findUnusedType = [differentParallelForbidden, &step](const TrackEntry *pTE)
|
|
{
|
|
auto x = std::find_if(differentParallelForbidden.constBegin(), differentParallelForbidden.constEnd(), [pTE, step](const QPair<TrackEntryType, TrackEntryType> &pair)
|
|
{
|
|
bool x = (pair.first == pTE->m_type || pair.second == pTE->m_type);
|
|
if (x)
|
|
qDebug() << ">>"<<step<<".1 type" << (int)pTE->m_type << "in type list - not removing";
|
|
return x;
|
|
});
|
|
|
|
if (x == differentParallelForbidden.constEnd())
|
|
qDebug() << ">>"<<step<<".2 type" << (int)pTE->m_type << "not found in type list - removing!";
|
|
|
|
return (x == differentParallelForbidden.constEnd());
|
|
};
|
|
|
|
|
|
// Remove all non-relevant PreviewTrackEntries
|
|
auto itE = std::remove_if(previewCopy.begin(), previewCopy.end(), findUnusedType);
|
|
previewCopy.erase(itE, previewCopy.end());
|
|
|
|
|
|
step = 2;
|
|
// Remove all non-relevant TrackEntries
|
|
itE = std::remove_if(vecTrackEntries.begin(), vecTrackEntries.end(), findUnusedType);
|
|
vecTrackEntries.erase(itE, vecTrackEntries.end());
|
|
|
|
|
|
if (ignoreSelectedTopics)
|
|
{
|
|
// remove all selected topics
|
|
itE = std::remove_if(previewCopy.begin(), previewCopy.end(),
|
|
[tcw](TrackEntry *pTE)
|
|
{
|
|
return tcw->getSelectedEntries().contains(pTE);
|
|
});
|
|
previewCopy.erase(itE, previewCopy.end());
|
|
}
|
|
|
|
|
|
// Exclude all selected TrackEntries (=preview TrackEntries) from collision detection
|
|
itE = std::remove_if(vecTrackEntries.begin(), vecTrackEntries.end(),
|
|
[tcw](TrackEntry *pTE)
|
|
{
|
|
bool x = tcw->getSelectedEntries().contains(pTE);
|
|
//qWarning() << ">> 3 removing te #" << pTE->m_topicId << "of type" << (int)pTE->m_type;
|
|
return x;
|
|
});
|
|
vecTrackEntries.erase(itE, vecTrackEntries.end());
|
|
|
|
|
|
// Releveant topics left to verify against each other?
|
|
if (!previewCopy.size() || !vecTrackEntries.size())
|
|
{
|
|
//qWarning() << "nothing left for check #2";
|
|
return !vecParallelTopics.isEmpty();
|
|
}
|
|
|
|
|
|
auto leftMostI = std::min_element(previewCopy.constBegin(), previewCopy.constEnd(), [](const TrackEntry * a, const TrackEntry * b)
|
|
{
|
|
return a->m_startTime < b->m_startTime;
|
|
});
|
|
|
|
if (leftMostI == previewCopy.constEnd())
|
|
return !vecParallelTopics.isEmpty();
|
|
|
|
int startTimeMostLeft_s = (*leftMostI)->m_startTime;
|
|
//qWarning() << "startTimeMostLeft_s" << startTimeMostLeft_s;
|
|
|
|
|
|
QVector<TrackEntryType> correspondingDifferentTopicTypes;
|
|
// Verify all remaining TrackEntries against Preview TrackEntries
|
|
for (TrackEntry *previewEntry : qAsConst(previewCopy))
|
|
{
|
|
correspondingDifferentTopicTypes.clear();
|
|
//qWarning() << "#2 checking topic #" << previewEntry->m_topicId << "with type" << (int)previewEntry->m_type;
|
|
|
|
int startOffset_s = previewEntry->m_startTime;
|
|
|
|
if (!hack)
|
|
startOffset_s -= startTimeMostLeft_s;
|
|
|
|
|
|
for (const QPair<TrackEntryType, TrackEntryType> &pair : qAsConst(differentParallelForbidden))
|
|
{
|
|
if (pair.first == previewEntry->m_type)
|
|
correspondingDifferentTopicTypes += pair.second;
|
|
|
|
if (pair.second == previewEntry->m_type)
|
|
correspondingDifferentTopicTypes += pair.first;
|
|
}
|
|
|
|
Q_ASSERT(!correspondingDifferentTopicTypes.isEmpty());
|
|
|
|
if (correspondingDifferentTopicTypes.isEmpty())
|
|
continue;
|
|
|
|
for (const TrackEntryType correspondingDifferentTopicType : qAsConst(correspondingDifferentTopicTypes))
|
|
{
|
|
//qWarning() << "old start time" << previewEntry->m_startTime << "new start time" << startOffset_s;
|
|
|
|
for (TrackEntry *pTrackEntry : qAsConst(vecTrackEntries))
|
|
{
|
|
//qWarning() << "#2 ... against topic #" << pTrackEntry->m_topicId << "with type" << (int)pTrackEntry->m_type;
|
|
|
|
if (pTrackEntry->m_type != correspondingDifferentTopicType)
|
|
{
|
|
//qWarning() << "#2 ... but is not of corresponding type";
|
|
continue;
|
|
}
|
|
|
|
// Exclude currently selected topics from collision detection
|
|
if (previewEntry->m_topicId == pTrackEntry->m_topicId)
|
|
{
|
|
//qWarning() << "#2 ... but id is same";
|
|
continue;
|
|
}
|
|
|
|
int startPreview_s = startOffset_s;
|
|
|
|
if (!hack)
|
|
startPreview_s += dropTime_s;
|
|
|
|
int endPreview_s = startPreview_s + previewEntry->m_length;
|
|
|
|
if (LtsScriptTimeline::isCollision2(startPreview_s, endPreview_s, pTrackEntry))
|
|
{
|
|
//qWarning() << "#2 ... collides!";
|
|
vecParallelTopics.append(previewEntry);
|
|
break;
|
|
}
|
|
|
|
//qWarning() << "#2 ... is okay";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return !vecParallelTopics.isEmpty();
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::checkForParallelTopics(const TrackContentWidget *pTCW, const QVector<TrackEntry *> &vecPreviewTrackEntries, int dropTime_s, bool hack)
|
|
{
|
|
QVector<TrackEntry *> dummy_vecParallelTopics;
|
|
return checkForParallelTopics(pTCW, vecPreviewTrackEntries, dropTime_s, dummy_vecParallelTopics, false, hack);
|
|
}
|
|
|
|
|
|
QString TrackContentWidget::mime = QStringLiteral("application/rheinmetall.script.topic");
|
|
|
|
|
|
TrackContentWidget::TrackContentWidget(QWidget *parent, bool isFilter, Tools *tools, int mainTopicId)
|
|
: QWidget(parent)
|
|
, m_selectHandler(new SelectHandler(this))
|
|
, m_resizeHandler(new ResizeHandler(this))
|
|
, m_dragHandler(new DragHandler(this))
|
|
, m_dragOffsetTime(-1)
|
|
, m_dragPos(-1)
|
|
, m_mainSelected(nullptr)
|
|
, m_width(0)
|
|
, m_cursorSet(false)
|
|
, m_oldEntryLength(-1)
|
|
, m_unselectAllImmune(false)
|
|
, m_indicatorTime(-1)
|
|
, m_isFilter(isFilter)
|
|
, m_readonly(false)
|
|
, m_trackWidget(nullptr)
|
|
, m_scriptTimeline(nullptr)
|
|
, m_tools(tools)
|
|
, m_isMultiLineMultiSelectionMode(false)
|
|
, m_multiLineMultiSelectionOkay(false)
|
|
, m_dragPossible(false) // mlms mode
|
|
, m_moveValid(false) // non-mlms mode
|
|
, m_mainTopicId(mainTopicId)
|
|
{
|
|
setAcceptDrops(true);
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
setMouseTracking(true);
|
|
|
|
addFilterType(TrackEntryType::TacticalPlan);
|
|
addFilterType(TrackEntryType::SituationMapFilter);
|
|
|
|
// topics that can be used as main- or as filter topic - always allowed
|
|
if (m_isFilter)
|
|
{
|
|
addAllowedType(TrackEntryType::Text);
|
|
addAllowedType(TrackEntryType::Pdf);
|
|
addAllowedType(TrackEntryType::Image);
|
|
}
|
|
}
|
|
|
|
|
|
TrackContentWidget::~TrackContentWidget()
|
|
{
|
|
delete m_selectHandler;
|
|
delete m_resizeHandler;
|
|
delete m_dragHandler;
|
|
|
|
QVector<TrackEntry *> entryList = m_entryList; // simply copy list of ptrs, as the original list is modified on delete
|
|
|
|
// these signals are dangerous while deleting - so disconnect them first
|
|
for (TrackEntry *te : entryList)
|
|
{
|
|
disconnect(te, &TrackEntry::destroyed,
|
|
this, nullptr);
|
|
}
|
|
|
|
qDeleteAll(entryList);
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::setDescriptionForSelected()
|
|
{
|
|
if (getSelectedEntries().count() > 1) // cannot edit more than 1 description at a time
|
|
{
|
|
QApplication::beep();
|
|
return false;
|
|
}
|
|
|
|
m_selectHandler->slotSetDescription();
|
|
return true;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::addCorrespondingTrackWidget(TrackContentWidget *pCorrespondingTrackWidget)
|
|
{
|
|
m_tcwCorresponding += pCorrespondingTrackWidget;
|
|
m_dragHandler->m_tcwCorresponding += pCorrespondingTrackWidget;
|
|
m_resizeHandler->m_tcwCorresponding += pCorrespondingTrackWidget;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::setTools(Tools *tools)
|
|
{
|
|
m_tools = tools;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::setTrackWidget(TrackWidget *trackWidget)
|
|
{
|
|
m_trackWidget = trackWidget;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::setReadonly(bool readonly)
|
|
{
|
|
m_readonly = readonly;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::addFilterType(const TrackEntryType &tet)
|
|
{
|
|
if (m_isFilter)
|
|
addAllowedType(tet);
|
|
else
|
|
addDisallowedType(tet);
|
|
}
|
|
|
|
|
|
void TrackContentWidget::setIndicatorTime(int time)
|
|
{
|
|
m_indicatorTime = time;
|
|
update();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::mouseMoveEvent(QMouseEvent *event) // give the user the chance to move his trach entry
|
|
{
|
|
if (!m_resizeHandler->mouseMoveEvent(event)) // prevent the drag handler to drag when the user is performing a resize
|
|
m_dragHandler->mouseMoveEvent(event);
|
|
|
|
emit signalModifyRubberband();
|
|
}
|
|
|
|
|
|
TrackEntry *TrackContentWidget::getTrackEntryAt(int clickedTime)
|
|
{
|
|
for (auto i = m_entryList.begin(); i != m_entryList.end(); ++i)
|
|
{
|
|
TrackEntry *te = *i;
|
|
|
|
if (te->m_startTime <= clickedTime
|
|
&& (te->m_startTime + te->m_length) >= clickedTime) // FIXME: unpraezise ! warum ?
|
|
return te;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
m_selectHandler->mousePressEvent(event);
|
|
m_resizeHandler->mousePressEvent(event);
|
|
m_dragHandler->mousePressEvent(event);
|
|
|
|
if (!m_selected.isEmpty()) // no selection, no resize & drag - open rubberband
|
|
return;
|
|
|
|
if (event->buttons() != Qt::LeftButton)
|
|
return;
|
|
emit signalStartRubberband(event->pos());
|
|
}
|
|
|
|
|
|
void TrackContentWidget::resizeEvent(QResizeEvent *event)
|
|
{
|
|
QWidget::resizeEvent(event);
|
|
m_width = event->size().width();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
if (!m_resizeHandler->m_resizing) // select handler only active if there is currently no resizing!
|
|
m_selectHandler->mouseReleaseEvent(event);
|
|
|
|
m_resizeHandler->mouseReleaseEvent(event);
|
|
|
|
emit signalStopRubberband();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::paintEvent(QPaintEvent *event)
|
|
{
|
|
Q_UNUSED(event);
|
|
|
|
QPainter painter(this);
|
|
|
|
painter.save();
|
|
|
|
|
|
// draw background
|
|
if (m_isFilter) // no need to draw bg white if it's already white, so only do it on filters (as otherwise the bg would be gray)
|
|
{
|
|
painter.setBrush(QGuiApplication::palette().window().color());
|
|
painter.drawRect(rect());
|
|
}
|
|
|
|
|
|
// draw base
|
|
painter.setPen(QColor(36, 36, 36));
|
|
painter.drawLine(QPoint(0, 0), QPoint(0, height()));
|
|
|
|
if (m_isFilter) // draw right line if this is for filters ;)
|
|
painter.drawLine(QPoint(width() - 1, 0), QPoint(width() - 1, height()));
|
|
|
|
|
|
// clip entries
|
|
painter.setClipping(true);
|
|
painter.setClipRect(1, 1, width() - 1, height() - 1);
|
|
|
|
|
|
// draw entries
|
|
for (auto i = m_entryList.begin(); i != m_entryList.end(); ++i)
|
|
{
|
|
TrackEntry *te = *i;
|
|
|
|
int startPx = m_tools->getPos(te->m_startTime, m_isFilter);
|
|
int endPx = m_tools->getPos(te->m_startTime + te->m_length, m_isFilter);
|
|
|
|
if (endPx < 0 || startPx > width()) // don't draw what's not visible :)
|
|
continue;
|
|
|
|
int heightPx = height() - 2;
|
|
int lengthPx = endPx - startPx;
|
|
//QRect trackRect(startPx, 1, lengthPx, heightPx);
|
|
|
|
painter.save();
|
|
painter.translate(startPx, 1); // set null-pt for drawTrackEntry
|
|
|
|
bool isSelected = m_selected.contains(te);
|
|
QRect trackRect(0, 0, lengthPx, heightPx);
|
|
drawTrackEntry(te, painter, trackRect, isSelected);
|
|
|
|
if (isSelected)
|
|
{
|
|
// Draw area where resizing is enabled
|
|
/*
|
|
int dragLinePos_px = endPx - m_tools->getPos(te->time + te->length - 4, m_isFilter);
|
|
painter.drawLine(trackRect.width() - dragLinePos_px, trackRect.y(),
|
|
trackRect.width() - dragLinePos_px, trackRect.y() + trackRect.height());
|
|
*/
|
|
}
|
|
|
|
painter.restore();
|
|
}
|
|
|
|
|
|
// draw drags (when dragging ;)
|
|
if (m_dragPos > -1 && !m_preview.isEmpty()) // is te
|
|
{
|
|
int firstTeTime = -1;
|
|
|
|
for (const TrackEntry *te : qAsConst(m_preview))
|
|
{
|
|
if (firstTeTime > te->m_startTime || firstTeTime == -1)
|
|
firstTeTime = te->m_startTime;
|
|
}
|
|
|
|
int drop = m_tools->getTime(m_dragPos, m_isFilter);
|
|
int finalDrop = drop - m_dragOffsetTime;
|
|
int diff = finalDrop - firstTeTime;
|
|
int heightPx = height() - 2;
|
|
|
|
for (auto i = m_preview.constBegin(); i != m_preview.constEnd(); ++i)
|
|
{
|
|
const TrackEntry *te = *i;
|
|
|
|
if (m_dragHandler->m_vecCollisions.contains(*i) || m_dragHandler->m_vecParallel.contains(*i) || !isDragPossible() || !m_moveValid)
|
|
painter.setBrush(QColor("red"));
|
|
else
|
|
painter.setBrush(QColor("lightgreen"));
|
|
|
|
int time = te->m_startTime + diff;
|
|
|
|
int startPx = m_tools->getPos(time, m_isFilter);
|
|
int endPx = m_tools->getPos(time + te->m_length, m_isFilter);
|
|
|
|
if (endPx < 0 || startPx > width()) // don't draw what's not visible :)
|
|
continue;
|
|
|
|
int lengthPx = endPx - startPx;
|
|
painter.drawRect(startPx, 1, lengthPx, heightPx);
|
|
}
|
|
}
|
|
|
|
|
|
// draw position pointer (if available)
|
|
if (m_indicatorTime > -1)
|
|
{
|
|
int pos = m_tools->getPos(m_indicatorTime, m_isFilter);
|
|
|
|
if (pos > 0 && pos <= m_tools->m_paintWidth)
|
|
{
|
|
painter.setPen(QColor("cyan"));
|
|
painter.drawLine(pos, 0, pos, height());
|
|
}
|
|
}
|
|
|
|
|
|
painter.restore();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::dragEnterEvent(QDragEnterEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
m_dragHandler->dragEnterEvent(event);
|
|
}
|
|
|
|
|
|
void TrackContentWidget::dragLeave()
|
|
{
|
|
|
|
}
|
|
|
|
|
|
void TrackContentWidget::dragLeaveEvent(QDragLeaveEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
m_dragHandler->dragLeaveEvent(event);
|
|
}
|
|
|
|
|
|
void TrackContentWidget::unselect()
|
|
{
|
|
m_unselectAllImmune = true; // so that we know the call came from US!
|
|
emit signalUnselectAll(); // serial call!
|
|
m_unselectAllImmune = false; // done, set back
|
|
}
|
|
|
|
|
|
void TrackContentWidget::forceLocalUnselect()
|
|
{
|
|
m_mainSelected = nullptr;
|
|
m_selected.clear();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::selectAll()
|
|
{
|
|
m_selected.clear(); // prevent doubles
|
|
|
|
for (TrackEntry *te : qAsConst(m_entryList))
|
|
{
|
|
m_selected.append(te);
|
|
m_mainSelected = te;
|
|
}
|
|
|
|
emit signalTrackEntrySelectionChanged(nullptr);
|
|
update();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::unselectAll()
|
|
{
|
|
if (!m_unselectAllImmune) // this was NOT called by us: unselect anytime, even if ctrl is pressed, because the CTRL is NOT for us! BUT DO NOT send unselect-signal, as the caller did this already, because his sender() was himself! u know?
|
|
forceLocalUnselect();
|
|
else // if this WAS called by us AND ctrl was active: unselect!
|
|
{
|
|
if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier))
|
|
{
|
|
forceLocalUnselect();
|
|
emit signalTrackEntrySelectionChanged(nullptr); // only send selectionChanged once!
|
|
}
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
|
|
TrackEntry *TrackContentWidget::getParentTopicOfFilter(TrackEntry *filterTe) const
|
|
{
|
|
// check if inserted filter topic is longer than this tcw - if so, set max len to end of tcw length
|
|
if (m_isFilter && m_trackWidget != nullptr)
|
|
{
|
|
// find TrackWidget for this filter line
|
|
int mainTopicId = m_trackWidget->getParentTopicIdOfFilter(filterTe);
|
|
TrackContentWidget *mainTcw = m_trackWidget->getTrackContentWidget();
|
|
const QVector<TrackEntry *> &teList = mainTcw->getTrackEntries();
|
|
|
|
for (auto i = teList.constBegin(); i != teList.constEnd(); ++i)
|
|
{
|
|
TrackEntry *mainTe = *i;
|
|
|
|
if (mainTe->m_topicId != mainTopicId)
|
|
continue;
|
|
|
|
return mainTe;
|
|
}
|
|
}
|
|
|
|
return nullptr; // not found
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::addTrackEntry(TrackEntry *te, bool selectTrackEntry)
|
|
{
|
|
if (!allowedByType(te)) // only allow to add this TrackEntry if it's not blacklisted (or if it's whitelisted)
|
|
{
|
|
qWarning() << "TrackContentWidget::addTrackEntry: topic #" << te->m_topicId << "type" << (int)te->m_type << "not allowed";
|
|
return false;
|
|
}
|
|
|
|
connect(te, &TrackEntry::destroyed,
|
|
this, [this]()
|
|
{
|
|
TrackEntry *o = static_cast<TrackEntry *>(sender());
|
|
m_entryList.removeAll(o);
|
|
m_selected.removeAll(o);
|
|
|
|
if (m_mainSelected == o)
|
|
{
|
|
m_mainSelected = nullptr;
|
|
emit signalTrackEntrySelectionChanged(nullptr);
|
|
}
|
|
});
|
|
|
|
te->m_channelName = m_trackName;
|
|
m_entryList.append(te);
|
|
|
|
|
|
TrackEntry *mainTe = getParentTopicOfFilter(te);
|
|
|
|
if (mainTe != nullptr)
|
|
{
|
|
// filter cannot be longer than the main te itself !
|
|
if ((te->m_startTime + te->m_length) > mainTe->m_length)
|
|
te->m_length = mainTe->m_length - te->m_startTime;
|
|
|
|
te->m_filterParentChannel = mainTe->m_channelName;
|
|
}
|
|
|
|
|
|
if (selectTrackEntry && !m_readonly)
|
|
selectEntry(te);
|
|
|
|
emit signalTrackEntryAdded(te);
|
|
|
|
update();
|
|
return true;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::removeTrackEntryByTopicId(TrackEntry *te)
|
|
{
|
|
auto itE = std::remove_if(m_entryList.begin(), m_entryList.end(),
|
|
[te](TrackEntry * pTE)
|
|
{
|
|
if (pTE->m_topicId == te->m_topicId)
|
|
{
|
|
pTE->deleteLater();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
m_entryList.erase(itE, m_entryList.end());
|
|
|
|
|
|
itE = std::remove_if(m_selected.begin(), m_selected.end(),
|
|
[te](TrackEntry * pTE)
|
|
{
|
|
if (pTE->m_topicId == te->m_topicId)
|
|
{
|
|
pTE->deleteLater();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
m_selected.erase(itE, m_selected.end());
|
|
}
|
|
|
|
|
|
void TrackContentWidget::dropEvent(QDropEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
m_dragHandler->dropEvent(event);
|
|
}
|
|
|
|
|
|
void TrackContentWidget::dragMoveEvent(QDragMoveEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
m_dragHandler->dragMoveEvent(event);
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::canMoveAll(int offset) const
|
|
{
|
|
if (m_selected.isEmpty())
|
|
return true; // nothing to move (as nothing is selected) - no checks needed (NOTE: other tcws which correspond to this must do at least parl checks if there is something moving!)
|
|
|
|
auto leftMostI = std::min_element(m_selected.constBegin(), m_selected.constEnd(), [](const TrackEntry * a, const TrackEntry * b)
|
|
{
|
|
return a->m_startTime < b->m_startTime;
|
|
});
|
|
|
|
if (leftMostI == m_selected.constEnd())
|
|
return false; // should never happen as we return true if there is no selection
|
|
|
|
int startTimeMostLeft = (*leftMostI)->m_startTime;
|
|
|
|
// check for cols 'n' parls
|
|
QVector<TrackEntry *> cols;
|
|
|
|
int newTime = startTimeMostLeft + offset;
|
|
checkForCollisions(this, m_selected, newTime, cols, true);
|
|
|
|
QVector<TrackEntry *> parls;
|
|
for (const auto &tcw : qAsConst(m_tcwCorresponding))
|
|
checkForParallelTopics(tcw, m_selected, newTime, parls);
|
|
|
|
if (!cols.isEmpty() || !parls.isEmpty()) // cols and/or parls
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::forceMoveAll(int offset)
|
|
{
|
|
for (auto i = m_selected.begin(); i != m_selected.end(); ++i)
|
|
{
|
|
TrackEntry *te = *i;
|
|
|
|
// check boundaries
|
|
int newTime = te->m_startTime + offset;
|
|
|
|
if (newTime < 0)
|
|
newTime = 0;
|
|
|
|
/* *: a Filter Track is almost always smaller than s_timelineLength (seconds how long the timeline is: initial 5 minutes)
|
|
* so we always calc the real duration of the Track, as it can be a Filter Track
|
|
* getDuration calcs with s_timelineLength, but this is relative to the Filter Track's width
|
|
*/
|
|
|
|
te->m_startTime = newTime;
|
|
}
|
|
|
|
update();
|
|
emit signalChangesMade();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::deleteSelectedTopics()
|
|
{
|
|
m_mainSelected = nullptr;
|
|
m_selectHandler->m_pressedEntry = nullptr;
|
|
|
|
bool somethingSeleted = false;
|
|
for (auto &pTopic : m_selected)
|
|
{
|
|
m_preview.removeAll(pTopic);
|
|
m_entryList.removeAll(pTopic);
|
|
emit signalTrackEntryDeleted(pTopic->m_topicId); // to delete the filter TCWs
|
|
pTopic->deleteLater();
|
|
somethingSeleted = true;
|
|
}
|
|
|
|
if (somethingSeleted)
|
|
emit signalTrackEntrySelectionChanged(nullptr);
|
|
|
|
m_selected.clear();
|
|
emit signalChangesMade();
|
|
update();
|
|
}
|
|
|
|
|
|
void TrackContentWidget::mouseDoubleClickEvent(QMouseEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
m_selectHandler->mouseDoubleClick(event);
|
|
}
|
|
|
|
|
|
void TrackContentWidget::keyPressEvent(QKeyEvent *event)
|
|
{
|
|
if (m_readonly)
|
|
return;
|
|
|
|
if (event->key() == Qt::Key_Delete)
|
|
{
|
|
emit signalDeleteSelectedTopics();
|
|
}
|
|
else if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_C)
|
|
{
|
|
emit signalCopyTopics();
|
|
}
|
|
else if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_V)
|
|
{
|
|
emit signalPasteTopics();
|
|
}
|
|
else if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_X)
|
|
{
|
|
emit signalCutTopics();
|
|
}
|
|
else if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_A)
|
|
{
|
|
emit signalSelectAllGlobal();
|
|
}
|
|
else if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_S)
|
|
{
|
|
emit signalSave();
|
|
}
|
|
else if (QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && event->key() == Qt::Key_D)
|
|
{
|
|
setDescriptionForSelected();
|
|
}
|
|
else
|
|
{
|
|
m_resizeHandler->keyPressEvent(event); // important: first!!
|
|
m_selectHandler->keyPressEvent(event); // important: second!!
|
|
m_dragHandler->keyPressEvent(event);
|
|
}
|
|
|
|
if (event->key() == Qt::Key_Escape)
|
|
{
|
|
emit signalEscapeRubberband();
|
|
emit signalMultiLineMultiSelectionDone(-1); // in case a drag was running, end it with failing state
|
|
}
|
|
}
|
|
|
|
|
|
void TrackContentWidget::selectEntry(int topicId) // no warning needed if not found
|
|
{
|
|
for (auto i = m_entryList.begin(); i != m_entryList.end(); ++i)
|
|
{
|
|
TrackEntry *te = *i;
|
|
|
|
if (te->m_topicId == topicId)
|
|
{
|
|
selectEntry(/*static_cast<TrackEntry *>*/(te));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void TrackContentWidget::selectEntriesAtPositions(int fromPos, int toPos)
|
|
{
|
|
m_unselectAllImmune = true; // while mass-selection don't unselect others
|
|
|
|
for (TrackEntry *te : qAsConst(m_entryList))
|
|
{
|
|
int start = m_tools->getPos(te->m_startTime);
|
|
int end = m_tools->getPos(te->m_startTime + te->m_length);
|
|
if (end >= fromPos && start <= toPos)
|
|
selectEntry(te);
|
|
}
|
|
|
|
m_unselectAllImmune = false; // reset behaviour
|
|
}
|
|
|
|
|
|
void TrackContentWidget::selectEntry(TrackEntry *te)
|
|
{
|
|
bool ctrl = QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier);
|
|
|
|
if (!m_unselectAllImmune && !ctrl) // only do this if not immune....
|
|
unselect(); // check automatically if we have to unselect!
|
|
|
|
if (te == nullptr)
|
|
return;
|
|
|
|
if (!m_selected.contains(te)) // do not add multiple times
|
|
{
|
|
m_selected.append(te);
|
|
m_mainSelected = te;
|
|
|
|
emit signalTrackEntrySelectionChanged(nullptr);
|
|
}
|
|
else // unselect if already selected
|
|
{
|
|
m_selected.removeAll(te);
|
|
if (m_mainSelected == te)
|
|
m_mainSelected = nullptr;
|
|
|
|
emit signalTrackEntrySelectionChanged(nullptr);
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::canPasteTopics(const QVector<TrackEntry *> &topics, int offsetTime, int pasteTime) const
|
|
{
|
|
TemporaryPointerVector<TrackEntry *> newTes;
|
|
|
|
newTes.reserve(topics.size());
|
|
for (TrackEntry *te : qAsConst(topics))
|
|
{
|
|
TrackEntry *newTe = new TrackEntry(te);
|
|
newTe->m_startTime = (newTe->m_startTime - offsetTime + pasteTime);
|
|
newTes += newTe;
|
|
}
|
|
|
|
//qWarning() << "parl checks possible for " << m_trackName << "?" << (m_tcwCorresponding != nullptr) << ":" << m_tcwCorresponding->m_trackName;
|
|
|
|
QVector<TrackEntry *> dummy_vecCollidingTopics;
|
|
|
|
bool parl = false;
|
|
|
|
for (const auto &tcw : qAsConst(m_tcwCorresponding))
|
|
{
|
|
parl = checkForParallelTopics(tcw, newTes, pasteTime, true /* FIXME: HACK! */);
|
|
if (parl)
|
|
break;
|
|
}
|
|
|
|
if (checkForCollisions(this, newTes, pasteTime, dummy_vecCollidingTopics, false, true) || parl)
|
|
return false;
|
|
|
|
return true;
|
|
|
|
/*
|
|
if (cntCollisions == topics.size())
|
|
LtsWidgets::LtsMessageDialog::showMessage(nullptr, tr("Cannot paste here, all topics intersect with other topics"), tr("Error"));
|
|
else if (cntCollisions)
|
|
LtsWidgets::LtsMessageDialog::showMessage(nullptr, tr("Could not paste %1 topic(s) since they intersected with other topics").arg(cntCollisions), tr("Information"));
|
|
*/
|
|
}
|
|
|
|
|
|
void TrackContentWidget::forcePasteTopics(const QVector<TrackEntry *> &topics, int offsetTime, int pasteTime)
|
|
{
|
|
/*
|
|
* Copied topic might still be selected. If the copied topic would be pasted such that
|
|
* it overlaps with the select original, no collision check would be performed.
|
|
* Hence, we remove the selection when pasting.
|
|
*/
|
|
m_selected.clear();
|
|
|
|
// Copy topics, exclude topics with collisions
|
|
for (TrackEntry *te : qAsConst(topics))
|
|
{
|
|
TrackEntry *newTe = new TrackEntry(te);
|
|
newTe->m_topicId = m_scriptTimeline->nextUniqueId();
|
|
newTe->m_startTime = (newTe->m_startTime - offsetTime + pasteTime);
|
|
newTe->isSet = true;
|
|
|
|
m_unselectAllImmune = true;
|
|
if (!m_trackWidget->addTrackEntry(newTe, false)) // adds filters for us (if any inside newTe's detail data)
|
|
{
|
|
qWarning() << "TrackContentWidget::forcePasteTopics: topic #" << te->m_topicId << "type" << (int)te->m_type << "not allowed";
|
|
delete newTe;
|
|
delete te;
|
|
continue;
|
|
}
|
|
m_unselectAllImmune = false;
|
|
}
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::allowedByType(const TrackEntry *te) const
|
|
{
|
|
if (m_disallowedTypes.contains(te->m_type)) // disallowment has more weight
|
|
return false;
|
|
|
|
if (m_allowedTypes.isEmpty()) // only take into account if it is not empty
|
|
return true;
|
|
|
|
return (m_allowedTypes.contains(te->m_type));
|
|
}
|
|
|
|
|
|
void TrackContentWidget::setTrackName(const QString &trackName)
|
|
{
|
|
m_trackName = trackName;
|
|
|
|
for (auto i = m_entryList.begin(); i != m_entryList.end(); ++i)
|
|
{
|
|
TrackEntry *te = *i;
|
|
te->m_channelName = m_trackName;
|
|
}
|
|
}
|
|
|
|
|
|
TrackEntry *TrackContentWidget::getTrackEntryOnPos(QPoint globalPos) // const
|
|
{
|
|
QPoint clickedPos = mapFromGlobal(globalPos);
|
|
int clickedPosY = clickedPos.y();
|
|
int clickedPosX = clickedPos.x();
|
|
|
|
if (clickedPosY < 0 || clickedPosY > height())
|
|
return nullptr;
|
|
|
|
int clickedTime = m_tools->getTime(clickedPosX, m_isFilter);
|
|
|
|
return getTrackEntryAt(clickedTime);
|
|
}
|
|
|
|
|
|
QVector<TrackEntry *> TrackContentWidget::dataToItems(const QByteArray &data, int &offset)
|
|
{
|
|
if (data.isEmpty())
|
|
return {};
|
|
|
|
QDataStream dataStream(data);
|
|
|
|
int count;
|
|
dataStream >> count;
|
|
dataStream >> offset;
|
|
|
|
QVector<TrackEntry *> tes;
|
|
tes.reserve(count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
TrackEntry *te = new TrackEntry;
|
|
dataStream >> *te;
|
|
te->m_startTime -= offset;
|
|
tes += te;
|
|
}
|
|
|
|
return tes;
|
|
}
|
|
|
|
|
|
bool TrackContentWidget::dragEnter(const QByteArray &itemData)
|
|
{
|
|
dragEnd();
|
|
|
|
TemporaryPointerVector<TrackEntry *> tes(dataToItems(itemData, m_dragOffsetTime));
|
|
|
|
if (tes.isEmpty())
|
|
return false;
|
|
|
|
for (TrackEntry *te : qAsConst(tes))
|
|
{
|
|
if (!allowedByType(te))
|
|
{
|
|
qWarning() << "TrackContentWidget::dragEnter: topic #" << te->m_topicId << "type" << (int)te->m_type << "not allowed";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (m_isFilter && m_mainTopicId != -1) // don't allow a main te to be put into its own filter line, otherwise it would be swalloed by the void and disappear
|
|
{
|
|
auto it = std::find_if(tes.begin(), tes.end(), [this](const TrackEntry * a)
|
|
{
|
|
return (a->m_topicId == m_mainTopicId);
|
|
});
|
|
|
|
if (it != tes.end())
|
|
return false; // drag content contains the main event, abort!
|
|
}
|
|
|
|
const QVector<TrackEntry *> &thisTes = getTrackEntries();
|
|
|
|
// mlms d&ds can only d&d into their own tcw
|
|
if (m_isMultiLineMultiSelectionMode)
|
|
{
|
|
for (TrackEntry *te : qAsConst(tes))
|
|
{
|
|
bool found = false;
|
|
|
|
for (TrackEntry *thisTe : qAsConst(thisTes))
|
|
{
|
|
if (thisTe->m_topicId == te->m_topicId)
|
|
{
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
return false;
|
|
}
|
|
}
|
|
|
|
sortTrackEntriesChronologically(tes);
|
|
|
|
/*
|
|
* Is the user adding a completely new topic? If yes, remove selection
|
|
* to include all topics into collision detection!
|
|
*/
|
|
if (!m_isFilter && tes.size() == 1)
|
|
{
|
|
TrackEntry *pPreview = tes.front();
|
|
bool hasJustBeenAddedByUser = true;
|
|
|
|
for (auto pTE : qAsConst(m_entryList))
|
|
{
|
|
if (pPreview->m_topicId == pTE->m_topicId)
|
|
{
|
|
hasJustBeenAddedByUser = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check corresponding TrackContentWidget since the user is maybe trying to drag topic from one track to another
|
|
for (const auto &tcw : qAsConst(m_tcwCorresponding))
|
|
{
|
|
for (auto pTE : qAsConst(tcw->m_entryList))
|
|
{
|
|
if (pPreview->m_topicId == pTE->m_topicId)
|
|
{
|
|
hasJustBeenAddedByUser = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasJustBeenAddedByUser)
|
|
{
|
|
// Clear user selections since selections are not considered for collision detection which would be wrong in this context
|
|
m_selected.clear();
|
|
|
|
for (const auto &tcw : qAsConst(m_tcwCorresponding))
|
|
tcw->m_selected.clear();
|
|
}
|
|
}
|
|
|
|
tes.setDiscardable(false); // do not delete the TEs inside tes after losing scope
|
|
m_preview = tes;
|
|
return true;
|
|
}
|
|
|
|
void TrackContentWidget::clearDrag()
|
|
{
|
|
qDeleteAll(m_preview);
|
|
m_preview.clear();
|
|
m_dragOffsetTime = -1;
|
|
m_dragPos = -1;
|
|
m_dragPossible = false;
|
|
m_moveValid = false;
|
|
update();
|
|
}
|
|
|
|
int TrackContentWidget::getDropPos() const
|
|
{
|
|
return (m_dragHandler == nullptr) ? -1 : m_dragHandler->m_dropPos;
|
|
}
|
|
|
|
|
|
void TrackContentWidget::dragEnd()
|
|
{
|
|
clearDrag();
|
|
}
|
|
|
|
bool TrackContentWidget::dragDrop(int pos)
|
|
{
|
|
if (!isDragPossible() || !m_moveValid) // mlsm and normal mode check
|
|
{
|
|
dragEnd();
|
|
return false;
|
|
}
|
|
|
|
int dropTime = m_tools->getTime(pos, m_isFilter) - m_dragOffsetTime;
|
|
QVector<TrackEntry *> selectedOld = m_selected; // copy list, only delete OLD selection (the following functions would alter m_selected)
|
|
|
|
if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier) && !m_isMultiLineMultiSelectionMode)
|
|
unselect(); // unselect all BEFORE adding new entries, BUT: don't unselect if CTRL is pressed! (or not in mlsm)
|
|
|
|
int firstTeTime = -1;
|
|
|
|
for (const TrackEntry *te : qAsConst(m_preview))
|
|
{
|
|
if (firstTeTime > te->m_startTime || firstTeTime == -1)
|
|
firstTeTime = te->m_startTime;
|
|
}
|
|
|
|
int selected = firstTeTime;
|
|
int diff = dropTime - selected;
|
|
|
|
// todo: new notwendig? wieso nicht einfach die Zeit ummappen?
|
|
for (auto i = m_preview.constBegin(); i != m_preview.constEnd(); ++i)
|
|
{
|
|
TrackEntry *prevTe = *i;
|
|
TrackEntry *newTe = new TrackEntry(prevTe);
|
|
newTe->m_topicId = m_scriptTimeline->nextUniqueId(); // moved tracks need a new id, otherwise filters cannot be added to the new track as old track and new track have the same id
|
|
newTe->m_startTime = prevTe->m_startTime + diff; // set new time
|
|
newTe->isSet = true; // on a TCW, they are already set
|
|
|
|
// TODO: get widget width, cut topic at end of widget if m_filter == true (here?)
|
|
|
|
m_unselectAllImmune = true; // we want to select ALL new copied entries
|
|
if (m_isFilter) // there is no m_trackWidget then !
|
|
{
|
|
addTrackEntry(newTe, true);
|
|
}
|
|
else
|
|
{
|
|
if (m_trackWidget == nullptr || !m_trackWidget->addTrackEntry(newTe, true))
|
|
{
|
|
delete newTe;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
update();
|
|
|
|
// Drop may come from the corresponding TrackWidget. Hence, we have to remove it there
|
|
for (const auto &tcw : qAsConst(m_tcwCorresponding))
|
|
tcw->removeTrackEntryByTopicId(prevTe);
|
|
|
|
m_unselectAllImmune = false; // done!
|
|
}
|
|
|
|
dragEnd();
|
|
|
|
if (m_isMultiLineMultiSelectionMode)
|
|
{
|
|
// move action - TEs were moved
|
|
for (TrackEntry *oldSel : qAsConst(selectedOld))
|
|
emit signalTrackEntryDeleted(oldSel->m_topicId); // triggers the deletion of old filters
|
|
qDeleteAll(selectedOld);
|
|
|
|
emit signalMultiLineMultiSelectionDone(pos);
|
|
}
|
|
|
|
emit signalChangesMade();
|
|
return true;
|
|
}
|
|
|
|
bool TrackContentWidget::dragMove(int pos)
|
|
{
|
|
m_dragPos = pos;
|
|
|
|
// collision check between the preview and all entries
|
|
int dropTime_s = m_tools->getTime(m_dragPos, m_isFilter) - m_dragOffsetTime;
|
|
checkForCollisions(this, m_preview, dropTime_s, m_dragHandler->m_vecCollisions, true);
|
|
|
|
m_dragHandler->m_vecParallel.clear();
|
|
for (const auto &tcw : qAsConst(m_tcwCorresponding))
|
|
checkForParallelTopics(tcw, m_preview, dropTime_s, m_dragHandler->m_vecParallel);
|
|
|
|
m_moveValid = m_dragHandler->m_vecCollisions.isEmpty() && m_dragHandler->m_vecParallel.isEmpty();
|
|
|
|
if (!m_moveValid || !isDragPossible())
|
|
{
|
|
//qWarning() << m_trackName << "cols?" << !m_dragHandler->m_vecCollisions.isEmpty() << "parals?" << !m_dragHandler->m_vecParallel.isEmpty();
|
|
setCursor(Qt::ForbiddenCursor);
|
|
}
|
|
else
|
|
{
|
|
setCursor(Qt::DragMoveCursor); // FIXME: bad cursor for dragging..?
|
|
}
|
|
|
|
update();
|
|
|
|
if (m_isMultiLineMultiSelectionMode)
|
|
{
|
|
if (m_moveValid) // only tells if ITSELF fails, not if dragPossible (which is set by the script timeline if *someone* fails) is sset
|
|
emit signalMultiLineMultiSelectionOkay();
|
|
else
|
|
emit signalMultiLineMultiSelectionFailed();
|
|
}
|
|
|
|
|
|
// move the timeline to the left/right when close to the left or right
|
|
static int scrollByPixels = 1;
|
|
|
|
if (pos >= (m_width - 60))
|
|
emit signalScroll(scrollByPixels);
|
|
else if (pos < 60)
|
|
emit signalScroll(-scrollByPixels);
|
|
|
|
return m_moveValid;
|
|
}
|
|
|
|
|
|
TrackEntry *TrackContentWidget::trackEntryAtPos(int x) const
|
|
{
|
|
int time = m_tools->getTime(x);
|
|
const QVector<TrackEntry *> tes = getTrackEntries();
|
|
|
|
for (TrackEntry *te : tes)
|
|
{
|
|
int start = te->m_startTime;
|
|
if (time >= start && time <= (start + te->m_length))
|
|
return te;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
QByteArray TrackContentWidget::itemsToData(const QVector<TrackEntry *> &items, int dragPosTime, bool calculateFirstTeTime)
|
|
{
|
|
int count = items.count();
|
|
if (!count)
|
|
return {};
|
|
|
|
QByteArray itemData;
|
|
QDataStream dataStream(&itemData, QIODevice::WriteOnly);
|
|
|
|
dataStream << count;
|
|
|
|
if (dragPosTime != -1)
|
|
{
|
|
int firstTeTime = -1;
|
|
|
|
if (calculateFirstTeTime)
|
|
{
|
|
for (TrackEntry *te : items)
|
|
{
|
|
if (firstTeTime > te->m_startTime || firstTeTime == -1)
|
|
firstTeTime = te->m_startTime;
|
|
}
|
|
}
|
|
else
|
|
firstTeTime = 0;
|
|
|
|
int finalOffsetTime = dragPosTime - firstTeTime;
|
|
dataStream << finalOffsetTime;
|
|
}
|
|
else
|
|
dataStream << 0; // no offset
|
|
|
|
// serialize all TEs
|
|
for (auto i = items.constBegin(); i != items.constEnd(); ++i)
|
|
{
|
|
TrackEntry *te = *i;
|
|
dataStream << *te;
|
|
}
|
|
|
|
return itemData;
|
|
}
|
|
|
|
|
|
QByteArray TrackContentWidget::selectedItemsToData(int dragPosTime, bool calculateFirstTeTime)
|
|
{
|
|
return itemsToData(m_selected, dragPosTime, calculateFirstTeTime);
|
|
}
|
|
|
|
|
|
void TrackContentWidget::setDragPossible(bool possible)
|
|
{
|
|
m_dragPossible = possible;
|
|
update(); // draw TEs red or green now
|
|
}
|
|
|
|
} // namespace LtsScriptTimeline
|