Files
qt5-legatus-legacy/Lts/Plugins/PresentationManagement/Gui/Script/Common/topic_designer.cpp
T
2026-08-13 18:33:10 +02:00

1075 lines
23 KiB
C++
Executable File

/**
* @RDE_HEADER@
* Topic Designer for the Script Editor - Set Geometry & Z-Index, delete Topics, etc.
* $Id$
* $HeadURL$
* $Author$
*
* Created by Mike Wille, SLSE, 2017-06-09
*/
#include "topic_designer.h"
#include <QDebug>
#include <QPaintEvent>
#include <QPainter>
#include <QPen>
#include <QBrush>
#include <QColor>
#include <QPoint>
#include <QRect>
#include <QSize>
#include <QCursor>
#include <QTimer>
#include <QScrollArea>
#include <QScrollBar>
namespace PresentationManagement
{
static const double minVal = 0.01f;
static const double maxVal = 0.98f;
static int handleSize = 8;
TopicDesigner::TopicDesigner(QWidget *parent, QWidget *previewWidget)
: QWidget(parent)
, m_selectedTopicId(-1)
, m_lowestImportIndex(-1)
, m_currentResizeHandle(HandleType::None)
, m_resizeHandleActive(HandleType::None)
, m_drawWidgetPreviewContent(previewWidget)
, m_scrollArea(new QScrollArea(parent))
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
m_scrollArea->setWidget(m_drawWidgetPreviewContent);
for (QScrollBar *sb : findChildren<QScrollBar *>())
{
qCritical() << "isVisible?" << sb->isVisible() << "" << sb;
}
// TESTS
// int idx = 0;
// int id = 0;
// double x = 0.2f;
// double y = 0.2f;
// double w = 0.1f;
// double h = 0.1f;
// QVector<LtsScriptTimeline::TrackEntry *> topicsList;
// {
// TopicData data = new LtsScriptTimeline::TrackEntry(LtsScriptTimeline::TrackEntryType::Audio);
// te->index = idx++;
// te->topicId = id++;
// te->x = x+=0.01f;
// te->y = y+=0.01f;
// te->w = w+=0.01f;
// te->h = h+=0.01f;
// topicsList.append(te);
// }
// {
// TopicData data = new LtsScriptTimeline::TrackEntry(LtsScriptTimeline::TrackEntryType::Audio);
// te->index = idx++;
// te->topicId = id++;
// te->x = x+=0.01f;
// te->y = y+=0.01f;
// te->w = w+=0.01f;
// te->h = h+=0.01f;
// topicsList.append(te);
// }
// {
// TopicData data = new LtsScriptTimeline::TrackEntry(LtsScriptTimeline::TrackEntryType::Audio);
// te->index = idx++;
// te->topicId = id++;
// te->x = x+=0.01f;
// te->y = y+=0.01f;
// te->w = w+=0.01f;
// te->h = h+=0.01f;
// topicsList.append(te);
// }
// {
// TopicData data = new LtsScriptTimeline::TrackEntry(LtsScriptTimeline::TrackEntryType::Audio);
// te->index = idx++;
// m_selectedTopic = te->topicId = id++;
// te->x = x+=0.01f;
// te->y = y+=0.01f;
// te->w = w+=0.01f;
// te->h = h+=0.01f;
// topicsList.append(te);
// }
// loadTopicList(topicsList);
QTimer *timer = new QTimer(this);
timer->setInterval(1000);
connect(timer, &QTimer::timeout, this, [this]()
{
raise();
});
timer->start();
}
TopicDesigner::~TopicDesigner()
{
delete m_scrollArea;
}
int TopicDesigner::heightForWidth(int width) const
{
int result = width / 4;
//qCritical() << "h4w width" << width << "result" << result;
return result;
}
QVector<TopicEditorBase::TopicData> TopicDesigner::exportTopicList() const
{
QVector<TopicEditorBase::TopicData> result;
int lowestIndex = m_lowestImportIndex;
for (auto i = m_topicDataList.begin(); i != m_topicDataList.end(); ++i)
{
TopicEditorBase::TopicData data = *i;
data.index = lowestIndex++;
result.append(data);
}
return result;
}
void TopicDesigner::selectTopic(int topicId)
{
if (topicId < 0)
unselect();
else
m_selectedTopicId = topicId;
update();
//emit signalChangesMade();
}
void TopicDesigner::loadTopicList(const QVector<TopicEditorBase::TopicData> &topicsList)
{
m_lowestImportIndex = -1;
int zIndex = -1;
// sort: find HIGHEST & LOWEST index
for (auto i = topicsList.constBegin(); i != topicsList.constEnd(); ++i)
{
TopicEditorBase::TopicData data = *i;
int teZIndex = data.index;
if (zIndex < teZIndex || zIndex == -1)
zIndex = teZIndex;
if (m_lowestImportIndex > teZIndex || m_lowestImportIndex == -1)
m_lowestImportIndex = teZIndex;
}
m_topicDataList.clear();
while (zIndex >= -1)
{
for (auto i = topicsList.constBegin(); i != topicsList.constEnd(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.index == zIndex)
{
checkConstraints(data); // check first, before adding
m_topicDataList.prepend(data);
}
}
zIndex--;
}
}
void TopicDesigner::setValues(const TopicEditorBase::TopicData &data, const QPoint &pos)
{
if (data.id == -1)
{
m_currentResizeHandle = HandleType::None;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (m_resizeHandleActive != HandleType::None) // do not change anything while a resize is going on
return;
if (handleRect(data, HandleType::TopLeft).contains(pos))
{
m_currentResizeHandle = HandleType::TopLeft;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::BottomRight).contains(pos))
{
m_currentResizeHandle = HandleType::BottomRight;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::TopCenter).contains(pos))
{
m_currentResizeHandle = HandleType::TopCenter;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::BottomCenter).contains(pos))
{
m_currentResizeHandle = HandleType::BottomCenter;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::TopRight).contains(pos))
{
m_currentResizeHandle = HandleType::TopRight;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::BottomLeft).contains(pos))
{
m_currentResizeHandle = HandleType::BottomLeft;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::CenterLeft).contains(pos))
{
m_currentResizeHandle = HandleType::CenterLeft;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
if (handleRect(data, HandleType::CenterRight).contains(pos))
{
m_currentResizeHandle = HandleType::CenterRight;
setCursorFromHandleType(m_currentResizeHandle);
return;
}
// if the user does not touch a handle, then the rect is touched directly
if (topicItemRect(data).contains(pos))
{
m_currentResizeHandle = HandleType::Whole;
setCursorFromHandleType(m_currentResizeHandle);
double x = pos.x();
x /= width();
double y = pos.y();
y /= height();
m_wholeOrgPos = QPointF(x, y);
//qCritical() << "org pos" << m_wholeOrgPos;
return;
}
// otherwise ..
m_currentResizeHandle = HandleType::None;
setCursorFromHandleType(m_currentResizeHandle);
}
void TopicDesigner::setCursorFromHandleType(const HandleType &ht)
{
switch (ht)
{
case HandleType::TopLeft:
case HandleType::BottomRight:
{
setCursor(QCursor(Qt::SizeFDiagCursor));
break;
}
case HandleType::TopCenter:
case HandleType::BottomCenter:
{
setCursor(QCursor(Qt::SizeVerCursor));
break;
}
case HandleType::TopRight:
case HandleType::BottomLeft:
{
setCursor(QCursor(Qt::SizeBDiagCursor));
break;
}
case HandleType::CenterLeft:
case HandleType::CenterRight:
{
setCursor(QCursor(Qt::SizeHorCursor));
break;
}
case HandleType::Whole:
{
setCursor(QCursor(Qt::SizeAllCursor));
break;
}
default:
{
setCursor(QCursor(Qt::ArrowCursor));
break;
}
}
}
bool TopicDesigner::checkConstraints(TopicEditorBase::TopicData &selected)
{
// if size of selected te on current action is too small => set to minimum size
bool result = true;
if (selected.x < 0)
{
// qCritical() << "affect: minimum value size constraint not met: x" << selected.x;
selected.x = 0;
result = false;
}
if (selected.y < 0)
{
// qCritical() << "affect: minimum value size constraint not met: y" << selected.y;
selected.y = 0;
result = false;
}
if (selected.w < minVal)
{
// qCritical() << "affect: minimum value size constraint not met: w" << selected.w;
selected.w = minVal;
result = false;
}
if (selected.h < minVal)
{
// qCritical() << "affect: minimum value size constraint not met: h" << selected.h;
selected.h = minVal;
result = false;
}
if (selected.x > maxVal)
{
// qCritical() << "affect: maximum value size constraint not met: x" << selected.x;
selected.x = maxVal;
result = false;
}
if (selected.y > maxVal)
{
// qCritical() << "affect: maximum value size constraint not met: y" << selected.y;
selected.y = maxVal;
result = false;
}
return result;
}
void TopicDesigner::affect(TopicEditorBase::TopicData &selected, const Affection &affection, const QPoint &pos)
{
double posX = pos.x();
posX /= width();
double posY = pos.y();
posY /= height();
// i guess wPos/hPos must always be bigger than x/y
switch (affection)
{
case Affection::x:
{
double x = selected.x;
double oldX = x;
x = posX;
selected.x = x;
double w = selected.w;
w += oldX - x;
selected.w = w;
break;
}
case Affection::y:
{
double y = selected.y;
double oldY = y;
y = posY;
selected.y = y;
double h = selected.h;
h += oldY - y;
selected.h = h;
break;
}
case Affection::w:
{
selected.w = posX - selected.x;
break;
}
case Affection::h:
{
selected.h = posY - selected.y;
break;
}
case Affection::Whole:
{
double x = selected.x;
double y = selected.y;
//qCritical() << "b4 whole x" << m_wholeOrgPos.x() << "te x" << x << "pos x" << posX;
x = -m_wholeOrgPos.x() + x + posX;
y = -m_wholeOrgPos.y() + y + posY;
m_wholeOrgPos = QPointF(posX, posY);
selected.x = x;
selected.y = y;
//qCritical() << "aftr whole x" << m_wholeOrgPos.x() << "te x" << x << "pos x" << posX;
}
}
// if (m_drawWidgetPreviewContent != nullptr)
// {
// m_drawWidgetPreviewContent->setGeometry(0, 0, width(), height());
// }
checkConstraints(selected);
emit changesMade();
}
void TopicDesigner::incIndex(const TopicEditorBase::TopicData &selected)
{
for (auto i = m_topicDataList.begin(); i != m_topicDataList.end(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.id == selected.id)
{
auto next = (i + 1);
if (next == m_topicDataList.end())
{
//qCritical() << "is already last!";
return;
}
next = (i+2); // can be end or not
m_topicDataList.insert(next, data);
m_topicDataList.erase(i);
return; // !!
}
}
}
void TopicDesigner::toFront(const TopicEditorBase::TopicData &selected)
{
auto begin = m_topicDataList.begin();
for (auto i = m_topicDataList.begin(); i != m_topicDataList.end(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.id == selected.id)
{
m_topicDataList.insert(begin, data);
m_topicDataList.erase(i);
return; // !!
}
}
}
void TopicDesigner::toBack(const TopicEditorBase::TopicData &selected)
{
auto end = m_topicDataList.begin();
for (auto i = m_topicDataList.begin(); i != m_topicDataList.end(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.id == selected.id)
{
m_topicDataList.insert(end, data);
m_topicDataList.erase(i);
return; // !!
}
}
}
void TopicDesigner::decIndex(const TopicEditorBase::TopicData &selected)
{
auto last = m_topicDataList.begin();
for (auto i = m_topicDataList.begin(); i != m_topicDataList.end(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.id == selected.id)
{
m_topicDataList.insert(last, data);
m_topicDataList.erase(i);
return; // !!
}
last = i;
}
}
double TopicDesigner::oneWidthPixelInPercent()
{
double result = width();
return (1 / result);
}
double TopicDesigner::oneHeightPixelInPercent()
{
double result = height();
return (1 / result);
}
void TopicDesigner::keyPressEvent(QKeyEvent *event)
{
QWidget::keyPressEvent(event);
TopicEditorBase::TopicData selected;
if (m_selectedTopicId > -1) // TODO: to method !
{
// find selected
for (auto i = m_topicDataList.constBegin(); i != m_topicDataList.constEnd(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.id == m_selectedTopicId)
{
selected = data;
break;
}
}
}
if (selected.id == -1)
return;
double times = 5;
if (event->modifiers().testFlag(Qt::ControlModifier))
{
times = 1;
}
bool shapeShift = false;
if (event->modifiers().testFlag(Qt::ShiftModifier))
{
shapeShift = true;
}
switch (event->key()) // TODO: process multiple keys being pressed
{
// case Qt::Key_Minus:
// {
// decIndex(selected);
// break;
// }
// case Qt::Key_Plus:
// {
// incIndex(selected);
// break;
// }
case Qt::Key_Up:
{
double moveBy = oneHeightPixelInPercent() * times;
if (shapeShift)
selected.h -= moveBy;
else
selected.y -= moveBy;
break;
}
case Qt::Key_Down:
{
double moveBy = oneHeightPixelInPercent() * times;
if (shapeShift)
selected.h += moveBy;
else
selected.y += moveBy;
break;
}
case Qt::Key_Left:
{
double moveBy = oneHeightPixelInPercent() * times;
if (shapeShift)
selected.w -= moveBy;
else
selected.x -= moveBy;
break;
}
case Qt::Key_Right:
{
double moveBy = oneHeightPixelInPercent() * times;
if (shapeShift)
selected.w += moveBy;
else
selected.x += moveBy;
break;
}
case Qt::Key_Escape:
{
unselect();
break;
}
// case Qt::Key_Delete:
// {
// deleteTrackEntry(selected);
// break;
// }
}
checkConstraints(selected);
update();
emit changesMade();
}
void TopicDesigner::deleteTopicItem(const TopicEditorBase::TopicData &data)
{
// find selected
for (auto i = m_topicDataList.begin(); i != m_topicDataList.end(); ++i)
{
TopicEditorBase::TopicData other = *i;
if (other.id == data.id)
{
m_topicDataList.erase(i);
break;
}
}
m_currentResizeHandle = HandleType::None;
setCursorFromHandleType(m_currentResizeHandle);
update();
emit deleteTopicItem(data.id);
emit changesMade();
}
TopicEditorBase::TopicData &TopicDesigner::selectedTopicData() noexcept(false)
{
if (m_selectedTopicId == -1)
throw QStringLiteral("getSelectedTopic: no selected topic");
// find selected
for (auto it = m_topicDataList.begin(); it != m_topicDataList.end(); ++it)
{
const TopicEditorBase::TopicData &data = *it;
if (data.id == m_selectedTopicId)
return *it;
}
throw QStringLiteral("getSelectedTopic: data not found for selected topic with id %1").arg(m_selectedTopicId);
}
void TopicDesigner::setTopicData(const TopicEditorBase::TopicData &data)
{
auto before = m_topicDataList.begin();
for (auto it = m_topicDataList.begin(); it != m_topicDataList.end(); ++it)
{
if (it->id == data.id)
{
m_topicDataList.insert(before, data);
m_topicDataList.erase(it);
return;
}
before = it;
}
}
void TopicDesigner::mouseMoveEvent(QMouseEvent *event)
{
QWidget::mouseMoveEvent(event);
try
{
TopicEditorBase::TopicData &selected = selectedTopicData();
setValues(selected, mapFromGlobal(event->globalPos()));
if (selected.id == -1)
return;
if (m_resizeHandleActive == HandleType::None)
return;
QPoint pos = mapFromGlobal(event->globalPos());
switch (m_resizeHandleActive)
{
case HandleType::TopLeft: // x & y
{
affect(selected, Affection::x, pos);
affect(selected, Affection::y, pos);
break;
}
case HandleType::TopCenter: // y
{
affect(selected, Affection::y, pos);
break;
}
case HandleType::TopRight: // w & y
{
affect(selected, Affection::w, pos);
affect(selected, Affection::y, pos);
break;
}
case HandleType::CenterLeft: // x
{
affect(selected, Affection::x, pos);
break;
}
case HandleType::CenterRight: // w
{
affect(selected, Affection::w, pos);
break;
}
case HandleType::BottomLeft: // x & h
{
affect(selected, Affection::x, pos);
affect(selected, Affection::h, pos);
break;
}
case HandleType::BottomCenter: // h
{
affect(selected, Affection::h, pos);
break;
}
case HandleType::BottomRight: // w & h
{
affect(selected, Affection::w, pos);
affect(selected, Affection::h, pos);
break;
}
case HandleType::Whole:
{
affect(selected, Affection::Whole, pos);
break;
}
default:
{
qCritical() << "cannot affect this!";
break;
}
} // switch
update();
emit changesMade();
}
catch (const QString &errMsg) { qCritical() << errMsg; }
}
void TopicDesigner::mousePressEvent(QMouseEvent *event)
{
QWidget::mousePressEvent(event);
if (m_currentResizeHandle == HandleType::None)
return;
m_resizeHandleActive = m_currentResizeHandle;
update();
//emit signalChangesMade();
}
void TopicDesigner::mouseReleaseEvent(QMouseEvent *event)
{
QWidget::mouseReleaseEvent(event);
if (m_resizeHandleActive == HandleType::None)
{
unselect();
findSelection(mapFromGlobal(event->globalPos()));
}
m_resizeHandleActive = HandleType::None;
update();
emit changesMade();
}
void TopicDesigner::internalSelectTopic(int topicId)
{
if (topicId < 0)
unselect();
m_selectedTopicId = topicId;
emit topicSelected(m_selectedTopicId);
}
void TopicDesigner::findSelection(const QPoint &pos)
{
for (auto i = m_topicDataList.crbegin(); i != m_topicDataList.crend(); ++i) // stl its (reverse order)!
{
TopicEditorBase::TopicData data = *i;
if (topicItemRect(data).contains(pos))
{
// found!
internalSelectTopic(data.id);
update();
//emit signalChangesMade();
return;
}
}
}
void TopicDesigner::unselect()
{
m_resizeHandleActive = HandleType::None;
m_selectedTopicId = -1;
emit unselected();
}
QRect TopicDesigner::handleRect(const TopicEditorBase::TopicData &data, const HandleType &ht)
{
QRect teRect = topicItemRect(data);
int x = teRect.x();
int y = teRect.y();
int w = teRect.width();
int h = teRect.height();
int halfHandleSize = handleSize / 2;
QSize handleSized(handleSize, handleSize);
int leftX = x - halfHandleSize + 1;
int centerX = x + (w / 2) - halfHandleSize;
int rightX = x + w - halfHandleSize - 1;
int topY = y - halfHandleSize + 1;
int centerY = y + (h / 2) - halfHandleSize;
int bottomY = y + h - halfHandleSize - 1;
switch (ht)
{
case HandleType::TopLeft:
return QRect(QPoint(leftX, topY), handleSized);
case HandleType::TopCenter:
return QRect(QPoint(centerX, topY), handleSized);
case HandleType::TopRight:
return QRect(QPoint(rightX, topY), handleSized);
case HandleType::CenterLeft:
return QRect(QPoint(leftX, centerY), handleSized);
case HandleType::CenterRight:
return QRect(QPoint(rightX, centerY), handleSized);
case HandleType::BottomLeft:
return QRect(QPoint(leftX, bottomY), handleSized);
case HandleType::BottomCenter:
return QRect(QPoint(centerX, bottomY), handleSized);
case HandleType::BottomRight:
return QRect(QPoint(rightX, bottomY), handleSized);
default:
return QRect();
}
}
QRect TopicDesigner::topicItemRect(const TopicEditorBase::TopicData &data)
{
// const double minVal = 0.01f;
if (data.x < 0 || data.y < 0 || data.w < minVal || data.h < minVal)
{
qCritical() << "trackEntryRect failed: x" << data.x << "y" << data.y << "w" << data.w << "h" << data.h;
return QRect();
}
//qCritical() << "te geo % : x" << te.x << "y" << te.y << "w" << te.w << "h" << te.h;
int x = width() * data.x;
int y = height() * data.y;
int w = width() * data.w;
int h = height() * data.h;
//qCritical() << "te geo px: x" << x << "y" << y << "w" << w << "h" << h;
return QRect(x-4, y-4, w, h);
}
void TopicDesigner::paintSelectedTopicItem(QPainter *p, const TopicEditorBase::TopicData &data)
{
p->save();
// draw handles
p->setBrush(QColor("white"));
p->setPen(QPen(QColor("black")));
// top left
p->drawRect(handleRect(data, HandleType::TopLeft));
// top center
p->drawRect(handleRect(data, HandleType::TopCenter));
// top right
p->drawRect(handleRect(data, HandleType::TopRight));
// center left
p->drawRect(handleRect(data, HandleType::CenterLeft));
// center right
p->drawRect(handleRect(data, HandleType::CenterRight));
// bottom left
p->drawRect(handleRect(data, HandleType::BottomLeft));
// bottom center
p->drawRect(handleRect(data, HandleType::BottomCenter));
// bottom right
p->drawRect(handleRect(data, HandleType::BottomRight));
p->restore();
}
void TopicDesigner::paintEvent(QPaintEvent *)
{
//QWidget::paintEvent(event);
QPainter painter(this);
painter.save();
painter.setPen(QPen(QColor("black"), 1));
//painter.setBrush(QColor("white"));
painter.drawRect(0, 0, width()-1, height()-1);
painter.restore();
if (m_topicDataList.isEmpty())
return;
for (auto i = m_topicDataList.constBegin(); i != m_topicDataList.constEnd(); ++i)
{
TopicEditorBase::TopicData data = *i;
//paintTrackEntry(te);
painter.save();
if (data.id == m_selectedTopicId)
{
painter.setBrush(QColor("red"));
painter.setPen(QPen(QColor("blue"), 2));
}
else
{
painter.setBrush(QColor("white"));
painter.setPen(QPen(QColor("black"), 2));
}
painter.drawRect(topicItemRect(data));
painter.restore();
}
if (m_selectedTopicId > -1 || m_drawWidgetPreviewContent != nullptr) // draw if either the topic is selected or if a preview widget is available (both are different modes)
{
// find selected
for (auto i = m_topicDataList.constBegin(); i != m_topicDataList.constEnd(); ++i)
{
TopicEditorBase::TopicData data = *i;
if (data.id == m_selectedTopicId || m_drawWidgetPreviewContent != nullptr)
{
if (m_drawWidgetPreviewContent != nullptr)
{
//qWarning() << "te x" << data.x << "y" << data.y << "w" << data.w << "h" << data.h;
m_drawWidgetPreviewContent->setGeometry(0, 0, (width() * data.w) - 4, (height() * data.h) - 4);
painter.drawPixmap((width() * data.x) - 2, (height() * data.y) - 2, (width() * data.w) - 4 , (height() * data.h) - 4, m_drawWidgetPreviewContent->grab());
}
paintSelectedTopicItem(&painter, data);
//qCritical() << "drew selected track entry";
}
}
}
}
} // namespace PresentationManagement