Files
qt5-legatus-legacy/Lts/Common/LtsScriptTimeline/track_content_widget.cpp
T
2026-08-13 18:33:10 +02:00

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