/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "QCurveWidget.h" #include <VariableWidgets/ui_QCurveWidget.h> #include <QPainter> #include <QPainterPath> #include <QMouseEvent> #include "../ContextMenu.h" #ifdef EDITOR_QT_UI_EXPORTS #include <QCurveWidget.moc> #endif static const float kControlPointSize = 2; static const float kControlPointHoverDistance = 12; static Q_DECL_CONSTEXPR inline const QPointF operator*(const QPointF& a, const QPointF& b) { return QPointF(a.x() * b.x(), a.y() * b.y()); } const bool QCurveWidget::PointEntry::operator < (const PointEntry& other) { return mPosition.x() < other.mPosition.x(); } static inline QPointF saturate(const QPointF& p) { QPointF clamped; clamped.setX(p.x() < 0.0f ? 0.0f : (p.x() > 1.0f ? 1.0f : p.x())); clamped.setY(p.y() < 0.0f ? 0.0f : (p.y() > 1.0f ? 1.0f : p.y())); return clamped; } QCurveWidget::QCurveWidget(QWidget* parent) : QWidget(parent) , ui(new Ui::QCurveWidget) , m_mouseLeftDown(false) , m_mouseRightDown(false) , m_selectedID(-1) , m_hoverID(-1) , m_uniquePointIdCounter(0) , m_contextMenu(NULL) { ui->setupUi(this); setMouseTracking(true); // Create menu bar setContextMenuPolicy(Qt::CustomContextMenu); m_contextMenu = new ContextMenu(tr("Point Menu"), this); m_contextMenu->addAction(new QAction(tr("Delete"), m_contextMenu)); addPoint(QPointF(0.0f, 0.5f)); addPoint(QPointF(1.0f, 0.5f)); } QCurveWidget::~QCurveWidget() { delete ui; } void QCurveWidget::mouseMoveEvent(QMouseEvent* e) { QWidget::mouseMoveEvent(e); updateHoverControlPoint(e); if (m_selectedID != -1 && m_mouseLeftDown) { setPointPosition(m_selectedID, trans(e->localPos(), true, false, true)); onChange(); } } void QCurveWidget::mousePressEvent(QMouseEvent* e) { QWidget::mousePressEvent(e); // Open control-point context menu if (m_hoverID != -1 && e->button() == Qt::MouseButton::RightButton && m_contextMenu->isEnabled()) { PointEntry* entry = getPoint(m_hoverID); if (entry) { QPointF p = trans(entry->mPosition, true, true, false); QPoint pInt((int)p.x(), (int)p.y()); m_contextMenu->setFocus(); QAction* action = m_contextMenu->exec(mapToGlobal(pInt)); if (action) { onSetFlags(action); m_hoverID = -1; return; } m_hoverID = -1; } } switch (e->button()) { case Qt::MouseButton::LeftButton: m_mouseLeftDown = true; break; case Qt::MouseButton::RightButton: m_mouseRightDown = true; break; default: break; } // Select control point if (m_mouseLeftDown) { const int prevSelectedID = m_selectedID; m_selectedID = -1; if (m_hoverID != -1) { m_selectedID = m_hoverID; } if (prevSelectedID != m_selectedID) { repaint(); } } } void QCurveWidget::mouseReleaseEvent(QMouseEvent* e) { QWidget::mouseReleaseEvent(e); switch (e->button()) { case Qt::MouseButton::LeftButton: m_mouseLeftDown = false; break; case Qt::MouseButton::RightButton: m_mouseRightDown = false; break; default: break; } } void QCurveWidget::mouseDoubleClickEvent(QMouseEvent* e) { QWidget::mouseDoubleClickEvent(e); switch (e->button()) { case Qt::MouseButton::LeftButton: { if (m_selectedID == -1) { QPointF normPos = trans(e->localPos(), true, false, true); addPoint(normPos); updateHoverControlPoint(e); repaint(); onChange(); } break; } case Qt::MouseButton::RightButton: { break; } default: break; } } void QCurveWidget::updateHoverControlPoint(QMouseEvent* e) { const int prevHoverID = m_hoverID; m_hoverID = -1; PointEntry* entry = getPoint(e->localPos(), kControlPointHoverDistance); if (entry != NULL) { m_hoverID = entry->mID; } if (prevHoverID != m_hoverID) { repaint(); } } QCurveWidget::PointEntry& QCurveWidget::addPoint(const QPointF& p, int flags) { m_pointList.push_back(PointEntry(saturate(p), m_uniquePointIdCounter++)); sort(); repaint(); return m_pointList.back(); } void QCurveWidget::deletePoint(int pointID) { for (unsigned int i = 0; i < m_pointList.size(); i++) { if (m_pointList[i].mID == pointID) { m_pointList.erase(m_pointList.begin() + i); repaint(); onChange(); return; } } } void QCurveWidget::clear() { m_pointList.clear(); sort(); repaint(); } QCurveWidget::PointEntry* QCurveWidget::getPoint(int pointID) { for (unsigned int i = 0; i < m_pointList.size(); i++) { PointEntry& entry = m_pointList[i]; if (entry.mID == pointID) { return &entry; } } return NULL; } QCurveWidget::PointEntry* QCurveWidget::getPoint(const QPointF& p, int dist) { const int sqrDist = dist * dist; float closestDist = 1000000.0f; PointEntry* closestEntry = NULL; for (unsigned int i = 0; i < m_pointList.size(); i++) { PointEntry& entry = m_pointList[i]; const QPointF dp = trans(entry.mPosition, true, true, false) - p; const float sqrDp = QPointF::dotProduct(dp, dp); if (sqrDp < closestDist && sqrDp <= sqrDist) { closestDist = sqrDp; closestEntry = &entry; } } return closestEntry; } void QCurveWidget::setPointPosition(int id, QPointF p) { PointEntry* entry = getPoint(id); if (entry) { entry->mPosition = saturate(p); sort(); repaint(); } } QPointF QCurveWidget::trans(const QPointF& p, bool flipY, bool normalizedIn, bool normalizedOut) { QPointF newPoint = p * QPointF(1.0f, flipY ? -1.0f : 1.0f); if (normalizedIn) { if (flipY && normalizedIn) { newPoint.setY(newPoint.y() + 1.0f); } if (!normalizedOut) { newPoint = newPoint * QPointF(width(), height()); } } else { if (flipY) { newPoint.setY(height() + newPoint.y()); } if (normalizedOut) { newPoint = newPoint * QPointF(1.0f / width(), 1.0f / height()); } } return newPoint; } // Sort points in ascending order on the Axis void QCurveWidget::sort() { std::sort(m_pointList.begin(), m_pointList.end()); } void QCurveWidget::setCustomLine(const PointList& line) { m_customLine = line; } void QCurveWidget::onChange() { } void QCurveWidget::onSetFlags(QAction* a) { const QString& t = a->text(); PointEntry* p = getPoint(m_hoverID); if (t == "Delete") { deletePoint(m_hoverID); } } void QCurveWidget::paintEvent(QPaintEvent* e) { QWidget::paintEvent(e); const QPointF scale(width(), height()); const QPointF scaleInv(1.0f / width(), 1.0f / height()); QPainter painter(this); painter.setClipping(true); painter.setClipRect(0, 0, width(), height()); // Flip Y-axis QMatrix m = painter.matrix(); m.setMatrix(m.m11(), -m.m12(), m.m21(), -m.m22(), m.dx(), height()); painter.setMatrix(m); QPen pen; // Draw background rectangle { painter.fillRect(QRect(0, 0, width(), height()), QColor(128, 128, 128)); } // Draw Grid { pen.setWidth(1); pen.setColor(QColor(96, 96, 96)); pen.setStyle(Qt::PenStyle::SolidLine); painter.setPen(pen); for (unsigned int i = 1; i < 10; i++) { const float w = 1.0f / 10 * i * width(); const float h = 1.0f / 10 * i * height(); if (i == 5) { pen.setStyle(Qt::PenStyle::SolidLine); painter.setPen(pen); } else { pen.setStyle(Qt::PenStyle::DotLine); painter.setPen(pen); } painter.drawLine(QPointF(0.0f, h), QPointF(width(), h)); painter.drawLine(QPointF(w, 0.0f), QPointF(w, height())); } } // Draw Curves { QPainterPath path; pen.setWidth(2); pen.setColor(QColor(0, 255, 128)); pen.setStyle(Qt::PenStyle::SolidLine); painter.setRenderHint(QPainter::Antialiasing, true); painter.setPen(pen); std::vector<QPointF> drawList; if (m_customLine.empty()) { if (m_pointList.size() > 1) { if (m_pointList.front().mPosition.x() > 0.0f) { drawList.push_back(QPointF(0.0f, m_pointList.front().mPosition.y())); } for (unsigned int i = 0; i < m_pointList.size(); i++) { drawList.push_back(m_pointList[i].mPosition); } if (drawList.back().x() < 1.0f) { drawList.push_back(QPointF(1.0f, drawList.back().y())); } // Set begin point and setup the draw-path path.moveTo(drawList.front() * scale); for (int i = 1; i < drawList.size(); i++) { QPointF posCurr = path.currentPosition() * scaleInv; QPointF posNext = drawList[i]; // For now, just flatten current control points QPointF c1 = (posCurr + posNext) * 0.5f * scale; QPointF c2 = (posCurr + posNext) * 0.5f * scale; path.cubicTo(c1, c2, posNext * scale); } } } else if (m_customLine.size() > 1) { for (unsigned int i = 0; i < m_customLine.size(); i++) { drawList.push_back(m_customLine[i] * scale); } path.moveTo(drawList.front()); for (unsigned int i = 1; i < drawList.size(); i++) { path.lineTo(drawList[i]); } } painter.drawPath(path); } // Draw control points { painter.setRenderHint(QPainter::Antialiasing, true); for (unsigned int i = 0; i < m_pointList.size(); i++) { PointEntry& entry = m_pointList[i]; const QPointF p = entry.mPosition * scale; QColor color(0, 255, 255); if (entry.mID == m_selectedID) { color = QColor(255, 0, 0); } else if (entry.mID == m_hoverID) { color = QColor(255, 255, 0); } pen.setColor(color); painter.setBrush(color); painter.setPen(pen); painter.drawEllipse(p, kControlPointSize, kControlPointSize); } painter.setBrush(Qt::NoBrush); } // Draw borders { pen.setWidth(2); pen.setColor(QColor(0, 0, 0)); painter.setRenderHint(QPainter::Antialiasing, false); painter.setPen(pen); painter.drawRect(QRect(0, 0, width(), height())); } }