/** * @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 #include #include #include #include #include #include #include #include #include #include #include #include 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()) { 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 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 TopicDesigner::exportTopicList() const { QVector 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 &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