diff options
Diffstat (limited to 'src/plugins/qmldesigner/components/timelineeditor')
141 files changed, 13327 insertions, 0 deletions
diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvas.cpp b/src/plugins/qmldesigner/components/timelineeditor/canvas.cpp new file mode 100644 index 0000000000..eab3d0bf56 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvas.cpp @@ -0,0 +1,359 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "canvas.h" +#include "easingcurve.h" + +#include <QPainter> +#include <QPointF> +#include <QSize> + +namespace QmlDesigner { + +Canvas::Canvas(int width, + int height, + int marginX, + int marginY, + int cellCountX, + int cellCountY, + int offsetX, + int offsetY) + : m_width(width) + , m_height(height) + , m_marginX(marginX) + , m_marginY(marginY) + , m_cellCountX(cellCountX) + , m_cellCountY(cellCountY) + , m_offsetX(offsetX) + , m_offsetY(offsetY) + , m_scale(1.0) +{} + +QRectF Canvas::gridRect() const +{ + double w = static_cast<double>(m_width); + double h = static_cast<double>(m_height); + double mx = static_cast<double>(m_marginX); + double my = static_cast<double>(m_marginY); + double gw = w - 2.0 * mx; + double gh = h - 2.0 * my; + + if (m_style.aspect != 0) { + if (m_style.aspect < (w / h)) + gw = gh * m_style.aspect; + else + gh = gw / m_style.aspect; + } + + auto rect = QRectF(mx, my, gw * m_scale, gh * m_scale); + rect.moveCenter(QPointF(w / 2.0, h / 2.0)); + return rect; +} + +void Canvas::setCanvasStyle(const CanvasStyle &style) +{ + m_style = style; +} + +void Canvas::setScale(double scale) +{ + if (scale > 0.05) + m_scale = scale; +} + +void Canvas::resize(const QSize &size) +{ + m_width = size.width(); + m_height = size.height(); +} + +void Canvas::paintGrid(QPainter *painter, const QBrush &background) +{ + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + QPen pen = painter->pen(); + + pen.setWidthF(m_style.thinLineWidth); + pen.setColor(m_style.thinLineColor); + painter->setPen(pen); + + painter->fillRect(0, 0, m_width, m_height, background); + + QRectF rect = gridRect(); + + // Thin lines. + const int lineCountX = m_cellCountX + 1; + const double cellWidth = rect.width() / static_cast<double>(m_cellCountX); + + // Vertical + double x = rect.left(); + for (int i = 0; i < lineCountX; ++i) { + paintLine(painter, QPoint(x, rect.top()), QPoint(x, rect.bottom())); + x += cellWidth; + } + + const int lineCountY = m_cellCountY + 1; + const double cellHeight = rect.height() / static_cast<double>(m_cellCountY); + + // Horizontal + double y = rect.top(); + for (int i = 0; i < lineCountY; ++i) { + paintLine(painter, QPoint(rect.left(), y), QPoint(rect.right(), y)); + y += cellHeight; + } + + // Thick lines. + pen.setWidthF(m_style.thickLineWidth); + pen.setColor(m_style.thickLineColor); + painter->setPen(pen); + + if (m_offsetX != 0) { + const int minX = rect.left() + (cellWidth * m_offsetX); + const int maxX = rect.right() - (cellWidth * m_offsetX); + paintLine(painter, QPoint(minX, rect.top()), QPoint(minX, rect.bottom())); + paintLine(painter, QPoint(maxX, rect.top()), QPoint(maxX, rect.bottom())); + } + + if (m_offsetY != 0) { + const int minY = rect.top() + (cellHeight * m_offsetY); + const int maxY = rect.bottom() - (cellHeight * m_offsetY); + paintLine(painter, QPoint(rect.left(), minY), QPoint(rect.right(), minY)); + paintLine(painter, QPoint(rect.left(), maxY), QPoint(rect.right(), maxY)); + } + + painter->restore(); +} + +void Canvas::paintCurve(QPainter *painter, const EasingCurve &curve, const QColor &color) +{ + EasingCurve mapped = mapTo(curve); + painter->strokePath(mapped.path(), QPen(QBrush(color), m_style.curveWidth)); +} + +void Canvas::paintControlPoints(QPainter *painter, const EasingCurve &curve) +{ + QVector<QPointF> points = curve.toCubicSpline(); + int count = points.count(); + + if (count <= 1) + return; + + painter->save(); + + QPen pen = painter->pen(); + pen.setWidthF(m_style.handleLineWidth); + pen.setColor(m_style.endPointColor); + + painter->setPen(pen); + painter->setBrush(m_style.endPointColor); + + // First and last point including handle. + paintLine(painter, mapTo(QPointF(0.0, 0.0)).toPoint(), mapTo(points.at(0)).toPoint()); + paintPoint(painter, QPointF(0.0, 0.0), false); + paintPoint(painter, points.at(0), false, curve.active() == 0); + + paintLine(painter, mapTo(QPointF(1.0, 1.0)).toPoint(), mapTo(points.at(count - 2)).toPoint()); + paintPoint(painter, QPointF(1.0, 1.0), false); + paintPoint(painter, points.at(count - 2), false, curve.active() == (count - 2)); + + pen.setColor(m_style.interPointColor); + painter->setPen(pen); + painter->setBrush(m_style.interPointColor); + + for (int i = 0; i < count - 1; ++i) { + if (curve.isHandle(i)) + continue; + + paintLine(painter, mapTo(points.at(i)).toPoint(), mapTo(points.at(i + 1)).toPoint()); + + if (i > 0) + paintLine(painter, mapTo(points.at(i - 1)).toPoint(), mapTo(points.at(i)).toPoint()); + } + + // Paint Points. + int active = curve.active(); + for (int i = 1; i < count - 2; ++i) + paintPoint(painter, points.at(i), curve.isSmooth(i), active == i); + + painter->restore(); +} + +void Canvas::paintProgress(QPainter *painter, const EasingCurve &curve, double progress) +{ + painter->save(); + + painter->setPen(Qt::green); + painter->setBrush(QBrush(Qt::green)); + + QPointF pos1(progress, curve.valueForProgress(progress)); + pos1 = mapTo(pos1); + + QRectF rect = gridRect(); + + painter->drawLine(rect.left(), pos1.y(), rect.right(), pos1.y()); + painter->drawLine(pos1.x(), rect.top(), pos1.x(), rect.bottom()); + + painter->restore(); +} + +QPointF Canvas::mapTo(const QPointF &point) const +{ + QRectF rect = gridRect(); + + const double cellWidth = rect.width() / static_cast<double>(m_cellCountX); + const double cellHeight = rect.height() / static_cast<double>(m_cellCountY); + + const double offsetX = cellWidth * m_offsetX; + const double offsetY = cellHeight * m_offsetY; + + const int width = rect.width() - 2 * offsetX; + const int height = rect.height() - 2 * offsetY; + + auto tmp = QPointF(point.x() * width + rect.left() + offsetX, + height - point.y() * height + rect.top() + offsetY); + + return tmp; +} + +CanvasStyle Canvas::canvasStyle() const +{ + return m_style; +} + +double Canvas::scale() const +{ + return m_scale; +} + +QPointF Canvas::normalize(const QPointF &point) const +{ + QRectF rect = gridRect(); + return QPointF(point.x() / rect.width(), point.y() / rect.height()); +} + +EasingCurve Canvas::mapTo(const EasingCurve &curve) const +{ + QVector<QPointF> controlPoints = curve.toCubicSpline(); + + for (auto &point : controlPoints) + point = mapTo(point); + + return EasingCurve(mapTo(curve.start()), controlPoints); +} + +QPointF Canvas::mapFrom(const QPointF &point) const +{ + QRectF rect = gridRect(); + + const double cellWidth = rect.width() / static_cast<double>(m_cellCountX); + const double cellHeight = rect.height() / static_cast<double>(m_cellCountY); + + const double offsetX = cellWidth * m_offsetX; + const double offsetY = cellHeight * m_offsetY; + + const int width = rect.width() - 2 * offsetX; + const int height = rect.height() - 2 * offsetY; + + return QPointF((point.x() - rect.left() - offsetX) / width, + 1 - (point.y() - rect.top() - offsetY) / height); +} + +EasingCurve Canvas::mapFrom(const EasingCurve &curve) const +{ + QVector<QPointF> controlPoints = curve.toCubicSpline(); + for (auto &point : controlPoints) + point = mapFrom(point); + + EasingCurve result; + result.fromCubicSpline(controlPoints); + return result; +} + +QPointF Canvas::clamp(const QPointF &point) const +{ + QRectF r = gridRect(); + QPointF p = point; + + if (p.x() > r.right()) + p.rx() = r.right(); + + if (p.x() < r.left()) + p.rx() = r.left(); + + if (p.y() < r.top()) + p.ry() = r.top(); + + if (p.y() > r.bottom()) + p.ry() = r.bottom(); + + return p; +} + +void Canvas::paintLine(QPainter *painter, const QPoint &p1, const QPoint &p2) +{ + painter->drawLine(p1 + QPointF(0.5, 0.5), p2 + QPointF(0.5, 0.5)); +} + +void Canvas::paintPoint(QPainter *painter, const QPointF &point, bool smooth, bool active) +{ + const double pointSize = m_style.handleSize; + const double activePointSize = pointSize + 2; + if (smooth) { + if (active) { + painter->save(); + painter->setPen(Qt::white); + painter->setBrush(QBrush()); + painter->drawEllipse(QRectF(mapTo(point).x() - activePointSize + 0.5, + mapTo(point).y() - activePointSize + 0.5, + activePointSize * 2, + activePointSize * 2)); + painter->restore(); + } + + painter->drawEllipse(QRectF(mapTo(point).x() - pointSize + 0.5, + mapTo(point).y() - pointSize + 0.5, + pointSize * 2, + pointSize * 2)); + + } else { + if (active) { + painter->save(); + painter->setPen(Qt::white); + painter->setBrush(QBrush()); + painter->drawRect(QRectF(mapTo(point).x() - activePointSize + 0.5, + mapTo(point).y() - activePointSize + 0.5, + activePointSize * 2, + activePointSize * 2)); + painter->restore(); + } + painter->drawRect(QRectF(mapTo(point).x() - pointSize + 0.5, + mapTo(point).y() - pointSize + 0.5, + pointSize * 2, + pointSize * 2)); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvas.h b/src/plugins/qmldesigner/components/timelineeditor/canvas.h new file mode 100644 index 0000000000..c4491e4b79 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvas.h @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "canvasstyledialog.h" +#include <qglobal.h> + +QT_FORWARD_DECLARE_CLASS(QBrush); +QT_FORWARD_DECLARE_CLASS(QColor); +QT_FORWARD_DECLARE_CLASS(QPainter); +QT_FORWARD_DECLARE_CLASS(QPointF); +QT_FORWARD_DECLARE_CLASS(QPoint); +QT_FORWARD_DECLARE_CLASS(QSize); + +namespace QmlDesigner { + +class EasingCurve; + +class Canvas +{ +public: + Canvas(int width, + int height, + int marginX, + int marginY, + int cellCountX, + int cellCountY, + int offsetX, + int offsetY); + +public: + CanvasStyle canvasStyle() const; + + double scale() const; + + QRectF gridRect() const; + + QPointF normalize(const QPointF &point) const; + + QPointF mapTo(const QPointF &point) const; + + EasingCurve mapTo(const EasingCurve &curve) const; + + QPointF mapFrom(const QPointF &point) const; + + EasingCurve mapFrom(const EasingCurve &curve) const; + + QPointF clamp(const QPointF &point) const; + + void setCanvasStyle(const CanvasStyle &style); + + void setScale(double scale); + + void resize(const QSize &size); + + void paintGrid(QPainter *painter, const QBrush &background); + + void paintCurve(QPainter *painter, const EasingCurve &curve, const QColor &color); + + void paintControlPoints(QPainter *painter, const EasingCurve &curve); + + void paintProgress(QPainter *painter, const EasingCurve &curve, double progress); + +private: + void paintLine(QPainter *painter, const QPoint &p1, const QPoint &p2); + + void paintPoint(QPainter *painter, const QPointF &point, bool smooth, bool active = false); + +private: + int m_width; + int m_height; + + int m_marginX; + int m_marginY; + + int m_cellCountX; + int m_cellCountY; + + int m_offsetX; + int m_offsetY; + + double m_scale; + + CanvasStyle m_style; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp new file mode 100644 index 0000000000..88d78615ca --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "canvasstyledialog.h" + +#include <QColorDialog> +#include <QDoubleSpinBox> +#include <QLabel> +#include <QPaintEvent> +#include <QPainter> +#include <QVBoxLayout> + +namespace QmlDesigner { + +CanvasStyleDialog::CanvasStyleDialog(const CanvasStyle &style, QWidget *parent) + : QDialog(parent) + , m_aspect(new QDoubleSpinBox(this)) + , m_thinLineWidth(new QDoubleSpinBox(this)) + , m_thickLineWidth(new QDoubleSpinBox(this)) + , m_thinLineColor(new ColorControl(style.thinLineColor, this)) + , m_thickLineColor(new ColorControl(style.thickLineColor, this)) + , m_handleSize(new QDoubleSpinBox(this)) + , m_handleLineWidth(new QDoubleSpinBox(this)) + , m_endPointColor(new ColorControl(style.endPointColor, this)) + , m_interPointColor(new ColorControl(style.interPointColor, this)) + , m_curveWidth(new QDoubleSpinBox(this)) +{ + m_aspect->setValue(style.aspect); + m_thinLineWidth->setValue(style.thinLineWidth); + m_thickLineWidth->setValue(style.thickLineWidth); + m_handleSize->setValue(style.handleSize); + m_handleLineWidth->setValue(style.handleLineWidth); + m_curveWidth->setValue(style.curveWidth); + + int labelWidth = QFontMetrics(this->font()).horizontalAdvance("Inter Handle ColorXX"); + auto addControl = [labelWidth](QVBoxLayout *layout, const QString &name, QWidget *control) { + auto *hbox = new QHBoxLayout; + + QLabel *label = new QLabel(name); + label->setAlignment(Qt::AlignLeft); + label->setFixedWidth(labelWidth); + + hbox->addWidget(label); + hbox->addWidget(control); + layout->addLayout(hbox); + }; + + auto layout = new QVBoxLayout; + addControl(layout, "Aspect Ratio", m_aspect); + + addControl(layout, "Thin Line Width", m_thinLineWidth); + addControl(layout, "Thin Line Color", m_thinLineColor); + + addControl(layout, "Thick Line Width", m_thickLineWidth); + addControl(layout, "Thick Line Color", m_thickLineColor); + + addControl(layout, "Handle Size", m_handleSize); + addControl(layout, "Handle Line Width", m_handleLineWidth); + addControl(layout, "End Handle Color", m_endPointColor); + addControl(layout, "Inter Handle Color", m_interPointColor); + + addControl(layout, "Curve Width", m_curveWidth); + + setLayout(layout); + + auto emitValueChanged = [this]() { + CanvasStyle out; + out.aspect = m_aspect->value(); + out.thinLineWidth = m_thinLineWidth->value(); + out.thickLineWidth = m_thickLineWidth->value(); + out.thinLineColor = m_thinLineColor->value(); + out.thickLineColor = m_thickLineColor->value(); + out.handleSize = m_handleSize->value(); + out.handleLineWidth = m_handleLineWidth->value(); + out.endPointColor = m_endPointColor->value(); + out.interPointColor = m_interPointColor->value(); + out.curveWidth = m_curveWidth->value(); + emit styleChanged(out); + }; + + auto doubleValueChanged = QOverload<double>::of( + &QDoubleSpinBox::valueChanged); + auto colorValueChanged = &ColorControl::valueChanged; + + connect(m_aspect, doubleValueChanged, this, emitValueChanged); + + connect(m_thinLineWidth, doubleValueChanged, this, emitValueChanged); + connect(m_thickLineWidth, doubleValueChanged, this, emitValueChanged); + + connect(m_thinLineColor, colorValueChanged, this, emitValueChanged); + connect(m_thickLineColor, colorValueChanged, this, emitValueChanged); + + connect(m_handleSize, doubleValueChanged, this, emitValueChanged); + connect(m_handleLineWidth, doubleValueChanged, this, emitValueChanged); + + connect(m_endPointColor, colorValueChanged, this, emitValueChanged); + connect(m_interPointColor, colorValueChanged, this, emitValueChanged); + + connect(m_curveWidth, doubleValueChanged, this, emitValueChanged); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h new file mode 100644 index 0000000000..b3b1a86c95 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinecontrols.h" + +#include <QColor> +#include <QDialog> + +QT_FORWARD_DECLARE_CLASS(QDoubleSpinBox); + +namespace QmlDesigner { + +struct CanvasStyle +{ + qreal aspect = 1.5; + + qreal thinLineWidth = 0.3; + qreal thickLineWidth = 2.5; + + QColor thinLineColor = qRgb(0x99, 0x99, 0x99); + QColor thickLineColor = qRgb(0x5f, 0x5f, 0x5f); + + qreal handleSize = 7.0; + qreal handleLineWidth = 2.0; + + QColor endPointColor = qRgb(0xd6, 0xd3, 0x51); + QColor interPointColor = qRgb(0xce, 0x17, 0x17); + + qreal curveWidth = 3.0; +}; + +class CanvasStyleDialog : public QDialog +{ + Q_OBJECT + +public: + CanvasStyleDialog(const CanvasStyle &style, QWidget *parent = nullptr); + +signals: + void styleChanged(const CanvasStyle &style); + +private: + QDoubleSpinBox *m_aspect; + + QDoubleSpinBox *m_thinLineWidth; + QDoubleSpinBox *m_thickLineWidth; + + ColorControl *m_thinLineColor; + ColorControl *m_thickLineColor; + + QDoubleSpinBox *m_handleSize; + QDoubleSpinBox *m_handleLineWidth; + + ColorControl *m_endPointColor; + ColorControl *m_interPointColor; + + QDoubleSpinBox *m_curveWidth; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp new file mode 100644 index 0000000000..cae0bfab09 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp @@ -0,0 +1,502 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "easingcurve.h" +#include "timelineutils.h" + +#include <QDataStream> +#include <QDebug> +#include <QLineF> +#include <QPainterPath> +#include <QPointF> + +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +EasingCurve::EasingCurve() + : QEasingCurve(QEasingCurve::BezierSpline) + , m_active(-1) + , m_start(0.0, 0.0) +{} + +EasingCurve::EasingCurve(const QEasingCurve &curve) + : QEasingCurve(curve) + , m_active(-1) + , m_start(0.0, 0.0) +{} + +EasingCurve::EasingCurve(const EasingCurve &curve) = default; + +EasingCurve::EasingCurve(const QPointF &start, const QVector<QPointF> &points) + : QEasingCurve(QEasingCurve::BezierSpline) + , m_active(-1) + , m_start(start) +{ + fromCubicSpline(points); +} + +EasingCurve &EasingCurve::operator=(const EasingCurve &curve) = default; + +EasingCurve::~EasingCurve() = default; + +bool EasingCurve::IsValidIndex(int idx) +{ + return idx >= 0; +} + +void EasingCurve::registerStreamOperators() +{ + qRegisterMetaType<QmlDesigner::EasingCurve>("QmlDesigner::EasingCurve"); + qRegisterMetaType<QmlDesigner::NamedEasingCurve>("QmlDesigner::NamedEasingCurve"); + qRegisterMetaTypeStreamOperators<QmlDesigner::EasingCurve>("QmlDesigner::EasingCurve"); + qRegisterMetaTypeStreamOperators<QmlDesigner::NamedEasingCurve>("QmlDesigner::NamedEasingCurve"); +} + +int EasingCurve::count() const +{ + return toCubicSpline().count(); +} + +int EasingCurve::active() const +{ + return m_active; +} + +int EasingCurve::segmentCount() const +{ + return toCubicSpline().count() / 3; +} + +bool EasingCurve::isLegal() const +{ + QPainterPath painterPath(path()); + + double increment = 1.0 / 30.0; + QPointF max = painterPath.pointAtPercent(0.0); + for (double i = increment; i <= 1.0; i += increment) { + QPointF current = painterPath.pointAtPercent(i); + if (current.x() < max.x()) + return false; + else + max = current; + } + return true; +} + +bool EasingCurve::hasActive() const +{ + QTC_ASSERT(m_active < toCubicSpline().size(), return false); + return m_active >= 0; +} + +bool EasingCurve::isValidIndex(int idx) const +{ + return idx >= 0 && idx < toCubicSpline().size(); +} + +bool EasingCurve::isSmooth(int idx) const +{ + auto iter = std::find(m_smoothIds.begin(), m_smoothIds.end(), idx); + return iter != m_smoothIds.end(); +} + +bool EasingCurve::isHandle(int idx) const +{ + return (idx + 1) % 3; +} + +bool EasingCurve::isLeftHandle(int idx) const +{ + return ((idx + 2) % 3) == 0; +} + +QString EasingCurve::toString() const +{ + QLatin1Char c(','); + QString s = QLatin1String("["); + for (const QPointF &point : toCubicSpline()) { + auto x = QString::number(point.x(), 'g', 3); + auto y = QString::number(point.y(), 'g', 3); + s += x + c + y + c; + } + + // Replace last "," with "]" + s.chop(1); + s.append(QLatin1Char(']')); + + return s; +} + +bool EasingCurve::fromString(const QString &code) +{ + if (code.startsWith(QLatin1Char('[')) && code.endsWith(QLatin1Char(']'))) { + const QStringRef cleanCode(&code, 1, code.size() - 2); + const auto stringList = cleanCode.split(QLatin1Char(','), QString::SkipEmptyParts); + + if (stringList.count() >= 6 && (stringList.count() % 6 == 0)) { + bool checkX, checkY; + QVector<QPointF> points; + for (int i = 0; i < stringList.count(); ++i) { + QPointF point; + point.rx() = stringList[i].toDouble(&checkX); + point.ry() = stringList[++i].toDouble(&checkY); + + if (!checkX || !checkY) + return false; + + points.push_back(point); + } + + if (points.constLast() != QPointF(1.0, 1.0)) + return false; + + QEasingCurve easingCurve(QEasingCurve::BezierSpline); + + for (int i = 0; i < points.count() / 3; ++i) { + easingCurve.addCubicBezierSegment(points.at(i * 3), + points.at(i * 3 + 1), + points.at(i * 3 + 2)); + } + + fromCubicSpline(easingCurve.toCubicSpline()); + return true; + } + } + return false; +} + +QPointF EasingCurve::start() const +{ + return m_start; +} + +QPointF EasingCurve::end() const +{ + return toCubicSpline().last(); +} + +QPainterPath EasingCurve::path() const +{ + QPainterPath path; + path.moveTo(m_start); + + QVector<QPointF> controlPoints = toCubicSpline(); + + int numSegments = controlPoints.count() / 3; + for (int i = 0; i < numSegments; i++) { + QPointF p1 = controlPoints.at(i * 3); + QPointF p2 = controlPoints.at(i * 3 + 1); + QPointF p3 = controlPoints.at(i * 3 + 2); + path.cubicTo(p1, p2, p3); + } + + return path; +} + +int EasingCurve::curvePoint(int idx) const +{ + if (isHandle(idx)) { + if (isLeftHandle(idx)) + return idx + 1; + else + return idx - 1; + } + return idx; +} + +QPointF EasingCurve::point(int idx) const +{ + QVector<QPointF> controlPoints = toCubicSpline(); + + QTC_ASSERT(controlPoints.count() > idx || idx < 0, return QPointF()); + + return controlPoints.at(idx); +} + +int EasingCurve::hit(const QPointF &point, double threshold) const +{ + int id = -1; + qreal distance = std::numeric_limits<qreal>::max(); + + QVector<QPointF> controlPoints = toCubicSpline(); + for (int i = 0; i < controlPoints.size() - 1; ++i) { + qreal d = QLineF(point, controlPoints.at(i)).length(); + if (d < threshold && d < distance) { + distance = d; + id = i; + } + } + return id; +} + +void EasingCurve::makeDefault() +{ + QVector<QPointF> controlPoints; + controlPoints.append(QPointF(0.0, 0.2)); + controlPoints.append(QPointF(0.3, 0.5)); + controlPoints.append(QPointF(0.5, 0.5)); + + controlPoints.append(QPointF(0.7, 0.5)); + controlPoints.append(QPointF(1.0, 0.8)); + controlPoints.append(QPointF(1.0, 1.0)); + + fromCubicSpline(controlPoints); + + m_smoothIds.push_back(2); +} + +void EasingCurve::clearActive() +{ + m_active = -1; +} + +void EasingCurve::setActive(int idx) +{ + m_active = idx; +} + +void EasingCurve::makeSmooth(int idx) +{ + if (!isSmooth(idx) && !isHandle(idx)) { + QVector<QPointF> controlPoints = toCubicSpline(); + + QPointF before = m_start; + if (idx > 3) + before = controlPoints.at(idx - 3); + + QPointF after = end(); + if ((idx + 3) < controlPoints.count()) + after = controlPoints.at(idx + 3); + + QPointF tangent = (after - before) / 6; + + QPointF thisPoint = controlPoints.at(idx); + + if (idx > 0) + controlPoints[idx - 1] = thisPoint - tangent; + + if (idx + 1 < controlPoints.count()) + controlPoints[idx + 1] = thisPoint + tangent; + + fromCubicSpline(controlPoints); + + m_smoothIds.push_back(idx); + } +} + +void EasingCurve::breakTangent(int idx) +{ + if (isSmooth(idx) && !isHandle(idx)) { + QVector<QPointF> controlPoints = toCubicSpline(); + + QPointF before = m_start; + if (idx > 3) + before = controlPoints.at(idx - 3); + + QPointF after = end(); + if ((idx + 3) < controlPoints.count()) + after = controlPoints.at(idx + 3); + + QPointF thisPoint = controlPoints.at(idx); + + if (idx > 0) + controlPoints[idx - 1] = (before - thisPoint) / 3 + thisPoint; + + if (idx + 1 < controlPoints.count()) + controlPoints[idx + 1] = (after - thisPoint) / 3 + thisPoint; + + fromCubicSpline(controlPoints); + + auto iter = std::find(m_smoothIds.begin(), m_smoothIds.end(), idx); + m_smoothIds.erase(iter); + } +} + +void EasingCurve::addPoint(const QPointF &point) +{ + QVector<QPointF> controlPoints = toCubicSpline(); + + int splitIndex = 0; + for (int i = 0; i < controlPoints.size() - 1; ++i) { + if (!isHandle(i)) { + if (controlPoints.at(i).x() > point.x()) + break; + + splitIndex = i; + } + } + + QPointF before = m_start; + if (splitIndex > 0) { + before = controlPoints.at(splitIndex); + } + + QPointF after = end(); + if ((splitIndex + 3) < controlPoints.count()) { + after = controlPoints.at(splitIndex + 3); + } + + int newIdx; + + if (splitIndex > 0) { + newIdx = splitIndex + 3; + controlPoints.insert(splitIndex + 2, (point + after) / 2); + controlPoints.insert(splitIndex + 2, point); + controlPoints.insert(splitIndex + 2, (point + before) / 2); + } else { + newIdx = splitIndex + 2; + controlPoints.insert(splitIndex + 1, (point + after) / 2); + controlPoints.insert(splitIndex + 1, point); + controlPoints.insert(splitIndex + 1, (point + before) / 2); + } + + fromCubicSpline(controlPoints); + + QTC_ASSERT(!isHandle(newIdx), return ); + + m_active = newIdx; + + breakTangent(newIdx); + makeSmooth(newIdx); +} + +void EasingCurve::setPoint(int idx, const QPointF &point) +{ + if (!isValidIndex(idx)) + return; + + QVector<QPointF> controlPoints = toCubicSpline(); + + controlPoints[idx] = point; + + fromCubicSpline(controlPoints); +} + +void EasingCurve::movePoint(int idx, const QPointF &vector) +{ + if (!isValidIndex(idx)) + return; + + QVector<QPointF> controlPoints = toCubicSpline(); + + controlPoints[idx] += vector; + + fromCubicSpline(controlPoints); +} + +void EasingCurve::deletePoint(int idx) +{ + if (!isValidIndex(idx)) + return; + + QVector<QPointF> controlPoints = toCubicSpline(); + + controlPoints.remove(idx - 1, 3); + + fromCubicSpline(controlPoints); +} + +void EasingCurve::fromCubicSpline(const QVector<QPointF> &points) +{ + QEasingCurve tmp(QEasingCurve::BezierSpline); + + int numSegments = points.count() / 3; + for (int i = 0; i < numSegments; ++i) { + tmp.addCubicBezierSegment(points.at(i * 3), points.at(i * 3 + 1), points.at(i * 3 + 2)); + } + swap(tmp); +} + +QDebug &operator<<(QDebug &stream, const EasingCurve &curve) +{ + stream << static_cast<QEasingCurve>(curve); + stream << "\"active:" << curve.m_active << "\""; + stream << "\"smooth ids:" << curve.m_smoothIds << "\""; + return stream; +} + +QDataStream &operator<<(QDataStream &stream, const EasingCurve &curve) +{ + // Ignore the active flag. + stream << static_cast<QEasingCurve>(curve); + stream << curve.toCubicSpline(); + stream << curve.m_smoothIds; + return stream; +} + +QDataStream &operator>>(QDataStream &stream, EasingCurve &curve) +{ + // This is to circumvent a bug in QEasingCurve serialization. + QVector<QPointF> points; + + // Ignore the active flag. + stream >> static_cast<QEasingCurve &>(curve); + stream >> points; + curve.fromCubicSpline(points); + stream >> curve.m_smoothIds; + + return stream; +} + +NamedEasingCurve::NamedEasingCurve() + : m_name() + , m_curve() +{} + +NamedEasingCurve::NamedEasingCurve(const QString &name, const EasingCurve &curve) + : m_name(name) + , m_curve(curve) +{} + +NamedEasingCurve::NamedEasingCurve(const NamedEasingCurve &other) = default; + +NamedEasingCurve::~NamedEasingCurve() = default; + +QString NamedEasingCurve::name() const +{ + return m_name; +} + +EasingCurve NamedEasingCurve::curve() const +{ + return m_curve; +} + +QDataStream &operator<<(QDataStream &stream, const NamedEasingCurve &curve) +{ + stream << curve.m_name; + stream << curve.m_curve; + return stream; +} + +QDataStream &operator>>(QDataStream &stream, NamedEasingCurve &curve) +{ + stream >> curve.m_name; + stream >> curve.m_curve; + return stream; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurve.h b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.h new file mode 100644 index 0000000000..d46f5600aa --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.h @@ -0,0 +1,157 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QEasingCurve> +#include <QMetaType> +#include <QPointF> + +QT_FORWARD_DECLARE_CLASS(QPainterPath); + +namespace QmlDesigner { + +class EasingCurve : public QEasingCurve +{ +public: + EasingCurve(); + + EasingCurve(const QEasingCurve &curve); + + EasingCurve(const EasingCurve &curve); + + EasingCurve(const QPointF &start, const QVector<QPointF> &points); + + virtual ~EasingCurve(); + + EasingCurve &operator=(const EasingCurve &curve); + + static bool IsValidIndex(int idx); + + static void registerStreamOperators(); + +public: + int count() const; + + int active() const; + + int segmentCount() const; + + bool hasActive() const; + + bool isLegal() const; + + bool isValidIndex(int idx) const; + + bool isSmooth(int idx) const; + + bool isHandle(int idx) const; + + bool isLeftHandle(int idx) const; + + QString toString() const; + + QPointF start() const; + + QPointF end() const; + + QPainterPath path() const; + + int curvePoint(int idx) const; + + QPointF point(int idx) const; + + int hit(const QPointF &point, double threshold) const; + +public: + void makeDefault(); + + void clearActive(); + + void setActive(int idx); + + void makeSmooth(int idx); + + void breakTangent(int idx); + + void addPoint(const QPointF &point); + + void setPoint(int idx, const QPointF &point); + + void movePoint(int idx, const QPointF &vector); + + void deletePoint(int idx); + + bool fromString(const QString &string); + + void fromCubicSpline(const QVector<QPointF> &points); + + friend QDebug &operator<<(QDebug &stream, const EasingCurve &curve); + + friend QDataStream &operator<<(QDataStream &stream, const EasingCurve &curve); + + friend QDataStream &operator>>(QDataStream &stream, EasingCurve &curve); + + friend std::ostream &operator<<(std::ostream &stream, const EasingCurve &curve); + + friend std::istream &operator>>(std::istream &stream, EasingCurve &curve); + +private: + int m_active; + + QPointF m_start; + + std::vector<int> m_smoothIds; +}; + +class NamedEasingCurve +{ +public: + NamedEasingCurve(); + + NamedEasingCurve(const QString &name, const EasingCurve &curve); + + NamedEasingCurve(const NamedEasingCurve &other); + + virtual ~NamedEasingCurve(); + + QString name() const; + + EasingCurve curve() const; + + friend QDataStream &operator<<(QDataStream &stream, const NamedEasingCurve &curve); + + friend QDataStream &operator>>(QDataStream &stream, NamedEasingCurve &curve); + +private: + QString m_name; + + EasingCurve m_curve; +}; + +} // namespace QmlDesigner + +Q_DECLARE_METATYPE(QmlDesigner::EasingCurve); +Q_DECLARE_METATYPE(QmlDesigner::NamedEasingCurve); diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp new file mode 100644 index 0000000000..a069dc187b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp @@ -0,0 +1,298 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "easingcurvedialog.h" + +#include "preseteditor.h" +#include "splineeditor.h" + +#include <QApplication> +#include <QGridLayout> +#include <QGroupBox> +#include <QHBoxLayout> +#include <QLabel> +#include <QMessageBox> +#include <QPlainTextEdit> +#include <QPushButton> +#include <QSizePolicy> +#include <QSpinBox> +#include <QTabBar> +#include <QTabWidget> +#include <QVBoxLayout> + +#include <abstractview.h> +#include <bindingproperty.h> +#include <rewritingexception.h> +#include <theme.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +EasingCurveDialog::EasingCurveDialog(const QList<ModelNode> &frames, QWidget *parent) + : QDialog(parent) + , m_splineEditor(new SplineEditor(this)) + , m_text(new QPlainTextEdit(this)) + , m_presets(new PresetEditor(this)) + , m_durationLayout(new QHBoxLayout) + , m_buttons(new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel + | QDialogButtonBox::Ok)) + , m_label(new QLabel) + , m_frames(frames) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + auto tw = new QTabWidget; + tw->setTabPosition(QTabWidget::East); + tw->addTab(m_splineEditor, "Curve"); + tw->addTab(m_text, "Text"); + + connect(tw, &QTabWidget::currentChanged, this, &EasingCurveDialog::tabClicked); + connect(m_text, &QPlainTextEdit::textChanged, this, &EasingCurveDialog::textChanged); + + auto labelFont = m_label->font(); + labelFont.setPointSize(labelFont.pointSize() + 2); + m_label->setFont(labelFont); + + auto hSpacing = qApp->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); + auto vSpacing = qApp->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); + auto *vbox = new QVBoxLayout; + vbox->setContentsMargins(2, 0, 0, vSpacing); + vbox->addWidget(m_label); + + auto *presetBar = new QTabBar; + + auto smallFont = presetBar->font(); + smallFont.setPixelSize(Theme::instance()->smallFontPixelSize()); + + presetBar->setFont(smallFont); + presetBar->setExpanding(false); + presetBar->setDrawBase(false); + presetBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + auto *durationLabel = new QLabel("Duration (ms)"); + auto *durationEdit = new QSpinBox; + durationEdit->setMaximum(std::numeric_limits<int>::max()); + durationEdit->setValue(1000); + auto *animateButton = new QPushButton("Preview"); + + m_durationLayout->setContentsMargins(0, vSpacing, 0, 0); + m_durationLayout->addWidget(durationLabel); + m_durationLayout->addWidget(durationEdit); + m_durationLayout->addWidget(animateButton); + + m_durationLayout->insertSpacing(1, hSpacing); + m_durationLayout->insertSpacing(2, hSpacing); + m_durationLayout->insertSpacing(4, hSpacing); + m_durationLayout->addStretch(hSpacing); + + m_buttons->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + auto callButtonsClicked = [this](QAbstractButton *button) { + buttonsClicked(m_buttons->standardButton(button)); + }; + + connect(m_buttons, &QDialogButtonBox::clicked, this, callButtonsClicked); + + auto *buttonLayout = new QVBoxLayout; + buttonLayout->setContentsMargins(0, vSpacing, 0, 0); + buttonLayout->addWidget(m_buttons); + + auto *grid = new QGridLayout; + grid->setVerticalSpacing(0); + grid->addLayout(vbox, 0, 0); + grid->addWidget(presetBar, 0, 1, Qt::AlignBottom); + + grid->addWidget(tw); + grid->addWidget(m_presets, 1, 1); + grid->addLayout(m_durationLayout, 2, 0); + grid->addLayout(buttonLayout, 2, 1); + + auto *groupBox = new QGroupBox; + groupBox->setLayout(grid); + + auto *tabWidget = new QTabWidget(this); + tabWidget->addTab(groupBox, "Easing Curve Editor"); + + auto *mainBox = new QVBoxLayout; + mainBox->addWidget(tabWidget); + setLayout(mainBox); + + connect(m_splineEditor, + &SplineEditor::easingCurveChanged, + this, + &EasingCurveDialog::updateEasingCurve); + + connect(m_presets, &PresetEditor::presetChanged, m_splineEditor, &SplineEditor::setEasingCurve); + + connect(durationEdit, + QOverload<int>::of(&QSpinBox::valueChanged), + m_splineEditor, + &SplineEditor::setDuration); + + connect(animateButton, &QPushButton::clicked, m_splineEditor, &SplineEditor::animate); + + m_presets->initialize(presetBar); + + m_splineEditor->setDuration(durationEdit->value()); + + resize(QSize(1421, 918)); +} + +void EasingCurveDialog::initialize(const QString &curveString) +{ + EasingCurve curve; + if (curveString.isEmpty()) { + QEasingCurve qcurve; + qcurve.addCubicBezierSegment(QPointF(0.2, 0.2), QPointF(0.8, 0.8), QPointF(1.0, 1.0)); + curve = EasingCurve(qcurve); + } else + curve.fromString(curveString); + + m_splineEditor->setEasingCurve(curve); +} + +void EasingCurveDialog::runDialog(const QList<ModelNode> &frames, QWidget *parent) +{ + if (frames.empty()) + return; + + EasingCurveDialog dialog(frames, parent); + + ModelNode current = frames.last(); + + if (current.hasBindingProperty("easing.bezierCurve")) + dialog.initialize(current.bindingProperty("easing.bezierCurve").expression()); + else + dialog.initialize(""); + + dialog.exec(); +} + +bool EasingCurveDialog::apply() +{ + QTC_ASSERT(!m_frames.empty(), return false); + + EasingCurve curve = m_splineEditor->easingCurve(); + if (!curve.isLegal()) { + QMessageBox msgBox; + msgBox.setText("Attempting to apply invalid curve to keyframe"); + msgBox.setInformativeText("Please solve the issue before proceeding."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + return false; + } + AbstractView *view = m_frames.first().view(); + + return view->executeInTransaction("EasingCurveDialog::apply", [this, view](){ + auto expression = m_splineEditor->easingCurve().toString(); + for (const auto &frame : m_frames) + frame.bindingProperty("easing.bezierCurve").setExpression(expression); + }); +} + +void EasingCurveDialog::textChanged() +{ + auto curve = m_splineEditor->easingCurve(); + curve.fromString(m_text->toPlainText()); + m_splineEditor->setEasingCurve(curve); +} + +void EasingCurveDialog::tabClicked(int id) +{ + if (auto tw = qobject_cast<const QTabWidget *>(sender())) { + int seid = tw->indexOf(m_splineEditor); + if (seid == id) { + for (int i = 0; i < m_durationLayout->count(); ++i) { + auto *item = m_durationLayout->itemAt(i); + if (auto *widget = item->widget()) + widget->show(); + } + + auto curve = m_splineEditor->easingCurve(); + curve.fromString(m_text->toPlainText()); + m_splineEditor->setEasingCurve(curve); + + } else { + for (int i = 0; i < m_durationLayout->count(); ++i) { + auto *item = m_durationLayout->itemAt(i); + if (auto *widget = item->widget()) + widget->hide(); + } + + auto curve = m_splineEditor->easingCurve(); + m_text->setPlainText(curve.toString()); + } + } +} + +void EasingCurveDialog::presetTabClicked(int id) +{ + m_presets->activate(id); +} + +void EasingCurveDialog::updateEasingCurve(const EasingCurve &curve) +{ + if (!curve.isLegal()) { + auto *save = m_buttons->button(QDialogButtonBox::Save); + save->setEnabled(false); + + auto *ok = m_buttons->button(QDialogButtonBox::Ok); + ok->setEnabled(false); + + m_label->setText("Invalid Curve!"); + } else { + auto *save = m_buttons->button(QDialogButtonBox::Save); + save->setEnabled(true); + + auto *ok = m_buttons->button(QDialogButtonBox::Ok); + ok->setEnabled(true); + + m_label->setText(""); + } + + m_presets->update(curve); +} + +void EasingCurveDialog::buttonsClicked(QDialogButtonBox::StandardButton button) +{ + switch (button) { + case QDialogButtonBox::Ok: + if (apply()) + close(); + break; + + case QDialogButtonBox::Cancel: + close(); + break; + + case QDialogButtonBox::Save: + m_presets->writePresets(m_splineEditor->easingCurve()); + break; + + default: + break; + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h new file mode 100644 index 0000000000..a8c026989c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QDialog> +#include <QDialogButtonBox> + +#include <modelnode.h> + +QT_BEGIN_NAMESPACE +class QLabel; +class QPlainTextEdit; +class QHBoxLayout; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class SplineEditor; +class PresetEditor; +class EasingCurve; + +class EasingCurveDialog : public QDialog +{ + Q_OBJECT + +public: + EasingCurveDialog(const QList<ModelNode> &frames, QWidget *parent = nullptr); + + void initialize(const QString &curveString); + + static void runDialog(const QList<ModelNode> &frames, QWidget *parent = nullptr); + +private: + bool apply(); + + void textChanged(); + + void tabClicked(int id); + + void presetTabClicked(int id); + + void buttonsClicked(QDialogButtonBox::StandardButton button); + + void updateEasingCurve(const EasingCurve &curve); + +private: + SplineEditor *m_splineEditor = nullptr; + + QPlainTextEdit *m_text = nullptr; + + PresetEditor *m_presets = nullptr; + + QHBoxLayout *m_durationLayout = nullptr; + + QDialogButtonBox *m_buttons = nullptr; + + QLabel *m_label = nullptr; + + QList<ModelNode> m_frames; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png Binary files differnew file mode 100644 index 0000000000..af651276ed --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png Binary files differnew file mode 100644 index 0000000000..58a7174288 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/animation.png b/src/plugins/qmldesigner/components/timelineeditor/images/animation.png Binary files differnew file mode 100644 index 0000000000..20ad0273b5 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/animation.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png Binary files differnew file mode 100644 index 0000000000..1ecf1857c7 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png Binary files differnew file mode 100644 index 0000000000..69c93ebe3e --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png Binary files differnew file mode 100644 index 0000000000..9bd8a52e59 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png Binary files differnew file mode 100644 index 0000000000..bda4dc0095 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png Binary files differnew file mode 100644 index 0000000000..3d5c3abe05 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png Binary files differnew file mode 100644 index 0000000000..4842ac0738 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png Binary files differnew file mode 100644 index 0000000000..0d99fc180c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png Binary files differnew file mode 100644 index 0000000000..0846f194e0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png Binary files differnew file mode 100644 index 0000000000..8e5ddc3930 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png Binary files differnew file mode 100644 index 0000000000..64a28ca075 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png Binary files differnew file mode 100644 index 0000000000..534737f385 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png Binary files differnew file mode 100644 index 0000000000..5655e0b278 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..2f522c22b6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png Binary files differnew file mode 100644 index 0000000000..6e1c9f912a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png Binary files differnew file mode 100644 index 0000000000..6bf7d1ad53 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..5102e279a1 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png Binary files differnew file mode 100644 index 0000000000..8a3eaa7888 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png Binary files differnew file mode 100644 index 0000000000..e0168a097a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png Binary files differnew file mode 100644 index 0000000000..2c12d98e01 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..4bbbe6cd3f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png Binary files differnew file mode 100644 index 0000000000..58ccb7c765 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png Binary files differnew file mode 100644 index 0000000000..829dd99391 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png Binary files differnew file mode 100644 index 0000000000..a195ac5fca --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png Binary files differnew file mode 100644 index 0000000000..fd879e5837 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png Binary files differnew file mode 100644 index 0000000000..b84a800097 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..0ad868dcd6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png Binary files differnew file mode 100644 index 0000000000..e840819f2d --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png Binary files differnew file mode 100644 index 0000000000..e5f63f1fc9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png Binary files differnew file mode 100644 index 0000000000..f85d3f78fd --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png Binary files differnew file mode 100644 index 0000000000..2f65f7970c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png Binary files differnew file mode 100644 index 0000000000..9798c76115 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..b4ee45a566 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png Binary files differnew file mode 100644 index 0000000000..1e39f84502 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png Binary files differnew file mode 100644 index 0000000000..b99474718c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png Binary files differnew file mode 100644 index 0000000000..4b6a7c8978 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png Binary files differnew file mode 100644 index 0000000000..fd85a10758 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png Binary files differnew file mode 100644 index 0000000000..9c0a1fd550 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..1299a370fc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png Binary files differnew file mode 100644 index 0000000000..7b5faebae0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png Binary files differnew file mode 100644 index 0000000000..726ead7a43 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png Binary files differnew file mode 100644 index 0000000000..d68aa73214 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png Binary files differnew file mode 100644 index 0000000000..f5265a2218 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png Binary files differnew file mode 100644 index 0000000000..f38fbef1d4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png Binary files differnew file mode 100644 index 0000000000..b760a04133 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png Binary files differnew file mode 100644 index 0000000000..415ec0127f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..3f1e24e04a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png Binary files differnew file mode 100644 index 0000000000..001ca37b1c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png Binary files differnew file mode 100644 index 0000000000..95e8567ccb --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png b/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png Binary files differnew file mode 100644 index 0000000000..518a77f404 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png Binary files differnew file mode 100644 index 0000000000..7f6778556b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png Binary files differnew file mode 100644 index 0000000000..52ba668973 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..df151051fc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png Binary files differnew file mode 100644 index 0000000000..0589f982a7 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png Binary files differnew file mode 100644 index 0000000000..9eed9ce3c3 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png Binary files differnew file mode 100644 index 0000000000..0cf0865c48 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png Binary files differnew file mode 100644 index 0000000000..f05dfcd9ed --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png b/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png Binary files differnew file mode 100644 index 0000000000..d4ecf00031 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png Binary files differnew file mode 100644 index 0000000000..910b856638 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png Binary files differnew file mode 100644 index 0000000000..abefa72fb3 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png Binary files differnew file mode 100644 index 0000000000..d6bc429196 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png Binary files differnew file mode 100644 index 0000000000..affc3c9848 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png Binary files differnew file mode 100644 index 0000000000..83d441d64f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png Binary files differnew file mode 100644 index 0000000000..0fec4b269e --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png Binary files differnew file mode 100644 index 0000000000..611684a7f6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png Binary files differnew file mode 100644 index 0000000000..c1dbdbc56c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png Binary files differnew file mode 100644 index 0000000000..eec61eb86c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png Binary files differnew file mode 100644 index 0000000000..1706de0bb4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png Binary files differnew file mode 100644 index 0000000000..20433d99c4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png Binary files differnew file mode 100644 index 0000000000..326ea32c25 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp new file mode 100644 index 0000000000..8ea7692e3d --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp @@ -0,0 +1,560 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "preseteditor.h" + +#include "canvas.h" +#include "easingcurve.h" +#include "timelineicons.h" + +#include <QAbstractButton> +#include <QApplication> +#include <QContextMenuEvent> +#include <QMenu> +#include <QMessageBox> +#include <QPainter> +#include <QPixmap> +#include <QSettings> +#include <QStandardItemModel> +#include <QString> + +#include <coreplugin/icore.h> +#include <theme.h> + +namespace QmlDesigner { + +constexpr int iconWidth = 86; +constexpr int iconHeight = 86; +constexpr int itemFrame = 3; +constexpr int itemWidth = iconWidth + 2 * itemFrame; +constexpr int unsavedMarkSize = 18; + +constexpr int spacingg = 5; + +const QColor background = Qt::white; + +const QColor labelBackground = qRgb(0x70, 0x70, 0x70); +const QColor canvasBackground = qRgb(0x46, 0x46, 0x46); +const QColor curveLine = qRgb(0xe6, 0xe7, 0xe8); + +PresetItemDelegate::PresetItemDelegate() = default; + +void PresetItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &opt, + const QModelIndex &index) const +{ + QStyleOptionViewItem option = opt; + initStyleOption(&option, index); + + auto *w = option.widget; + auto *style = w == nullptr ? qApp->style() : w->style(); + + QSize textSize = QSize(option.rect.width(), + style->subElementRect(QStyle::SE_ItemViewItemText, &option, w).height()); + + auto textRect = QRect(option.rect.topLeft(), textSize); + textRect.moveBottom(option.rect.bottom()); + + option.font.setPixelSize(Theme::instance()->smallFontPixelSize()); + + painter->save(); + painter->fillRect(option.rect, canvasBackground); + + if (option.text.isEmpty()) + painter->fillRect(textRect, canvasBackground); + else + painter->fillRect(textRect, Theme::instance()->qmlDesignerButtonColor()); + + style->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget); + + QVariant dirty = option.index.data(PresetList::ItemRole_Dirty); + if (dirty.isValid()) { + if (dirty.toBool()) { + QRect asteriskRect(option.rect.right() - unsavedMarkSize, + itemFrame, + unsavedMarkSize, + unsavedMarkSize); + + QFont font = painter->font(); + font.setPixelSize(unsavedMarkSize); + painter->setFont(font); + + auto pen = painter->pen(); + pen.setColor(Qt::white); + painter->setPen(pen); + + painter->drawText(asteriskRect, Qt::AlignTop | Qt::AlignRight, "*"); + } + } + painter->restore(); +} + +QSize PresetItemDelegate::sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &index) const +{ + QSize size = QStyledItemDelegate::sizeHint(opt, index); + size.rwidth() = itemWidth; + return size; +} + +QIcon paintPreview() +{ + QPixmap pm(iconWidth, iconHeight); + pm.fill(canvasBackground); + return QIcon(pm); +} + +QIcon paintPreview(const EasingCurve &curve) +{ + QPixmap pm(iconWidth, iconHeight); + pm.fill(canvasBackground); + + QPainter painter(&pm); + painter.setRenderHint(QPainter::Antialiasing, true); + + Canvas canvas(iconWidth, iconHeight, 2, 2, 9, 6, 0, 1); + canvas.paintCurve(&painter, curve, curveLine); + + return QIcon(pm); +} + +namespace Internal { + +static const char settingsKey[] = "EasingCurveList"; +static const char settingsFileName[] = "/EasingCurves.ini"; + +QString settingsFullFilePath(const QSettings::Scope &scope) +{ + if (scope == QSettings::SystemScope) + return Core::ICore::installerResourcePath() + settingsFileName; + + return Core::ICore::userResourcePath() + settingsFileName; +} + +} // namespace Internal + +PresetList::PresetList(QSettings::Scope scope, QWidget *parent) + : QListView(parent) + , m_scope(scope) + , m_index(-1) + , m_filename(Internal::settingsFullFilePath(scope)) +{ + int magic = 4; + int scrollBarWidth = this->style()->pixelMetric(QStyle::PM_ScrollBarExtent); + const int width = 3 * itemWidth + 4 * spacingg + scrollBarWidth + magic; + + setFixedWidth(width); + + setModel(new QStandardItemModel); + + setItemDelegate(new PresetItemDelegate); + + setSpacing(spacingg); + + setUniformItemSizes(true); + + setIconSize(QSize(iconWidth, iconHeight)); + + setSelectionMode(QAbstractItemView::SingleSelection); + + setViewMode(QListView::IconMode); + + setFlow(QListView::LeftToRight); + + setMovement(QListView::Static); + + setWrapping(true); + + setTextElideMode(Qt::ElideMiddle); + + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); +} + +void PresetList::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + for (const QModelIndex &index : deselected.indexes()) { + if (dirty(index)) { + QMessageBox msgBox; + msgBox.setText("The preset has been modified."); + msgBox.setInformativeText("Do you want to save your changes?"); + msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard + | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Save); + + if (QAbstractButton *button = msgBox.button(QMessageBox::Discard)) + button->setText("Discard Changes"); + + if (QAbstractButton *button = msgBox.button(QMessageBox::Cancel)) + button->setText("Cancel Selection"); + + int ret = msgBox.exec(); + + switch (ret) { + case QMessageBox::Save: + // Save the preset and continue selection. + writePresets(); + break; + case QMessageBox::Discard: + // Discard changes to the curve and continue selection. + revert(index); + break; + + case QMessageBox::Cancel: + // Cancel selection operation and leave the curve untouched. + selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); + return; + + default: + // should never be reachedDiscard + break; + } + } + } + + for (const auto &index : selected.indexes()) { + QVariant curveData = model()->data(index, ItemRole_Data); + if (curveData.isValid()) + emit presetChanged(curveData.value<EasingCurve>()); + } +} + +bool PresetList::hasSelection() const +{ + return selectionModel()->hasSelection(); +} + +bool PresetList::dirty(const QModelIndex &index) const +{ + return model()->data(index, ItemRole_Dirty).toBool(); +} + +int PresetList::index() const +{ + return m_index; +} + +bool PresetList::isEditable(const QModelIndex &index) const +{ + QFlags<Qt::ItemFlag> flags(model()->flags(index)); + return flags.testFlag(Qt::ItemIsEditable); +} + +void PresetList::initialize(int index) +{ + m_index = index; + + readPresets(); +} + +void PresetList::readPresets() +{ + auto *simodel = qobject_cast<QStandardItemModel *>(model()); + + simodel->clear(); + + QList<NamedEasingCurve> curves = storedCurves(); + + for (int i = 0; i < curves.size(); ++i) { + QVariant curveData = QVariant::fromValue(curves[i].curve()); + + auto *item = new QStandardItem(paintPreview(curves[i].curve()), curves[i].name()); + item->setData(curveData, ItemRole_Data); + item->setEditable(m_scope == QSettings::UserScope); + item->setToolTip(curves[i].name()); + + simodel->setItem(i, item); + } +} + +void PresetList::writePresets() +{ + QList<QVariant> presets; + for (int i = 0; i < model()->rowCount(); ++i) { + QModelIndex index = model()->index(i, 0); + + QVariant nameData = model()->data(index, Qt::DisplayRole); + QVariant curveData = model()->data(index, ItemRole_Data); + + if (nameData.isValid() && curveData.isValid()) { + NamedEasingCurve curve(nameData.toString(), curveData.value<QmlDesigner::EasingCurve>()); + + presets << QVariant::fromValue(curve); + } + + model()->setData(index, false, ItemRole_Dirty); + } + + QSettings settings(m_filename, QSettings::IniFormat); + settings.clear(); + settings.setValue(Internal::settingsKey, QVariant::fromValue(presets)); +} + +void PresetList::revert(const QModelIndex &index) +{ + auto *simodel = qobject_cast<QStandardItemModel *>(model()); + if (auto *item = simodel->itemFromIndex(index)) { + QString name = item->data(Qt::DisplayRole).toString(); + QList<NamedEasingCurve> curves = storedCurves(); + + for (const auto &curve : curves) { + if (curve.name() == name) { + item->setData(false, ItemRole_Dirty); + item->setData(paintPreview(curve.curve()), Qt::DecorationRole); + item->setData(QVariant::fromValue(curve.curve()), ItemRole_Data); + item->setToolTip(name); + return; + } + } + } +} + +void PresetList::updateCurve(const EasingCurve &curve) +{ + if (!selectionModel()->hasSelection()) + return; + + QVariant icon = QVariant::fromValue(paintPreview(curve)); + QVariant curveData = QVariant::fromValue(curve); + + for (const auto &index : selectionModel()->selectedIndexes()) + setItemData(index, curveData, icon); +} + +void PresetList::contextMenuEvent(QContextMenuEvent *event) +{ + event->accept(); + + if (m_scope == QSettings::SystemScope) + return; + + QMenu menu; + + QAction *addAction = menu.addAction(tr("Add Preset")); + + connect(addAction, &QAction::triggered, [&]() { createItem(); }); + + if (selectionModel()->hasSelection()) { + QAction *removeAction = menu.addAction(tr("Delete Selected Preset")); + connect(removeAction, &QAction::triggered, [&]() { removeSelectedItem(); }); + } + + menu.exec(event->globalPos()); +} + +void PresetList::dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector<int> &roles) +{ + if (topLeft == bottomRight && roles.contains(0)) { + QVariant name = model()->data(topLeft, 0); + model()->setData(topLeft, name, Qt::ToolTipRole); + } +} + +void PresetList::createItem() +{ + EasingCurve curve; + curve.makeDefault(); + createItem(createUniqueName(), curve); +} + +void PresetList::createItem(const QString &name, const EasingCurve &curve) +{ + auto *item = new QStandardItem(paintPreview(curve), name); + item->setData(QVariant::fromValue(curve), ItemRole_Data); + item->setToolTip(name); + + int row = model()->rowCount(); + qobject_cast<QStandardItemModel *>(model())->setItem(row, item); + + QModelIndex index = model()->index(row, 0); + + // Why is that needed? SingleSelection is specified. + selectionModel()->clear(); + selectionModel()->select(index, QItemSelectionModel::Select); +} + +void PresetList::removeSelectedItem() +{ + for (const auto &index : selectionModel()->selectedIndexes()) + model()->removeRow(index.row()); + + writePresets(); +} + +void PresetList::setItemData(const QModelIndex &index, const QVariant &curve, const QVariant &icon) +{ + if (isEditable(index)) { + model()->setData(index, curve, PresetList::ItemRole_Data); + model()->setData(index, true, PresetList::ItemRole_Dirty); + model()->setData(index, icon, Qt::DecorationRole); + } +} + +QString PresetList::createUniqueName() const +{ + QStringList names = allNames(); + auto nameIsUnique = [&](const QString &name) { + auto iter = std::find(names.begin(), names.end(), name); + if (iter == names.end()) + return true; + else + return false; + }; + + int counter = 0; + QString tmp("Default"); + QString name = tmp; + + while (!nameIsUnique(name)) + name = tmp + QString(" %1").arg(counter++); + + return name; +} + +QStringList PresetList::allNames() const +{ + QStringList names; + for (int i = 0; i < model()->rowCount(); ++i) { + QModelIndex index = model()->index(i, 0); + QVariant nameData = model()->data(index, Qt::DisplayRole); + if (nameData.isValid()) + names << nameData.toString(); + } + + return names; +} + +QList<NamedEasingCurve> PresetList::storedCurves() const +{ + QSettings settings(m_filename, QSettings::IniFormat); + QVariant presetSettings = settings.value(Internal::settingsKey); + + if (!presetSettings.isValid()) + return QList<NamedEasingCurve>(); + + QList<QVariant> presets = presetSettings.toList(); + + QList<NamedEasingCurve> out; + for (const QVariant &preset : presets) + if (preset.isValid()) + out << preset.value<NamedEasingCurve>(); + + return out; +} + +PresetEditor::PresetEditor(QWidget *parent) + : QStackedWidget(parent) + , m_presets(new PresetList(QSettings::SystemScope)) + , m_customs(new PresetList(QSettings::UserScope)) +{ + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + addWidget(m_presets); + addWidget(m_customs); + + connect(m_presets, &PresetList::presetChanged, this, &PresetEditor::presetChanged); + connect(m_customs, &PresetList::presetChanged, this, &PresetEditor::presetChanged); +} + +void PresetEditor::initialize(QTabBar *bar) +{ + m_presets->initialize(bar->addTab("Presets")); + m_customs->initialize(bar->addTab("Custom")); + + connect(bar, &QTabBar::currentChanged, this, &PresetEditor::activate); + connect(this, &PresetEditor::currentChanged, bar, &QTabBar::setCurrentIndex); + + m_presets->selectionModel()->clear(); + m_customs->selectionModel()->clear(); + + activate(m_presets->index()); +} + +void PresetEditor::activate(int id) +{ + if (id == m_presets->index()) + setCurrentWidget(m_presets); + else + setCurrentWidget(m_customs); +} + +void PresetEditor::update(const EasingCurve &curve) +{ + if (isCurrent(m_presets)) + m_presets->selectionModel()->clear(); + else { + if (m_customs->selectionModel()->hasSelection()) { + QVariant icon = QVariant::fromValue(paintPreview(curve)); + QVariant curveData = QVariant::fromValue(curve); + for (const QModelIndex &index : m_customs->selectionModel()->selectedIndexes()) + m_customs->setItemData(index, curveData, icon); + } + } +} + +bool PresetEditor::writePresets(const EasingCurve &curve) +{ + if (!curve.isLegal()) { + QMessageBox msgBox; + msgBox.setText("Attempting to save invalid curve"); + msgBox.setInformativeText("Please solve the issue before proceeding."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + return false; + } + + if (auto current = qobject_cast<const PresetList *>(currentWidget())) { + if (current->index() == m_presets->index() + || (current->index() == m_customs->index() && !m_customs->hasSelection())) { + bool ok; + QString name = QInputDialog::getText(this, + tr("Save Preset"), + tr("Name"), + QLineEdit::Normal, + QString(), + &ok); + + if (ok && !name.isEmpty()) { + activate(m_customs->index()); + m_customs->createItem(name, curve); + } + } + + m_customs->writePresets(); + return true; + } + + return false; +} + +bool PresetEditor::isCurrent(PresetList *list) +{ + if (auto current = qobject_cast<const PresetList *>(currentWidget())) + return list->index() == current->index(); + + return false; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/preseteditor.h b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.h new file mode 100644 index 0000000000..6fab3e7adb --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.h @@ -0,0 +1,152 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QInputDialog> +#include <QListView> +#include <QSettings> +#include <QStackedWidget> +#include <QStyledItemDelegate> + +QT_FORWARD_DECLARE_CLASS(QString) + +namespace QmlDesigner { + +class EasingCurve; +class NamedEasingCurve; + +class PresetItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + PresetItemDelegate(); + + void paint(QPainter *painter, + const QStyleOptionViewItem &opt, + const QModelIndex &index) const override; + + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +class PresetList : public QListView +{ + Q_OBJECT + +public: + enum ItemRoles { + ItemRole_Undefined = Qt::UserRole, + ItemRole_Data, + ItemRole_Dirty, + ItemRole_Modifiable + }; + +signals: + void presetChanged(const EasingCurve &curve); + +public: + PresetList(QSettings::Scope scope, QWidget *parent = nullptr); + + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; + + bool hasSelection() const; + + bool dirty(const QModelIndex &index) const; + + int index() const; + + bool isEditable(const QModelIndex &index) const; + + void initialize(int index); + + void readPresets(); + + void writePresets(); + + void revert(const QModelIndex &index); + + void updateCurve(const EasingCurve &curve); + + void createItem(); + + void createItem(const QString &name, const EasingCurve &curve); + + void setItemData(const QModelIndex &index, const QVariant &curve, const QVariant &icon); + +protected: + void contextMenuEvent(QContextMenuEvent *event) override; + + void dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector<int> &roles = QVector<int>()) override; + +private: + QStringList allNames() const; + + QList<NamedEasingCurve> storedCurves() const; + + QString createUniqueName() const; + + void removeSelectedItem(); + +private: + QSettings::Scope m_scope; + + int m_index; + + QString m_filename; +}; + +class PresetEditor : public QStackedWidget +{ + Q_OBJECT + +signals: + void presetChanged(const EasingCurve &curve); + +public: + explicit PresetEditor(QWidget *parent = nullptr); + + void initialize(QTabBar *bar); + + void activate(int id); + + void update(const EasingCurve &curve); + + void readPresets(); + + bool writePresets(const EasingCurve &curve); + +private: + bool isCurrent(PresetList *list); + +private: + PresetList *m_presets; + + PresetList *m_customs; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp new file mode 100644 index 0000000000..4120aaee21 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "setframevaluedialog.h" +#include "ui_setframevaluedialog.h" + +namespace QmlDesigner { + +SetFrameValueDialog::SetFrameValueDialog(QWidget *parent) + : QDialog(parent) + , ui(new Ui::SetFrameValueDialog) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); +} + +SetFrameValueDialog::~SetFrameValueDialog() +{ + delete ui; +} + +QLineEdit *SetFrameValueDialog::lineEdit() const +{ + return ui->lineEdit; +} + +void SetFrameValueDialog::setPropertName(const QString &name) +{ + setWindowTitle(tr("Change %1").arg(name)); + ui->label->setText(name); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h new file mode 100644 index 0000000000..e7ed226b67 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QDialog> + +QT_FORWARD_DECLARE_CLASS(QLineEdit) + +namespace QmlDesigner { + +namespace Ui { +class SetFrameValueDialog; +} + +class SetFrameValueDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SetFrameValueDialog(QWidget *parent = nullptr); + ~SetFrameValueDialog() override; + + QLineEdit *lineEdit() const; + + void setPropertName(const QString &name); + +private: + Ui::SetFrameValueDialog *ui; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui new file mode 100644 index 0000000000..2fa1241e4a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::SetFrameValueDialog</class> + <widget class="QDialog" name="QmlDesigner::SetFrameValueDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>184</width> + <height>79</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Value</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="lineEdit"/> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QmlDesigner::SetFrameValueDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QmlDesigner::SetFrameValueDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp new file mode 100644 index 0000000000..44ef1f194c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp @@ -0,0 +1,274 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "splineeditor.h" + +#include <theme.h> + +#include <QAction> +#include <QApplication> +#include <QContextMenuEvent> +#include <QMenu> +#include <QMouseEvent> +#include <QPainter> +#include <QPropertyAnimation> +#include <QResizeEvent> + +namespace QmlDesigner { + +SplineEditor::SplineEditor(QWidget *parent) + : QWidget(parent) + , m_canvas(0, 0, 25, 25, 9, 6, 0, 1) + , m_animation(new QPropertyAnimation(this, "progress")) +{ + m_animation->setStartValue(0.0); + m_animation->setEndValue(1.0); + m_animation->setLoopCount(1); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +double SplineEditor::progress() const +{ + return m_progress; +} + +EasingCurve SplineEditor::easingCurve() const +{ + return m_curve; +} + +void SplineEditor::animate() const +{ + m_animation->start(); +} + +void SplineEditor::setDuration(int duration) +{ + m_animation->setDuration(duration); +} + +void SplineEditor::setProgress(double progress) +{ + m_progress = progress; + update(); +} + +void SplineEditor::setEasingCurve(const EasingCurve &curve) +{ + m_curve = curve; + update(); +} + +void SplineEditor::resizeEvent(QResizeEvent *event) +{ + m_canvas.resize(event->size()); + QWidget::resizeEvent(event); +} + +void SplineEditor::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + QPen pen(Qt::black); + pen.setWidth(1); + painter.drawRect(0, 0, width() - 1, height() - 1); + + painter.setRenderHint(QPainter::Antialiasing); + + pen = QPen(Qt::darkGray); + pen.setWidth(1); + painter.setPen(pen); + + QColor curveColor = Qt::white; + if (!m_curve.isLegal()) + curveColor = Qt::red; + + QBrush background(Theme::instance()->qmlDesignerBackgroundColorDarker()); + m_canvas.paintGrid(&painter, background); + m_canvas.paintCurve(&painter, m_curve, curveColor); + m_canvas.paintControlPoints(&painter, m_curve); + + if (m_animation->state() == QAbstractAnimation::Running) + m_canvas.paintProgress(&painter, m_curve, m_progress); +} + +void SplineEditor::mousePressEvent(QMouseEvent *e) +{ + bool clearActive = true; + if (e->button() == Qt::LeftButton) { + EasingCurve mappedCurve = m_canvas.mapTo(m_curve); + int active = mappedCurve.hit(e->pos(), 10); + + if (EasingCurve::IsValidIndex(active)) { + clearActive = false; + m_curve.setActive(active); + mouseMoveEvent(e); + } + + m_mousePress = e->pos(); + e->accept(); + } + + if (clearActive) { + m_curve.clearActive(); + update(); + } +} + +void SplineEditor::mouseReleaseEvent(QMouseEvent *e) +{ + if (e->button() == Qt::LeftButton) { + m_mouseDrag = false; + e->accept(); + } +} + +void dragHandle(EasingCurve &curve, int id, const QPointF &pos) +{ + QPointF distance = pos - curve.point(id); + + curve.setPoint(id, pos); + + if (curve.isLeftHandle(id)) + curve.movePoint(id + 2, -distance); + else + curve.movePoint(id - 2, -distance); +} + +void SplineEditor::mouseMoveEvent(QMouseEvent *e) +{ + // If we've moved more then 25 pixels, assume user is dragging + if (!m_mouseDrag + && QPoint(m_mousePress - e->pos()).manhattanLength() > qApp->startDragDistance()) + m_mouseDrag = true; + + if (m_mouseDrag && m_curve.hasActive()) { + QPointF p = m_canvas.mapFrom(e->pos()); + int active = m_curve.active(); + + if ((active == 0 || active == m_curve.count() - 2) + && e->modifiers().testFlag(Qt::ShiftModifier)) { + if (active == 0) { + QPointF opposite = QPointF(1.0, 1.0) - p; + dragHandle(m_curve, active, p); + dragHandle(m_curve, m_curve.count() - 2, opposite); + + } else { + QPointF opposite = QPointF(1.0, 1.0) - p; + dragHandle(m_curve, active, p); + dragHandle(m_curve, 0, opposite); + } + + } else if (m_curve.isHandle(active)) { + int poc = m_curve.curvePoint(active); + + if (!m_curve.isSmooth(poc)) + m_curve.setPoint(active, p); + else + dragHandle(m_curve, active, p); + + } else { + QPointF targetPoint = p; + QPointF distance = targetPoint - m_curve.point(m_curve.active()); + + m_curve.setPoint(active, targetPoint); + m_curve.movePoint(active + 1, distance); + m_curve.movePoint(active - 1, distance); + } + + update(); + emit easingCurveChanged(m_curve); + } +} + +void SplineEditor::contextMenuEvent(QContextMenuEvent *e) +{ + m_curve.clearActive(); + + QMenu menu; + + EasingCurve mappedCurve = m_canvas.mapTo(m_curve); + int index = mappedCurve.hit(e->pos(), 10); + + if (index > 0 && !m_curve.isHandle(index)) { + QAction *deleteAction = menu.addAction(tr("Delete Point")); + connect(deleteAction, &QAction::triggered, [this, index]() { + m_curve.deletePoint(index); + update(); + emit easingCurveChanged(m_curve); + }); + + QAction *smoothAction = menu.addAction(tr("Smooth Point")); + smoothAction->setCheckable(true); + smoothAction->setChecked(m_curve.isSmooth(index)); + connect(smoothAction, &QAction::triggered, [this, index]() { + m_curve.makeSmooth(index); + update(); + emit easingCurveChanged(m_curve); + }); + + QAction *cornerAction = menu.addAction(tr("Corner Point")); + connect(cornerAction, &QAction::triggered, [this, index]() { + m_curve.breakTangent(index); + update(); + emit easingCurveChanged(m_curve); + }); + + } else { + QAction *addAction = menu.addAction(tr("Add Point")); + connect(addAction, &QAction::triggered, [&]() { + m_curve.addPoint(m_canvas.mapFrom(e->pos())); + m_curve.makeSmooth(m_curve.active()); + update(); + emit easingCurveChanged(m_curve); + }); + } + + QAction *zoomAction = menu.addAction(tr("Reset Zoom")); + connect(zoomAction, &QAction::triggered, [&]() { + m_canvas.setScale(1.0); + update(); + }); + + menu.exec(e->globalPos()); + e->accept(); +} + +void SplineEditor::mouseDoubleClickEvent(QMouseEvent *event) +{ + m_animation->start(); + QWidget::mouseDoubleClickEvent(event); +} + +void SplineEditor::wheelEvent(QWheelEvent *event) +{ + double tmp = event->angleDelta().y() > 0 ? 0.05 : -0.05; + + m_canvas.setScale(m_canvas.scale() + tmp); + event->accept(); + update(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/splineeditor.h b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.h new file mode 100644 index 0000000000..8b454943ad --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QWidget> + +#include "canvas.h" +#include "easingcurve.h" + +QT_FORWARD_DECLARE_CLASS(QPropertyAnimation) + +namespace QmlDesigner { + +class SegmentProperties; + +class SplineEditor : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(double progress READ progress WRITE setProgress) + +signals: + void easingCurveChanged(const EasingCurve &curve); + +public: + explicit SplineEditor(QWidget *parent = nullptr); + + double progress() const; + + EasingCurve easingCurve() const; + + void animate() const; + + void setDuration(int duration); + + void setProgress(double progress); + + void setEasingCurve(const EasingCurve &curve); + +protected: + void resizeEvent(QResizeEvent *) override; + + void paintEvent(QPaintEvent *) override; + + void mousePressEvent(QMouseEvent *) override; + + void mouseMoveEvent(QMouseEvent *) override; + + void mouseReleaseEvent(QMouseEvent *) override; + + void contextMenuEvent(QContextMenuEvent *) override; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + + void wheelEvent(QWheelEvent *event) override; + +private: + Canvas m_canvas; + + EasingCurve m_curve; + + QPoint m_mousePress; + + bool m_mouseDrag = false; + + bool m_block = false; + + double m_progress = 0; + + QPropertyAnimation *m_animation; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo b/src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo new file mode 100644 index 0000000000..c1d7e92bf5 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo @@ -0,0 +1,35 @@ +MetaInfo { + Type { + name: "QtQuick.Timeline.Timeline" + icon: ":/timelineplugin/images/timeline-16px.png" + + ItemLibraryEntry { + name: "Timeline" + category: "none" + version: "1.0" + requiredImport: "none" + } + } + Type { + name: "QtQuick.Timeline.Keyframe" + icon: ":/timelineplugin/images/keyframe-16px.png" + + ItemLibraryEntry { + name: "Keyframe" + category: "none" + version: "1.0" + requiredImport: "none" + } + } + Type { + name: "QtQuick.Timeline.KeyframeGroup" + icon: ":/timelineplugin/images/keyframe-16px.png" + + ItemLibraryEntry { + name: "KeyframeGroup" + category: "none" + version: "1.0" + requiredImport: "none" + } + } +} diff --git a/src/plugins/qmldesigner/components/timelineeditor/timeline.qrc b/src/plugins/qmldesigner/components/timelineeditor/timeline.qrc new file mode 100644 index 0000000000..b793c1f8da --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timeline.qrc @@ -0,0 +1,77 @@ +<RCC> + <qresource prefix="/timelineplugin"> + <file>timeline.metainfo</file> + <file>images/add_timeline.png</file> + <file>images/add_timeline@2x.png</file> + <file>images/animation.png</file> + <file>images/animation@2x.png</file> + <file>images/back_one_frame.png</file> + <file>images/back_one_frame@2x.png</file> + <file>images/curve_editor.png</file> + <file>images/curve_editor@2x.png</file> + <file>images/curve_picker.png</file> + <file>images/curve_picker@2x.png</file> + <file>images/forward_one_frame.png</file> + <file>images/forward_one_frame@2x.png</file> + <file>images/global_record_keyframes.png</file> + <file>images/global_record_keyframes@2x.png</file> + <file>images/is_keyframe.png</file> + <file>images/is_keyframe@2x.png</file> + <file>images/keyframe.png</file> + <file>images/keyframe@2x.png</file> + <file>images/keyframe_autobezier_active.png</file> + <file>images/keyframe_autobezier_active@2x.png</file> + <file>images/keyframe_autobezier_inactive.png</file> + <file>images/keyframe_autobezier_inactive@2x.png</file> + <file>images/keyframe_autobezier_selected.png</file> + <file>images/keyframe_autobezier_selected@2x.png</file> + <file>images/keyframe_linear_active.png</file> + <file>images/keyframe_linear_active@2x.png</file> + <file>images/keyframe_linear_inactive.png</file> + <file>images/keyframe_linear_inactive@2x.png</file> + <file>images/keyframe_linear_selected.png</file> + <file>images/keyframe_linear_selected@2x.png</file> + <file>images/keyframe_lineartobezier_active.png</file> + <file>images/keyframe_lineartobezier_active@2x.png</file> + <file>images/keyframe_lineartobezier_inactive.png</file> + <file>images/keyframe_lineartobezier_inactive@2x.png</file> + <file>images/keyframe_lineartobezier_selected.png</file> + <file>images/keyframe_lineartobezier_selected@2x.png</file> + <file>images/keyframe_manualbezier_active.png</file> + <file>images/keyframe_manualbezier_active@2x.png</file> + <file>images/keyframe_manualbezier_inactive.png</file> + <file>images/keyframe_manualbezier_inactive@2x.png</file> + <file>images/keyframe_manualbezier_selected.png</file> + <file>images/keyframe_manualbezier_selected@2x.png</file> + <file>images/local_record_keyframes.png</file> + <file>images/local_record_keyframes@2x.png</file> + <file>images/loop_playback.png</file> + <file>images/loop_playback@2x.png</file> + <file>images/next_keyframe.png</file> + <file>images/next_keyframe@2x.png</file> + <file>images/previous_keyframe.png</file> + <file>images/previous_keyframe@2x.png</file> + <file>images/start_playback.png</file> + <file>images/start_playback@2x.png</file> + <file>images/to_first_frame.png</file> + <file>images/to_first_frame@2x.png</file> + <file>images/to_last_frame.png</file> + <file>images/to_last_frame@2x.png</file> + <file>images/pause_playback.png</file> + <file>images/pause_playback@2x.png</file> + <file>images/playhead.png</file> + <file>images/playhead@2x.png</file> + <file>images/work_area_handle_left.png</file> + <file>images/work_area_handle_left@2x.png</file> + <file>images/work_area_handle_right.png</file> + <file>images/work_area_handle_right@2x.png</file> + <file>images/zoom_big.png</file> + <file>images/zoom_big@2x.png</file> + <file>images/zoom_small.png</file> + <file>images/zoom_small@2x.png</file> + <file>images/keyframe-16px.png</file> + <file>images/timeline-16px.png</file> + <file>images/remove_timeline.png</file> + <file>images/remove_timeline@2x.png</file> + </qresource> +</RCC> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp new file mode 100644 index 0000000000..485bb8dbb0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineabstracttool.h" + +#include "timelinetooldelegate.h" + +#include <QPointF> + +namespace QmlDesigner { + +TimelineAbstractTool::TimelineAbstractTool(TimelineGraphicsScene *scene) + : m_scene(scene) + , m_delegate(nullptr) +{} + +TimelineAbstractTool::TimelineAbstractTool(TimelineGraphicsScene *scene, + TimelineToolDelegate *delegate) + : m_scene(scene) + , m_delegate(delegate) +{} + +TimelineAbstractTool::~TimelineAbstractTool() = default; + +TimelineGraphicsScene *TimelineAbstractTool::scene() const +{ + return m_scene; +} + +TimelineToolDelegate *TimelineAbstractTool::delegate() const +{ + return m_delegate; +} + +QPointF TimelineAbstractTool::startPosition() const +{ + return m_delegate->startPoint(); +} + +TimelineMovableAbstractItem *TimelineAbstractTool::currentItem() const +{ + return m_delegate->item(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h new file mode 100644 index 0000000000..0411a8d166 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QtGlobal> + +QT_FORWARD_DECLARE_CLASS(QGraphicsSceneMouseEvent) +QT_FORWARD_DECLARE_CLASS(QKeyEvent) +QT_FORWARD_DECLARE_CLASS(QPointF) + +namespace QmlDesigner { + +enum class ToolType { Move, Select }; + +class TimelineMovableAbstractItem; +class TimelineGraphicsScene; +class TimelineToolDelegate; + +class TimelineAbstractTool +{ +public: + explicit TimelineAbstractTool(TimelineGraphicsScene *scene); + explicit TimelineAbstractTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate); + virtual ~TimelineAbstractTool(); + + virtual void mousePressEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) = 0; + virtual void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) = 0; + virtual void mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) = 0; + virtual void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) = 0; + + virtual void keyPressEvent(QKeyEvent *keyEvent) = 0; + virtual void keyReleaseEvent(QKeyEvent *keyEvent) = 0; + + TimelineGraphicsScene *scene() const; + + TimelineToolDelegate *delegate() const; + + QPointF startPosition() const; + + TimelineMovableAbstractItem *currentItem() const; + +private: + TimelineGraphicsScene *m_scene; + + TimelineToolDelegate *m_delegate; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp new file mode 100644 index 0000000000..4751765874 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp @@ -0,0 +1,322 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineactions.h" + +#include "timelineutils.h" +#include "timelineview.h" + +#include <bindingproperty.h> +#include <designdocument.h> +#include <designdocumentview.h> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <utils/algorithm.h> +#include <utils/qtcassert.h> +#include <variantproperty.h> +#include <qmldesignerplugin.h> +#include <qmlobjectnode.h> +#include <qmltimelinekeyframegroup.h> + +namespace QmlDesigner { + +TimelineActions::TimelineActions() = default; + +void TimelineActions::deleteAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + targetNode.view()->executeInTransaction("TimelineActions::deleteAllKeyframesForTarget", [=](){ + if (timeline.isValid()) { + for (auto frames : timeline.keyframeGroupsForTarget(targetNode)) + frames.destroy(); + } + }); +} + +void TimelineActions::insertAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + targetNode.view()->executeInTransaction("TimelineActions::insertAllKeyframesForTarget", [=](){ + auto object = QmlObjectNode(targetNode); + if (timeline.isValid() && object.isValid()) { + for (auto frames : timeline.keyframeGroupsForTarget(targetNode)) { + QVariant value = object.instanceValue(frames.propertyName()); + frames.setValue(value, timeline.currentKeyframe()); + } + } + + }); +} + +void TimelineActions::copyAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + DesignDocumentView::copyModelNodes(Utils::transform(timeline.keyframeGroupsForTarget(targetNode), + &QmlTimelineKeyframeGroup::modelNode)); +} + +void TimelineActions::pasteKeyframesToTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + if (timeline.isValid()) { + QScopedPointer<Model> pasteModel(DesignDocumentView::pasteToModel()); + + if (!pasteModel) + return; + + DesignDocumentView view; + pasteModel->attachView(&view); + + if (!view.rootModelNode().isValid()) + return; + + ModelNode rootNode = view.rootModelNode(); + + //Sanity check + if (!QmlTimelineKeyframeGroup::checkKeyframesType(rootNode)) { + for (const ModelNode &node : rootNode.directSubModelNodes()) + if (!QmlTimelineKeyframeGroup::checkKeyframesType(node)) + return; + } + + pasteModel->detachView(&view); + + view.executeInTransaction("TimelineActions::pasteKeyframesToTarget", [=, &view](){ + + + targetNode.view()->model()->attachView(&view); + + ModelNode nonConstTargetNode = targetNode; + nonConstTargetNode.validId(); + + if (QmlTimelineKeyframeGroup::checkKeyframesType(rootNode)) { + /* Single selection */ + + ModelNode newNode = view.insertModel(rootNode); + QmlTimelineKeyframeGroup frames(newNode); + frames.setTarget(targetNode); + + timeline.modelNode().defaultNodeListProperty().reparentHere(newNode); + + } else { + /* Multi selection */ + for (const ModelNode &node : rootNode.directSubModelNodes()) { + ModelNode newNode = view.insertModel(node); + QmlTimelineKeyframeGroup frames(newNode); + frames.setTarget(targetNode); + timeline.modelNode().defaultNodeListProperty().reparentHere(newNode); + } + } + }); + } +} + +void TimelineActions::copyKeyframes(const QList<ModelNode> &keyframes) +{ + QList<ModelNode> nodes; + for (const auto &node : keyframes) { + NodeAbstractProperty pp = node.parentProperty(); + QTC_ASSERT(pp.isValid(), return ); + + ModelNode parentModelNode = pp.parentModelNode(); + for (const auto &property : parentModelNode.properties()) { + auto name = property.name(); + if (property.isBindingProperty()) { + BindingProperty bp = property.toBindingProperty(); + ModelNode bpNode = bp.resolveToModelNode(); + if (bpNode.isValid()) + node.setAuxiliaryData(name, bpNode.id()); + } else if (property.isVariantProperty()) { + VariantProperty vp = property.toVariantProperty(); + node.setAuxiliaryData(name, vp.value()); + } + } + + nodes << node; + } + + DesignDocumentView::copyModelNodes(nodes); +} + +bool isKeyframe(const ModelNode &node) +{ + return node.isValid() && node.metaInfo().isValid() + && node.metaInfo().isSubclassOf("QtQuick.Timeline.Keyframe"); +} + +QVariant getValue(const ModelNode &node) +{ + if (node.isValid()) + return node.variantProperty("value").value(); + + return QVariant(); +} + +qreal getTime(const ModelNode &node) +{ + Q_ASSERT(node.isValid()); + Q_ASSERT(node.hasProperty("frame")); + + return node.variantProperty("frame").value().toReal(); +} + +QmlTimelineKeyframeGroup getFrameGroup(const ModelNode &node, + AbstractView *timelineView, + const QmlTimeline &timeline) +{ + QVariant targetId = node.auxiliaryData("target"); + QVariant property = node.auxiliaryData("property"); + + if (targetId.isValid() && property.isValid()) { + ModelNode targetNode = timelineView->modelNodeForId(targetId.toString()); + if (targetNode.isValid()) { + for (QmlTimelineKeyframeGroup frameGrp : timeline.keyframeGroupsForTarget(targetNode)) { + if (frameGrp.propertyName() == property.toByteArray()) + return frameGrp; + } + } + } + return QmlTimelineKeyframeGroup(); +} + +void pasteKeyframe(const qreal expectedTime, + const ModelNode &keyframe, + AbstractView *timelineView, + const QmlTimeline &timeline) +{ + QmlTimelineKeyframeGroup group = getFrameGroup(keyframe, timelineView, timeline); + if (group.isValid()) { + const qreal clampedTime = TimelineUtils::clamp(expectedTime, + timeline.startKeyframe(), + timeline.endKeyframe()); + + // Create a new frame ... + group.setValue(getValue(keyframe), clampedTime); + + // ... look it up by time ... + for (const ModelNode &key : group.keyframePositions()) { + qreal time = key.variantProperty("frame").value().toReal(); + if (qFuzzyCompare(clampedTime, time)) { + // ... and transfer the properties. + for (const auto &property : keyframe.properties()) { + if (property.name() == "frame" || property.name() == "value") + continue; + + if (property.isVariantProperty()) { + auto vp = property.toVariantProperty(); + key.variantProperty(vp.name()).setValue(vp.value()); + } else if (property.isBindingProperty()) { + auto bp = property.toBindingProperty(); + key.bindingProperty(bp.name()).setExpression(bp.expression()); + } + } + } + } + } +} + +std::vector<std::tuple<ModelNode, qreal>> getFramesRelative(const ModelNode &parent) +{ + auto byTime = [](const ModelNode &lhs, const ModelNode &rhs) { + return getTime(lhs) < getTime(rhs); + }; + + std::vector<std::tuple<ModelNode, qreal>> result; + + QList<ModelNode> sortedByTime; + QList<ModelNode> subs(parent.directSubModelNodes()); + + std::copy_if(subs.begin(), subs.end(), std::back_inserter(sortedByTime), &isKeyframe); + std::sort(sortedByTime.begin(), sortedByTime.end(), byTime); + + if (!sortedByTime.empty()) { + qreal firstTime = getTime(sortedByTime.first()); + for (ModelNode keyframe : sortedByTime) + result.emplace_back(keyframe, getTime(keyframe) - firstTime); + } + + return result; +} + +void TimelineActions::pasteKeyframes(AbstractView *timelineView, const QmlTimeline &timeline) +{ + QScopedPointer<Model> pasteModel(DesignDocumentView::pasteToModel()); + + if (!pasteModel) + return; + + DesignDocumentView view; + pasteModel->attachView(&view); + + if (!view.rootModelNode().isValid()) + return; + + const qreal currentTime = timeline.currentKeyframe(); + + ModelNode rootNode = view.rootModelNode(); + + timelineView->executeInTransaction("TimelineActions::pasteKeyframes", [=](){ + if (isKeyframe(rootNode)) + pasteKeyframe(currentTime, rootNode, timelineView, timeline); + else + for (auto frame : getFramesRelative(rootNode)) + pasteKeyframe(currentTime + std::get<1>(frame), + std::get<0>(frame), + timelineView, + timeline); + + }); +} + +bool TimelineActions::clipboardContainsKeyframes() +{ + QScopedPointer<Model> pasteModel(DesignDocumentView::pasteToModel()); + + if (!pasteModel) + return false; + + DesignDocumentView view; + pasteModel->attachView(&view); + + if (!view.rootModelNode().isValid()) + return false; + + ModelNode rootNode = view.rootModelNode(); + + if (!rootNode.hasAnySubModelNodes()) + return false; + + //Sanity check + if (!QmlTimelineKeyframeGroup::checkKeyframesType(rootNode)) { + for (const ModelNode &node : rootNode.directSubModelNodes()) + if (!QmlTimelineKeyframeGroup::checkKeyframesType(node)) + return false; + } + + return true; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineactions.h b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.h new file mode 100644 index 0000000000..bae09b110f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.h @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <modelnode.h> +#include <qmltimeline.h> + +namespace QmlDesigner { + +class TimelineActions +{ +public: + static void deleteAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline); + static void insertAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline); + static void copyAllKeyframesForTarget(const ModelNode &targetNode, const QmlTimeline &timeline); + static void pasteKeyframesToTarget(const ModelNode &targetNode, const QmlTimeline &timeline); + + static void copyKeyframes(const QList<ModelNode> &keyframes); + static void pasteKeyframes(AbstractView *timelineView, const QmlTimeline &TimelineActions); + + static bool clipboardContainsKeyframes(); + +private: + TimelineActions(); +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp new file mode 100644 index 0000000000..032a133f89 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp @@ -0,0 +1,262 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineanimationform.h" +#include "ui_timelineanimationform.h" + +#include <abstractview.h> +#include <bindingproperty.h> +#include <exception> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <signalhandlerproperty.h> +#include <variantproperty.h> +#include <qmlitemnode.h> +#include <qmlobjectnode.h> + +#include <coreplugin/messagebox.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +TimelineAnimationForm::TimelineAnimationForm(QWidget *parent) + : QWidget(parent) + , ui(new Ui::TimelineAnimationForm) +{ + ui->setupUi(this); + + connectSpinBox(ui->duration, "duration"); + connectSpinBox(ui->loops, "loops"); + + connectSpinBox(ui->startFrame, "from"); + connectSpinBox(ui->endFrame, "to"); + + connect(ui->loops, QOverload<int>::of(&QSpinBox::valueChanged), [this]() { + ui->continuous->setChecked(ui->loops->value() == -1); + }); + + connect(ui->continuous, &QCheckBox::toggled, [this](bool checked) { + if (checked) { + setProperty("loops", -1); + ui->loops->setValue(-1); + } else { + setProperty("loops", 1); + ui->loops->setValue(1); + } + }); + + connect(ui->idLineEdit, &QLineEdit::editingFinished, [this]() { + QTC_ASSERT(m_timeline.isValid(), return ); + + static QString lastString; + + const QString newId = ui->idLineEdit->text(); + + if (lastString == newId) + return; + + lastString = newId; + + if (newId == animation().id()) + return; + + bool error = false; + + if (!ModelNode::isValidId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 is an invalid id.").arg(newId)); + error = true; + } else if (animation().view()->hasId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 already exists.").arg(newId)); + } else { + animation().setIdWithRefactoring(newId); + error = true; + } + + if (error) { + lastString.clear(); + ui->idLineEdit->setText(animation().id()); + } + }); + + connect(ui->running, &QCheckBox::clicked, [this](bool checked) { + if (checked) { + setProperty("running", true); + } else { + setProperty("running", false); + } + }); + + connect(ui->pingPong, &QCheckBox::clicked, [this](bool checked) { + if (checked) { + setProperty("pingPong", true); + } else { + setProperty("pingPong", false); + } + }); + + connect(ui->transitionToState, + QOverload<int>::of(&QComboBox::activated), + [this](int index) { + if (!m_animation.isValid()) + return; + if (!m_animation.view()->rootModelNode().hasId()) + return; + + ModelNode rootNode = m_animation.view()->rootModelNode(); + + if (index == 0) { + if (m_animation.signalHandlerProperty("onFinished").isValid()) + m_animation.removeProperty("onFinished"); + } else if (index == 1) { + m_animation.signalHandlerProperty("onFinished") + .setSource(rootNode.id() + ".state = \"" + "\""); + } else { + m_animation.signalHandlerProperty("onFinished") + .setSource(rootNode.id() + ".state = \"" + + ui->transitionToState->currentText() + "\""); + } + }); +} + +TimelineAnimationForm::~TimelineAnimationForm() +{ + delete ui; +} + +void TimelineAnimationForm::setup(const ModelNode &animation) +{ + m_timeline = QmlTimeline(animation.parentProperty().parentModelNode()); + setAnimation(animation); + setupAnimation(); +} + +ModelNode TimelineAnimationForm::animation() const +{ + return m_animation; +} + +void TimelineAnimationForm::setAnimation(const ModelNode &animation) +{ + m_animation = animation; +} + +void TimelineAnimationForm::setupAnimation() +{ + if (!m_animation.isValid()) + setEnabled(false); + + if (m_animation.isValid()) { + setEnabled(true); + + ui->idLineEdit->setText(m_animation.id()); + + if (m_animation.hasVariantProperty("duration")) + ui->duration->setValue(m_animation.variantProperty("duration").value().toInt()); + else + ui->duration->setValue(0); + + ui->startFrame->setValue(m_animation.variantProperty("from").value().toInt()); + ui->endFrame->setValue(m_animation.variantProperty("to").value().toInt()); + + if (m_animation.hasVariantProperty("loops")) + ui->loops->setValue(m_animation.variantProperty("loops").value().toInt()); + else + ui->loops->setValue(0); + + if (m_animation.hasVariantProperty("running")) + ui->running->setChecked(m_animation.variantProperty("running").value().toBool()); + else + ui->running->setChecked(false); + + if (m_animation.hasVariantProperty("pingPong")) + ui->pingPong->setChecked(m_animation.variantProperty("pingPong").value().toBool()); + else + ui->pingPong->setChecked(false); + + ui->continuous->setChecked(ui->loops->value() == -1); + } + + populateStateComboBox(); + + ui->duration->setEnabled(m_animation.isValid()); + ui->running->setEnabled(m_animation.isValid()); + ui->continuous->setEnabled(m_animation.isValid()); + ui->loops->setEnabled(m_animation.isValid()); +} + +void TimelineAnimationForm::setProperty(const PropertyName &propertyName, const QVariant &value) +{ + QTC_ASSERT(m_animation.isValid(), return ); + + try { + m_animation.variantProperty(propertyName).setValue(value); + } catch (const Exception &e) { + e.showException(); + } +} + +void TimelineAnimationForm::connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName) +{ + connect(spinBox, &QSpinBox::editingFinished, [this, propertyName, spinBox]() { + setProperty(propertyName, spinBox->value()); + }); +} + +void TimelineAnimationForm::populateStateComboBox() +{ + ui->transitionToState->clear(); + ui->transitionToState->addItem(tr("none")); + ui->transitionToState->addItem(tr("Base State")); + if (!m_animation.isValid()) + return; + QmlObjectNode rootNode = QmlObjectNode(m_animation.view()->rootModelNode()); + if (rootNode.isValid() && rootNode.modelNode().hasId()) { + for (const QmlModelState &state : QmlItemNode(rootNode).states().allStates()) { + ui->transitionToState + ->addItem(state.modelNode().variantProperty("name").value().toString(), + QVariant::fromValue<ModelNode>(state.modelNode())); + } + if (m_animation.signalHandlerProperty("onFinished").isValid()) { + const QString source = m_animation.signalHandlerProperty("onFinished").source(); + const QStringList list = source.split("="); + if (list.count() == 2) { + QString name = list.last().trimmed(); + name.chop(1); + name.remove(0, 1); + if (name.isEmpty()) + ui->transitionToState->setCurrentIndex(1); + else + ui->transitionToState->setCurrentText(name); + } + } + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h new file mode 100644 index 0000000000..1b5eaf6cab --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QWidget> + +QT_FORWARD_DECLARE_CLASS(QSpinBox) + +namespace QmlDesigner { + +namespace Ui { +class TimelineAnimationForm; +} + +class TimelineAnimationForm : public QWidget +{ + Q_OBJECT + +public: + explicit TimelineAnimationForm(QWidget *parent); + ~TimelineAnimationForm() override; + void setup(const ModelNode &animation); + ModelNode animation() const; + +private: + void setupAnimation(); + + void setAnimation(const ModelNode &animation); + void setProperty(const PropertyName &propertyName, const QVariant &value); + void connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName); + void populateStateComboBox(); + + Ui::TimelineAnimationForm *ui; + + QmlTimeline m_timeline; + ModelNode m_animation; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui new file mode 100644 index 0000000000..5d13cfa726 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui @@ -0,0 +1,327 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::TimelineAnimationForm</class> + <widget class="QWidget" name="QmlDesigner::TimelineAnimationForm"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>641</width> + <height>176</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="4" column="3"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>Loops:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="4" column="4" colspan="2"> + <widget class="QSpinBox" name="loops"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-1</number> + </property> + <property name="maximum"> + <number>1000</number> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>Continuous</string> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QCheckBox" name="continuous"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="5" column="2"> + <widget class="QComboBox" name="transitionToState"> + <item> + <property name="text"> + <string>none</string> + </property> + </item> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="minimumSize"> + <size> + <width>160</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Animation Settings</string> + </property> + </widget> + </item> + <item row="1" column="9"> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_11"> + <property name="minimumSize"> + <size> + <width>140</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>Animation ID:</string> + </property> + </widget> + </item> + <item row="3" column="9"> + <spacer name="horizontalSpacer_7"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="8"> + <widget class="QSpinBox" name="duration"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLabel" name="label_23"> + <property name="text"> + <string>Finished:</string> + </property> + </widget> + </item> + <item row="4" column="7"> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>Ping pong</string> + </property> + </widget> + </item> + <item row="4" column="8"> + <widget class="QCheckBox" name="pingPong"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="4" column="9"> + <spacer name="horizontalSpacer_8"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_20"> + <property name="minimumSize"> + <size> + <width>140</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>Transition to state:</string> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLineEdit" name="idLineEdit"> + <property name="text"> + <string>animation02</string> + </property> + </widget> + </item> + <item row="2" column="3" colspan="2"> + <widget class="QLabel" name="label_18"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Running in base state</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QSpinBox" name="startFrame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Start frame:</string> + </property> + </widget> + </item> + <item row="2" column="5"> + <widget class="QCheckBox" name="running"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="2" column="9"> + <spacer name="horizontalSpacer_6"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="7"> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Duration:</string> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>End frame:</string> + </property> + </widget> + </item> + <item row="3" column="4" colspan="3"> + <widget class="QSpinBox" name="endFrame"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h b/src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h new file mode 100644 index 0000000000..6c012beec1 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QGraphicsItem> + +namespace QmlDesigner { +namespace TimelineConstants { +const int sectionHeight = 18; +const int rulerHeight = sectionHeight + 4; +const int sectionWidth = 200; +const int moveableAbstractItemUserType = QGraphicsItem::UserType + 1; +const int timelineSectionItemUserType = QGraphicsItem::UserType + 2; +const int timelinePropertyItemUserType = QGraphicsItem::UserType + 3; +const int textIndentationProperties = 54; +const int textIndentationSections = 24; +const int toolButtonSize = 11; +const int timelineBounds = 8; +const int timelineLeftOffset = 10; + +const char timelineCategory[] = "Timeline"; +const int priorityTimelineCategory = 110; +const char timelineCategoryDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Timeline"); + +const char timelineCopyKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Copy All Keyframes"); +const char timelinePasteKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Paste Keyframes"); +const char timelineInsertKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Add Keyframes at Current Frame"); +const char timelineDeleteKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Delete All Keyframes"); + +const char timelineStatusBarFrameNumber[] = QT_TRANSLATE_NOOP("QmlDesignerTimeline", "Frame %1"); + +const char C_QMLTIMELINE[] = "QmlDesigner::Timeline"; +const char C_SETTINGS[] = "QmlDesigner.Settings"; +const char C_ADD_TIMELINE[] = "QmlDesigner.AddTimeline"; +const char C_TO_START[] = "QmlDesigner.ToStart"; +const char C_TO_END[] = "QmlDesigner.ToEnd"; +const char C_PREVIOUS[] = "QmlDesigner.Previous"; +const char C_PLAY[] = "QmlDesigner.Play"; +const char C_NEXT[] = "QmlDesigner.Next"; +const char C_AUTO_KEYFRAME[] = "QmlDesigner.AutoKeyframe"; +const char C_CURVE_PICKER[] = "QmlDesigner.CurvePicker"; +const char C_ZOOM_IN[] = "QmlDesigner.ZoomIn"; +const char C_ZOOM_OUT[] = "QmlDesigner.ZoomOut"; + +const char C_BAR_ITEM_OVERRIDE[] = "Timeline.OverrideColor"; + +const int keyFrameSize = 17; +const int keyFrameMargin = 2; +} // namespace TimelineConstants +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp new file mode 100644 index 0000000000..b3e2e12a3f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinecontext.h" +#include "timelineconstants.h" +#include "timelinewidget.h" + +namespace QmlDesigner { + +TimelineContext::TimelineContext(QWidget *widget) + : IContext(widget) +{ + setWidget(widget); + setContext(Core::Context(TimelineConstants::C_QMLTIMELINE)); +} + +void TimelineContext::contextHelp(const Core::IContext::HelpCallback &callback) const +{ + if (auto *widget = qobject_cast<TimelineWidget *>(m_widget)) + widget->contextHelp(callback); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h new file mode 100644 index 0000000000..a8a6ca3b8f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h @@ -0,0 +1,43 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <coreplugin/icontext.h> + +#include <QWidget> + +namespace QmlDesigner { + +class TimelineContext : public Core::IContext +{ + Q_OBJECT + +public: + explicit TimelineContext(QWidget *widget); + void contextHelp(const HelpCallback &callback) const override; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp new file mode 100644 index 0000000000..682a14f1c7 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp @@ -0,0 +1,211 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinecontrols.h" +#include "timelinepropertyitem.h" + +#include <coreplugin/icore.h> + +#include <QColorDialog> +#include <QMouseEvent> +#include <QPaintEvent> +#include <QPainter> +#include <QToolTip> + +#include <theme.h> + +#include <limits> + +namespace QmlDesigner { + +TimelineControl *createTimelineControl(const TypeName &name) +{ + if (name == "real" || name == "double" || name == "float") + return new FloatControl; + if (name == "QColor" || name == "color") + return new ColorControl; + + return nullptr; +} + +FloatControl::FloatControl() + : QDoubleSpinBox(nullptr) +{ + setValue(0.0); + setButtonSymbols(QAbstractSpinBox::NoButtons); + setFrame(false); +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + setStepType(QAbstractSpinBox::AdaptiveDecimalStepType); +#endif + + setMinimum(std::numeric_limits<float>::lowest()); + setMaximum(std::numeric_limits<float>::max()); + + QColor bg = Theme::instance()->qmlDesignerBackgroundColorDarkAlternate(); + + auto p = palette(); + p.setColor(QPalette::Base, bg.darker(110)); + setPalette(p); + + m_timer.setInterval(100); + m_timer.setSingleShot(true); + + auto startTimer = [this]( ) { m_timer.start(); }; + auto deferredSlot = [this]( ) { emit controlValueChanged(QVariant(this->value())); }; + + QObject::connect(this, &QDoubleSpinBox::editingFinished, &m_timer, startTimer); + QObject::connect(&m_timer, &QTimer::timeout, deferredSlot); +} + +FloatControl::~FloatControl() = default; + +QWidget *FloatControl::widget() +{ + return this; +} + +void FloatControl::connect(TimelinePropertyItem *item) +{ + QObject::connect(this, + &FloatControl::controlValueChanged, + item, + &TimelinePropertyItem::changePropertyValue); +} + +QVariant FloatControl::controlValue() const +{ + return QVariant(value()); +} + +void FloatControl::setControlValue(const QVariant &value) +{ + if (value.userType() != QMetaType::Float && value.userType() != QMetaType::Double) + return; + + QSignalBlocker blocker(this); + setValue(value.toDouble()); +} + +void FloatControl::setSize(int width, int height) +{ + setFixedWidth(width); + setFixedHeight(height); +} + +ColorControl::ColorControl() + : QWidget(nullptr) + , m_color(Qt::black) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(20); +} + +ColorControl::ColorControl(const QColor &color, QWidget *parent) + : QWidget(parent) + , m_color(color) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(20); +} + +ColorControl::~ColorControl() = default; + +QWidget *ColorControl::widget() +{ + return this; +} + +void ColorControl::connect(TimelinePropertyItem *item) +{ + QObject::connect(this, + &ColorControl::controlValueChanged, + item, + &TimelinePropertyItem::changePropertyValue); +} + +QVariant ColorControl::controlValue() const +{ + return QVariant(value()); +} + +void ColorControl::setControlValue(const QVariant &value) +{ + if (value.userType() != QMetaType::QColor) + return; + + m_color = qvariant_cast<QColor>(value); +} + +void ColorControl::setSize(int width, int height) +{ + setFixedWidth(width); + setFixedHeight(height); +} + +QColor ColorControl::value() const +{ + return m_color; +} + +bool ColorControl::event(QEvent *event) +{ + if (event->type() == QEvent::ToolTip) { + if (auto helpEvent = static_cast<const QHelpEvent *>(event)) { + QToolTip::showText(helpEvent->globalPos(), m_color.name()); + return true; + } + } + return QWidget::event(event); +} + +void ColorControl::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.fillRect(event->rect(), m_color); +} + +void ColorControl::mouseReleaseEvent(QMouseEvent *event) +{ + QColor color = QColorDialog::getColor(m_color, Core::ICore::dialogParent()); + + event->accept(); + + if (color != m_color) { + m_color = color; + update(); + emit valueChanged(); + emit controlValueChanged(QVariant(m_color)); + } +} + +void ColorControl::mousePressEvent(QMouseEvent *event) +{ + // Needed to make the mouseRelease Event work if this + // widget is embedded inside a QGraphicsProxyWidget. + QWidget::mousePressEvent(event); + event->accept(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h new file mode 100644 index 0000000000..4c30357be9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h @@ -0,0 +1,123 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <nodemetainfo.h> + +#include <QDoubleSpinBox> +#include <QTimer> +#include <QWidget> + +namespace QmlDesigner { + +class TimelinePropertyItem; + +class TimelineControl +{ +public: + virtual ~TimelineControl() = default; + + virtual QWidget *widget() = 0; + + virtual void connect(TimelinePropertyItem *scene) = 0; + + virtual QVariant controlValue() const = 0; + + virtual void setControlValue(const QVariant &value) = 0; + + virtual void setSize(int width, int height) = 0; +}; + +TimelineControl *createTimelineControl(const TypeName &name); + +class FloatControl : public QDoubleSpinBox, public TimelineControl +{ + Q_OBJECT + +public: + FloatControl(); + + ~FloatControl() override; + + QWidget *widget() override; + + void connect(TimelinePropertyItem *scene) override; + + QVariant controlValue() const override; + + void setControlValue(const QVariant &value) override; + + void setSize(int width, int height) override; + +signals: + void controlValueChanged(const QVariant &value); + +private: + QTimer m_timer; +}; + +class ColorControl : public QWidget, public TimelineControl +{ + Q_OBJECT + +public: + ColorControl(); + + ColorControl(const QColor &color, QWidget *parent = nullptr); + + ~ColorControl() override; + + QWidget *widget() override; + + void connect(TimelinePropertyItem *item) override; + + QVariant controlValue() const override; + + void setControlValue(const QVariant &value) override; + + void setSize(int width, int height) override; + + QColor value() const; + +protected: + bool event(QEvent *event) override; + + void paintEvent(QPaintEvent *event) override; + + void mouseReleaseEvent(QMouseEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + +signals: + void valueChanged(); + + void controlValueChanged(const QVariant &value); + +private: + QColor m_color; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri b/src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri new file mode 100644 index 0000000000..8001748fb0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri @@ -0,0 +1,81 @@ +QT *= qml quick core + +VPATH += $$PWD + +INCLUDEPATH += $$PWD + +DEFINES += TIMELINE_QML_PATH=\\\"$$PWD/qml/\\\" + +SOURCES += \ + timelineview.cpp \ + timelinewidget.cpp \ + timelinegraphicsscene.cpp \ + timelinegraphicslayout.cpp \ + timelinepropertyitem.cpp \ + timelinesectionitem.cpp \ + timelineitem.cpp \ + timelinemovableabstractitem.cpp \ + timelineabstracttool.cpp \ + timelinemovetool.cpp \ + timelineselectiontool.cpp \ + timelineplaceholder.cpp \ + setframevaluedialog.cpp \ + timelinetoolbar.cpp \ + easingcurvedialog.cpp \ + timelinetoolbutton.cpp \ + timelinesettingsdialog.cpp \ + timelineactions.cpp \ + timelinecontext.cpp \ + timelineutils.cpp \ + timelineanimationform.cpp \ + timelineform.cpp \ + splineeditor.cpp \ + preseteditor.cpp \ + canvas.cpp \ + canvasstyledialog.cpp \ + easingcurve.cpp \ + timelinesettingsmodel.cpp \ + timelinetooldelegate.cpp \ + timelinecontrols.cpp + +HEADERS += \ + timelineview.h \ + timelinewidget.h \ + timelinegraphicsscene.h \ + timelinegraphicslayout.h \ + timelinepropertyitem.h \ + timelinesectionitem.h \ + timelineitem.h \ + timelineconstants.h \ + timelinemovableabstractitem.h \ + timelineabstracttool.h \ + timelinemovetool.h \ + timelineselectiontool.h \ + timelineplaceholder.h \ + timelineicons.h \ + timelinetoolbar.h \ + setframevaluedialog.h \ + easingcurvedialog.h \ + timelinetoolbutton.h \ + timelinesettingsdialog.h \ + timelineactions.h \ + timelinecontext.h \ + timelineutils.h \ + timelineanimationform.h \ + timelineform.h \ + splineeditor.h \ + preseteditor.h \ + canvas.h \ + canvasstyledialog.h \ + easingcurve.h \ + timelinesettingsmodel.h \ + timelinecontrols.h + +RESOURCES += \ + timeline.qrc + +FORMS += \ + setframevaluedialog.ui \ + timelinesettingsdialog.ui \ + timelineanimationform.ui \ + timelineform.ui diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp new file mode 100644 index 0000000000..eb63ad4883 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp @@ -0,0 +1,186 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineform.h" +#include "ui_timelineform.h" + +#include <abstractview.h> +#include <bindingproperty.h> +#include <exception> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <variantproperty.h> + +#include <coreplugin/messagebox.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +TimelineForm::TimelineForm(QWidget *parent) + : QWidget(parent) + , ui(new Ui::TimelineForm) +{ + ui->setupUi(this); + + ui->duration->setVisible(false); + + connect(ui->expressionBindingLineEdit, &QLineEdit::editingFinished, [this]() { + QTC_ASSERT(m_timeline.isValid(), return ); + + + static QString lastString; + + const QString bindingText = ui->expressionBindingLineEdit->text(); + + if (bindingText == lastString) + return; + + lastString = bindingText; + + if (bindingText.isEmpty()) { + ui->animation->setChecked(true); + try { + m_timeline.modelNode().removeProperty("currentFrame"); + } catch (const Exception &e) { + e.showException(); + } + return; + } + + ui->expressionBinding->setChecked(true); + + try { + m_timeline.modelNode() + .bindingProperty("currentFrame") + .setExpression(bindingText); + } catch (const Exception &e) { + e.showException(); + } + }); + + connect(ui->idLineEdit, &QLineEdit::editingFinished, [this]() { + QTC_ASSERT(m_timeline.isValid(), return ); + + static QString lastString; + + const QString newId = ui->idLineEdit->text(); + + if (newId == lastString) + return; + + lastString = newId; + + if (newId == m_timeline.modelNode().id()) + return; + + bool error = false; + + if (!ModelNode::isValidId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 is an invalid id.").arg(newId)); + error = true; + } else if (m_timeline.view()->hasId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 already exists.").arg(newId)); + error = true; + } else { + m_timeline.modelNode().setIdWithRefactoring(newId); + } + + if (error) { + lastString.clear(); + ui->idLineEdit->setText(m_timeline.modelNode().id()); + } + }); + + connectSpinBox(ui->startFrame, "startFrame"); + connectSpinBox(ui->endFrame, "endFrame"); +} + +TimelineForm::~TimelineForm() +{ + delete ui; +} + +void TimelineForm::setTimeline(const QmlTimeline &timeline) +{ + m_timeline = timeline; + + ui->expressionBindingLineEdit->clear(); + + if (m_timeline.isValid()) { + ui->idLineEdit->setText(m_timeline.modelNode().displayName()); + ui->duration->setValue(qRound(m_timeline.duration())); + ui->startFrame->setValue( + m_timeline.modelNode().variantProperty("startFrame").value().toInt()); + ui->endFrame->setValue(m_timeline.modelNode().variantProperty("endFrame").value().toInt()); + + ui->duration->setValue(qRound(m_timeline.duration())); + + if (m_timeline.modelNode().hasBindingProperty("currentFrame")) { + ui->expressionBindingLineEdit->setText( + m_timeline.modelNode().bindingProperty("currentFrame").expression()); + ui->expressionBinding->setChecked(true); + } else { + ui->expressionBinding->setChecked(false); + } + } +} + +QmlTimeline TimelineForm::timeline() const +{ + return m_timeline; +} + +void TimelineForm::setHasAnimation(bool b) +{ + ui->expressionBinding->setChecked(!b); + ui->animation->setChecked(b); + ui->expressionBindingLineEdit->setDisabled(b); +} + +void TimelineForm::setProperty(const PropertyName &propertyName, const QVariant &value) +{ + QTC_ASSERT(m_timeline.isValid(), return ); + + try { + m_timeline.modelNode().variantProperty(propertyName).setValue(value); + } catch (const Exception &e) { + e.showException(); + } + ui->duration->setValue(qRound(m_timeline.duration())); +} + +void TimelineForm::connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName) +{ + connect(spinBox, &QSpinBox::editingFinished, [this, propertyName, spinBox]() { + setProperty(propertyName, spinBox->value()); + }); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineform.h b/src/plugins/qmldesigner/components/timelineeditor/timelineform.h new file mode 100644 index 0000000000..7745de3a0f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineform.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QWidget> + +QT_FORWARD_DECLARE_CLASS(QSpinBox) + +namespace QmlDesigner { + +namespace Ui { +class TimelineForm; +} + +class TimelineForm : public QWidget +{ + Q_OBJECT + +public: + explicit TimelineForm(QWidget *parent); + ~TimelineForm() override; + void setTimeline(const QmlTimeline &timeline); + QmlTimeline timeline() const; + void setHasAnimation(bool b); + +private: + void setProperty(const PropertyName &propertyName, const QVariant &value); + void connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName); + + Ui::TimelineForm *ui; + QmlTimeline m_timeline; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineform.ui b/src/plugins/qmldesigner/components/timelineeditor/timelineform.ui new file mode 100644 index 0000000000..b8b47e4c70 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineform.ui @@ -0,0 +1,254 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::TimelineForm</class> + <widget class="QWidget" name="QmlDesigner::TimelineForm"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>627</width> + <height>170</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="8" colspan="2"> + <spacer name="horizontalSpacer_11"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="5"> + <widget class="QSpinBox" name="endFrame"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="2" column="2" colspan="2"> + <widget class="QSpinBox" name="startFrame"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="2" column="6"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Duration</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Expression binding:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="8" colspan="2"> + <spacer name="horizontalSpacer_10"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="4"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>End frame:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="3" colspan="2"> + <widget class="QRadioButton" name="animation"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Animation</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="8" colspan="2"> + <spacer name="horizontalSpacer_12"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="4" column="1" colspan="5"> + <widget class="QLineEdit" name="expressionBindingLineEdit"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="minimumSize"> + <size> + <width>240</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item row="4" column="8" colspan="2"> + <spacer name="horizontalSpacer_9"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="7"> + <widget class="QSpinBox" name="duration"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="maximum"> + <number>20000</number> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QRadioButton" name="expressionBinding"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Expression binding</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="5"> + <widget class="QLineEdit" name="idLineEdit"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Timeline ID:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="minimumSize"> + <size> + <width>160</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Timeline Settings</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Start frame:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp new file mode 100644 index 0000000000..af0f26d249 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp @@ -0,0 +1,162 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinegraphicslayout.h" + +#include "timelinegraphicsscene.h" +#include "timelineplaceholder.h" +#include "timelinesectionitem.h" +#include "timelineview.h" + +#include <QGraphicsLinearLayout> + +#include <cmath> + +namespace QmlDesigner { + +TimelineGraphicsLayout::TimelineGraphicsLayout(TimelineGraphicsScene *scene, TimelineItem *parent) + : TimelineItem(parent) + , m_layout(new QGraphicsLinearLayout) + , m_rulerItem(TimelineRulerSectionItem::create(scene, this)) + , m_placeholder1(TimelinePlaceholder::create(scene, this)) + , m_placeholder2(TimelinePlaceholder::create(scene, this)) +{ + m_layout->setOrientation(Qt::Vertical); + m_layout->setSpacing(0); + m_layout->setContentsMargins(0, 0, 0, 0); + + m_layout->addItem(m_rulerItem); + m_layout->addItem(m_placeholder1); + m_layout->addItem(m_placeholder2); + + setLayout(m_layout); + + setPos(QPointF(0, 0)); + + connect(m_rulerItem, + &TimelineRulerSectionItem::rulerClicked, + this, + &TimelineGraphicsLayout::rulerClicked); +} + +TimelineGraphicsLayout::~TimelineGraphicsLayout() = default; + +double TimelineGraphicsLayout::rulerWidth() const +{ + return m_rulerItem->preferredWidth(); +} + +double TimelineGraphicsLayout::rulerScaling() const +{ + return m_rulerItem->rulerScaling(); +} + +double TimelineGraphicsLayout::rulerDuration() const +{ + return m_rulerItem->rulerDuration(); +} + +double TimelineGraphicsLayout::startFrame() const +{ + return m_rulerItem->startFrame(); +} + +double TimelineGraphicsLayout::endFrame() const +{ + return m_rulerItem->endFrame(); +} + +void TimelineGraphicsLayout::setWidth(int width) +{ + m_rulerItem->setSizeHints(width); + m_placeholder1->setMinimumWidth(width); + m_placeholder2->setMinimumWidth(width); + setPreferredWidth(width); + setMaximumWidth(width); +} + +void TimelineGraphicsLayout::setTimeline(const QmlTimeline &timeline) +{ + m_layout->removeItem(m_rulerItem); + m_layout->removeItem(m_placeholder1); + m_layout->removeItem(m_placeholder2); + + m_rulerItem->setParentItem(nullptr); + m_placeholder1->setParentItem(nullptr); + m_placeholder2->setParentItem(nullptr); + + qDeleteAll(this->childItems()); + + m_rulerItem->setParentItem(this); + m_rulerItem->invalidateRulerSize(timeline); + m_layout->addItem(m_rulerItem); + + m_placeholder1->setParentItem(this); + m_layout->addItem(m_placeholder1); + + m_layout->invalidate(); + + if (timeline.isValid()) { + for (const ModelNode &target : timeline.allTargets()) { + if (target.isValid()) { + auto item = TimelineSectionItem::create(timeline, target, this); + m_layout->addItem(item); + } + } + } + + m_placeholder2->setParentItem(this); + m_layout->addItem(m_placeholder2); + + if (auto *scene = timelineScene()) + if (auto *view = scene->timelineView()) + if (!timeline.isValid() && view->isAttached()) + emit scaleFactorChanged(0); +} + +void TimelineGraphicsLayout::setRulerScaleFactor(int factor) +{ + m_rulerItem->setRulerScaleFactor(factor); +} + +void TimelineGraphicsLayout::invalidate() +{ + m_layout->invalidate(); +} + +int TimelineGraphicsLayout::maximumScrollValue() const +{ + const qreal w = this->geometry().width() - qreal(TimelineConstants::sectionWidth); + const qreal duration = m_rulerItem->rulerDuration() + m_rulerItem->rulerDuration() * 0.1; + const qreal maxr = m_rulerItem->rulerScaling() * duration - w; + return std::round(qMax(maxr, 0.0)); +} + +void TimelineGraphicsLayout::activate() +{ + m_layout->activate(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h new file mode 100644 index 0000000000..d5b7c4debc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineitem.h" + +QT_FORWARD_DECLARE_CLASS(QGraphicsLinearLayout) + +namespace QmlDesigner { + +class TimelineItem; +class TimelineRulerSectionItem; +class TimelinePlaceholder; + +class QmlTimeline; + +class TimelineGraphicsLayout : public TimelineItem +{ + Q_OBJECT + +signals: + void rulerClicked(const QPointF &pos); + + void scaleFactorChanged(int factor); + +public: + TimelineGraphicsLayout(TimelineGraphicsScene *scene, TimelineItem *parent = nullptr); + + ~TimelineGraphicsLayout() override; + +public: + double rulerWidth() const; + + double rulerScaling() const; + + double rulerDuration() const; + + double startFrame() const; + + double endFrame() const; + + void setWidth(int width); + + void setTimeline(const QmlTimeline &timeline); + + void setRulerScaleFactor(int factor); + + void invalidate(); + + int maximumScrollValue() const; + + void activate(); + +private: + QGraphicsLinearLayout *m_layout = nullptr; + + TimelineRulerSectionItem *m_rulerItem = nullptr; + + TimelinePlaceholder *m_placeholder1 = nullptr; + + TimelinePlaceholder *m_placeholder2 = nullptr; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp new file mode 100644 index 0000000000..99cc486710 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp @@ -0,0 +1,720 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinegraphicsscene.h" + +#include "timelineactions.h" +#include "timelinegraphicslayout.h" +#include "timelineitem.h" +#include "timelinemovableabstractitem.h" +#include "timelinemovetool.h" +#include "timelineplaceholder.h" +#include "timelinepropertyitem.h" +#include "timelinesectionitem.h" +#include "timelinetoolbar.h" +#include "timelineview.h" +#include "timelinewidget.h" + +#include <designdocumentview.h> +#include <exception.h> +#include <rewritertransaction.h> +#include <rewriterview.h> +#include <viewmanager.h> +#include <qmldesignerplugin.h> +#include <qmlobjectnode.h> +#include <qmltimelinekeyframegroup.h> + +#include <bindingproperty.h> + +#include <nodeabstractproperty.h> +#include <nodelistproperty.h> +#include <variantproperty.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +#include <QComboBox> +#include <QGraphicsLinearLayout> +#include <QGraphicsProxyWidget> +#include <QGraphicsSceneMouseEvent> +#include <QGraphicsView> +#include <QKeyEvent> + +#include <cmath> + +namespace QmlDesigner { + +QList<QmlTimelineKeyframeGroup> allTimelineFrames(const QmlTimeline &timeline) +{ + QList<QmlTimelineKeyframeGroup> returnList; + + for (const ModelNode &childNode : + timeline.modelNode().defaultNodeListProperty().toModelNodeList()) { + if (QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup(childNode)) + returnList.append(QmlTimelineKeyframeGroup(childNode)); + } + + return returnList; +} + +TimelineGraphicsScene::TimelineGraphicsScene(TimelineWidget *parent) + : QGraphicsScene(parent) + , m_parent(parent) + , m_layout(new TimelineGraphicsLayout(this)) + , m_currentFrameIndicator(new TimelineFrameHandle) + , m_tools(this) +{ + addItem(m_layout); + addItem(m_currentFrameIndicator); + + setSceneRect(m_layout->geometry()); + + connect(m_layout, &QGraphicsWidget::geometryChanged, this, [this]() { + auto rect = m_layout->geometry(); + + setSceneRect(rect); + + if (auto *gview = graphicsView()) + gview->setSceneRect(rect.adjusted(0, TimelineConstants::rulerHeight, 0, 0)); + + if (auto *rview = rulerView()) + rview->setSceneRect(rect); + + m_currentFrameIndicator->setHeight(m_layout->geometry().height()); + }); + + auto moveFrameIndicator = [this](const QPointF &pos) { + m_currentFrameIndicator->commitPosition(pos); + }; + connect(m_layout, &TimelineGraphicsLayout::rulerClicked, moveFrameIndicator); + + auto changeScale = [this](int factor) { + timelineWidget()->changeScaleFactor(factor); + setRulerScaling(qreal(factor)); + }; + connect(m_layout, &TimelineGraphicsLayout::scaleFactorChanged, changeScale); +} + +TimelineGraphicsScene::~TimelineGraphicsScene() +{ + QSignalBlocker block(this); + clearSelection(); + qDeleteAll(items()); +} + +void TimelineGraphicsScene::onShow() +{ + if (timelineView()->isAttached()) { + auto timeline = currentTimeline(); + if (timeline.isValid()) { + int cf = std::round(timeline.currentKeyframe()); + setCurrentFrame(cf); + } + + emit m_layout->scaleFactorChanged(0); + } +} + +void TimelineGraphicsScene::setTimeline(const QmlTimeline &timeline) +{ + if (qFuzzyCompare(timeline.duration(), 0.0)) + return; + + m_layout->setTimeline(timeline); +} + +void TimelineGraphicsScene::clearTimeline() +{ + m_layout->setTimeline(QmlTimeline()); +} + +void TimelineGraphicsScene::setWidth(int width) +{ + m_layout->setWidth(width); + invalidateScrollbar(); +} + +void TimelineGraphicsScene::invalidateLayout() +{ + m_layout->invalidate(); +} + +void TimelineGraphicsScene::setCurrenFrame(const QmlTimeline &timeline, qreal frame) +{ + if (timeline.isValid()) + m_currentFrameIndicator->setPosition(frame); + else + m_currentFrameIndicator->setPosition(0); + + invalidateCurrentValues(); +} + +void TimelineGraphicsScene::setCurrentFrame(int frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) { + timeline.modelNode().setAuxiliaryData("currentFrame@NodeInstance", frame); + m_currentFrameIndicator->setPosition(frame + timeline.startKeyframe()); + } else { + m_currentFrameIndicator->setPosition(0); + } + + invalidateCurrentValues(); + + emitStatusBarFrameMessageChanged(frame); +} + +void TimelineGraphicsScene::setStartFrame(int frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) + timeline.modelNode().variantProperty("startFrame").setValue(frame); +} + +void TimelineGraphicsScene::setEndFrame(int frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) + timeline.modelNode().variantProperty("endFrame").setValue(frame); +} + +qreal TimelineGraphicsScene::rulerScaling() const +{ + return m_layout->rulerScaling(); +} + +int TimelineGraphicsScene::rulerWidth() const +{ + return m_layout->rulerWidth(); +} + +qreal TimelineGraphicsScene::rulerDuration() const +{ + return m_layout->rulerDuration(); +} + +qreal TimelineGraphicsScene::startFrame() const +{ + return m_layout->startFrame(); +} + +qreal TimelineGraphicsScene::endFrame() const +{ + return m_layout->endFrame(); +} + +qreal TimelineGraphicsScene::mapToScene(qreal x) const +{ + return TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset + + (x - startFrame()) * rulerScaling() - scrollOffset(); +} + +qreal TimelineGraphicsScene::mapFromScene(qreal x) const +{ + auto xPosOffset = (x - TimelineConstants::sectionWidth - TimelineConstants::timelineLeftOffset) + + scrollOffset(); + + return xPosOffset / rulerScaling() + startFrame(); +} + +qreal TimelineGraphicsScene::currentFramePosition() const +{ + return currentTimeline().currentKeyframe(); +} + +QVector<qreal> TimelineGraphicsScene::keyframePositions() const +{ + QVector<qreal> positions; + for (const auto &frames : allTimelineFrames(currentTimeline())) + positions.append(keyframePositions(frames)); + return positions; +} + +QVector<qreal> TimelineGraphicsScene::keyframePositions(const QmlTimelineKeyframeGroup &frames) const +{ + const QList<ModelNode> keyframes = frames.keyframePositions(); + QVector<qreal> positions; + for (const ModelNode &modelNode : keyframes) + positions.append(modelNode.variantProperty("frame").value().toReal()); + return positions; +} + +void TimelineGraphicsScene::setRulerScaling(int scaleFactor) +{ + const qreal oldOffset = scrollOffset(); + const qreal oldScaling = m_layout->rulerScaling(); + const qreal oldPosition = mapToScene(currentFramePosition()); + m_layout->setRulerScaleFactor(scaleFactor); + + const qreal newScaling = m_layout->rulerScaling(); + const qreal newPosition = mapToScene(currentFramePosition()); + + const qreal newOffset = oldOffset + (newPosition - oldPosition); + + if (std::isinf(oldScaling) || std::isinf(newScaling)) + setScrollOffset(0); + else { + setScrollOffset(std::round(newOffset)); + + const qreal start = mapToScene(startFrame()); + const qreal head = TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset; + + if (start - head > 0) + setScrollOffset(0); + } + + invalidateSections(); + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) + setCurrenFrame(timeline, + timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal()); + + invalidateScrollbar(); + update(); +} + +void TimelineGraphicsScene::commitCurrentFrame(qreal frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) { + timeline.modelNode().setAuxiliaryData("currentFrame@NodeInstance", qRound(frame)); + setCurrenFrame(timeline, qRound(frame)); + invalidateCurrentValues(); + } + emitStatusBarFrameMessageChanged(int(frame)); +} + +QList<TimelineKeyframeItem *> TimelineGraphicsScene::selectedKeyframes() const +{ + return m_selectedKeyframes; +} + +bool TimelineGraphicsScene::hasSelection() const +{ + return !m_selectedKeyframes.empty(); +} + +bool TimelineGraphicsScene::isCurrent(TimelineKeyframeItem *keyframe) const +{ + if (m_selectedKeyframes.empty()) + return false; + + return m_selectedKeyframes.back() == keyframe; +} + +bool TimelineGraphicsScene::isKeyframeSelected(TimelineKeyframeItem *keyframe) const +{ + return m_selectedKeyframes.contains(keyframe); +} + +bool TimelineGraphicsScene::multipleKeyframesSelected() const +{ + return m_selectedKeyframes.count() > 1; +} + +void TimelineGraphicsScene::invalidateSectionForTarget(const ModelNode &target) +{ + if (!target.isValid()) + return; + + bool found = false; + for (auto child : m_layout->childItems()) + TimelineSectionItem::updateDataForTarget(child, target, &found); + + if (!found) + invalidateScene(); + + clearSelection(); + invalidateLayout(); +} + +void TimelineGraphicsScene::invalidateKeyframesForTarget(const ModelNode &target) +{ + for (auto child : m_layout->childItems()) + TimelineSectionItem::updateFramesForTarget(child, target); +} + +void TimelineGraphicsScene::invalidateScene() +{ + ModelNode node = timelineView()->modelNodeForId( + timelineWidget()->toolBar()->currentTimelineId()); + setTimeline(QmlTimeline(node)); + invalidateScrollbar(); +} + +void TimelineGraphicsScene::invalidateScrollbar() +{ + double max = m_layout->maximumScrollValue(); + timelineWidget()->setupScrollbar(0, max, scrollOffset()); + if (scrollOffset() > max) + setScrollOffset(max); +} + +void TimelineGraphicsScene::invalidateCurrentValues() +{ + for (auto item : items()) + TimelinePropertyItem::updateTextEdit(item); +} + +void TimelineGraphicsScene::invalidateRecordButtonsStatus() +{ + for (auto item : items()) + TimelinePropertyItem::updateRecordButtonStatus(item); +} + +int TimelineGraphicsScene::scrollOffset() const +{ + return m_scrollOffset; +} + +void TimelineGraphicsScene::setScrollOffset(int offset) +{ + m_scrollOffset = offset; + emitScrollOffsetChanged(); + update(); +} + +QGraphicsView *TimelineGraphicsScene::graphicsView() const +{ + for (auto *v : views()) + if (v->objectName() == "SceneView") + return v; + + return nullptr; +} + +QGraphicsView *TimelineGraphicsScene::rulerView() const +{ + for (auto *v : views()) + if (v->objectName() == "RulerView") + return v; + + return nullptr; +} + +QmlTimeline TimelineGraphicsScene::currentTimeline() const +{ + return QmlTimeline(timelineModelNode()); +} + +QRectF TimelineGraphicsScene::selectionBounds() const +{ + QRectF bbox; + + for (auto *frame : m_selectedKeyframes) + bbox = bbox.united(frame->rect()); + + return bbox; +} + +void TimelineGraphicsScene::selectKeyframes(const SelectionMode &mode, + const QList<TimelineKeyframeItem *> &items) +{ + if (mode == SelectionMode::Remove || mode == SelectionMode::Toggle) { + for (auto *item : items) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (m_selectedKeyframes.contains(keyframe)) { + keyframe->setHighlighted(false); + m_selectedKeyframes.removeAll(keyframe); + + } else if (mode == SelectionMode::Toggle) { + if (!m_selectedKeyframes.contains(keyframe)) { + keyframe->setHighlighted(true); + m_selectedKeyframes << keyframe; + } + } + } + } + + } else { + if (mode == SelectionMode::New) + clearSelection(); + + for (auto item : items) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (!m_selectedKeyframes.contains(keyframe)) { + keyframe->setHighlighted(true); + m_selectedKeyframes.append(keyframe); + } + } + } + } + emit selectionChanged(); +} + +void TimelineGraphicsScene::clearSelection() +{ + for (auto *keyframe : m_selectedKeyframes) + if (keyframe) + keyframe->setHighlighted(false); + + m_selectedKeyframes.clear(); +} + +QList<QGraphicsItem *> TimelineGraphicsScene::itemsAt(const QPointF &pos) +{ + QTransform transform; + + if (auto *gview = graphicsView()) + transform = gview->transform(); + + return items(pos, Qt::IntersectsItemShape, Qt::DescendingOrder, transform); +} + +void TimelineGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + m_tools.mousePressEvent(topItem, event); + QGraphicsScene::mousePressEvent(event); +} + +void TimelineGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + m_tools.mouseMoveEvent(topItem, event); + QGraphicsScene::mouseMoveEvent(event); +} + +void TimelineGraphicsScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + /* The tool has handle the event last. */ + QGraphicsScene::mouseReleaseEvent(event); + m_tools.mouseReleaseEvent(topItem, event); +} + +void TimelineGraphicsScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + m_tools.mouseDoubleClickEvent(topItem, event); + QGraphicsScene::mouseDoubleClickEvent(event); +} + +void TimelineGraphicsScene::keyPressEvent(QKeyEvent *keyEvent) +{ + if (qgraphicsitem_cast<QGraphicsProxyWidget *>(focusItem())) { + keyEvent->ignore(); + QGraphicsScene::keyPressEvent(keyEvent); + return; + } + + if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) { + switch (keyEvent->key()) { + case Qt::Key_C: + copySelectedKeyframes(); + break; + + case Qt::Key_V: + pasteSelectedKeyframes(); + break; + + default: + QGraphicsScene::keyPressEvent(keyEvent); + break; + } + } else { + switch (keyEvent->key()) { + case Qt::Key_Left: + emit scroll(TimelineUtils::Side::Left); + keyEvent->accept(); + break; + + case Qt::Key_Right: + emit scroll(TimelineUtils::Side::Right); + keyEvent->accept(); + break; + + default: + QGraphicsScene::keyPressEvent(keyEvent); + break; + } + } +} + +void TimelineGraphicsScene::keyReleaseEvent(QKeyEvent *keyEvent) +{ + if (qgraphicsitem_cast<QGraphicsProxyWidget *>(focusItem())) { + keyEvent->ignore(); + QGraphicsScene::keyReleaseEvent(keyEvent); + return; + } + + switch (keyEvent->key()) { + case Qt::Key_Delete: + handleKeyframeDeletion(); + break; + + default: + break; + } + + QGraphicsScene::keyReleaseEvent(keyEvent); +} + +void TimelineGraphicsScene::invalidateSections() +{ + for (auto child : m_layout->childItems()) + TimelineSectionItem::updateData(child); + + clearSelection(); + invalidateLayout(); +} + +TimelineView *TimelineGraphicsScene::timelineView() const +{ + return m_parent->timelineView(); +} + +TimelineWidget *TimelineGraphicsScene::timelineWidget() const +{ + return m_parent; +} + +TimelineToolBar *TimelineGraphicsScene::toolBar() const +{ + return timelineWidget()->toolBar(); +} + +ModelNode TimelineGraphicsScene::timelineModelNode() const +{ + if (timelineView()->isAttached()) { + const QString timelineId = timelineWidget()->toolBar()->currentTimelineId(); + return timelineView()->modelNodeForId(timelineId); + } + + return ModelNode(); +} + +void TimelineGraphicsScene::handleKeyframeDeletion() +{ + QList<ModelNode> nodesToBeDeleted; + for (auto keyframe : m_selectedKeyframes) { + nodesToBeDeleted.append(keyframe->frameNode()); + } + deleteKeyframes(nodesToBeDeleted); +} + +void TimelineGraphicsScene::deleteAllKeyframesForTarget(const ModelNode &targetNode) +{ + TimelineActions::deleteAllKeyframesForTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::insertAllKeyframesForTarget(const ModelNode &targetNode) +{ + TimelineActions::insertAllKeyframesForTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::copyAllKeyframesForTarget(const ModelNode &targetNode) +{ + TimelineActions::copyAllKeyframesForTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::pasteKeyframesToTarget(const ModelNode &targetNode) +{ + TimelineActions::pasteKeyframesToTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::copySelectedKeyframes() +{ + TimelineActions::copyKeyframes( + Utils::transform(m_selectedKeyframes, &TimelineKeyframeItem::frameNode)); +} + +void TimelineGraphicsScene::pasteSelectedKeyframes() +{ + TimelineActions::pasteKeyframes(timelineView(), currentTimeline()); +} + +void TimelineGraphicsScene::handleKeyframeInsertion(const ModelNode &target, + const PropertyName &propertyName) +{ + timelineView()->insertKeyframe(target, propertyName); +} + +void TimelineGraphicsScene::deleteKeyframeGroup(const ModelNode &group) +{ + if (!QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup(group)) + return; + + timelineView()->executeInTransaction("TimelineGraphicsScene::handleKeyframeGroupDeletion", [group](){ + ModelNode nonConst = group; + nonConst.destroy(); + }); + +} + +void TimelineGraphicsScene::deleteKeyframes(const QList<ModelNode> &frames) +{ + timelineView()->executeInTransaction("TimelineGraphicsScene::handleKeyframeDeletion", [frames](){ + for (auto keyframe : frames) { + if (keyframe.isValid()) { + ModelNode frame = keyframe; + ModelNode parent = frame.parentProperty().parentModelNode(); + keyframe.destroy(); + if (parent.isValid() && parent.defaultNodeListProperty().isEmpty()) + parent.destroy(); + } + } + }); +} + +void TimelineGraphicsScene::activateLayout() +{ + m_layout->activate(); +} + +void TimelineGraphicsScene::emitScrollOffsetChanged() +{ + for (QGraphicsItem *item : items()) + TimelineMovableAbstractItem::emitScrollOffsetChanged(item); +} + +void TimelineGraphicsScene::emitStatusBarFrameMessageChanged(int frame) +{ + emit statusBarMessageChanged( + QString(TimelineConstants::timelineStatusBarFrameNumber).arg(frame)); +} + +bool TimelineGraphicsScene::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::ShortcutOverride: + if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Delete) { + QGraphicsScene::keyPressEvent(static_cast<QKeyEvent *>(event)); + event->accept(); + return true; + } + Q_FALLTHROUGH(); + default: + return QGraphicsScene::event(event); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h new file mode 100644 index 0000000000..b8f93595c4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h @@ -0,0 +1,181 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinetooldelegate.h" +#include "timelineutils.h" + +#include <qmltimeline.h> + +#include <QGraphicsScene> + +#include <memory> + +QT_FORWARD_DECLARE_CLASS(QGraphicsLinearLayout) +QT_FORWARD_DECLARE_CLASS(QComboBox) + +namespace QmlDesigner { + +class TimelineView; +class TimelineWidget; +class TimelineItem; +class TimelineRulerSectionItem; +class TimelineFrameHandle; +class TimelineAbstractTool; +class TimelineMoveTool; +class TimelineKeyframeItem; +class TimelinePlaceholder; +class TimelineGraphicsLayout; +class TimelineToolBar; + +class TimelineGraphicsScene : public QGraphicsScene +{ + Q_OBJECT + +signals: + void selectionChanged(); + + void scroll(const TimelineUtils::Side &side); + +public: + explicit TimelineGraphicsScene(TimelineWidget *parent); + + ~TimelineGraphicsScene() override; + + void onShow(); + + void setTimeline(const QmlTimeline &timeline); + void clearTimeline(); + + void setWidth(int width); + + void invalidateLayout(); + void setCurrenFrame(const QmlTimeline &timeline, qreal frame); + void setCurrentFrame(int frame); + void setStartFrame(int frame); + void setEndFrame(int frame); + + TimelineView *timelineView() const; + TimelineWidget *timelineWidget() const; + TimelineToolBar *toolBar() const; + + qreal rulerScaling() const; + int rulerWidth() const; + qreal rulerDuration() const; + qreal startFrame() const; + qreal endFrame() const; + + qreal mapToScene(qreal x) const; + qreal mapFromScene(qreal x) const; + + qreal currentFramePosition() const; + QVector<qreal> keyframePositions() const; + QVector<qreal> keyframePositions(const QmlTimelineKeyframeGroup &frames) const; + + void setRulerScaling(int scaling); + + void commitCurrentFrame(qreal frame); + + QList<TimelineKeyframeItem *> selectedKeyframes() const; + + bool hasSelection() const; + bool isCurrent(TimelineKeyframeItem *keyframe) const; + bool isKeyframeSelected(TimelineKeyframeItem *keyframe) const; + bool multipleKeyframesSelected() const; + + void invalidateSectionForTarget(const ModelNode &modelNode); + void invalidateKeyframesForTarget(const ModelNode &modelNode); + + void invalidateScene(); + void invalidateScrollbar(); + void invalidateCurrentValues(); + void invalidateRecordButtonsStatus(); + + int scrollOffset() const; + void setScrollOffset(int offset); + QGraphicsView *graphicsView() const; + QGraphicsView *rulerView() const; + + QmlTimeline currentTimeline() const; + + QRectF selectionBounds() const; + + void selectKeyframes(const SelectionMode &mode, const QList<TimelineKeyframeItem *> &items); + void clearSelection(); + + void handleKeyframeDeletion(); + void deleteAllKeyframesForTarget(const ModelNode &targetNode); + void insertAllKeyframesForTarget(const ModelNode &targetNode); + void copyAllKeyframesForTarget(const ModelNode &targetNode); + void pasteKeyframesToTarget(const ModelNode &targetNode); + + void handleKeyframeInsertion(const ModelNode &target, const PropertyName &propertyName); + + void deleteKeyframeGroup(const ModelNode &group); + void deleteKeyframes(const QList<ModelNode> &frames); + + void activateLayout(); + +signals: + void statusBarMessageChanged(const QString &message); + +protected: + bool event(QEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; + +private: + void copySelectedKeyframes(); + void pasteSelectedKeyframes(); + + void invalidateSections(); + ModelNode timelineModelNode() const; + + void emitScrollOffsetChanged(); + void emitStatusBarFrameMessageChanged(int frame); + + QList<QGraphicsItem *> itemsAt(const QPointF &pos); + +private: + TimelineWidget *m_parent = nullptr; + + TimelineGraphicsLayout *m_layout = nullptr; + + TimelineFrameHandle *m_currentFrameIndicator = nullptr; + + TimelineToolDelegate m_tools; + + QList<TimelineKeyframeItem *> m_selectedKeyframes; + + int m_scrollOffset = 0; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineicons.h b/src/plugins/qmldesigner/components/timelineeditor/timelineicons.h new file mode 100644 index 0000000000..641d4e77b6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineicons.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <utils/icon.h> + +namespace QmlDesigner { +namespace TimelineIcons { + +// Icons on the timeline ruler +const Utils::Icon WORK_AREA_HANDLE_LEFT( + ":/timelineplugin/images/work_area_handle_left.png"); +const Utils::Icon WORK_AREA_HANDLE_RIGHT( + ":/timelineplugin/images/work_area_handle_right.png"); +const Utils::Icon PLAYHEAD( + ":/timelineplugin/images/playhead.png"); + +// Icons on the timeline tracks +const Utils::Icon KEYFRAME_LINEAR_INACTIVE( + ":/timelineplugin/images/keyframe_linear_inactive.png"); +const Utils::Icon KEYFRAME_LINEAR_ACTIVE( + ":/timelineplugin/images/keyframe_linear_active.png"); +const Utils::Icon KEYFRAME_LINEAR_SELECTED( + ":/timelineplugin/images/keyframe_linear_selected.png"); +const Utils::Icon KEYFRAME_MANUALBEZIER_INACTIVE( + ":/timelineplugin/images/keyframe_manualbezier_inactive.png"); +const Utils::Icon KEYFRAME_MANUALBEZIER_ACTIVE( + ":/timelineplugin/images/keyframe_manualbezier_active.png"); +const Utils::Icon KEYFRAME_MANUALBEZIER_SELECTED( + ":/timelineplugin/images/keyframe_manualbezier_selected.png"); +const Utils::Icon KEYFRAME_AUTOBEZIER_INACTIVE( + ":/timelineplugin/images/keyframe_autobezier_inactive.png"); +const Utils::Icon KEYFRAME_AUTOBEZIER_ACTIVE( + ":/timelineplugin/images/keyframe_autobezier_active.png"); +const Utils::Icon KEYFRAME_AUTOBEZIER_SELECTED( + ":/timelineplugin/images/keyframe_autobezier_selected.png"); +const Utils::Icon KEYFRAME_LINEARTOBEZIER_INACTIVE( + ":/timelineplugin/images/keyframe_lineartobezier_inactive.png"); +const Utils::Icon KEYFRAME_LINEARTOBEZIER_ACTIVE( + ":/timelineplugin/images/keyframe_lineartobezier_active.png"); +const Utils::Icon KEYFRAME_LINEARTOBEZIER_SELECTED( + ":/timelineplugin/images/keyframe_lineartobezier_selected.png"); + +// Icons on the "section" +const Utils::Icon KEYFRAME( + ":/timelineplugin/images/keyframe.png"); +const Utils::Icon IS_KEYFRAME( + ":/timelineplugin/images/is_keyframe.png"); +const Utils::Icon NEXT_KEYFRAME({ + {":/timelineplugin/images/next_keyframe.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon PREVIOUS_KEYFRAME({ + {":/timelineplugin/images/previous_keyframe.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon LOCAL_RECORD_KEYFRAMES({ + {":/timelineplugin/images/local_record_keyframes.png", Utils::Theme::IconsStopToolBarColor}}); +const Utils::Icon ADD_TIMELINE({ + {":/timelineplugin/images/add_timeline.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon REMOVE_TIMELINE({ + {":/timelineplugin/images/remove_timeline.png", Utils::Theme::IconsBaseColor}}); + +// Icons on the toolbars +const Utils::Icon ANIMATION({ + {":/timelineplugin/images/animation.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon TO_FIRST_FRAME({ + {":/timelineplugin/images/to_first_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon BACK_ONE_FRAME({ + {":/timelineplugin/images/back_one_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon START_PLAYBACK({ + {":/timelineplugin/images/start_playback.png", Utils::Theme::IconsRunToolBarColor}}); +const Utils::Icon PAUSE_PLAYBACK({ + {":/timelineplugin/images/pause_playback.png", Utils::Theme::IconsInterruptToolBarColor}}); +const Utils::Icon FORWARD_ONE_FRAME({ + {":/timelineplugin/images/forward_one_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon TO_LAST_FRAME({ + {":/timelineplugin/images/to_last_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon LOOP_PLAYBACK({ + {":/timelineplugin/images/loop_playback.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon CURVE_PICKER({ + {":/timelineplugin/images/curve_picker.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon CURVE_EDITOR({ + {":/timelineplugin/images/curve_editor.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon GLOBAL_RECORD_KEYFRAMES({ + {":/timelineplugin/images/global_record_keyframes.png", Utils::Theme::IconsStopToolBarColor}}); +const Utils::Icon GLOBAL_RECORD_KEYFRAMES_OFF({ + {":/timelineplugin/images/global_record_keyframes.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon ZOOM_SMALL({ + {":/timelineplugin/images/zoom_small.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon ZOOM_BIG({ + {":/timelineplugin/images/zoom_big.png", Utils::Theme::IconsBaseColor}}); + +} // namespace TimelineIcons +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp new file mode 100644 index 0000000000..ebe3644e4e --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp @@ -0,0 +1,293 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineitem.h" + +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinesectionitem.h" +#include "timelineutils.h" +#include "timelinewidget.h" + +#include <theme.h> + +#include <coreplugin/icore.h> + +#include <QApplication> +#include <QCursor> +#include <QGraphicsView> +#include <QPainter> + +#include <cmath> + +namespace QmlDesigner { + +TimelineItem::TimelineItem(TimelineItem *parent) + : QGraphicsWidget(parent) +{} + +TimelineGraphicsScene *TimelineItem::timelineScene() const +{ + return static_cast<TimelineGraphicsScene *>(scene()); + ; +} + +TimelineFrameHandle::TimelineFrameHandle(TimelineItem *parent) + : TimelineMovableAbstractItem(parent) +{ + static const QColor color = Theme::getColor(Theme::IconsWarningToolBarColor); + setBrush(color); + setPen(color); + + setRect(0, 0, TimelineConstants::rulerHeight, TimelineConstants::rulerHeight); + setZValue(40); + setCursor(Qt::ClosedHandCursor); + + m_timer.setSingleShot(true); + m_timer.setInterval(15); + QObject::connect(&m_timer, &QTimer::timeout, [this]() { + if (QApplication::mouseButtons() == Qt::LeftButton) + scrollOutOfBounds(); + }); +} + +void TimelineFrameHandle::setHeight(int height) +{ + setRect(rect().x(), rect().y(), rect().width(), height); +} + +void TimelineFrameHandle::setPosition(qreal position) +{ + const qreal scenePos = mapFromFrameToScene(position); + QRectF newRect(scenePos - rect().width() / 2, rect().y(), rect().width(), rect().height()); + + if (!qFuzzyCompare(newRect.x(), rect().x())) { + setRect(newRect); + } + m_position = position; +} + +void TimelineFrameHandle::setPositionInteractive(const QPointF &position) +{ + const double width = timelineScene()->width(); + + if (position.x() > width) { + callSetClampedXPosition(width - (rect().width() / 2) - 1); + m_timer.start(); + } else if (position.x() < TimelineConstants::sectionWidth) { + callSetClampedXPosition(TimelineConstants::sectionWidth); + m_timer.start(); + } else { + callSetClampedXPosition(position.x() - rect().width() / 2); + const qreal frame = std::round(mapFromSceneToFrame(rect().center().x())); + timelineScene()->commitCurrentFrame(frame); + } +} + +void TimelineFrameHandle::commitPosition(const QPointF &point) +{ + setPositionInteractive(point); +} + +void TimelineItem::drawLine(QPainter *painter, qreal x1, qreal y1, qreal x2, qreal y2) +{ + painter->drawLine(QPointF(x1 + 0.5, y1 + 0.5), QPointF(x2 + 0.5, y2 + 0.5)); +} + +qreal TimelineFrameHandle::position() const +{ + return m_position; +} + +TimelineFrameHandle *TimelineFrameHandle::asTimelineFrameHandle() +{ + return this; +} + +void TimelineFrameHandle::scrollOffsetChanged() +{ + setPosition(position()); +} + +QPainterPath TimelineFrameHandle::shape() const +{ + QPainterPath path; + QRectF rect = boundingRect(); + rect.setHeight(TimelineConstants::sectionHeight); + rect.adjust(-4, 0, 4, 0); + path.addEllipse(rect); + return path; +} + +static int devicePixelWidth(const QPixmap &pixmap) +{ + return pixmap.width() / pixmap.devicePixelRatioF(); +} + +static int devicePixelHeight(const QPixmap &pixmap) +{ + return pixmap.height() / pixmap.devicePixelRatioF(); +} + +void TimelineFrameHandle::paint(QPainter *painter, + const QStyleOptionGraphicsItem * /*option*/, + QWidget * /*widget*/) +{ + static const QPixmap playHead = TimelineIcons::PLAYHEAD.pixmap(); + + static const int pixmapHeight = devicePixelHeight(playHead); + static const int pixmapWidth = devicePixelWidth(playHead); + + if (rect().x() < TimelineConstants::sectionWidth - rect().width() / 2) + return; + + painter->save(); + painter->setOpacity(0.8); + const qreal center = rect().width() / 2 + rect().x(); + + painter->setPen(pen()); + + auto offsetTop = pixmapHeight - 7; + TimelineItem::drawLine(painter, center, offsetTop, center, rect().height() - 1); + + const QPointF pmTopLeft(center - pixmapWidth / 2, -4.); + painter->drawPixmap(pmTopLeft, playHead); + + painter->restore(); +} + +QPointF TimelineFrameHandle::mapFromGlobal(const QPoint &pos) const +{ + for (auto *view : timelineScene()->views()) { + if (view->objectName() == "SceneView") { + auto graphicsViewCoords = view->mapFromGlobal(pos); + auto sceneCoords = view->mapToScene(graphicsViewCoords); + return sceneCoords; + } + } + return {}; +} + +int TimelineFrameHandle::computeScrollSpeed() const +{ + const double mouse = mapFromGlobal(QCursor::pos()).x(); + const double width = timelineScene()->width(); + + const double acc = mouse > width ? mouse - width + : double(TimelineConstants::sectionWidth) - mouse; + const double delta = TimelineUtils::clamp<double>(acc, 0., 200.); + const double blend = TimelineUtils::reverseLerp(delta, 0., 200.); + const double factor = TimelineUtils::lerp<double>(blend, 5, 20); + + if (mouse > width) + return scrollOffset() + std::round(factor); + else + return scrollOffset() - std::round(factor); + + return 0; +} + +void TimelineFrameHandle::callSetClampedXPosition(double x) +{ + const int minimumWidth = TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset + - rect().width() / 2; + const int maximumWidth = minimumWidth + + timelineScene()->rulerDuration() * timelineScene()->rulerScaling() + - scrollOffset(); + + setClampedXPosition(x, minimumWidth, maximumWidth); +} + +// Auto scroll when dragging playhead out of bounds. +void TimelineFrameHandle::scrollOutOfBounds() +{ + const double width = timelineScene()->width(); + const double mouse = mapFromGlobal(QCursor::pos()).x(); + + if (mouse > width) + scrollOutOfBoundsMax(); + else if (mouse < TimelineConstants::sectionWidth) + scrollOutOfBoundsMin(); +} + +void TimelineFrameHandle::scrollOutOfBoundsMax() +{ + const double width = timelineScene()->width(); + if (QApplication::mouseButtons() == Qt::LeftButton) { + const double frameWidth = timelineScene()->rulerScaling(); + const double upperThreshold = width - frameWidth; + + if (rect().center().x() > upperThreshold) { + timelineScene()->setScrollOffset(computeScrollSpeed()); + timelineScene()->invalidateScrollbar(); + } + + callSetClampedXPosition(width - (rect().width() / 2) - 1); + m_timer.start(); + } else { + // Mouse release + callSetClampedXPosition(width - (rect().width() / 2) - 1); + + const int frame = std::floor(mapFromSceneToFrame(rect().center().x())); + const int ef = timelineScene()->endFrame(); + timelineScene()->commitCurrentFrame(frame <= ef ? frame : ef); + } +} + +void TimelineFrameHandle::scrollOutOfBoundsMin() +{ + if (QApplication::mouseButtons() == Qt::LeftButton) { + auto offset = computeScrollSpeed(); + + if (offset >= 0) + timelineScene()->setScrollOffset(offset); + else + timelineScene()->setScrollOffset(0); + + timelineScene()->invalidateScrollbar(); + + callSetClampedXPosition(TimelineConstants::sectionWidth); + m_timer.start(); + } else { + // Mouse release + callSetClampedXPosition(TimelineConstants::sectionWidth); + + int frame = mapFromSceneToFrame(rect().center().x()); + + const int sframe = timelineScene()->startFrame(); + if (frame != sframe) { + const qreal framePos = mapFromFrameToScene(frame); + + if (framePos + <= (TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset)) + frame++; + } + + timelineScene()->commitCurrentFrame(frame >= sframe ? frame : sframe); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.h new file mode 100644 index 0000000000..87fb9e3ec8 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.h @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinemovableabstractitem.h" + +#include <QGraphicsRectItem> +#include <QGraphicsWidget> +#include <QTimer> + +namespace QmlDesigner { + +class TimelineItem : public QGraphicsWidget +{ + Q_OBJECT + +public: + explicit TimelineItem(TimelineItem *parent = nullptr); + + static void drawLine(QPainter *painter, qreal x1, qreal y1, qreal x2, qreal y2); + TimelineGraphicsScene *timelineScene() const; +}; + +class TimelineFrameHandle : public TimelineMovableAbstractItem +{ +public: + explicit TimelineFrameHandle(TimelineItem *parent = nullptr); + + void setHeight(int height); + void setPosition(qreal position); + void setPositionInteractive(const QPointF &postion) override; + void commitPosition(const QPointF &point) override; + qreal position() const; + + TimelineFrameHandle *asTimelineFrameHandle() override; + +protected: + void scrollOffsetChanged() override; + QPainterPath shape() const override; + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget = nullptr) override; + +private: + QPointF mapFromGlobal(const QPoint &pos) const; + int computeScrollSpeed() const; + + void callSetClampedXPosition(double x); + void scrollOutOfBounds(); + void scrollOutOfBoundsMax(); + void scrollOutOfBoundsMin(); + +private: + qreal m_position = 0; + + QTimer m_timer; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp new file mode 100644 index 0000000000..4db4567fd6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp @@ -0,0 +1,154 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinemovableabstractitem.h" + +#include "timelinegraphicsscene.h" + +#include "timelineitem.h" + +#include <QGraphicsSceneMouseEvent> + +namespace QmlDesigner { + +TimelineMovableAbstractItem::TimelineMovableAbstractItem(QGraphicsItem *parent) + : QGraphicsRectItem(parent) +{} + +void TimelineMovableAbstractItem::setPositionInteractive(const QPointF &) {} + +void TimelineMovableAbstractItem::commitPosition(const QPointF &) {} + +void TimelineMovableAbstractItem::itemMoved(const QPointF & /*start*/, const QPointF &end) +{ + setPositionInteractive(end); +} + +int TimelineMovableAbstractItem::scrollOffset() const +{ + return timelineScene()->scrollOffset(); +} + +int TimelineMovableAbstractItem::xPosScrollOffset(int x) const +{ + return x + scrollOffset(); +} + +qreal TimelineMovableAbstractItem::mapFromFrameToScene(qreal x) const +{ + return TimelineConstants::sectionWidth + (x - timelineScene()->startFrame()) * rulerScaling() + - scrollOffset() + TimelineConstants::timelineLeftOffset; +} + +qreal TimelineMovableAbstractItem::mapFromSceneToFrame(qreal x) const +{ + return xPosScrollOffset(x - TimelineConstants::sectionWidth + - TimelineConstants::timelineLeftOffset) + / timelineScene()->rulerScaling() + + timelineScene()->startFrame(); +} + +void TimelineMovableAbstractItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + event->accept(); +} + +void TimelineMovableAbstractItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + event->accept(); +} + +void TimelineMovableAbstractItem::setClampedXPosition(qreal x, + qreal minimumWidth, + qreal maximumWidth) +{ + if (x > minimumWidth) { + if (x < maximumWidth) + setRect(x, rect().y(), rect().width(), rect().height()); + else + setRect(maximumWidth, rect().y(), rect().width(), rect().height()); + } else { + setRect(minimumWidth, rect().y(), rect().width(), rect().height()); + } +} + +TimelineMovableAbstractItem *TimelineMovableAbstractItem::cast(QGraphicsItem *item) +{ + return qgraphicsitem_cast<TimelineMovableAbstractItem *>(item); +} + +TimelineMovableAbstractItem *TimelineMovableAbstractItem::topMoveableItem( + const QList<QGraphicsItem *> &items) +{ + for (auto item : items) + if (auto castedItem = TimelineMovableAbstractItem::cast(item)) + return castedItem; + + return nullptr; +} + +void TimelineMovableAbstractItem::emitScrollOffsetChanged(QGraphicsItem *item) +{ + auto movableItem = TimelineMovableAbstractItem::cast(item); + if (movableItem) + movableItem->scrollOffsetChanged(); +} + +TimelineKeyframeItem *TimelineMovableAbstractItem::asTimelineKeyframeItem(QGraphicsItem *item) +{ + auto movableItem = TimelineMovableAbstractItem::cast(item); + + if (movableItem) + return movableItem->asTimelineKeyframeItem(); + + return nullptr; +} + +qreal TimelineMovableAbstractItem::rulerScaling() const +{ + return static_cast<TimelineGraphicsScene *>(scene())->rulerScaling(); +} + +int TimelineMovableAbstractItem::type() const +{ + return Type; +} + +TimelineGraphicsScene *TimelineMovableAbstractItem::timelineScene() const +{ + return static_cast<TimelineGraphicsScene *>(scene()); +} + +TimelineKeyframeItem *TimelineMovableAbstractItem::asTimelineKeyframeItem() +{ + return nullptr; +} + +TimelineFrameHandle *TimelineMovableAbstractItem::asTimelineFrameHandle() +{ + return nullptr; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h new file mode 100644 index 0000000000..4bc11675c2 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineconstants.h" + +#include <QCoreApplication> +#include <QGraphicsRectItem> + +namespace QmlDesigner { + +class TimelineGraphicsScene; +class TimelineKeyframeItem; +class TimelineFrameHandle; + +class TimelineMovableAbstractItem : public QGraphicsRectItem +{ + Q_DECLARE_TR_FUNCTIONS(TimelineMovableAbstractItem) + +public: + enum { Type = TimelineConstants::moveableAbstractItemUserType }; + + explicit TimelineMovableAbstractItem(QGraphicsItem *item); + + int type() const override; + + static TimelineMovableAbstractItem *cast(QGraphicsItem *item); + static TimelineMovableAbstractItem *topMoveableItem(const QList<QGraphicsItem *> &items); + static void emitScrollOffsetChanged(QGraphicsItem *item); + static TimelineKeyframeItem *asTimelineKeyframeItem(QGraphicsItem *item); + + qreal rulerScaling() const; + + virtual void setPositionInteractive(const QPointF &point); + virtual void commitPosition(const QPointF &point); + virtual void itemMoved(const QPointF &start, const QPointF &end); + + int xPosScrollOffset(int x) const; + + qreal mapFromFrameToScene(qreal x) const; + qreal mapFromSceneToFrame(qreal x) const; + + virtual void scrollOffsetChanged() = 0; + + virtual TimelineKeyframeItem *asTimelineKeyframeItem(); + virtual TimelineFrameHandle *asTimelineFrameHandle(); + +protected: + int scrollOffset() const; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + + void setClampedXPosition(qreal x, qreal min, qreal max); + TimelineGraphicsScene *timelineScene() const; + +private: + bool m_multiSelectedMove = false; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp new file mode 100644 index 0000000000..0384d7c0a3 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp @@ -0,0 +1,173 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinemovetool.h" + +#include "timelinegraphicsscene.h" +#include "timelinemovableabstractitem.h" +#include "timelinepropertyitem.h" +#include "timelineview.h" + +#include <exception.h> + +#include <QGraphicsScene> +#include <QGraphicsSceneMouseEvent> + +#include <cmath> + +namespace QmlDesigner { + +static QPointF mapPointToItem(TimelineMovableAbstractItem *item, const QPointF &pos) +{ + if (auto parent = item->parentItem()) + return parent->mapFromScene(pos); + return pos; +} + +QPointF mapToItem(TimelineMovableAbstractItem *item, const QPointF &pos) +{ + if (auto parent = item->parentItem()) + return parent->mapFromScene(pos); + return pos; +} + +QPointF mapToItem(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) +{ + if (auto parent = item->parentItem()) + return parent->mapFromScene(event->scenePos()); + return event->scenePos(); +} + +TimelineMoveTool::TimelineMoveTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate) + : TimelineAbstractTool(scene, delegate) +{} + +void TimelineMoveTool::mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); +} + +void TimelineMoveTool::mouseMoveEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + + if (!currentItem()) + return; + + if (auto *current = currentItem()->asTimelineKeyframeItem()) { + const qreal sourceFrame = qRound(current->mapFromSceneToFrame(current->rect().center().x())); + const qreal targetFrame = qRound(current->mapFromSceneToFrame(event->scenePos().x())); + qreal deltaFrame = targetFrame - sourceFrame; + + const qreal minFrame = scene()->startFrame(); + const qreal maxFrame = scene()->endFrame(); + + auto bbox = scene()->selectionBounds().united(current->rect()); + + double firstFrame = std::round(current->mapFromSceneToFrame(bbox.center().x())); + double lastFrame = std::round(current->mapFromSceneToFrame(bbox.center().x())); + + if ((lastFrame + deltaFrame) > maxFrame) + deltaFrame = maxFrame - lastFrame; + + if ((firstFrame + deltaFrame) <= minFrame) + deltaFrame = minFrame - firstFrame; + + current->setPosition(sourceFrame + deltaFrame); + + for (auto *keyframe : scene()->selectedKeyframes()) { + if (keyframe != current) { + qreal pos = std::round(current->mapFromSceneToFrame(keyframe->rect().center().x())); + keyframe->setPosition(pos + deltaFrame); + } + } + + } else { + currentItem()->itemMoved(mapPointToItem(currentItem(), startPosition()), + mapToItem(currentItem(), event)); + } +} + +void TimelineMoveTool::mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + if (auto *current = currentItem()) { + if (current->asTimelineFrameHandle()) { + double mousePos = event->pos().x(); + double start = current->mapFromFrameToScene(scene()->startFrame()); + double end = current->mapFromFrameToScene(scene()->endFrame()); + + if (mousePos < start) { + scene()->setCurrentFrame(scene()->startFrame()); + scene()->statusBarMessageChanged(QObject::tr("Frame %1").arg(scene()->startFrame())); + return; + } else if (mousePos > end) { + scene()->setCurrentFrame(scene()->endFrame()); + scene()->statusBarMessageChanged(QObject::tr("Frame %1").arg(scene()->endFrame())); + return; + } + } + + scene()->timelineView()->executeInTransaction("TimelineMoveTool::mouseReleaseEvent", [this, current](){ + current->commitPosition(mapToItem(current, current->rect().center())); + + if (current->asTimelineKeyframeItem()) { + double frame = std::round( + current->mapFromSceneToFrame(current->rect().center().x())); + + scene()->statusBarMessageChanged(QObject::tr("Frame %1").arg(frame)); + + for (auto keyframe : scene()->selectedKeyframes()) + if (keyframe != current) + keyframe->commitPosition(mapToItem(current, keyframe->rect().center())); + } + }); + } +} + +void TimelineMoveTool::mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); +} + +void TimelineMoveTool::keyPressEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +void TimelineMoveTool::keyReleaseEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h new file mode 100644 index 0000000000..55b9a39417 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineabstracttool.h" + +#include <QPointF> + +QT_FORWARD_DECLARE_CLASS(QGraphicsRectItem) + +namespace QmlDesigner { + +class TimelineMovableAbstractItem; + +class TimelineMoveTool : public TimelineAbstractTool +{ +public: + explicit TimelineMoveTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate); + void mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp new file mode 100644 index 0000000000..7e7cb69fc9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineplaceholder.h" + +#include <theme.h> + +#include <QPainter> + +namespace QmlDesigner { + +TimelinePlaceholder::TimelinePlaceholder(TimelineItem *parent) + : TimelineItem(parent) +{ + setPreferredHeight(TimelineConstants::sectionHeight); + setMinimumHeight(TimelineConstants::sectionHeight); + setMaximumHeight(TimelineConstants::sectionHeight); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); +} + +TimelinePlaceholder *TimelinePlaceholder::create(QGraphicsScene * /*parentScene*/, + TimelineItem *parent) +{ + auto item = new TimelinePlaceholder(parent); + + return item; +} + +void TimelinePlaceholder::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + painter->save(); + static const QColor penColor = Theme::instance()->qmlDesignerBackgroundColorDarker(); + static const QColor backgroundColor = Theme::instance() + ->qmlDesignerBackgroundColorDarkAlternate(); + static const QColor backgroundColorSection = Theme::getColor(Theme::BackgroundColorDark); + + painter->fillRect(0, 0, size().width(), size().height(), backgroundColor); + painter->fillRect(0, 0, TimelineConstants::sectionWidth, size().height(), backgroundColorSection); + + painter->setPen(penColor); + + drawLine(painter, + TimelineConstants::sectionWidth - 1, + 0, + TimelineConstants::sectionWidth - 1, + size().height() - 1); + + drawLine(painter, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight - 1, + size().width(), + TimelineConstants::sectionHeight - 1); + painter->restore(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h new file mode 100644 index 0000000000..14d6d8a2fc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineitem.h" + +namespace QmlDesigner { + +class TimelinePlaceholder : public TimelineItem +{ + Q_OBJECT + +public: + static TimelinePlaceholder *create(QGraphicsScene *parentScene, TimelineItem *parent); + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; + +private: + TimelinePlaceholder(TimelineItem *parent = nullptr); +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp new file mode 100644 index 0000000000..beeca23183 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp @@ -0,0 +1,641 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinepropertyitem.h" + +#include "abstractview.h" +#include "easingcurvedialog.h" +#include "setframevaluedialog.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinetoolbar.h" +#include "timelinetoolbutton.h" + +#include <rewritertransaction.h> +#include <rewritingexception.h> +#include <theme.h> +#include <variantproperty.h> +#include <qmlobjectnode.h> + +#include <coreplugin/icore.h> +#include <utils/qtcassert.h> +#include <utils/utilsicons.h> + +#include <utils/algorithm.h> +#include <utils/fileutils.h> + +#include <coreplugin/icore.h> + +#include <QCursor> +#include <QGraphicsProxyWidget> +#include <QGraphicsSceneMouseEvent> +#include <QGraphicsView> +#include <QLineEdit> +#include <QMenu> +#include <QPainter> + +#include <algorithm> + +namespace QmlDesigner { + +static bool s_blockUpdates = false; + +static qreal findNext(const QVector<qreal> &vector, qreal current) +{ + for (qreal n : vector) + if (n > current) + return n; + return current; +} + +static qreal findPrev(const QVector<qreal> &vector, qreal current) +{ + for (qreal n : vector) + if (n < current) + return n; + return current; +} + +static QVector<qreal> getPositions(const QmlTimelineKeyframeGroup &frames) +{ + const QList<ModelNode> keyframes = frames.keyframePositions(); + QVector<qreal> positions; + for (const ModelNode &modelNode : keyframes) + positions.append(modelNode.variantProperty("frame").value().toReal()); + return positions; +} + +static ModelNode getModelNodeForFrame(const QmlTimelineKeyframeGroup &frames, qreal frame) +{ + if (frames.isValid()) { + const QList<ModelNode> keyframes = frames.keyframePositions(); + for (const ModelNode &modelNode : keyframes) + if (qFuzzyCompare(modelNode.variantProperty("frame").value().toReal(), frame)) + return modelNode; + } + + return {}; +} + +static void setEasingCurve(TimelineGraphicsScene *scene, const QList<ModelNode> &keys) +{ + QTC_ASSERT(scene, return ); + EasingCurveDialog::runDialog(keys); +} + +static void editValue(const ModelNode &frame, const QString &propertyName) +{ + const QVariant value = frame.variantProperty("value").value(); + auto dialog = new SetFrameValueDialog(Core::ICore::dialogParent()); + + dialog->lineEdit()->setText(value.toString()); + dialog->setPropertName(propertyName); + + QObject::connect(dialog, &SetFrameValueDialog::rejected, [dialog]() { dialog->deleteLater(); }); + + QObject::connect(dialog, &SetFrameValueDialog::accepted, [dialog, frame, value]() { + dialog->deleteLater(); + int userType = value.userType(); + const QVariant result = dialog->lineEdit()->text(); + + if (result.canConvert(userType)) { + QVariant newValue = result; + newValue.convert(userType); + // canConvert gives true in case if the result is a double but the usertype was interger + // try to fix that with a workaround to convert it to double if convertion resulted in isNull + if (newValue.isNull()) { + newValue = result; + newValue.convert(QMetaType::Double); + } + frame.variantProperty("value").setValue(result); + } + }); + + dialog->show(); +} + +TimelinePropertyItem *TimelinePropertyItem::create(const QmlTimelineKeyframeGroup &frames, + TimelineSectionItem *parent) +{ + ModelNode modelnode = frames.target(); + + bool isRecording = false; + + if (frames.isValid()) + isRecording = frames.isRecording(); + + auto item = new TimelinePropertyItem(parent); + + auto sectionItem = new QGraphicsWidget(item); + + sectionItem->setGeometry(0, + 0, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight); + + sectionItem->setZValue(10); + sectionItem->setCursor(Qt::ArrowCursor); + + item->m_frames = frames; + item->setToolTip(item->propertyName()); + item->resize(parent->size()); + item->setupKeyframes(); + + TimelineToolButton *buttonPrev + = new TimelineToolButton(new QAction(TimelineIcons::PREVIOUS_KEYFRAME.icon(), + tr("Previous Frame")), + sectionItem); + buttonPrev->setToolTip("Jump to previous frame."); + + TimelineToolButton *buttonNext + = new TimelineToolButton(new QAction(TimelineIcons::NEXT_KEYFRAME.icon(), tr("Next Frame")), + sectionItem); + buttonNext->setToolTip("Jump to next frame."); + + connect(buttonPrev, &TimelineToolButton::clicked, item, [item]() { + if (item->m_frames.isValid()) { + QVector<qreal> positions = getPositions(item->m_frames); + std::sort(positions.begin(), positions.end(), std::greater<qreal>()); + const qreal prev = findPrev(positions, item->currentFrame()); + item->timelineScene()->commitCurrentFrame(prev); + } + }); + + connect(buttonNext, &TimelineToolButton::clicked, item, [item]() { + if (item->m_frames.isValid()) { + QVector<qreal> positions = getPositions(item->m_frames); + std::sort(positions.begin(), positions.end(), std::less<qreal>()); + const qreal next = findNext(positions, item->currentFrame()); + item->timelineScene()->commitCurrentFrame(next); + } + }); + + QIcon autoKeyIcon = TimelineUtils::mergeIcons(TimelineIcons::GLOBAL_RECORD_KEYFRAMES, + TimelineIcons::GLOBAL_RECORD_KEYFRAMES_OFF); + auto recact = new QAction(autoKeyIcon, tr("Auto Record")); + recact->setCheckable(true); + recact->setChecked(isRecording); + + auto toggleRecord = [frames](bool check) { frames.toogleRecording(check); }; + connect(recact, &QAction::toggled, toggleRecord); + item->m_recording = new TimelineToolButton(recact, sectionItem); + item->m_recording->setToolTip("Per property recording"); + + const int buttonsY = (TimelineConstants::sectionHeight - 1 - TimelineConstants::toolButtonSize) + / 2; + buttonPrev->setPos(2, buttonsY); + buttonNext->setPos(buttonPrev->size().width() + TimelineConstants::toolButtonSize + 4, buttonsY); + item->m_recording->setPos(buttonNext->geometry().right() + 2, buttonsY); + + QRectF hideToolTipDummy(buttonPrev->geometry().topRight(), buttonNext->geometry().bottomLeft()); + + auto *dummy = new QGraphicsRectItem(sectionItem); + dummy->setPen(Qt::NoPen); + dummy->setRect(hideToolTipDummy); + dummy->setToolTip("Frame indicator"); + + if (!item->m_frames.isValid()) + return item; + + QmlObjectNode objectNode(modelnode); + if (!objectNode.isValid()) + return item; + + auto nameOfType = objectNode.modelNode().metaInfo().propertyTypeName( + item->m_frames.propertyName()); + item->m_control = createTimelineControl(nameOfType); + if (item->m_control) { + item->m_control->setSize((TimelineConstants::sectionWidth / 2.6) - 10, + item->size().height() - 2 + 1); + item->m_control->connect(item); + QGraphicsProxyWidget *proxy = item->timelineScene()->addWidget(item->m_control->widget()); + proxy->setParentItem(sectionItem); + proxy->setPos(qreal(TimelineConstants::sectionWidth) * 2.0 / 3, 0); + item->updateTextEdit(); + } + + updateRecordButtonStatus(item); + + return item; +} + +int TimelinePropertyItem::type() const +{ + return Type; +} + +void TimelinePropertyItem::updateData() +{ + for (auto child : childItems()) + delete qgraphicsitem_cast<TimelineMovableAbstractItem *>(child); + + setupKeyframes(); + updateTextEdit(); +} + +void TimelinePropertyItem::updateFrames() +{ + for (auto child : (childItems())) { + if (auto frameItem = qgraphicsitem_cast<TimelineMovableAbstractItem *>(child)) + static_cast<TimelineKeyframeItem *>(frameItem)->updateFrame(); + } +} + +bool TimelinePropertyItem::isSelected() const +{ + if (m_frames.isValid() && m_frames.target().isValid()) + return m_frames.target().isSelected(); + + return false; +} + +QString convertVariant(const QVariant &variant) +{ + if (variant.userType() == QMetaType::QColor) + return variant.toString(); + + return QString::number(variant.toFloat(), 'f', 2); +} + +void TimelinePropertyItem::updateTextEdit() +{ + if (!m_frames.isValid()) + return; + + QmlObjectNode objectNode(m_frames.target()); + if (objectNode.isValid() && m_control) + m_control->setControlValue(objectNode.instanceValue(m_frames.propertyName())); +} + +void TimelinePropertyItem::updateTextEdit(QGraphicsItem *item) +{ + if (auto timelinePropertyItem = qgraphicsitem_cast<TimelinePropertyItem *>(item)) + timelinePropertyItem->updateTextEdit(); +} + +void TimelinePropertyItem::updateRecordButtonStatus(QGraphicsItem *item) +{ + if (auto timelinePropertyItem = qgraphicsitem_cast<TimelinePropertyItem *>(item)) { + auto frames = timelinePropertyItem->m_frames; + if (frames.isValid()) { + timelinePropertyItem->m_recording->setChecked(frames.isRecording()); + if (frames.timeline().isValid()) + timelinePropertyItem->m_recording->setDisabled(frames.timeline().isRecording()); + } + } +} + +QmlTimelineKeyframeGroup TimelinePropertyItem::frames() const +{ + return m_frames; +} + +QString TimelinePropertyItem::propertyName() const +{ + if (m_frames.isValid()) + return QString::fromUtf8(m_frames.propertyName()); + return QString(); +} + +void TimelinePropertyItem::changePropertyValue(const QVariant &value) +{ + Q_ASSERT(m_frames.isValid()); + + auto timeline = timelineScene()->currentTimeline(); + + if (timelineScene()->toolBar()->recording() || m_recording->isChecked()) { + QmlTimelineKeyframeGroup frames = m_frames; + auto deferredFunc = [frames, value, timeline]() { + auto constFrames = frames; + qreal frame = timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal(); + try { + constFrames.setValue(value, frame); + } catch (const RewritingException &e) { + e.showException(); + } + }; + + // QmlTimelineKeyframeGroup::setValue might create a new keyframe. + // This might result in a temporal cleanup of the graphicsscene and + // therefore a deletion of this property item. + // Adding a keyframe to this already deleted item results in a crash. + QTimer::singleShot(0, deferredFunc); + + } else { + QmlObjectNode objectNode(m_frames.target()); + objectNode.setVariantProperty(m_frames.propertyName(), value); + } +} + +static int devicePixelHeight(const QPixmap &pixmap) +{ + return pixmap.height() / pixmap.devicePixelRatioF(); +} + +void TimelinePropertyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + painter->save(); + + static const QColor penColor = Theme::instance()->qmlDesignerBackgroundColorDarker(); + static const QColor textColor = Theme::getColor(Theme::PanelTextColorLight); + static const QColor backgroundColor = Theme::instance() + ->qmlDesignerBackgroundColorDarkAlternate(); + + static const QPixmap keyframe = TimelineIcons::KEYFRAME.pixmap(); + static const QPixmap isKeyframe = TimelineIcons::IS_KEYFRAME.pixmap(); + + painter->fillRect(0, 0, TimelineConstants::sectionWidth, size().height(), backgroundColor); + painter->fillRect(TimelineConstants::textIndentationProperties - 4, + 0, + TimelineConstants::sectionWidth - TimelineConstants::textIndentationProperties + + 4, + size().height(), + backgroundColor.darker(110)); + + painter->setPen(penColor); + + drawLine(painter, + TimelineConstants::sectionWidth - 1, + 0, + TimelineConstants::sectionWidth - 1, + size().height()); + + drawLine(painter, + TimelineConstants::textIndentationProperties - 4, + TimelineConstants::sectionHeight - 1, + size().width(), + TimelineConstants::sectionHeight - 1); + + painter->setPen(textColor); + + const QFontMetrics metrics(font()); + + const QString elidedText = metrics.elidedText(propertyName(), + Qt::ElideMiddle, + qreal(TimelineConstants::sectionWidth) * 2.0 / 3 + - TimelineConstants::textIndentationProperties, + 0); + + painter->drawText(TimelineConstants::textIndentationProperties, 12, elidedText); + + const bool onKeyFrame = m_frames.isValid() && getPositions(m_frames).contains(currentFrame()); + painter->drawPixmap(TimelineConstants::toolButtonSize + 3, + (TimelineConstants::sectionHeight - 1 - devicePixelHeight(isKeyframe)) / 2, + onKeyFrame ? isKeyframe : keyframe); + painter->restore(); +} + +void TimelinePropertyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + if (event->pos().x() < TimelineConstants::toolButtonSize * 2 + 3 + && event->pos().x() > TimelineConstants::toolButtonSize) { + QMenu mainMenu; + + const ModelNode currentFrameNode = getModelNodeForFrame(m_frames, currentFrame()); + + QAction *insertAction = mainMenu.addAction(tr("Insert Keyframe")); + QObject::connect(insertAction, &QAction::triggered, [this]() { + timelineScene()->handleKeyframeInsertion(m_frames.target(), propertyName().toUtf8()); + }); + + QAction *removeAction = mainMenu.addAction(tr("Delete Keyframe")); + QObject::connect(removeAction, &QAction::triggered, [this, currentFrameNode]() { + timelineScene()->deleteKeyframes({currentFrameNode}); + }); + + QAction *editEasingAction = mainMenu.addAction(tr("Edit Easing Curve...")); + QObject::connect(editEasingAction, &QAction::triggered, [this, currentFrameNode]() { + setEasingCurve(timelineScene(), {currentFrameNode}); + }); + + QAction *editValueAction = mainMenu.addAction(tr("Edit Value for Keyframe...")); + QObject::connect(editValueAction, &QAction::triggered, [this, currentFrameNode]() { + editValue(currentFrameNode, propertyName()); + }); + + const bool hasKeyframe = currentFrameNode.isValid(); + + insertAction->setEnabled(!hasKeyframe); + removeAction->setEnabled(hasKeyframe); + editEasingAction->setEnabled(hasKeyframe); + editValueAction->setEnabled(hasKeyframe); + + mainMenu.exec(event->screenPos()); + event->accept(); + } else if (event->pos().x() > TimelineConstants::toolButtonSize * 3 + 3 + && event->pos().x() < TimelineConstants::sectionWidth) { + QMenu mainMenu; + QAction *deleteAction = mainMenu.addAction(tr("Remove Property")); + + QObject::connect(deleteAction, &QAction::triggered, [this]() { + auto deleteKeyframeGroup = [this]() { timelineScene()->deleteKeyframeGroup(m_frames); }; + QTimer::singleShot(0, deleteKeyframeGroup); + }); + + mainMenu.exec(event->screenPos()); + event->accept(); + } +} + +TimelinePropertyItem::TimelinePropertyItem(TimelineSectionItem *parent) + : TimelineItem(parent) +{ + setPreferredHeight(TimelineConstants::sectionHeight); + setMinimumHeight(TimelineConstants::sectionHeight); + setMaximumHeight(TimelineConstants::sectionHeight); +} + +void TimelinePropertyItem::setupKeyframes() +{ + for (const ModelNode &frame : m_frames.keyframePositions()) + new TimelineKeyframeItem(this, frame); +} + +qreal TimelinePropertyItem::currentFrame() +{ + QmlTimeline timeline = timelineScene()->currentTimeline(); + if (timeline.isValid()) + return timeline.currentKeyframe(); + return 0; +} + +TimelineKeyframeItem::TimelineKeyframeItem(TimelinePropertyItem *parent, const ModelNode &frame) + : TimelineMovableAbstractItem(parent) + , m_frame(frame) + +{ + setPosition(frame.variantProperty("frame").value().toReal()); + setCursor(Qt::ClosedHandCursor); +} + +TimelineKeyframeItem::~TimelineKeyframeItem() +{ + timelineScene()->selectKeyframes(SelectionMode::Remove, {this}); +} + +void TimelineKeyframeItem::updateFrame() +{ + if (s_blockUpdates) + return; + + QTC_ASSERT(m_frame.isValid(), return ); + setPosition(m_frame.variantProperty("frame").value().toReal()); +} + +void TimelineKeyframeItem::setPosition(qreal position) +{ + int offset = (TimelineConstants::sectionHeight - TimelineConstants::keyFrameSize) / 2; + const qreal scenePostion = mapFromFrameToScene(position); + + setRect(scenePostion - TimelineConstants::keyFrameSize / 2, + offset, + TimelineConstants::keyFrameSize, + TimelineConstants::keyFrameSize); +} + +void TimelineKeyframeItem::setPositionInteractive(const QPointF &postion) +{ + qreal left = postion.x() - qreal(TimelineConstants::keyFrameSize) / qreal(2); + setRect(left, rect().y(), rect().width(), rect().height()); +} + +void TimelineKeyframeItem::commitPosition(const QPointF &point) +{ + setPositionInteractive(point); + + const qreal frame = qRound(mapFromSceneToFrame(rect().center().x())); + + setPosition(frame); + + QTC_ASSERT(m_frame.isValid(), return ); + + blockUpdates(); + + m_frame.view()->executeInTransaction("TimelineKeyframeItem::commitPosition", [this, frame](){ + m_frame.variantProperty("frame").setValue(frame); + }); + + enableUpdates(); +} + +TimelineKeyframeItem *TimelineKeyframeItem::asTimelineKeyframeItem() +{ + return this; +} + +void TimelineKeyframeItem::blockUpdates() +{ + s_blockUpdates = true; +} + +void TimelineKeyframeItem::enableUpdates() +{ + s_blockUpdates = false; +} + +bool TimelineKeyframeItem::hasManualBezier() const +{ + return m_frame.isValid() && m_frame.hasProperty("easing.bezierCurve"); +} + +void TimelineKeyframeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + if (rect().x() < TimelineConstants::sectionWidth - rect().width() / 2) + return; + + painter->save(); + + Utils::Icon icon([this]() { + const bool itemIsSelected = propertyItem()->isSelected(); + const bool manualBezier = hasManualBezier(); + + if (m_highlight && manualBezier) { + return TimelineIcons::KEYFRAME_MANUALBEZIER_SELECTED; + } else if (m_highlight) { + return TimelineIcons::KEYFRAME_LINEAR_SELECTED; + } else if (itemIsSelected && manualBezier) { + return TimelineIcons::KEYFRAME_MANUALBEZIER_ACTIVE; + } else if (itemIsSelected) { + return TimelineIcons::KEYFRAME_LINEAR_ACTIVE; + } else if (manualBezier) { + return TimelineIcons::KEYFRAME_MANUALBEZIER_INACTIVE; + } + + return TimelineIcons::KEYFRAME_LINEAR_INACTIVE; + }()); + + painter->drawPixmap(rect().topLeft() - QPointF(0, 1), icon.pixmap()); + + painter->restore(); +} + +ModelNode TimelineKeyframeItem::frameNode() const +{ + return m_frame; +} + +void TimelineKeyframeItem::setHighlighted(bool b) +{ + m_highlight = b; + update(); +} + +TimelinePropertyItem *TimelineKeyframeItem::propertyItem() const +{ + /* The parentItem is always a TimelinePropertyItem. See constructor */ + return qgraphicsitem_cast<TimelinePropertyItem *>(parentItem()); +} + +void TimelineKeyframeItem::scrollOffsetChanged() +{ + updateFrame(); +} + +void TimelineKeyframeItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + QMenu mainMenu; + QAction *removeAction = mainMenu.addAction(tr("Delete Keyframe")); + QObject::connect(removeAction, &QAction::triggered, [this]() { + timelineScene()->handleKeyframeDeletion(); + }); + + QAction *editEasingAction = mainMenu.addAction(tr("Edit Easing Curve...")); + QObject::connect(editEasingAction, &QAction::triggered, [this]() { + const QList<ModelNode> keys = Utils::transform(timelineScene()->selectedKeyframes(), + &TimelineKeyframeItem::m_frame); + + setEasingCurve(timelineScene(), keys); + }); + + QAction *editValueAction = mainMenu.addAction(tr("Edit Value for Keyframe...")); + QObject::connect(editValueAction, &QAction::triggered, [this]() { + editValue(m_frame, propertyItem()->propertyName()); + }); + + mainMenu.exec(event->screenPos()); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h new file mode 100644 index 0000000000..2b8c00c59b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinecontrols.h" +#include "timelinesectionitem.h" + +#include <qmltimelinekeyframegroup.h> + +#include <modelnode.h> + +#include <QGraphicsRectItem> + +QT_FORWARD_DECLARE_CLASS(QLineEdit) + +namespace QmlDesigner { + +class TimelinePropertyItem; +class TimelineGraphicsScene; +class TimelineToolButton; + +class TimelineKeyframeItem : public TimelineMovableAbstractItem +{ + Q_DECLARE_TR_FUNCTIONS(TimelineKeyframeItem) + +public: + explicit TimelineKeyframeItem(TimelinePropertyItem *parent, const ModelNode &frame); + ~TimelineKeyframeItem() override; + + static void blockUpdates(); + static void enableUpdates(); + + ModelNode frameNode() const; + + void updateFrame(); + + void setHighlighted(bool b); + + void setPosition(qreal position); + + void commitPosition(const QPointF &point) override; + + TimelineKeyframeItem *asTimelineKeyframeItem() override; + +protected: + bool hasManualBezier() const; + + void scrollOffsetChanged() override; + + void setPositionInteractive(const QPointF &postion) override; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + TimelinePropertyItem *propertyItem() const; + + ModelNode m_frame; + + bool m_highlight = false; +}; + +class TimelinePropertyItem : public TimelineItem +{ + Q_OBJECT + +public: + enum { Type = TimelineConstants::timelinePropertyItemUserType }; + + static TimelinePropertyItem *create(const QmlTimelineKeyframeGroup &frames, + TimelineSectionItem *parent = nullptr); + + int type() const override; + + void updateData(); + void updateFrames(); + bool isSelected() const; + + static void updateTextEdit(QGraphicsItem *item); + static void updateRecordButtonStatus(QGraphicsItem *item); + + QmlTimelineKeyframeGroup frames() const; + + QString propertyName() const; + + void changePropertyValue(const QVariant &value); + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + TimelinePropertyItem(TimelineSectionItem *parent = nullptr); + + void setupKeyframes(); + qreal currentFrame(); + void updateTextEdit(); + + QmlTimelineKeyframeGroup m_frames; + TimelineControl *m_control = nullptr; + TimelineToolButton *m_recording = nullptr; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp new file mode 100644 index 0000000000..7bd784a7dd --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp @@ -0,0 +1,1062 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinesectionitem.h" + +#include "abstractview.h" +#include "timelineactions.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinepropertyitem.h" +#include "timelinetoolbutton.h" +#include "timelineutils.h" + +#include <qmltimeline.h> +#include <qmltimelinekeyframegroup.h> + +#include <rewritingexception.h> + +#include <theme.h> + +#include <utils/qtcassert.h> + +#include <QAction> +#include <QColorDialog> +#include <QComboBox> +#include <QGraphicsProxyWidget> +#include <QGraphicsScene> +#include <QGraphicsSceneMouseEvent> +#include <QGraphicsView> +#include <QHBoxLayout> +#include <QMenu> +#include <QPainter> +#include <QToolBar> + +#include <QGraphicsView> + +#include <QDebug> + +#include <cmath> + +static int textOffset = 8; + +namespace QmlDesigner { + +class ClickDummy : public TimelineItem +{ +public: + explicit ClickDummy(TimelineSectionItem *parent) + : TimelineItem(parent) + { + setGeometry(0, 0, TimelineConstants::sectionWidth, TimelineConstants::sectionHeight); + + setZValue(10); + setCursor(Qt::ArrowCursor); + } + +protected: + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } + void mousePressEvent(QGraphicsSceneMouseEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } +}; + +TimelineSectionItem::TimelineSectionItem(TimelineItem *parent) + : TimelineItem(parent) +{} + +TimelineSectionItem *TimelineSectionItem::create(const QmlTimeline &timeline, + const ModelNode &target, + TimelineItem *parent) +{ + auto item = new TimelineSectionItem(parent); + + if (target.isValid()) + item->setToolTip(target.id()); + + item->m_targetNode = target; + item->m_timeline = timeline; + + item->createPropertyItems(); + + item->m_dummyItem = new ClickDummy(item); + item->m_dummyItem->update(); + + item->m_barItem = new TimelineBarItem(item); + item->invalidateBar(); + item->invalidateHeight(); + + return item; +} + +void TimelineSectionItem::invalidateBar() +{ + qreal min = m_timeline.minActualKeyframe(m_targetNode); + qreal max = m_timeline.maxActualKeyframe(m_targetNode); + + const qreal sceneMin = m_barItem->mapFromFrameToScene(min); + + QRectF barRect(sceneMin, + 0, + (max - min) * m_barItem->rulerScaling(), + TimelineConstants::sectionHeight - 1); + + m_barItem->setRect(barRect); +} + +int TimelineSectionItem::type() const +{ + return Type; +} + +void TimelineSectionItem::updateData(QGraphicsItem *item) +{ + if (auto sectionItem = qgraphicsitem_cast<TimelineSectionItem *>(item)) + sectionItem->updateData(); +} + +void TimelineSectionItem::updateDataForTarget(QGraphicsItem *item, const ModelNode &target, bool *b) +{ + if (!target.isValid()) + return; + + if (auto sectionItem = qgraphicsitem_cast<TimelineSectionItem *>(item)) { + if (sectionItem->m_targetNode == target) { + sectionItem->updateData(); + if (b) + *b = true; + } + } +} + +void TimelineSectionItem::updateFramesForTarget(QGraphicsItem *item, const ModelNode &target) +{ + if (auto sectionItem = qgraphicsitem_cast<TimelineSectionItem *>(item)) { + if (sectionItem->m_targetNode == target) + sectionItem->updateFrames(); + } +} + +void TimelineSectionItem::moveAllFrames(qreal offset) +{ + if (m_timeline.isValid()) + m_timeline.moveAllKeyframes(m_targetNode, offset); +} + +void TimelineSectionItem::scaleAllFrames(qreal scale) +{ + if (m_timeline.isValid()) + m_timeline.scaleAllKeyframes(m_targetNode, scale); +} + +qreal TimelineSectionItem::firstFrame() +{ + if (!m_timeline.isValid()) + return 0; + + return m_timeline.minActualKeyframe(m_targetNode); +} + +AbstractView *TimelineSectionItem::view() const +{ + return m_timeline.view(); +} + +bool TimelineSectionItem::isSelected() const +{ + return m_targetNode.isValid() && m_targetNode.isSelected(); +} + +ModelNode TimelineSectionItem::targetNode() const +{ + return m_targetNode; +} + +QVector<qreal> TimelineSectionItem::keyframePositions() const +{ + QVector<qreal> out; + for (auto frame : m_timeline.keyframeGroupsForTarget(m_targetNode)) + out.append(timelineScene()->keyframePositions(frame)); + + return out; +} + +QTransform rotatationTransform(qreal degrees) +{ + QTransform transform; + transform.rotate(degrees); + + return transform; +} + +QPixmap rotateby90(const QPixmap &pixmap) +{ + QImage sourceImage = pixmap.toImage(); + QImage destImage(pixmap.height(), pixmap.width(), sourceImage.format()); + + for (int x = 0; x < pixmap.width(); x++) + for (int y = 0; y < pixmap.height(); y++) + destImage.setPixel(y, x, sourceImage.pixel(x, y)); + + QPixmap result = QPixmap::fromImage(destImage); + + result.setDevicePixelRatio(pixmap.devicePixelRatio()); + + return result; +} + +static int devicePixelHeight(const QPixmap &pixmap) +{ + return pixmap.height() / pixmap.devicePixelRatioF(); +} + +void TimelineSectionItem::paint(QPainter *painter, + const QStyleOptionGraphicsItem * /*option*/, + QWidget *) +{ + if (m_targetNode.isValid()) { + painter->save(); + + const QColor textColor = Theme::getColor(Theme::PanelTextColorLight); + const QColor penColor = Theme::instance()->qmlDesignerBackgroundColorDarker(); + QColor brushColor = Theme::getColor(Theme::BackgroundColorDark); + + int fillOffset = 0; + if (isSelected()) { + brushColor = Theme::getColor(Theme::QmlDesigner_HighlightColor); + fillOffset = 1; + } + + painter->fillRect(0, + 0, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight - fillOffset, + brushColor); + painter->fillRect(TimelineConstants::sectionWidth, + 0, + size().width() - TimelineConstants::sectionWidth, + size().height(), + Theme::instance()->qmlDesignerBackgroundColorDarkAlternate()); + + painter->setPen(penColor); + drawLine(painter, + TimelineConstants::sectionWidth - 1, + 0, + TimelineConstants::sectionWidth - 1, + size().height() - 1); + drawLine(painter, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight - 1, + size().width(), + TimelineConstants::sectionHeight - 1); + + static const QPixmap arrow = Theme::getPixmap("down-arrow"); + + static const QPixmap arrow90 = rotateby90(arrow); + + const QPixmap rotatedArrow = collapsed() ? arrow90 : arrow; + + const int textOffset = QFontMetrics(font()).ascent() + + (TimelineConstants::sectionHeight - QFontMetrics(font()).height()) + / 2; + + painter->drawPixmap(collapsed() ? 6 : 4, + (TimelineConstants::sectionHeight - devicePixelHeight(rotatedArrow)) / 2, + rotatedArrow); + + painter->setPen(textColor); + + QFontMetrics fm(painter->font()); + const QString elidedId = fm.elidedText(m_targetNode.id(), + Qt::ElideMiddle, + TimelineConstants::sectionWidth + - TimelineConstants::textIndentationSections); + painter->drawText(TimelineConstants::textIndentationSections, textOffset, elidedId); + + painter->restore(); + } +} + +void TimelineSectionItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->pos().y() > TimelineConstants::sectionHeight + || event->pos().x() < TimelineConstants::textIndentationSections) { + TimelineItem::mouseDoubleClickEvent(event); + return; + } + + if (event->button() == Qt::LeftButton) { + event->accept(); + toggleCollapsed(); + } +} + +void TimelineSectionItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->pos().y() > TimelineConstants::sectionHeight) { + TimelineItem::mousePressEvent(event); + return; + } + + if (event->button() == Qt::LeftButton) + event->accept(); +} + +void TimelineSectionItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->pos().y() > TimelineConstants::sectionHeight) { + TimelineItem::mouseReleaseEvent(event); + return; + } + + if (event->button() != Qt::LeftButton) + return; + + event->accept(); + + if (event->pos().x() > TimelineConstants::textIndentationSections + && event->button() == Qt::LeftButton) { + if (m_targetNode.isValid()) + m_targetNode.view()->setSelectedModelNode(m_targetNode); + } else { + toggleCollapsed(); + } + update(); +} + +void TimelineSectionItem::resizeEvent(QGraphicsSceneResizeEvent *event) +{ + TimelineItem::resizeEvent(event); + + for (auto child : propertyItems()) { + TimelinePropertyItem *item = static_cast<TimelinePropertyItem *>(child); + item->resize(size().width(), TimelineConstants::sectionHeight); + } +} + +void TimelineSectionItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + if (event->pos().x() < TimelineConstants::sectionWidth + && event->pos().y() < TimelineConstants::sectionHeight) { + QMenu mainMenu; + + auto timeline = timelineScene()->currentTimeline(); + + QAction *removeAction = mainMenu.addAction( + TimelineConstants::timelineDeleteKeyframesDisplayName); + QObject::connect(removeAction, &QAction::triggered, [this]() { + timelineScene()->deleteAllKeyframesForTarget(m_targetNode); + }); + + QAction *addKeyframesAction = mainMenu.addAction( + TimelineConstants::timelineInsertKeyframesDisplayName); + QObject::connect(addKeyframesAction, &QAction::triggered, [this]() { + timelineScene()->insertAllKeyframesForTarget(m_targetNode); + }); + + QAction *copyAction = mainMenu.addAction( + TimelineConstants::timelineCopyKeyframesDisplayName); + QObject::connect(copyAction, &QAction::triggered, [this]() { + timelineScene()->copyAllKeyframesForTarget(m_targetNode); + }); + + QAction *pasteAction = mainMenu.addAction( + TimelineConstants::timelinePasteKeyframesDisplayName); + QObject::connect(pasteAction, &QAction::triggered, [this]() { + timelineScene()->pasteKeyframesToTarget(m_targetNode); + }); + + pasteAction->setEnabled(TimelineActions::clipboardContainsKeyframes()); + + mainMenu.exec(event->screenPos()); + event->accept(); + } +} + +void TimelineSectionItem::updateData() +{ + invalidateBar(); + resize(rulerWidth(), size().height()); + invalidateProperties(); + update(); +} + +void TimelineSectionItem::updateFrames() +{ + invalidateBar(); + invalidateFrames(); + update(); +} + +void TimelineSectionItem::invalidateHeight() +{ + int height = 0; + bool visible = true; + + if (collapsed()) { + height = TimelineConstants::sectionHeight; + visible = false; + } else { + height = TimelineConstants::sectionHeight + + m_timeline.keyframeGroupsForTarget(m_targetNode).count() + * TimelineConstants::sectionHeight; + visible = true; + } + + for (auto child : propertyItems()) + child->setVisible(visible); + + setPreferredHeight(height); + setMinimumHeight(height); + setMaximumHeight(height); + timelineScene()->activateLayout(); +} + +void TimelineSectionItem::invalidateProperties() +{ + for (auto child : propertyItems()) { + delete child; + } + + createPropertyItems(); + + for (auto child : propertyItems()) { + TimelinePropertyItem *item = static_cast<TimelinePropertyItem *>(child); + item->updateData(); + item->resize(size().width(), TimelineConstants::sectionHeight); + } + invalidateHeight(); +} + +void TimelineSectionItem::invalidateFrames() +{ + for (auto child : propertyItems()) { + TimelinePropertyItem *item = static_cast<TimelinePropertyItem *>(child); + item->updateFrames(); + } +} + +bool TimelineSectionItem::collapsed() const +{ + return m_targetNode.isValid() && !m_targetNode.hasAuxiliaryData("timeline_expanded"); +} + +void TimelineSectionItem::createPropertyItems() +{ + auto framesList = m_timeline.keyframeGroupsForTarget(m_targetNode); + + int yPos = TimelineConstants::sectionHeight; + for (const auto &frames : framesList) { + auto item = TimelinePropertyItem::create(frames, this); + item->setY(yPos); + yPos = yPos + TimelineConstants::sectionHeight; + } +} + +qreal TimelineSectionItem::rulerWidth() const +{ + return static_cast<TimelineGraphicsScene *>(scene())->rulerWidth(); +} + +void TimelineSectionItem::toggleCollapsed() +{ + QTC_ASSERT(m_targetNode.isValid(), return ); + + if (collapsed()) + m_targetNode.setAuxiliaryData("timeline_expanded", true); + else + m_targetNode.removeAuxiliaryData("timeline_expanded"); + + invalidateHeight(); +} + +QList<QGraphicsItem *> TimelineSectionItem::propertyItems() const +{ + QList<QGraphicsItem *> list; + + for (auto child : childItems()) { + if (m_barItem != child && m_dummyItem != child) + list.append(child); + } + + return list; +} + +TimelineRulerSectionItem::TimelineRulerSectionItem(TimelineItem *parent) + : TimelineItem(parent) +{ + setPreferredHeight(TimelineConstants::rulerHeight); + setMinimumHeight(TimelineConstants::rulerHeight); + setMaximumHeight(TimelineConstants::rulerHeight); + setZValue(10); +} + +static void drawCenteredText(QPainter *p, int x, int y, const QString &text) +{ + QRect rect(x - 16, y - 4, 32, 8); + p->drawText(rect, Qt::AlignCenter, text); +} + +TimelineRulerSectionItem *TimelineRulerSectionItem::create(QGraphicsScene *parentScene, + TimelineItem *parent) +{ + auto item = new TimelineRulerSectionItem(parent); + item->setMaximumHeight(TimelineConstants::rulerHeight); + + auto widget = new QWidget; + widget->setFixedWidth(TimelineConstants::sectionWidth); + + auto toolBar = new QToolBar; + toolBar->setFixedHeight(TimelineConstants::rulerHeight); + + auto layout = new QHBoxLayout(widget); + layout->addWidget(toolBar); + layout->setMargin(0); + + layout->addWidget(toolBar); + layout->setMargin(0); + + QGraphicsProxyWidget *proxy = parentScene->addWidget(widget); + proxy->setParentItem(item); + + return item; +} + +void TimelineRulerSectionItem::invalidateRulerSize(const QmlTimeline &timeline) +{ + m_duration = timeline.duration(); + m_start = timeline.startKeyframe(); + m_end = timeline.endKeyframe(); +} + +void TimelineRulerSectionItem::setRulerScaleFactor(int scaling) +{ + qreal blend = qreal(scaling) / 100.0; + + qreal width = size().width() - qreal(TimelineConstants::sectionWidth); + qreal duration = rulerDuration(); + + qreal offset = duration * 0.1; + qreal maxCount = duration + offset; + qreal minCount = width + / qreal(TimelineConstants::keyFrameSize + + 2 * TimelineConstants::keyFrameMargin); + + qreal count = maxCount < minCount ? maxCount : TimelineUtils::lerp(blend, minCount, maxCount); + + if (count > std::numeric_limits<qreal>::min() && count <= maxCount) + m_scaling = width / count; + else + m_scaling = 1.0; + + update(); +} + +int TimelineRulerSectionItem::getRulerScaleFactor() const +{ + qreal width = size().width() - qreal(TimelineConstants::sectionWidth); + qreal duration = rulerDuration(); + + qreal offset = duration * 0.1; + qreal maxCount = duration + offset; + qreal minCount = width + / qreal(TimelineConstants::keyFrameSize + + 2 * TimelineConstants::keyFrameMargin); + + if (maxCount < minCount) + return -1; + + qreal rcount = width / m_scaling; + qreal rblend = TimelineUtils::reverseLerp(rcount, minCount, maxCount); + + int rfactor = std::round(rblend * 100); + return TimelineUtils::clamp(rfactor, 0, 100); +} + +qreal TimelineRulerSectionItem::rulerScaling() const +{ + return m_scaling; +} + +qreal TimelineRulerSectionItem::rulerDuration() const +{ + return m_duration; +} + +qreal TimelineRulerSectionItem::durationViewportLength() const +{ + return m_duration * m_scaling; +} + +qreal TimelineRulerSectionItem::startFrame() const +{ + return m_start; +} + +qreal TimelineRulerSectionItem::endFrame() const +{ + return m_end; +} + +void TimelineRulerSectionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + static const QColor backgroundColor = Theme::instance() + ->qmlDesignerBackgroundColorDarkAlternate(); + static const QColor penColor = Theme::getColor(Theme::PanelTextColorLight); + static const QColor highlightColor = Theme::instance()->Theme::qmlDesignerButtonColor(); + static const QColor handleColor = Theme::getColor(Theme::QmlDesigner_HighlightColor); + + painter->save(); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + painter->translate(-timelineScene()->scrollOffset(), 0); + painter->fillRect(TimelineConstants::sectionWidth, + 0, + size().width() - TimelineConstants::sectionWidth, + size().height(), + backgroundColor); + + painter->translate(TimelineConstants::timelineLeftOffset, 0); + + const QRectF rangeRect(TimelineConstants::sectionWidth, + 0, + m_duration * m_scaling, + size().height()); + + const qreal radius = 5; + const qreal handleWidth = TimelineConstants::timelineBounds * 2; + QRectF boundsRect(0, rangeRect.y(), handleWidth, rangeRect.height()); + + boundsRect.moveRight(rangeRect.left() + TimelineConstants::timelineBounds); + + QPainterPath leftBoundsPath; + leftBoundsPath.addRoundedRect(boundsRect, radius, radius); + painter->fillPath(leftBoundsPath, handleColor); + + boundsRect.moveLeft(rangeRect.right() - TimelineConstants::timelineBounds); + + QPainterPath rightBoundsPath; + rightBoundsPath.addRoundedRect(boundsRect, radius, radius); + painter->fillPath(rightBoundsPath, handleColor); + + painter->fillRect(rangeRect, highlightColor); + + painter->setPen(penColor); + + const int height = size().height() - 1; + + drawLine(painter, + TimelineConstants::sectionWidth + timelineScene()->scrollOffset() + - TimelineConstants::timelineLeftOffset, + height, + size().width() + timelineScene()->scrollOffset(), + height); + + QFont font = painter->font(); + font.setPixelSize(8); + painter->setFont(font); + + paintTicks(painter); + + painter->restore(); + + painter->fillRect(0, 0, TimelineConstants::sectionWidth, size().height(), backgroundColor); + painter->restore(); +} + +void TimelineRulerSectionItem::paintTicks(QPainter *painter) +{ + const int totalWidth = size().width() / m_scaling + timelineScene()->scrollOffset() / m_scaling; + + QFontMetrics fm(painter->font()); + + int minSpacingText = fm.horizontalAdvance(QString("X%1X").arg(rulerDuration())); + int minSpacingLine = 5; + + int deltaText = 0; + int deltaLine = 0; + + // Marks possibly at [1, 5, 10, 50, 100, ...] + int spacing = 1; + bool toggle = true; + while (deltaText == 0) { + int distance = spacing * m_scaling; + + if (distance > minSpacingLine && deltaLine == 0) + deltaLine = spacing; + + if (distance > minSpacingText) { + deltaText = spacing; + break; + } + + if (toggle) { + spacing *= 5; + toggle = false; + } else { + spacing *= 2; + toggle = true; + } + } + + int height = size().height(); + + for (int i = timelineScene()->scrollOffset() / m_scaling; i < totalWidth; ++i) { + if ((i % deltaText) == 0) { + drawCenteredText(painter, + TimelineConstants::sectionWidth + i * m_scaling, + textOffset, + QString::number(m_start + i)); + + drawLine(painter, + TimelineConstants::sectionWidth + i * m_scaling, + height - 2, + TimelineConstants::sectionWidth + i * m_scaling, + height * 0.6); + + } else if ((i % deltaLine) == 0) { + drawLine(painter, + TimelineConstants::sectionWidth + i * m_scaling, + height - 2, + TimelineConstants::sectionWidth + i * m_scaling, + height * 0.75); + } + } +} + +void TimelineRulerSectionItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + TimelineItem::mousePressEvent(event); + emit rulerClicked(event->pos()); +} + +void TimelineRulerSectionItem::resizeEvent(QGraphicsSceneResizeEvent *event) +{ + QGraphicsWidget::resizeEvent(event); + + auto factor = getRulerScaleFactor(); + + if (factor < 0) { + if (event->oldSize().width() < event->newSize().width()) + factor = 0; + else + factor = 100; + } + + emit scaleFactorChanged(factor); +} + +void TimelineRulerSectionItem::setSizeHints(int width) +{ + const int rulerWidth = width; + setPreferredWidth(rulerWidth); + setMinimumWidth(rulerWidth); + setMaximumWidth(rulerWidth); +} + +TimelineBarItem::TimelineBarItem(TimelineSectionItem *parent) + : TimelineMovableAbstractItem(parent) +{ + setAcceptHoverEvents(true); + setPen(Qt::NoPen); +} + +void TimelineBarItem::itemMoved(const QPointF &start, const QPointF &end) +{ + if (isActiveHandle(Location::Undefined)) + dragInit(rect(), start); + + const qreal min = qreal(TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset + - scrollOffset()); + const qreal max = qreal(timelineScene()->rulerWidth() - TimelineConstants::sectionWidth + + rect().width()); + + if (isActiveHandle(Location::Center)) + dragCenter(rect(), end, min, max); + else + dragHandle(rect(), end, min, max); + + timelineScene()->statusBarMessageChanged( + tr("Range from %1 to %2") + .arg(qRound(mapFromSceneToFrame(rect().x()))) + .arg(qRound(mapFromSceneToFrame(rect().width() + rect().x())))); +} + +void TimelineBarItem::commitPosition(const QPointF & /*point*/) +{ + if (sectionItem()->view()) { + if (m_handle != Location::Undefined) { + sectionItem()->view()->executeInTransaction("TimelineBarItem::commitPosition", [this](){ + qreal scaleFactor = rect().width() / m_oldRect.width(); + + qreal moved = (rect().topLeft().x() - m_oldRect.topLeft().x()) / rulerScaling(); + qreal supposedFirstFrame = qRound(sectionItem()->firstFrame() + moved); + + sectionItem()->scaleAllFrames(scaleFactor); + sectionItem()->moveAllFrames(supposedFirstFrame - sectionItem()->firstFrame()); + }); + } + } + + m_handle = Location::Undefined; + m_bounds = Location::Undefined; + m_pivot = 0.0; + m_oldRect = QRectF(); +} + +void TimelineBarItem::scrollOffsetChanged() +{ + sectionItem()->invalidateBar(); +} + +void TimelineBarItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + QColor brushColorSelected = Theme::getColor(Theme::QmlDesigner_HighlightColor); + QColor brushColor = Theme::getColor(Theme::QmlDesigner_HighlightColor).darker(120); + const QColor indicatorColor = Theme::getColor(Theme::PanelTextColorLight); + + ModelNode target = sectionItem()->targetNode(); + if (target.isValid()) { + QColor overrideColor = target.auxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE).value<QColor>(); + if (overrideColor.isValid()) { + brushColorSelected = overrideColor; + brushColor = brushColorSelected.darker(120); + } + } + + const QRectF itemRect = rect(); + + painter->save(); + painter->setClipRect(TimelineConstants::sectionWidth, + 0, + itemRect.width() + itemRect.x(), + itemRect.height()); + + if (sectionItem()->isSelected()) + painter->fillRect(itemRect, brushColorSelected); + else + painter->fillRect(itemRect, brushColor); + + auto positions = sectionItem()->keyframePositions(); + std::sort(positions.begin(), positions.end()); + + auto fcompare = [](auto v1, auto v2) { return qFuzzyCompare(v1, v2); }; + auto unique = std::unique(positions.begin(), positions.end(), fcompare); + positions.erase(unique, positions.end()); + + painter->setPen(indicatorColor); + auto margin = itemRect.height() * 0.166; + auto p1y = itemRect.top() + margin; + auto p2y = itemRect.bottom() - margin; + for (auto pos : positions) { + auto px = mapFromFrameToScene(pos) + 0.5; + painter->drawLine(QLineF(px, p1y, px, p2y)); + } + painter->restore(); +} + +void TimelineBarItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) +{ + const auto p = event->pos(); + + QRectF left, right; + if (handleRects(rect(), left, right)) { + if (left.contains(p) || right.contains(p)) { + if (cursor().shape() != Qt::SizeHorCursor) + setCursor(QCursor(Qt::SizeHorCursor)); + } else if (rect().contains(p)) { + if (cursor().shape() != Qt::ClosedHandCursor) + setCursor(QCursor(Qt::ClosedHandCursor)); + } + } else { + if (rect().contains(p)) + setCursor(QCursor(Qt::ClosedHandCursor)); + } +} + +void TimelineBarItem::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) +{ + QMenu menu; + QAction* overrideColor = menu.addAction(tr("Override Color")); + + auto setColor = [this] () { + ModelNode target = sectionItem()->targetNode(); + if (target.isValid()) { + QColor current = target.auxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE).value<QColor>(); + QColor color = QColorDialog::getColor(current, nullptr); + if (color.isValid()) + target.setAuxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE, color); + } + }; + + QObject::connect(overrideColor, &QAction::triggered, setColor); + + QAction* resetColor = menu.addAction(tr("Reset Color")); + auto reset = [this]() { + ModelNode target = sectionItem()->targetNode(); + if (target.isValid()) + target.removeAuxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE); + }; + QObject::connect(resetColor, &QAction::triggered, reset); + + menu.exec(event->screenPos()); +} + +TimelineSectionItem *TimelineBarItem::sectionItem() const +{ + /* The parentItem is always a TimelineSectionItem. See constructor */ + return qgraphicsitem_cast<TimelineSectionItem *>(parentItem()); +} + +void TimelineBarItem::dragInit(const QRectF &rect, const QPointF &pos) +{ + QRectF left, right; + m_oldRect = rect; + if (handleRects(rect, left, right)) { + if (left.contains(pos)) { + m_handle = Location::Left; + m_pivot = pos.x() - left.topLeft().x(); + } else if (right.contains(pos)) { + m_handle = Location::Right; + m_pivot = pos.x() - right.topRight().x(); + } else if (rect.contains(pos)) { + m_handle = Location::Center; + m_pivot = pos.x() - rect.topLeft().x(); + } + + } else { + if (rect.contains(pos)) { + m_handle = Location::Center; + m_pivot = pos.x() - rect.topLeft().x(); + } + } +} + +void TimelineBarItem::dragCenter(QRectF rect, const QPointF &pos, qreal min, qreal max) +{ + if (validateBounds(pos.x() - rect.topLeft().x())) { + rect.moveLeft(pos.x() - m_pivot); + if (rect.topLeft().x() < min) { + rect.moveLeft(min); + setOutOfBounds(Location::Left); + } else if (rect.topRight().x() > max) { + rect.moveRight(max); + setOutOfBounds(Location::Right); + } + setRect(rect); + } +} + +void TimelineBarItem::dragHandle(QRectF rect, const QPointF &pos, qreal min, qreal max) +{ + QRectF left, right; + handleRects(rect, left, right); + + if (isActiveHandle(Location::Left)) { + if (validateBounds(pos.x() - left.topLeft().x())) { + rect.setLeft(pos.x() - m_pivot); + if (rect.left() < min) { + rect.setLeft(min); + setOutOfBounds(Location::Left); + } else if (rect.left() >= rect.right() - minimumBarWidth) + rect.setLeft(rect.right() - minimumBarWidth); + + setRect(rect); + } + } else if (isActiveHandle(Location::Right)) { + if (validateBounds(pos.x() - right.topRight().x())) { + rect.setRight(pos.x() - m_pivot); + if (rect.right() > max) { + rect.setRight(max); + setOutOfBounds(Location::Right); + } else if (rect.right() <= rect.left() + minimumBarWidth) + rect.setRight(rect.left() + minimumBarWidth); + + setRect(rect); + } + } +} + +bool TimelineBarItem::handleRects(const QRectF &rect, QRectF &left, QRectF &right) const +{ + if (rect.width() < minimumBarWidth) + return false; + + const qreal handleSize = rect.height(); + + auto handleRect = QRectF(0, 0, handleSize, handleSize); + handleRect.moveCenter(rect.center()); + + handleRect.moveLeft(rect.left()); + left = handleRect; + + handleRect.moveRight(rect.right()); + right = handleRect; + + return true; +} + +bool TimelineBarItem::isActiveHandle(Location location) const +{ + return m_handle == location; +} + +void TimelineBarItem::setOutOfBounds(Location location) +{ + m_bounds = location; +} + +bool TimelineBarItem::validateBounds(qreal distance) +{ + if (m_bounds == Location::Left) { + if (distance > m_pivot) + m_bounds = Location::Center; + return false; + + } else if (m_bounds == Location::Right) { + if (distance < m_pivot) + m_bounds = Location::Center; + return false; + } + return true; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h new file mode 100644 index 0000000000..26db04f757 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h @@ -0,0 +1,192 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineitem.h" +#include "timelinemovableabstractitem.h" + +#include <modelnode.h> +#include <qmltimeline.h> + +QT_FORWARD_DECLARE_CLASS(QComboBox) +QT_FORWARD_DECLARE_CLASS(QPainter) + +namespace QmlDesigner { + +class TimelineSectionItem; + +class TimelineBarItem : public TimelineMovableAbstractItem +{ + Q_DECLARE_TR_FUNCTIONS(TimelineBarItem) + + enum class Location { Undefined, Center, Left, Right }; + +public: + explicit TimelineBarItem(TimelineSectionItem *parent); + + void itemMoved(const QPointF &start, const QPointF &end) override; + void commitPosition(const QPointF &point) override; + +protected: + void scrollOffsetChanged() override; + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget = nullptr) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent *) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent * event) override; +private: + TimelineSectionItem *sectionItem() const; + + void dragInit(const QRectF &rect, const QPointF &pos); + void dragCenter(QRectF rect, const QPointF &pos, qreal min, qreal max); + void dragHandle(QRectF rect, const QPointF &pos, qreal min, qreal max); + bool handleRects(const QRectF &rect, QRectF &left, QRectF &right) const; + bool isActiveHandle(Location location) const; + + void setOutOfBounds(Location location); + bool validateBounds(qreal pivot); + +private: + Location m_handle = Location::Undefined; + + Location m_bounds = Location::Undefined; + + qreal m_pivot = 0.0; + + QRectF m_oldRect; + + static constexpr qreal minimumBarWidth = 2.0 + * static_cast<qreal>(TimelineConstants::sectionHeight); +}; + +class TimelineSectionItem : public TimelineItem +{ + Q_OBJECT + +public: + enum { Type = TimelineConstants::timelineSectionItemUserType }; + + static TimelineSectionItem *create(const QmlTimeline &timelineScene, + const ModelNode &target, + TimelineItem *parent); + + void invalidateBar(); + + int type() const override; + + static void updateData(QGraphicsItem *item); + static void updateDataForTarget(QGraphicsItem *item, const ModelNode &target, bool *b); + static void updateFramesForTarget(QGraphicsItem *item, const ModelNode &target); + + void moveAllFrames(qreal offset); + void scaleAllFrames(qreal scale); + qreal firstFrame(); + AbstractView *view() const; + bool isSelected() const; + + ModelNode targetNode() const; + QVector<qreal> keyframePositions() const; + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void resizeEvent(QGraphicsSceneResizeEvent *event) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + void updateData(); + void updateFrames(); + void invalidateHeight(); + void invalidateProperties(); + void invalidateFrames(); + bool collapsed() const; + void createPropertyItems(); + qreal rulerWidth() const; + void toggleCollapsed(); + QList<QGraphicsItem *> propertyItems() const; + + TimelineSectionItem(TimelineItem *parent = nullptr); + ModelNode m_targetNode; + QmlTimeline m_timeline; + + TimelineBarItem *m_barItem; + TimelineItem *m_dummyItem; +}; + +class TimelineRulerSectionItem : public TimelineItem +{ + Q_OBJECT + +signals: + void rulerClicked(const QPointF &pos); + + void scaleFactorChanged(int scale); + +public: + static TimelineRulerSectionItem *create(QGraphicsScene *parentScene, TimelineItem *parent); + + void invalidateRulerSize(const QmlTimeline &timeline); + + void setRulerScaleFactor(int scaling); + + int getRulerScaleFactor() const; + + qreal rulerScaling() const; + qreal rulerDuration() const; + qreal durationViewportLength() const; + qreal startFrame() const; + qreal endFrame() const; + + QComboBox *comboBox() const; + + void setSizeHints(int width); + +signals: + void addTimelineClicked(); + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + + void resizeEvent(QGraphicsSceneResizeEvent *event) override; + +private: + TimelineRulerSectionItem(TimelineItem *parent = nullptr); + + void paintTicks(QPainter *painter); + + QComboBox *m_comboBox = nullptr; + + qreal m_duration = 0; + qreal m_start = 0; + qreal m_end = 0; + qreal m_scaling = 1; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp new file mode 100644 index 0000000000..53367c9b67 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp @@ -0,0 +1,179 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineselectiontool.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelinemovableabstractitem.h" +#include "timelinepropertyitem.h" +#include "timelinetooldelegate.h" + +#include <QGraphicsRectItem> +#include <QGraphicsSceneMouseEvent> +#include <QPen> + +namespace QmlDesigner { + +TimelineSelectionTool::TimelineSelectionTool(TimelineGraphicsScene *scene, + TimelineToolDelegate *delegate) + : TimelineAbstractTool(scene, delegate) + , m_selectionRect(new QGraphicsRectItem) +{ + scene->addItem(m_selectionRect); + QPen pen(Qt::black); + pen.setJoinStyle(Qt::MiterJoin); + pen.setCosmetic(true); + m_selectionRect->setPen(pen); + m_selectionRect->setBrush(QColor(128, 128, 128, 50)); + m_selectionRect->setZValue(100); + m_selectionRect->hide(); +} + +TimelineSelectionTool::~TimelineSelectionTool() = default; + +SelectionMode TimelineSelectionTool::selectionMode(QGraphicsSceneMouseEvent *event) +{ + if (event->modifiers().testFlag(Qt::ControlModifier)) { + if (event->modifiers().testFlag(Qt::ShiftModifier)) + return SelectionMode::Add; + else + return SelectionMode::Toggle; + } + + return SelectionMode::New; +} + +void TimelineSelectionTool::mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + if (event->buttons() == Qt::LeftButton && selectionMode(event) == SelectionMode::New) + deselect(); +} + +void TimelineSelectionTool::mouseMoveEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + + if (event->buttons() == Qt::LeftButton) { + auto endPoint = event->scenePos(); + if (endPoint.x() < 0) + endPoint.rx() = 0; + if (endPoint.y() < 0) + endPoint.ry() = 0; + m_selectionRect->setRect(QRectF(startPosition(), endPoint).normalized()); + m_selectionRect->show(); + + aboutToSelect(selectionMode(event), + scene()->items(m_selectionRect->rect(), Qt::ContainsItemShape)); + } +} + +void TimelineSelectionTool::mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + commitSelection(selectionMode(event)); + + reset(); +} + +void TimelineSelectionTool::mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + reset(); +} + +void TimelineSelectionTool::keyPressEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +void TimelineSelectionTool::keyReleaseEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +void TimelineSelectionTool::deselect() +{ + resetHighlights(); + scene()->clearSelection(); + delegate()->clearSelection(); +} + +void TimelineSelectionTool::reset() +{ + m_selectionRect->hide(); + m_selectionRect->setRect(0, 0, 0, 0); + resetHighlights(); +} + +void TimelineSelectionTool::resetHighlights() +{ + for (auto *keyframe : m_aboutToSelectBuffer) + if (scene()->isKeyframeSelected(keyframe)) + keyframe->setHighlighted(true); + else + keyframe->setHighlighted(false); + + m_aboutToSelectBuffer.clear(); +} + +void TimelineSelectionTool::aboutToSelect(SelectionMode mode, QList<QGraphicsItem *> items) +{ + resetHighlights(); + + for (auto *item : items) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (mode == SelectionMode::Remove) + keyframe->setHighlighted(false); + else if (mode == SelectionMode::Toggle) + if (scene()->isKeyframeSelected(keyframe)) + keyframe->setHighlighted(false); + else + keyframe->setHighlighted(true); + else + keyframe->setHighlighted(true); + + m_aboutToSelectBuffer << keyframe; + } + } +} + +void TimelineSelectionTool::commitSelection(SelectionMode mode) +{ + scene()->selectKeyframes(mode, m_aboutToSelectBuffer); + m_aboutToSelectBuffer.clear(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h new file mode 100644 index 0000000000..3485e087ec --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineabstracttool.h" + +#include <QList> + +QT_FORWARD_DECLARE_CLASS(QGraphicsItem) +QT_FORWARD_DECLARE_CLASS(QGraphicsRectItem) + +namespace QmlDesigner { + +class TimelineToolDelegate; + +class TimelineKeyframeItem; + +class TimelineGraphicsScene; + +enum class SelectionMode { New, Add, Remove, Toggle }; + +class TimelineSelectionTool : public TimelineAbstractTool +{ +public: + explicit TimelineSelectionTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate); + + ~TimelineSelectionTool() override; + + static SelectionMode selectionMode(QGraphicsSceneMouseEvent *event); + + void mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) override; + + void mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + + void keyReleaseEvent(QKeyEvent *keyEvent) override; + +private: + void deselect(); + + void reset(); + + void resetHighlights(); + + void aboutToSelect(SelectionMode mode, QList<QGraphicsItem *> items); + + void commitSelection(SelectionMode mode); + +private: + QGraphicsRectItem *m_selectionRect; + + QList<TimelineKeyframeItem *> m_aboutToSelectBuffer; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp new file mode 100644 index 0000000000..960c409553 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp @@ -0,0 +1,272 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinesettingsdialog.h" +#include "ui_timelinesettingsdialog.h" + +#include "timelineanimationform.h" +#include "timelineform.h" +#include "timelineicons.h" +#include "timelinesettingsmodel.h" +#include "timelineview.h" + +#include <abstractview.h> +#include <bindingproperty.h> +#include <exception> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <variantproperty.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +#include <QKeyEvent> +#include <QToolBar> + +namespace QmlDesigner { + +static void deleteAllTabs(QTabWidget *tabWidget) +{ + while (tabWidget->count() > 0) { + QWidget *w = tabWidget->widget(0); + tabWidget->removeTab(0); + delete w; + } +} + +static ModelNode getAnimationFromTabWidget(QTabWidget *tabWidget) +{ + QWidget *w = tabWidget->currentWidget(); + if (w) + return qobject_cast<TimelineAnimationForm *>(w)->animation(); + return ModelNode(); +} + +static QmlTimeline getTimelineFromTabWidget(QTabWidget *tabWidget) +{ + QWidget *w = tabWidget->currentWidget(); + if (w) + return qobject_cast<TimelineForm *>(w)->timeline(); + return QmlTimeline(); +} + +static void setTabForTimeline(QTabWidget *tabWidget, const QmlTimeline &timeline) +{ + for (int i = 0; i < tabWidget->count(); ++i) { + QWidget *w = tabWidget->widget(i); + if (qobject_cast<TimelineForm *>(w)->timeline() == timeline) { + tabWidget->setCurrentIndex(i); + return; + } + } +} + +static void setTabForAnimation(QTabWidget *tabWidget, const ModelNode &animation) +{ + for (int i = 0; i < tabWidget->count(); ++i) { + QWidget *w = tabWidget->widget(i); + if (qobject_cast<TimelineAnimationForm *>(w)->animation() == animation) { + tabWidget->setCurrentIndex(i); + return; + } + } +} + +TimelineSettingsDialog::TimelineSettingsDialog(QWidget *parent, TimelineView *view) + : QDialog(parent) + , ui(new Ui::TimelineSettingsDialog) + , m_timelineView(view) +{ + m_timelineSettingsModel = new TimelineSettingsModel(this, view); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); + + auto *timelineCornerWidget = new QToolBar; + + auto *timelineAddAction = new QAction(TimelineIcons::ADD_TIMELINE.icon(), tr("Add Timeline")); + auto *timelineRemoveAction = new QAction(TimelineIcons::REMOVE_TIMELINE.icon(), + tr("Remove Timeline")); + + connect(timelineAddAction, &QAction::triggered, this, [this]() { + setupTimelines(m_timelineView->addNewTimeline()); + }); + + connect(timelineRemoveAction, &QAction::triggered, this, [this]() { + QmlTimeline timeline = getTimelineFromTabWidget(ui->timelineTab); + if (timeline.isValid()) { + timeline.destroy(); + setupTimelines(QmlTimeline()); + } + }); + + timelineCornerWidget->addAction(timelineAddAction); + timelineCornerWidget->addAction(timelineRemoveAction); + + ui->timelineTab->setCornerWidget(timelineCornerWidget, Qt::TopRightCorner); + + auto *animationCornerWidget = new QToolBar; + + auto *animationAddAction = new QAction(TimelineIcons::ADD_TIMELINE.icon(), tr("Add Animation")); + auto *animationRemoveAction = new QAction(TimelineIcons::REMOVE_TIMELINE.icon(), + tr("Remove Animation")); + + animationCornerWidget->addAction(animationAddAction); + animationCornerWidget->addAction(animationRemoveAction); + + connect(animationAddAction, &QAction::triggered, this, [this]() { + setupAnimations(m_timelineView->addAnimation(m_currentTimeline)); + }); + + connect(animationRemoveAction, &QAction::triggered, this, [this]() { + ModelNode node = getAnimationFromTabWidget(ui->animationTab); + if (node.isValid()) { + node.destroy(); + setupAnimations(m_currentTimeline); + } + }); + + ui->animationTab->setCornerWidget(animationCornerWidget, Qt::TopRightCorner); + ui->buttonBox->clearFocus(); + + setupTimelines(QmlTimeline()); + setupAnimations(m_currentTimeline); + + connect(ui->timelineTab, &QTabWidget::currentChanged, this, [this]() { + m_currentTimeline = getTimelineFromTabWidget(ui->timelineTab); + setupAnimations(m_currentTimeline); + }); + setupTableView(); +} + +void TimelineSettingsDialog::setCurrentTimeline(const QmlTimeline &timeline) +{ + m_currentTimeline = timeline; + setTabForTimeline(ui->timelineTab, m_currentTimeline); +} + +TimelineSettingsDialog::~TimelineSettingsDialog() +{ + delete ui; +} + +void TimelineSettingsDialog::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + /* ignore */ + break; + + default: + QDialog::keyPressEvent(event); + } +} + +void TimelineSettingsDialog::setupTableView() +{ + ui->tableView->setModel(m_timelineSettingsModel); + m_timelineSettingsModel->setupDelegates(ui->tableView); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + ui->tableView->verticalHeader()->hide(); + ui->tableView->setSelectionMode(QAbstractItemView::NoSelection); + m_timelineSettingsModel->resetModel(); +} + +void TimelineSettingsDialog::setupTimelines(const QmlTimeline &timeline) +{ + deleteAllTabs(ui->timelineTab); + + const QList<QmlTimeline> &timelines = m_timelineView->getTimelines(); + + if (timelines.isEmpty()) { + m_currentTimeline = QmlTimeline(); + auto timelineForm = new TimelineForm(this); + timelineForm->setDisabled(true); + ui->timelineTab->addTab(timelineForm, tr("No Timeline")); + return; + } + + for (const auto &timeline : timelines) + addTimelineTab(timeline); + + if (timeline.isValid()) { + m_currentTimeline = timeline; + } else { + m_currentTimeline = timelines.constFirst(); + } + + setTabForTimeline(ui->timelineTab, m_currentTimeline); + setupAnimations(m_currentTimeline); + m_timelineSettingsModel->resetModel(); +} + +void TimelineSettingsDialog::setupAnimations(const ModelNode &animation) +{ + deleteAllTabs(ui->animationTab); + + const QList<ModelNode> animations = m_timelineView->getAnimations(m_currentTimeline); + + for (const auto &animation : animations) + addAnimationTab(animation); + + if (animations.isEmpty()) { + auto animationForm = new TimelineAnimationForm(this); + animationForm->setDisabled(true); + ui->animationTab->addTab(animationForm, tr("No Animation")); + if (currentTimelineForm()) + currentTimelineForm()->setHasAnimation(false); + } else { + if (currentTimelineForm()) + currentTimelineForm()->setHasAnimation(true); + } + + if (animation.isValid()) + setTabForAnimation(ui->animationTab, animation); + m_timelineSettingsModel->resetModel(); +} + +void TimelineSettingsDialog::addTimelineTab(const QmlTimeline &node) +{ + auto timelineForm = new TimelineForm(this); + ui->timelineTab->addTab(timelineForm, node.modelNode().displayName()); + timelineForm->setTimeline(node); + setupAnimations(ModelNode()); +} + +void TimelineSettingsDialog::addAnimationTab(const ModelNode &node) +{ + auto timelineAnimationForm = new TimelineAnimationForm(this); + ui->animationTab->addTab(timelineAnimationForm, node.displayName()); + timelineAnimationForm->setup(node); +} + +TimelineForm *TimelineSettingsDialog::currentTimelineForm() const +{ + return qobject_cast<TimelineForm *>(ui->timelineTab->currentWidget()); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h new file mode 100644 index 0000000000..da4ddac4f6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QDialog> + +QT_FORWARD_DECLARE_CLASS(QSpinBox) + +namespace QmlDesigner { + +class TimelineForm; +class TimelineAnimationForm; +class TimelineView; +class TimelineSettingsModel; + +namespace Ui { +class TimelineSettingsDialog; +} + +class TimelineSettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TimelineSettingsDialog(QWidget *parent, TimelineView *view); + void setCurrentTimeline(const QmlTimeline &timeline); + ~TimelineSettingsDialog() override; + +protected: + void keyPressEvent(QKeyEvent *event) override; + +private: + void setupTableView(); + void setupTimelines(const QmlTimeline &node); + void setupAnimations(const ModelNode &node); + + void addTimelineTab(const QmlTimeline &node); + void addAnimationTab(const ModelNode &node); + + TimelineForm *currentTimelineForm() const; + + Ui::TimelineSettingsDialog *ui; + + TimelineView *m_timelineView; + QmlTimeline m_currentTimeline; + TimelineSettingsModel *m_timelineSettingsModel; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui new file mode 100644 index 0000000000..f3dfa6f094 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::TimelineSettingsDialog</class> + <widget class="QDialog" name="QmlDesigner::TimelineSettingsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>519</width> + <height>582</height> + </rect> + </property> + <property name="windowTitle"> + <string>Timeline Settings</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="timelineTab"> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="animationTab"> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + <item> + <widget class="QTableView" name="tableView"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QmlDesigner::TimelineSettingsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QmlDesigner::TimelineSettingsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp new file mode 100644 index 0000000000..f75d129983 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp @@ -0,0 +1,484 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinesettingsmodel.h" + +#include "timelineview.h" + +#include <modelnode.h> +#include <variantproperty.h> +#include <qmlitemnode.h> + +#include <utils/qtcassert.h> + +#include <QComboBox> +#include <QItemEditorFactory> +#include <QMessageBox> +#include <QStyledItemDelegate> +#include <QTimer> + +namespace QmlDesigner { + +class CustomDelegate : public QStyledItemDelegate +{ +public: + explicit CustomDelegate(QWidget *parent = nullptr); + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +CustomDelegate::CustomDelegate(QWidget *parent) + : QStyledItemDelegate(parent) +{} + +void CustomDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + opt.state &= ~QStyle::State_HasFocus; + QStyledItemDelegate::paint(painter, opt, index); +} + +class TimelineEditorDelegate : public CustomDelegate +{ +public: + TimelineEditorDelegate(QWidget *parent = nullptr); + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +TimelineEditorDelegate::TimelineEditorDelegate(QWidget *parent) + : CustomDelegate(parent) +{ + static QItemEditorFactory *factory = nullptr; + if (factory == nullptr) { + factory = new QItemEditorFactory; + QItemEditorCreatorBase *creator = new QItemEditorCreator<QComboBox>("currentText"); + factory->registerEditor(QVariant::String, creator); + } + + setItemEditorFactory(factory); +} + +QWidget *TimelineEditorDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QWidget *widget = QStyledItemDelegate::createEditor(parent, option, index); + + const auto timelineSettingsModel = qobject_cast<const TimelineSettingsModel *>(index.model()); + + auto comboBox = qobject_cast<QComboBox *>(widget); + + QTC_ASSERT(timelineSettingsModel, return widget); + QTC_ASSERT(timelineSettingsModel->timelineView(), return widget); + + QmlTimeline qmlTimeline = timelineSettingsModel->timelineForRow(index.row()); + + switch (index.column()) { + case TimelineSettingsModel::TimelineRow: { + QTC_ASSERT(comboBox, return widget); + comboBox->addItem(TimelineSettingsModel::tr("None")); + for (const auto &timeline : timelineSettingsModel->timelineView()->getTimelines()) { + if (!timeline.modelNode().id().isEmpty()) + comboBox->addItem(timeline.modelNode().id()); + } + } break; + case TimelineSettingsModel::AnimationRow: { + QTC_ASSERT(comboBox, return widget); + comboBox->addItem(TimelineSettingsModel::tr("None")); + for (const auto &animation : + timelineSettingsModel->timelineView()->getAnimations(qmlTimeline)) { + if (!animation.id().isEmpty()) + comboBox->addItem(animation.id()); + } + } break; + case TimelineSettingsModel::FixedFrameRow: { + } break; + + default: + qWarning() << "TimelineEditorDelegate::createEditor column" << index.column(); + } + + if (comboBox) { + connect(comboBox, QOverload<int>::of(&QComboBox::activated), this, [=]() { + auto delegate = const_cast<TimelineEditorDelegate *>(this); + emit delegate->commitData(comboBox); + }); + } + + return widget; +} + +TimelineSettingsModel::TimelineSettingsModel(QObject *parent, TimelineView *view) + : QStandardItemModel(parent) + , m_timelineView(view) +{ + connect(this, &QStandardItemModel::dataChanged, this, &TimelineSettingsModel::handleDataChanged); +} + +void TimelineSettingsModel::resetModel() +{ + beginResetModel(); + clear(); + setHorizontalHeaderLabels( + QStringList({tr("State"), tr("Timeline"), tr("Animation"), tr("Fixed Frame")})); + + if (timelineView()->isAttached() && timelineView()->rootModelNode().hasId()) { + addState(ModelNode()); + for (const QmlModelState &state : + QmlItemNode(timelineView()->rootModelNode()).states().allStates()) + addState(state); + } + + endResetModel(); +} + +TimelineView *TimelineSettingsModel::timelineView() const +{ + return m_timelineView; +} + +void TimelineSettingsModel::setupDelegates(QAbstractItemView *view) +{ + view->setItemDelegate(new TimelineEditorDelegate); +} + +static int propertyValueForState(const ModelNode &modelNode, + QmlModelState state, + const PropertyName &propertyName) +{ + if (!modelNode.isValid()) + return -1; + + if (state.isBaseState()) { + if (modelNode.hasVariantProperty(propertyName)) + return modelNode.variantProperty(propertyName).value().toInt(); + return -1; + } + + if (state.hasPropertyChanges(modelNode)) { + QmlPropertyChanges propertyChanges(state.propertyChanges(modelNode)); + if (propertyChanges.modelNode().hasVariantProperty(propertyName)) + return propertyChanges.modelNode().variantProperty(propertyName).value().toInt(); + } + + return -1; +} + +static QStandardItem *createStateItem(const ModelNode &state) +{ + if (state.isValid()) + return new QStandardItem(state.variantProperty("name").value().toString()); + else + return new QStandardItem(TimelineSettingsModel::tr("Base State")); +} + +void TimelineSettingsModel::addState(const ModelNode &state) +{ + QList<QStandardItem *> items; + + QmlTimeline timeline = timelineView()->timelineForState(state); + const QString timelineId = timeline.isValid() ? timeline.modelNode().id() : QString(""); + ModelNode animation = animationForTimelineAndState(timeline, state); + const QString animationId = animation.isValid() ? animation.id() : QString(""); + + QStandardItem *stateItem = createStateItem(state); + auto *timelinelItem = new QStandardItem(timelineId); + auto *animationItem = new QStandardItem(animationId); + auto *fixedFrameItem = new QStandardItem(""); + + stateItem->setData(state.internalId()); + stateItem->setFlags(Qt::ItemIsEnabled); + + int fixedValue = propertyValueForState(timeline, state, "currentFrame"); + fixedFrameItem->setData(fixedValue, Qt::EditRole); + + items.append(stateItem); + items.append(timelinelItem); + items.append(animationItem); + items.append(fixedFrameItem); + + appendRow(items); +} + +void TimelineSettingsModel::handleException() +{ + QMessageBox::warning(nullptr, tr("Error"), m_exceptionError); + resetModel(); +} + +ModelNode TimelineSettingsModel::animationForTimelineAndState(const QmlTimeline &timeline, + const ModelNode &state) +{ + QmlModelState modelState(state); + + if (!timeline.isValid()) + return ModelNode(); + + const QList<ModelNode> &animations = timelineView()->getAnimations(timeline); + + if (modelState.isBaseState()) { + for (const auto &animation : animations) { + if (animation.hasVariantProperty("running") + && animation.variantProperty("running").value().toBool()) + return animation; + } + return ModelNode(); + } + + for (const auto &animation : animations) { + if (modelState.affectsModelNode(animation)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(animation)); + + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("running") + && propertyChanges.modelNode().variantProperty("running").value().toBool()) + return animation; + } + } + return ModelNode(); +} + +void TimelineSettingsModel::updateTimeline(int row) +{ + + timelineView()->executeInTransaction("TimelineSettingsModel::updateTimeline", [this, row](){ + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + ModelNode animation(animationForRow(row)); + QmlTimeline oldTimeline = timelineView()->timelineForState(modelState); + + if (modelState.isBaseState()) { + if (oldTimeline.isValid()) + oldTimeline.modelNode().variantProperty("enabled").setValue(false); + if (timeline.isValid()) + timeline.modelNode().variantProperty("enabled").setValue(true); + } else { + if (oldTimeline.isValid() && modelState.affectsModelNode(oldTimeline)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(oldTimeline)); + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("enabled")) + propertyChanges.modelNode().removeProperty("enabled"); + } + + QmlTimeline baseTimeline(timelineForRow(0)); + + if (baseTimeline.isValid()) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(baseTimeline)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("enabled").setValue(false); + } + + if (timeline.isValid()) { /* If timeline is invalid 'none' was selected */ + QmlPropertyChanges propertyChanges(modelState.propertyChanges(timeline)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("enabled").setValue(true); + } + } + }); + + resetRow(row); +} + +void TimelineSettingsModel::updateAnimation(int row) +{ + timelineView()->executeInTransaction("TimelineSettingsModel::updateAnimation", [this, row](){ + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + ModelNode animation(animationForRow(row)); + QmlTimeline oldTimeline = timelineView()->timelineForState(modelState); + ModelNode oldAnimation = animationForTimelineAndState(oldTimeline, modelState); + + if (modelState.isBaseState()) { + if (oldAnimation.isValid()) + oldAnimation.variantProperty("running").setValue(false); + if (animation.isValid()) + animation.variantProperty("running").setValue(true); + if (timeline.isValid() && timeline.modelNode().hasProperty("currentFrame")) + timeline.modelNode().removeProperty("currentFrame"); + } else { + if (oldAnimation.isValid() && modelState.affectsModelNode(oldAnimation)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(oldAnimation)); + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("running")) + propertyChanges.modelNode().removeProperty("running"); + } + + ModelNode baseAnimation(animationForRow(0)); + + if (baseAnimation.isValid()) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(baseAnimation)); + if (propertyChanges.isValid()) { + propertyChanges.modelNode().variantProperty("running").setValue(false); + if (propertyChanges.modelNode().hasProperty("currentFrame")) + propertyChanges.modelNode().removeProperty("currentFrame"); + } + } + + if (animation.isValid()) { /* If animation is invalid 'none' was selected */ + QmlPropertyChanges propertyChanges(modelState.propertyChanges(animation)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("running").setValue(true); + } + } + }); + resetRow(row); +} + +void TimelineSettingsModel::updateFixedFrameRow(int row) +{ + timelineView()->executeInTransaction("TimelineSettingsModel::updateFixedFrameRow", [this, row](){ + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + + ModelNode animation = animationForTimelineAndState(timeline, modelState); + + int fixedFrame = fixedFrameForRow(row); + + if (modelState.isBaseState()) { + if (animation.isValid()) + animation.variantProperty("running").setValue(false); + if (timeline.isValid()) + timeline.modelNode().variantProperty("currentFrame").setValue(fixedFrame); + } else { + if (animation.isValid() && modelState.affectsModelNode(animation)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(animation)); + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("running")) + propertyChanges.modelNode().removeProperty("running"); + } + + QmlPropertyChanges propertyChanges(modelState.propertyChanges(timeline)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("currentFrame").setValue(fixedFrame); + } + + }); + + resetRow(row); +} + +void TimelineSettingsModel::resetRow(int row) +{ + m_lock = true; + QStandardItem *animationItem = item(row, AnimationRow); + QStandardItem *fixedFrameItem = item(row, FixedFrameRow); + + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + ModelNode animation = animationForTimelineAndState(timeline, modelState); + + if (animationItem) { + const QString animationId = animation.isValid() ? animation.id() : QString(); + animationItem->setText(animationId); + } + + if (fixedFrameItem) { + int fixedValue = propertyValueForState(timeline, modelState, "currentFrame"); + if (fixedFrameItem->data(Qt::EditRole).toInt() != fixedValue) + fixedFrameItem->setData(fixedValue, Qt::EditRole); + } + + m_lock = false; +} + +QmlTimeline TimelineSettingsModel::timelineForRow(int row) const +{ + QStandardItem *standardItem = item(row, TimelineRow); + + if (standardItem) + return QmlTimeline(timelineView()->modelNodeForId(standardItem->text())); + + return QmlTimeline(); +} + +ModelNode TimelineSettingsModel::animationForRow(int row) const +{ + QStandardItem *standardItem = item(row, AnimationRow); + + if (standardItem) + return timelineView()->modelNodeForId(standardItem->text()); + + return ModelNode(); +} + +ModelNode TimelineSettingsModel::stateForRow(int row) const +{ + QStandardItem *standardItem = item(row, StateRow); + + if (standardItem) + return timelineView()->modelNodeForInternalId(standardItem->data().toInt()); + + return ModelNode(); +} + +int TimelineSettingsModel::fixedFrameForRow(int row) const +{ + QStandardItem *standardItem = item(row, FixedFrameRow); + + if (standardItem) + return standardItem->data(Qt::EditRole).toInt(); + + return -1; +} + +void TimelineSettingsModel::handleDataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight) +{ + if (topLeft != bottomRight) { + qWarning() << "TimelineSettingsModel::handleDataChanged multi edit?"; + return; + } + + if (m_lock) + return; + + m_lock = true; + + int currentColumn = topLeft.column(); + int currentRow = topLeft.row(); + + switch (currentColumn) { + case StateRow: { + /* read only */ + } break; + case TimelineRow: { + updateTimeline(currentRow); + } break; + case AnimationRow: { + updateAnimation(currentRow); + } break; + case FixedFrameRow: { + updateFixedFrameRow(currentRow); + } break; + + default: + qWarning() << "ConnectionModel::handleDataChanged column" << currentColumn; + } + + m_lock = false; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h new file mode 100644 index 0000000000..afd4b58e1b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QAbstractItemView> +#include <QStandardItemModel> + +namespace QmlDesigner { + +class ModelNode; +class BindingProperty; +class SignalHandlerProperty; +class VariantProperty; + +class TimelineView; + +class TimelineSettingsModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum ColumnRoles { StateRow = 0, TimelineRow = 1, AnimationRow = 2, FixedFrameRow = 3 }; + TimelineSettingsModel(QObject *parent, TimelineView *view); + void resetModel(); + + TimelineView *timelineView() const; + + QStringList getSignalsForRow(int row) const; + ModelNode getTargetNodeForConnection(const ModelNode &connection) const; + + void addConnection(); + + void bindingPropertyChanged(const BindingProperty &bindingProperty); + void variantPropertyChanged(const VariantProperty &variantProperty); + + void setupDelegates(QAbstractItemView *view); + + QmlTimeline timelineForRow(int row) const; + ModelNode animationForRow(int row) const; + ModelNode stateForRow(int row) const; + int fixedFrameForRow(int row) const; + +protected: + void addState(const ModelNode &modelNode); + +private: + void handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void handleException(); + ModelNode animationForTimelineAndState(const QmlTimeline &timeline, const ModelNode &state); + + void updateTimeline(int row); + void updateAnimation(int row); + void updateFixedFrameRow(int row); + + void resetRow(int row); + +private: + TimelineView *m_timelineView; + bool m_lock = false; + QString m_exceptionError; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp new file mode 100644 index 0000000000..adc49a97b1 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp @@ -0,0 +1,460 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinetoolbar.h" + +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" + +#include "timelinewidget.h" + +#include <designeractionmanager.h> +#include <nodelistproperty.h> +#include <theme.h> +#include <variantproperty.h> +#include <qmltimeline.h> +#include <qmltimelinekeyframegroup.h> + +#include <coreplugin/actionmanager/actionmanager.h> +#include <coreplugin/actionmanager/command.h> + +#include <utils/algorithm.h> + +#include <QApplication> +#include <QLabel> +#include <QLineEdit> +#include <QResizeEvent> +#include <QSlider> +#include <QIntValidator> + +namespace QmlDesigner { + +bool isSpacer(QObject *object) +{ + return object->property("spacer_widget").toBool(); +} + +QWidget *createSpacer() +{ + QWidget *spacer = new QWidget(); + spacer->setProperty("spacer_widget", true); + return spacer; +} + +int controlWidth(QToolBar *bar, QObject *control) +{ + QWidget *widget = nullptr; + + if (auto *action = qobject_cast<QAction *>(control)) + widget = bar->widgetForAction(action); + + if (widget == nullptr) + widget = qobject_cast<QWidget *>(control); + + if (widget) + return widget->width(); + + return 0; +} + +QAction *createAction(const Core::Id &id, + const QIcon &icon, + const QString &name, + const QKeySequence &shortcut) +{ + QString text = QString("%1 (%2)").arg(name).arg(shortcut.toString()); + + Core::Context context(TimelineConstants::C_QMLTIMELINE); + + auto *action = new QAction(icon, text); + auto *command = Core::ActionManager::registerAction(action, id, context); + command->setDefaultKeySequence(shortcut); + + return action; +} + +TimelineToolBar::TimelineToolBar(QWidget *parent) + : QToolBar(parent) + , m_grp() +{ + setContentsMargins(0, 0, 0, 0); + createLeftControls(); + createCenterControls(); + createRightControls(); +} + +void TimelineToolBar::reset() +{ + if (recording()) + m_recording->setChecked(false); +} + +bool TimelineToolBar::recording() const +{ + if (m_recording) + return m_recording->isChecked(); + + return false; +} + +int TimelineToolBar::scaleFactor() const +{ + if (m_scale) + return m_scale->value(); + return 0; +} + +QString TimelineToolBar::currentTimelineId() const +{ + return m_timelineLabel->text(); +} + +void TimelineToolBar::setCurrentState(const QString &name) +{ + if (name.isEmpty()) + m_stateLabel->setText(tr("Base State")); + else + m_stateLabel->setText(name); +} + +void TimelineToolBar::setCurrentTimeline(const QmlTimeline &timeline) +{ + if (timeline.isValid()) { + setStartFrame(timeline.startKeyframe()); + setEndFrame(timeline.endKeyframe()); + m_timelineLabel->setText(timeline.modelNode().id()); + } else { + m_timelineLabel->setText(""); + } +} + +void TimelineToolBar::setStartFrame(qreal frame) +{ + auto text = QString::number(frame, 'f', 0); + m_firstFrame->setText(text); + setupCurrentFrameValidator(); +} + +void TimelineToolBar::setCurrentFrame(qreal frame) +{ + auto text = QString::number(frame, 'f', 0); + m_currentFrame->setText(text); +} + +void TimelineToolBar::setEndFrame(qreal frame) +{ + auto text = QString::number(frame, 'f', 0); + m_lastFrame->setText(text); + setupCurrentFrameValidator(); +} + +void TimelineToolBar::setScaleFactor(int factor) +{ + const QSignalBlocker blocker(m_scale); + m_scale->setValue(factor); +} + +void TimelineToolBar::setActionEnabled(const QString &name, bool enabled) +{ + for (auto *action : actions()) + if (action->objectName() == name) + action->setEnabled(enabled); +} + +void TimelineToolBar::removeTimeline(const QmlTimeline &timeline) +{ + if (timeline.modelNode().id() == m_timelineLabel->text()) + setCurrentTimeline(QmlTimeline()); +} + +void TimelineToolBar::createLeftControls() +{ + auto addActionToGroup = [&](QAction *action) { + addAction(action); + m_grp << action; + }; + + auto addWidgetToGroup = [&](QWidget *widget) { + addWidget(widget); + m_grp << widget; + }; + + auto addSpacingToGroup = [&](int width) { + auto *widget = new QWidget; + widget->setFixedWidth(width); + addWidget(widget); + m_grp << widget; + }; + + addSpacingToGroup(5); + + auto *settingsAction = createAction(TimelineConstants::C_SETTINGS, + TimelineIcons::ANIMATION.icon(), + tr("Timeline Settings"), + QKeySequence(Qt::Key_S)); + + connect(settingsAction, &QAction::triggered, this, &TimelineToolBar::settingDialogClicked); + + addActionToGroup(settingsAction); + + addWidgetToGroup(createSpacer()); + + m_timelineLabel = new QLabel(this); + m_timelineLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); + addWidgetToGroup(m_timelineLabel); +} + +static QLineEdit *createToolBarLineEdit(QWidget *parent) +{ + auto lineEdit = new QLineEdit(parent); + lineEdit->setStyleSheet("* { background-color: rgba(0, 0, 0, 0); }"); + lineEdit->setFixedWidth(48); + lineEdit->setAlignment(Qt::AlignCenter); + + QPalette pal = parent->palette(); + pal.setColor(QPalette::Text, Theme::instance()->color(Utils::Theme::PanelTextColorLight)); + lineEdit->setPalette(pal); + QValidator *validator = new QIntValidator(-100000, 100000, lineEdit); + lineEdit->setValidator(validator); + + return lineEdit; +} + +void TimelineToolBar::createCenterControls() +{ + addSpacing(5); + + auto *toStart = createAction(TimelineConstants::C_TO_START, + TimelineIcons::TO_FIRST_FRAME.icon(), + tr("To Start"), + QKeySequence(Qt::Key_Home)); + + connect(toStart, &QAction::triggered, this, &TimelineToolBar::toFirstFrameTriggered); + addAction(toStart); + + addSpacing(2); + + auto *previous = createAction(TimelineConstants::C_PREVIOUS, + TimelineIcons::BACK_ONE_FRAME.icon(), + tr("Previous"), + QKeySequence(Qt::Key_Comma)); + + connect(previous, &QAction::triggered, this, &TimelineToolBar::previousFrameTriggered); + addAction(previous); + + addSpacing(2); + + auto *play = createAction(TimelineConstants::C_PLAY, + TimelineIcons::START_PLAYBACK.icon(), + tr("Play"), + QKeySequence(Qt::Key_Space)); + + connect(play, &QAction::triggered, this, &TimelineToolBar::playTriggered); + addAction(play); + + addSpacing(2); + + auto *next = createAction(TimelineConstants::C_NEXT, + TimelineIcons::FORWARD_ONE_FRAME.icon(), + tr("Next"), + QKeySequence(Qt::Key_Period)); + + connect(next, &QAction::triggered, this, &TimelineToolBar::nextFrameTriggered); + addAction(next); + + addSpacing(2); + + auto *toEnd = createAction(TimelineConstants::C_TO_END, + TimelineIcons::TO_LAST_FRAME.icon(), + tr("To End"), + QKeySequence(Qt::Key_End)); + + connect(toEnd, &QAction::triggered, this, &TimelineToolBar::toLastFrameTriggered); + addAction(toEnd); + +#if 0 + auto *loop = new QAction(TimelineIcons::LOOP_PLAYBACK.icon(), tr("Loop"), this); + addAction(loop); +#endif + + addSpacing(5); + + addSeparator(); + + m_currentFrame = createToolBarLineEdit(this); + addWidget(m_currentFrame); + + auto emitCurrentChanged = [this]() { emit currentFrameChanged(m_currentFrame->text().toInt()); }; + connect(m_currentFrame, &QLineEdit::editingFinished, emitCurrentChanged); + + addSeparator(); + + addSpacing(10); + + QIcon autoKeyIcon = TimelineUtils::mergeIcons(TimelineIcons::GLOBAL_RECORD_KEYFRAMES, + TimelineIcons::GLOBAL_RECORD_KEYFRAMES_OFF); + + m_recording = createAction(TimelineConstants::C_AUTO_KEYFRAME, + autoKeyIcon, + tr("Auto Key"), + QKeySequence(Qt::Key_K)); + + m_recording->setCheckable(true); + connect(m_recording, &QAction::toggled, [&](bool value) { emit recordToggled(value); }); + + addAction(m_recording); + + addSpacing(10); + + addSeparator(); + + addSpacing(10); + + auto *curvePicker = createAction(TimelineConstants::C_CURVE_PICKER, + TimelineIcons::CURVE_EDITOR.icon(), + tr("Curve Picker"), + QKeySequence(Qt::Key_C)); + + curvePicker->setObjectName("Curve Picker"); + connect(curvePicker, &QAction::triggered, this, &TimelineToolBar::openEasingCurveEditor); + addAction(curvePicker); + + addSpacing(10); + +#if 0 + addSeparator(); + + addSpacing(10); + + auto *curveEditor = new QAction(TimelineIcons::CURVE_PICKER.icon(), tr("Curve Editor")); + addAction(curveEditor); +#endif +} + +void TimelineToolBar::createRightControls() +{ + auto *spacer = createSpacer(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + addWidget(spacer); + + addSeparator(); + + m_firstFrame = createToolBarLineEdit(this); + addWidget(m_firstFrame); + + auto emitStartChanged = [this]() { emit startFrameChanged(m_firstFrame->text().toInt()); }; + connect(m_firstFrame, &QLineEdit::editingFinished, emitStartChanged); + + addSeparator(); + + addSpacing(10); + + auto *zoomOut = createAction(TimelineConstants::C_ZOOM_OUT, + TimelineIcons::ZOOM_SMALL.icon(), + tr("Zoom Out"), + QKeySequence(QKeySequence::ZoomOut)); + + connect(zoomOut, &QAction::triggered, [this]() { + m_scale->setValue(m_scale->value() - m_scale->pageStep()); + }); + addAction(zoomOut); + + addSpacing(10); + + m_scale = new QSlider(this); + m_scale->setOrientation(Qt::Horizontal); + m_scale->setMaximumWidth(200); + m_scale->setMinimumWidth(100); + m_scale->setMinimum(0); + m_scale->setMaximum(100); + m_scale->setValue(0); + + connect(m_scale, &QSlider::valueChanged, this, &TimelineToolBar::scaleFactorChanged); + addWidget(m_scale); + + addSpacing(10); + + auto *zoomIn = createAction(TimelineConstants::C_ZOOM_IN, + TimelineIcons::ZOOM_BIG.icon(), + tr("Zoom In"), + QKeySequence(QKeySequence::ZoomIn)); + + connect(zoomIn, &QAction::triggered, [this]() { + m_scale->setValue(m_scale->value() + m_scale->pageStep()); + }); + addAction(zoomIn); + + addSpacing(10); + + addSeparator(); + + m_lastFrame = createToolBarLineEdit(this); + addWidget(m_lastFrame); + + auto emitEndChanged = [this]() { emit endFrameChanged(m_lastFrame->text().toInt()); }; + connect(m_lastFrame, &QLineEdit::editingFinished, emitEndChanged); + + addSeparator(); + + m_stateLabel = new QLabel(this); + m_stateLabel->setFixedWidth(80); + m_stateLabel->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + addWidget(m_stateLabel); +} + +void TimelineToolBar::addSpacing(int width) +{ + auto *widget = new QWidget; + widget->setFixedWidth(width); + addWidget(widget); +} + +void TimelineToolBar::setupCurrentFrameValidator() +{ + auto validator = static_cast<const QIntValidator*>(m_currentFrame->validator()); + const_cast<QIntValidator*>(validator)->setRange(m_firstFrame->text().toInt(), m_lastFrame->text().toInt()); +} + +void TimelineToolBar::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event) + + int width = 0; + QWidget *spacer = nullptr; + for (auto *object : m_grp) { + if (isSpacer(object)) + spacer = qobject_cast<QWidget *>(object); + else + width += controlWidth(this, object); + } + + if (spacer) { + int spacerWidth = TimelineConstants::sectionWidth - width - 12; + spacer->setFixedWidth(spacerWidth > 0 ? spacerWidth : 0); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h new file mode 100644 index 0000000000..43d42b83f9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QToolBar> + +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QLineEdit) +QT_FORWARD_DECLARE_CLASS(QObject) +QT_FORWARD_DECLARE_CLASS(QResizeEvent) +QT_FORWARD_DECLARE_CLASS(QSlider) +QT_FORWARD_DECLARE_CLASS(QWidget) + +namespace QmlDesigner { + +class TimelineWidget; + +class QmlTimeline; + +class TimelineToolBar : public QToolBar +{ + Q_OBJECT + +signals: + void settingDialogClicked(); + //void addTimelineClicked(); + + void openEasingCurveEditor(); + + void playTriggered(); + void previousFrameTriggered(); + void nextFrameTriggered(); + void toFirstFrameTriggered(); + void toLastFrameTriggered(); + + void recordToggled(bool val); + void loopPlaybackToggled(bool val); + + void scaleFactorChanged(int value); + void startFrameChanged(int value); + void currentFrameChanged(int value); + void endFrameChanged(int value); + +public: + explicit TimelineToolBar(QWidget *parent = nullptr); + + void reset(); + + bool recording() const; + int scaleFactor() const; + QString currentTimelineId() const; + + void setCurrentState(const QString &name); + void setCurrentTimeline(const QmlTimeline &timeline); + void setStartFrame(qreal frame); + void setCurrentFrame(qreal frame); + void setEndFrame(qreal frame); + void setScaleFactor(int factor); + + void setActionEnabled(const QString &name, bool enabled); + void removeTimeline(const QmlTimeline &timeline); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void createLeftControls(); + void createCenterControls(); + void createRightControls(); + void addSpacing(int width); + void setupCurrentFrameValidator(); + + QList<QObject *> m_grp; + + QLabel *m_timelineLabel = nullptr; + QLabel *m_stateLabel = nullptr; + QSlider *m_scale = nullptr; + QLineEdit *m_firstFrame = nullptr; + QLineEdit *m_currentFrame = nullptr; + QLineEdit *m_lastFrame = nullptr; + + QAction *m_recording = nullptr; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp new file mode 100644 index 0000000000..e6a9454186 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp @@ -0,0 +1,164 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinetoolbutton.h" + +#include "timelineconstants.h" + +#include <QGraphicsSceneMouseEvent> +#include <QPainter> + +#include <utils/stylehelper.h> +#include <utils/theme/theme.h> + +#include <QAction> +#include <QDebug> + +namespace QmlDesigner { + +TimelineToolButton::TimelineToolButton(QAction *action, QGraphicsItem *parent) + : QGraphicsWidget(parent) + , m_action(action) +{ + resize(TimelineConstants::toolButtonSize, TimelineConstants::toolButtonSize); + setPreferredSize(size()); + setAcceptHoverEvents(true); + connect(action, &QAction::changed, this, [this]() { + setVisible(m_action->isVisible()); + update(); + }); + + connect(this, &TimelineToolButton::clicked, action, &QAction::trigger); + + setEnabled(m_action->isEnabled()); + setVisible(m_action->isVisible()); + setCursor(Qt::ArrowCursor); +} + +void TimelineToolButton::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + painter->save(); + + if (m_state == Normal) + setOpacity(0.8); + else if (m_state == Pressed) + setOpacity(0.3); + else + setOpacity(1.0); + + if (!isEnabled()) + setOpacity(0.5); + + if (isCheckable()) { + if (isChecked() || isDisabled()) + m_action->icon().paint(painter, + rect().toRect(), + Qt::AlignCenter, + QIcon::Normal, + QIcon::On); + else + m_action->icon().paint(painter, + rect().toRect(), + Qt::AlignCenter, + QIcon::Normal, + QIcon::Off); + } else + m_action->icon().paint(painter, rect().toRect()); + + painter->restore(); +} + +QRectF TimelineToolButton::boundingRect() const +{ + return QRectF(0, 0, TimelineConstants::toolButtonSize, TimelineConstants::toolButtonSize); +} + +bool TimelineToolButton::isCheckable() const +{ + return m_action->isCheckable(); +} + +bool TimelineToolButton::isChecked() const +{ + return m_action->isChecked(); +} + +bool TimelineToolButton::isDisabled() const +{ + return !m_action->isEnabled(); +} + +void TimelineToolButton::setChecked(bool b) +{ + m_action->setChecked(b); + update(); +} + +void TimelineToolButton::setDisabled(bool b) +{ + m_action->setDisabled(b); +} + +void TimelineToolButton::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + m_state = Hovered; + + QGraphicsObject::hoverEnterEvent(event); + event->accept(); + update(); +} + +void TimelineToolButton::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + m_state = Normal; + + QGraphicsWidget::hoverLeaveEvent(event); + event->accept(); + update(); +} + +void TimelineToolButton::hoverMoveEvent(QGraphicsSceneHoverEvent *event) +{ + m_state = Hovered; + QGraphicsWidget::hoverMoveEvent(event); + update(); +} + +void TimelineToolButton::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + m_state = Pressed; + event->accept(); + update(); +} + +void TimelineToolButton::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + m_state = Normal; + + event->accept(); + emit clicked(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h new file mode 100644 index 0000000000..cdf024c0b2 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QGraphicsWidget> +#include <QIcon> + +QT_BEGIN_NAMESPACE +class QAction; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class TimelineToolButton : public QGraphicsWidget +{ + Q_OBJECT + +public: + explicit TimelineToolButton(QAction *action, QGraphicsItem *parent = nullptr); + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget = nullptr) override; + QRectF boundingRect() const override; + + bool isCheckable() const; + bool isChecked() const; + bool isDisabled() const; + void setChecked(bool b); + void setDisabled(bool b); + +signals: + void clicked(); + +protected: + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + +private: + enum State { Pressed, Hovered, Normal }; + + State m_state = Normal; + QAction *m_action; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp new file mode 100644 index 0000000000..51c5a7d088 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinetooldelegate.h" + +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelinemovableabstractitem.h" +#include "timelinemovetool.h" +#include "timelineselectiontool.h" + +#include <QGraphicsSceneMouseEvent> + +#include "timelinepropertyitem.h" +#include <QDebug> + +namespace QmlDesigner { + +TimelineToolDelegate::TimelineToolDelegate(TimelineGraphicsScene *scene) + : m_scene(scene) + , m_start() + , m_moveTool(new TimelineMoveTool(scene, this)) + , m_selectTool(new TimelineSelectionTool(scene, this)) +{} + +QPointF TimelineToolDelegate::startPoint() const +{ + return m_start; +} + +TimelineMovableAbstractItem *TimelineToolDelegate::item() const +{ + return m_item; +} + +void TimelineToolDelegate::mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (event->buttons() == Qt::LeftButton && hitCanvas(event)) { + m_start = event->scenePos(); + + if (item) { + setItem(item, event->modifiers()); + m_currentTool = m_moveTool.get(); + } else + m_currentTool = m_selectTool.get(); + } else + m_currentTool = nullptr; + + if (m_currentTool) + m_currentTool->mousePressEvent(item, event); +} + +void TimelineToolDelegate::mouseMoveEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (m_currentTool) + m_currentTool->mouseMoveEvent(item, event); +} + +void TimelineToolDelegate::mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (m_currentTool) + m_currentTool->mouseReleaseEvent(item, event); + + reset(); +} + +void TimelineToolDelegate::mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (m_currentTool) + m_currentTool->mouseDoubleClickEvent(item, event); + + reset(); +} + +void TimelineToolDelegate::clearSelection() +{ + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(m_item)) + keyframe->setHighlighted(false); + + m_item = nullptr; +} + +void TimelineToolDelegate::setItem(TimelineMovableAbstractItem *item, + const Qt::KeyboardModifiers &modifiers) +{ + if (item) { + setItem(nullptr); + + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (modifiers.testFlag(Qt::ControlModifier)) { + if (m_scene->isKeyframeSelected(keyframe)) + m_scene->selectKeyframes(SelectionMode::Remove, {keyframe}); + else + m_scene->selectKeyframes(SelectionMode::Add, {keyframe}); + } else { + if (!m_scene->isKeyframeSelected(keyframe)) + m_scene->selectKeyframes(SelectionMode::New, {keyframe}); + } + } + + } else if (m_item) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(m_item)) + if (!m_scene->isKeyframeSelected(keyframe)) + keyframe->setHighlighted(false); + } + + m_item = item; +} + +bool TimelineToolDelegate::hitCanvas(QGraphicsSceneMouseEvent *event) +{ + return event->scenePos().x() > TimelineConstants::sectionWidth; +} + +void TimelineToolDelegate::reset() +{ + setItem(nullptr); + + m_currentTool = nullptr; + + m_start = QPointF(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h new file mode 100644 index 0000000000..f945c1a61b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinemovetool.h" +#include "timelineselectiontool.h" + +#include <memory> + +namespace QmlDesigner +{ + +class TimelineGraphicsScene; + +class TimelineToolDelegate +{ +public: + TimelineToolDelegate(TimelineGraphicsScene* scene); + + QPointF startPoint() const; + + TimelineMovableAbstractItem* item() const; + +public: + void mousePressEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void mouseReleaseEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void clearSelection(); + +private: + bool hitCanvas(QGraphicsSceneMouseEvent *event); + + void reset(); + + void setItem(TimelineMovableAbstractItem *item, const Qt::KeyboardModifiers& modifiers = Qt::NoModifier); + +private: + static const int dragDistance = 20; + + TimelineGraphicsScene* m_scene; + + QPointF m_start; + + TimelineMovableAbstractItem *m_item = nullptr; + + std::unique_ptr< TimelineMoveTool > m_moveTool; + + std::unique_ptr< TimelineSelectionTool > m_selectTool; + + TimelineAbstractTool *m_currentTool = nullptr; +}; + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp new file mode 100644 index 0000000000..6873fc0b65 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineutils.h" + +#include <QEvent> + +namespace QmlDesigner { + +namespace TimelineUtils { + +DisableContextMenu::DisableContextMenu(QObject *parent) + : QObject(parent) +{} + +bool DisableContextMenu::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::ContextMenu) + return true; + + return QObject::eventFilter(watched, event); +} + +} // End namespace TimelineUtils. + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineutils.h b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.h new file mode 100644 index 0000000000..0758733769 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <istream> +#include <utils/icon.h> +#include <vector> +#include <QDataStream> +#include <QObject> + +QT_FORWARD_DECLARE_CLASS(QEvent) + +namespace QmlDesigner { + +namespace TimelineUtils { + +enum class Side { Top, Right, Bottom, Left }; + +template<typename T> +inline T clamp(const T &value, const T &lo, const T &hi) +{ + return value < lo ? lo : value > hi ? hi : value; +} + +template<typename T> +inline T lerp(const T &blend, const T &lhs, const T &rhs) +{ + static_assert(std::is_floating_point<T>::value, + "TimelineUtils::lerp: For floating point types only!"); + return blend * lhs + (1.0 - blend) * rhs; +} + +template<typename T> +inline T reverseLerp(const T &val, const T &lhs, const T &rhs) +{ + static_assert(std::is_floating_point<T>::value, + "TimelineUtils::reverseLerp: For floating point types only!"); + return (val - rhs) / (lhs - rhs); +} + +inline QIcon mergeIcons(const Utils::Icon &on, const Utils::Icon &off) +{ + QIcon out; + out.addPixmap(on.pixmap(), QIcon::Normal, QIcon::On); + out.addPixmap(off.pixmap(), QIcon::Normal, QIcon::Off); + return out; +} + +class DisableContextMenu : public QObject +{ + Q_OBJECT + +public: + explicit DisableContextMenu(QObject *parent = nullptr); + + bool eventFilter(QObject *watched, QEvent *event) override; +}; + +} // End namespace TimelineUtils. + +template<typename T> +inline std::istream &operator>>(std::istream &stream, std::vector<T> &vec) +{ + quint64 s; + stream >> s; + + vec.clear(); + vec.reserve(s); + + T val; + for (quint64 i = 0; i < s; ++i) { + stream >> val; + vec.push_back(val); + } + return stream; +} + +template<typename T> +inline QDataStream &operator<<(QDataStream &stream, const std::vector<T> &vec) +{ + stream << static_cast<quint64>(vec.size()); + for (const auto &elem : vec) + stream << elem; + + return stream; +} + +template<typename T> +inline QDataStream &operator>>(QDataStream &stream, std::vector<T> &vec) +{ + quint64 s; + stream >> s; + + vec.clear(); + vec.reserve(s); + + T val; + for (quint64 i = 0; i < s; ++i) { + stream >> val; + vec.push_back(val); + } + return stream; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp new file mode 100644 index 0000000000..f39a17db2f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp @@ -0,0 +1,610 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineview.h" + +#include "easingcurve.h" +#include "timelineactions.h" +#include "timelineconstants.h" +#include "timelinecontext.h" +#include "timelinewidget.h" + +#include "timelinegraphicsscene.h" +#include "timelinesettingsdialog.h" +#include "timelinetoolbar.h" + +#include <bindingproperty.h> +#include <exception.h> +#include <modelnodecontextmenu_helper.h> +#include <nodeabstractproperty.h> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <variantproperty.h> +#include <qmldesignericons.h> +#include <qmldesignerplugin.h> +#include <qmlitemnode.h> +#include <qmlobjectnode.h> +#include <qmlstate.h> +#include <qmltimeline.h> +#include <qmltimelinekeyframegroup.h> +#include <viewmanager.h> + +#include <coreplugin/icore.h> + +#include <utils/qtcassert.h> + +#include <designmodecontext.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +#include <QTimer> + +namespace QmlDesigner { + +TimelineView::TimelineView(QObject *parent) + : AbstractView(parent) +{ + EasingCurve::registerStreamOperators(); +} + +TimelineView::~TimelineView() = default; + +void TimelineView::modelAttached(Model *model) +{ + AbstractView::modelAttached(model); + if (m_timelineWidget) + m_timelineWidget->init(); +} + +void TimelineView::modelAboutToBeDetached(Model *model) +{ + m_timelineWidget->reset(); + setTimelineRecording(false); + AbstractView::modelAboutToBeDetached(model); +} + +void TimelineView::nodeCreated(const ModelNode & /*createdNode*/) {} + +void TimelineView::nodeAboutToBeRemoved(const ModelNode &removedNode) +{ + if (removedNode.isValid()) { + if (QmlTimeline::isValidQmlTimeline(removedNode)) { + auto *toolBar = widget()->toolBar(); + + QString lastId = toolBar->currentTimelineId(); + toolBar->removeTimeline(QmlTimeline(removedNode)); + QString currentId = toolBar->currentTimelineId(); + + removedNode.setAuxiliaryData("removed@Internal", true); + + if (currentId.isEmpty()) + m_timelineWidget->graphicsScene()->clearTimeline(); + if (lastId != currentId) + m_timelineWidget->setTimelineId(currentId); + } else if (removedNode.parentProperty().isValid() + && QmlTimeline::isValidQmlTimeline(removedNode.parentProperty().parentModelNode())) { + if (removedNode.hasBindingProperty("target")) { + const ModelNode target = removedNode.bindingProperty("target").resolveToModelNode(); + if (target.isValid()) { + QmlTimeline timeline(removedNode.parentProperty().parentModelNode()); + if (timeline.hasKeyframeGroupForTarget(target)) + QTimer::singleShot(0, [this, target, timeline]() { + if (timeline.hasKeyframeGroupForTarget(target)) + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(target); + else + m_timelineWidget->graphicsScene()->invalidateScene(); + }); + } + } + } + } +} + +void TimelineView::nodeRemoved(const ModelNode & /*removedNode*/, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags /*propertyChange*/) +{ + if (parentProperty.isValid() + && QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup( + parentProperty.parentModelNode())) { + QmlTimelineKeyframeGroup frames(parentProperty.parentModelNode()); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } +} + +void TimelineView::nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty & /*oldPropertyParent*/, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (newPropertyParent.isValid() + && QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup( + newPropertyParent.parentModelNode())) { + QmlTimelineKeyframeGroup frames(newPropertyParent.parentModelNode()); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } else if (QmlTimelineKeyframeGroup::checkKeyframesType( + node)) { /* During copy and paste type info might be incomplete */ + QmlTimelineKeyframeGroup frames(node); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } +} + +void TimelineView::instancePropertyChanged(const QList<QPair<ModelNode, PropertyName>> &propertyList) +{ + QmlTimeline timeline = currentTimeline(); + bool updated = false; + for (const auto &pair : propertyList) { + if (pair.second == "startFrame" || pair.second == "endFrame") { + if (QmlTimeline::isValidQmlTimeline(pair.first)) { + m_timelineWidget->invalidateTimelineDuration(pair.first); + } + } else if (pair.second == "currentFrame") { + if (QmlTimeline::isValidQmlTimeline(pair.first)) { + m_timelineWidget->invalidateTimelinePosition(pair.first); + } + } else if (!updated && timeline.hasTimeline(pair.first, pair.second)) { + m_timelineWidget->graphicsScene()->invalidateCurrentValues(); + updated = true; + } + } +} + +void TimelineView::variantPropertiesChanged(const QList<VariantProperty> &propertyList, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + for (const auto &property : propertyList) { + if (property.name() == "frame" + && property.parentModelNode().type() == "QtQuick.Timeline.Keyframe" + && property.parentModelNode().isValid() + && property.parentModelNode().hasParentProperty()) { + const ModelNode framesNode + = property.parentModelNode().parentProperty().parentModelNode(); + if (QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup(framesNode)) { + QmlTimelineKeyframeGroup frames(framesNode); + m_timelineWidget->graphicsScene()->invalidateKeyframesForTarget(frames.target()); + } + } + } +} + +void TimelineView::selectedNodesChanged(const QList<ModelNode> & /*selectedNodeList*/, + const QList<ModelNode> & /*lastSelectedNodeList*/) +{ + if (m_timelineWidget) + m_timelineWidget->graphicsScene()->update(); +} + +void TimelineView::propertiesAboutToBeRemoved(const QList<AbstractProperty> &propertyList) +{ + for (const auto &property : propertyList) { + if (property.isNodeListProperty()) { + for (const auto &node : property.toNodeListProperty().toModelNodeList()) { + nodeAboutToBeRemoved(node); + } + } + } +} + +void TimelineView::propertiesRemoved(const QList<AbstractProperty> &propertyList) +{ + for (const auto &property : propertyList) { + if (property.name() == "keyframes" && property.parentModelNode().isValid()) { + if (QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup( + property.parentModelNode())) { + QmlTimelineKeyframeGroup frames(property.parentModelNode()); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } else if (QmlTimeline::isValidQmlTimeline(property.parentModelNode())) { + m_timelineWidget->graphicsScene()->invalidateScene(); + } + } + } +} + +bool TimelineView::hasWidget() const +{ + return true; +} + +void TimelineView::nodeIdChanged(const ModelNode &node, const QString &, const QString &) +{ + if (QmlTimeline::isValidQmlTimeline(node)) + m_timelineWidget->init(); +} + +void TimelineView::currentStateChanged(const ModelNode &) +{ + if (m_timelineWidget) + m_timelineWidget->init(); +} + +TimelineWidget *TimelineView::widget() const +{ + return m_timelineWidget; +} + +const QmlTimeline TimelineView::addNewTimeline() +{ + const TypeName timelineType = "QtQuick.Timeline.Timeline"; + + QTC_ASSERT(isAttached(), return QmlTimeline()); + + try { + ensureQtQuickTimelineImport(); + } catch (const Exception &e) { + e.showException(); + } + + NodeMetaInfo metaInfo = model()->metaInfo(timelineType); + + QTC_ASSERT(metaInfo.isValid(), return QmlTimeline()); + + ModelNode timelineNode; + + executeInTransaction("TimelineView::addNewTimeline", [=, &timelineNode](){ + bool hasTimelines = getTimelines().isEmpty(); + + timelineNode = createModelNode(timelineType, + metaInfo.majorVersion(), + metaInfo.minorVersion()); + timelineNode.validId(); + + timelineNode.variantProperty("startFrame").setValue(0); + timelineNode.variantProperty("endFrame").setValue(1000); + timelineNode.variantProperty("enabled").setValue(hasTimelines); + + rootModelNode().defaultNodeListProperty().reparentHere(timelineNode); + }); + + return QmlTimeline(timelineNode); +} + +ModelNode TimelineView::addAnimation(QmlTimeline timeline) +{ + const TypeName animationType = "QtQuick.Timeline.TimelineAnimation"; + + QTC_ASSERT(timeline.isValid(), return ModelNode()); + + QTC_ASSERT(isAttached(), return ModelNode()); + + NodeMetaInfo metaInfo = model()->metaInfo(animationType); + + QTC_ASSERT(metaInfo.isValid(), return ModelNode()); + + ModelNode animationNode; + + executeInTransaction("TimelineView::addAnimation", [=, &animationNode](){ + animationNode = createModelNode(animationType, + metaInfo.majorVersion(), + metaInfo.minorVersion()); + animationNode.variantProperty("duration").setValue(timeline.duration()); + animationNode.validId(); + + animationNode.variantProperty("from").setValue(timeline.startKeyframe()); + animationNode.variantProperty("to").setValue(timeline.endKeyframe()); + + animationNode.variantProperty("loops").setValue(1); + + animationNode.variantProperty("running").setValue(getAnimations(timeline).isEmpty()); + + timeline.modelNode().nodeListProperty("animations").reparentHere(animationNode); + + if (timeline.modelNode().hasProperty("currentFrame")) + timeline.modelNode().removeProperty("currentFrame"); + }); + + return animationNode; +} + +void TimelineView::addNewTimelineDialog() +{ + auto timeline = addNewTimeline(); + addAnimation(timeline); + openSettingsDialog(); +} + +void TimelineView::openSettingsDialog() +{ + auto dialog = new TimelineSettingsDialog(Core::ICore::dialogParent(), this); + + auto timeline = m_timelineWidget->graphicsScene()->currentTimeline(); + if (timeline.isValid()) + dialog->setCurrentTimeline(timeline); + + QObject::connect(dialog, &TimelineSettingsDialog::rejected, [this, dialog]() { + m_timelineWidget->init(); + dialog->deleteLater(); + }); + + QObject::connect(dialog, &TimelineSettingsDialog::accepted, [this, dialog]() { + m_timelineWidget->init(); + dialog->deleteLater(); + }); + + dialog->show(); +} + +void TimelineView::setTimelineRecording(bool value) +{ + ModelNode node = widget()->graphicsScene()->currentTimeline(); + + QTC_ASSERT(node.isValid(), return ); + + if (value) { + activateTimelineRecording(node); + } else { + deactivateTimelineRecording(); + activateTimeline(node); + } +} + +void TimelineView::customNotification(const AbstractView * /*view*/, + const QString &identifier, + const QList<ModelNode> &nodeList, + const QList<QVariant> &data) +{ + if (identifier == QStringLiteral("reset QmlPuppet")) { + QmlTimeline timeline = widget()->graphicsScene()->currentTimeline(); + if (timeline.isValid()) + timeline.modelNode().removeAuxiliaryData("currentFrame@NodeInstance"); + } else if (identifier == "INSERT_KEYFRAME" && !nodeList.isEmpty() && !data.isEmpty()) { + insertKeyframe(nodeList.constFirst(), data.constFirst().toString().toUtf8()); + } +} + +void TimelineView::insertKeyframe(const ModelNode &target, const PropertyName &propertyName) +{ + QmlTimeline timeline = widget()->graphicsScene()->currentTimeline(); + ModelNode targetNode = target; + if (timeline.isValid() && targetNode.isValid() + && QmlObjectNode::isValidQmlObjectNode(targetNode)) { + executeInTransaction("TimelineView::insertKeyframe", [=, &timeline, &targetNode](){ + + targetNode.validId(); + + QmlTimelineKeyframeGroup timelineFrames( + timeline.keyframeGroup(targetNode, propertyName)); + + QTC_ASSERT(timelineFrames.isValid(), return ); + + const qreal frame + = timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal(); + const QVariant value = QmlObjectNode(targetNode).instanceValue(propertyName); + + timelineFrames.setValue(value, frame); + + }); + } +} + +QList<QmlTimeline> TimelineView::getTimelines() const +{ + QList<QmlTimeline> timelines; + + if (!isAttached()) + return timelines; + + for (const ModelNode &modelNode : allModelNodes()) { + if (QmlTimeline::isValidQmlTimeline(modelNode) && !modelNode.hasAuxiliaryData("removed@Internal")) { + timelines.append(modelNode); + } + } + return timelines; +} + +QList<ModelNode> TimelineView::getAnimations(const QmlTimeline &timeline) +{ + if (!timeline.isValid()) + return QList<ModelNode>(); + + if (isAttached()) { + return Utils::filtered(timeline.modelNode().directSubModelNodes(), + [timeline](const ModelNode &node) { + if (node.metaInfo().isValid() && node.hasParentProperty() + && (node.parentProperty().parentModelNode() + == timeline.modelNode())) + return node.metaInfo().isSubclassOf( + "QtQuick.Timeline.TimelineAnimation"); + return false; + }); + } + return {}; +} + +QmlTimeline TimelineView::timelineForState(const ModelNode &state) const +{ + QmlModelState modelState(state); + + const QList<QmlTimeline> &timelines = getTimelines(); + + if (modelState.isBaseState()) { + for (const auto &timeline : timelines) { + if (timeline.modelNode().hasVariantProperty("enabled") + && timeline.modelNode().variantProperty("enabled").value().toBool()) + return timeline; + } + return QmlTimeline(); + } + + for (const auto &timeline : timelines) { + if (modelState.affectsModelNode(timeline)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(timeline)); + + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("enabled") + && propertyChanges.modelNode().variantProperty("enabled").value().toBool()) + return timeline; + } + } + return QmlTimeline(); +} + +QmlModelState TimelineView::stateForTimeline(const QmlTimeline &timeline) +{ + if (timeline.modelNode().hasVariantProperty("enabled") + && timeline.modelNode().variantProperty("enabled").value().toBool()) { + return QmlModelState(rootModelNode()); + } + + for (const QmlModelState &state : QmlItemNode(rootModelNode()).states().allStates()) { + if (timelineForState(state) == timeline) + return state; + } + + return QmlModelState(); +} + +void TimelineView::registerActions() +{ + auto &actionManager = QmlDesignerPlugin::instance()->viewManager().designerActionManager(); + + SelectionContextPredicate timelineEnabled = [this](const SelectionContext &context) { + return context.singleNodeIsSelected() + && widget()->graphicsScene()->currentTimeline().isValid(); + }; + + SelectionContextPredicate timelineHasKeyframes = + [this](const SelectionContext &context) { + auto timeline = widget()->graphicsScene()->currentTimeline(); + return !timeline.keyframeGroupsForTarget(context.currentSingleSelectedNode()).isEmpty(); + }; + + SelectionContextPredicate timelineHasClipboard = [](const SelectionContext &context) { + return !context.fastUpdate() && TimelineActions::clipboardContainsKeyframes(); + }; + + SelectionContextOperation deleteKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::deleteAllKeyframesForTarget(context.currentSingleSelectedNode(), + mutator); + }; + + SelectionContextOperation insertKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::insertAllKeyframesForTarget(context.currentSingleSelectedNode(), + mutator); + }; + + SelectionContextOperation copyKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::copyAllKeyframesForTarget(context.currentSingleSelectedNode(), mutator); + }; + + SelectionContextOperation pasteKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::pasteKeyframesToTarget(context.currentSingleSelectedNode(), mutator); + }; + + actionManager.addDesignerAction(new ActionGroup(TimelineConstants::timelineCategoryDisplayName, + TimelineConstants::timelineCategory, + TimelineConstants::priorityTimelineCategory, + timelineEnabled, + &SelectionContextFunctors::always)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline delete", + TimelineConstants::timelineDeleteKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 160, + deleteKeyframes, + timelineHasKeyframes)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline insert", + TimelineConstants::timelineInsertKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 140, + insertKeyframes, + timelineHasKeyframes)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline copy", + TimelineConstants::timelineCopyKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 120, + copyKeyframes, + timelineHasKeyframes)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline paste", + TimelineConstants::timelinePasteKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 100, + pasteKeyframes, + timelineHasClipboard)); +} + +TimelineWidget *TimelineView::createWidget() +{ + if (!m_timelineWidget) + m_timelineWidget = new TimelineWidget(this); + + auto *timelineContext = new TimelineContext(m_timelineWidget); + Core::ICore::addContextObject(timelineContext); + + return m_timelineWidget; +} + +WidgetInfo TimelineView::widgetInfo() +{ + return createWidgetInfo(createWidget(), + nullptr, + QStringLiteral("Timelines"), + WidgetInfo::BottomPane, + 0, + tr("Timeline")); +} + +bool TimelineView::hasQtQuickTimelineImport() +{ + if (isAttached()) { + Import import = Import::createLibraryImport("QtQuick.Timeline", "1.0"); + return model()->hasImport(import, true, true); + } + + return false; +} + +void TimelineView::ensureQtQuickTimelineImport() +{ + if (!hasQtQuickTimelineImport()) { + Import timelineImport = Import::createLibraryImport("QtQuick.Timeline", "1.0"); + model()->changeImports({timelineImport}, {}); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineview.h b/src/plugins/qmldesigner/components/timelineeditor/timelineview.h new file mode 100644 index 0000000000..057ff3047b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineview.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <abstractview.h> + +#include <QPointer> + +namespace QmlDesigner { + +class TimelineWidget; + +class TimelineView : public AbstractView +{ + Q_OBJECT + +public: + explicit TimelineView(QObject *parent = nullptr); + ~TimelineView() override; + //Abstract View + WidgetInfo widgetInfo() override; + void modelAttached(Model *model) override; + void modelAboutToBeDetached(Model *model) override; + void nodeCreated(const ModelNode &createdNode) override; + void nodeAboutToBeRemoved(const ModelNode &removedNode) override; + void nodeRemoved(const ModelNode &removedNode, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags propertyChange) override; + void nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + PropertyChangeFlags propertyChange) override; + void instancePropertyChanged(const QList<QPair<ModelNode, PropertyName>> &propertyList) override; + void variantPropertiesChanged(const QList<VariantProperty> &propertyList, + PropertyChangeFlags propertyChange) override; + void selectedNodesChanged(const QList<ModelNode> &selectedNodeList, + const QList<ModelNode> &lastSelectedNodeList) override; + + void propertiesAboutToBeRemoved(const QList<AbstractProperty> &propertyList) override; + void propertiesRemoved(const QList<AbstractProperty> &propertyList) override; + + bool hasWidget() const override; + + void nodeIdChanged(const ModelNode &node, const QString &, const QString &) override; + + void currentStateChanged(const ModelNode &node) override; + + TimelineWidget *widget() const; + + const QmlTimeline addNewTimeline(); + ModelNode addAnimation(QmlTimeline timeline); + void addNewTimelineDialog(); + void openSettingsDialog(); + + void setTimelineRecording(bool b); + + void customNotification(const AbstractView *view, + const QString &identifier, + const QList<ModelNode> &nodeList, + const QList<QVariant> &data) override; + void insertKeyframe(const ModelNode &target, const PropertyName &propertyName); + + QList<QmlTimeline> getTimelines() const; + QList<ModelNode> getAnimations(const QmlTimeline &timeline); + QmlTimeline timelineForState(const ModelNode &state) const; + QmlModelState stateForTimeline(const QmlTimeline &timeline); + + void registerActions(); + +private: + TimelineWidget *createWidget(); + TimelineWidget *m_timelineWidget = nullptr; + bool hasQtQuickTimelineImport(); + void ensureQtQuickTimelineImport(); +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp new file mode 100644 index 0000000000..8a58eb7dcf --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp @@ -0,0 +1,443 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinewidget.h" +#include "easingcurvedialog.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinepropertyitem.h" +#include "timelinetoolbar.h" +#include "timelineview.h" + +#include <qmldesignerplugin.h> +#include <qmlstate.h> +#include <qmltimeline.h> + +#include <theme.h> +#include <utils/algorithm.h> +#include <utils/fileutils.h> + +#include <QApplication> +#include <QComboBox> +#include <QGraphicsView> +#include <QHBoxLayout> +#include <QLabel> +#include <QMargins> +#include <QPushButton> +#include <QResizeEvent> +#include <QScrollBar> +#include <QShowEvent> +#include <QSlider> +#include <QVBoxLayout> +#include <QtGlobal> + +namespace QmlDesigner { + +class Eventfilter : public QObject +{ +public: + Eventfilter(QObject *parent) + : QObject(parent) + {} + + bool eventFilter(QObject *, QEvent *event) override + { + if (event->type() == QEvent::Wheel) { + event->accept(); + return true; + } + return false; + } +}; + +static qreal next(const QVector<qreal> &vector, qreal current) +{ + auto iter = std::find_if(vector.cbegin(), vector.cend(), [&](qreal val) { + return val > current; + }); + if (iter != vector.end()) + return *iter; + return current; +} + +static qreal previous(const QVector<qreal> &vector, qreal current) +{ + auto iter = std::find_if(vector.rbegin(), vector.rend(), [&](qreal val) { + return val < current; + }); + if (iter != vector.rend()) + return *iter; + return current; +} + +static qreal getcurrentFrame(const QmlTimeline &timeline) +{ + if (!timeline.isValid()) + return 0; + + if (timeline.modelNode().hasAuxiliaryData("currentFrame@NodeInstance")) + return timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal(); + return timeline.currentKeyframe(); +} + +TimelineWidget::TimelineWidget(TimelineView *view) + : QWidget() + , m_toolbar(new TimelineToolBar(this)) + , m_rulerView(new QGraphicsView(this)) + , m_graphicsView(new QGraphicsView(this)) + , m_scrollbar(new QScrollBar(this)) + , m_statusBar(new QLabel(this)) + , m_timelineView(view) + , m_graphicsScene(new TimelineGraphicsScene(this)) + , m_addButton(new QPushButton(this)) +{ + setWindowTitle(tr("Timeline", "Title of timeline view")); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + const QString css = Theme::replaceCssColors(QString::fromUtf8( + Utils::FileReader::fetchQrc(QLatin1String(":/qmldesigner/scrollbar.css")))); + + m_scrollbar->setStyleSheet(css); + m_scrollbar->setOrientation(Qt::Horizontal); + + QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Preferred); + sizePolicy1.setHorizontalStretch(0); + sizePolicy1.setVerticalStretch(0); + sizePolicy1.setHeightForWidth(m_graphicsView->sizePolicy().hasHeightForWidth()); + + m_rulerView->setObjectName("RulerView"); + m_rulerView->setScene(graphicsScene()); + m_rulerView->setFixedHeight(TimelineConstants::rulerHeight); + m_rulerView->setAlignment(Qt::AlignLeft | Qt::AlignTop); + m_rulerView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_rulerView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_rulerView->viewport()->installEventFilter(new Eventfilter(this)); + m_rulerView->viewport()->setFocusPolicy(Qt::NoFocus); + + m_graphicsView->setStyleSheet(css); + m_graphicsView->setObjectName("SceneView"); + m_graphicsView->setFrameShape(QFrame::NoFrame); + m_graphicsView->setFrameShadow(QFrame::Plain); + m_graphicsView->setLineWidth(0); + m_graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + m_graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + m_graphicsView->setSizePolicy(sizePolicy1); + m_graphicsView->setScene(graphicsScene()); + m_graphicsView->setAlignment(Qt::AlignLeft | Qt::AlignTop); + m_graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + auto *scrollBarLayout = new QHBoxLayout; + scrollBarLayout->addSpacing(TimelineConstants::sectionWidth); + scrollBarLayout->addWidget(m_scrollbar); + + QMargins margins(0, 0, 0, QApplication::style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); + + auto *contentLayout = new QVBoxLayout; + contentLayout->setContentsMargins(margins); + contentLayout->addWidget(m_rulerView); + contentLayout->addWidget(m_graphicsView); + contentLayout->addLayout(scrollBarLayout); + contentLayout->addWidget(m_statusBar); + m_statusBar->setIndent(2); + m_statusBar->setFixedHeight(TimelineConstants::rulerHeight); + + auto *widgetLayout = new QVBoxLayout; + widgetLayout->setContentsMargins(0, 0, 0, 0); + widgetLayout->setSpacing(0); + widgetLayout->addWidget(m_toolbar); + widgetLayout->addWidget(m_addButton); + + m_addButton->setIcon(TimelineIcons::ADD_TIMELINE.icon()); + m_addButton->setToolTip(tr("Add Timeline")); + m_addButton->setFlat(true); + m_addButton->setFixedSize(32, 32); + + widgetLayout->addLayout(contentLayout); + this->setLayout(widgetLayout); + + connectToolbar(); + + auto setScrollOffset = [this]() { graphicsScene()->setScrollOffset(m_scrollbar->value()); }; + connect(m_scrollbar, &QSlider::valueChanged, this, setScrollOffset); + + connect(graphicsScene(), + &TimelineGraphicsScene::statusBarMessageChanged, + this, + [this](const QString &message) { m_statusBar->setText(message); }); + + connect(m_addButton, &QPushButton::clicked, this, [this]() { + m_timelineView->addNewTimelineDialog(); + }); +} + +void TimelineWidget::connectToolbar() +{ + connect(graphicsScene(), + &TimelineGraphicsScene::selectionChanged, + this, + &TimelineWidget::selectionChanged); + + connect(graphicsScene(), &TimelineGraphicsScene::scroll, this, &TimelineWidget::scroll); + + auto setRulerScaling = [this](int val) { m_graphicsScene->setRulerScaling(val); }; + connect(m_toolbar, &TimelineToolBar::scaleFactorChanged, setRulerScaling); + + auto setToFirstFrame = [this]() { + graphicsScene()->setCurrentFrame(graphicsScene()->startFrame()); + }; + connect(m_toolbar, &TimelineToolBar::toFirstFrameTriggered, setToFirstFrame); + + auto setToLastFrame = [this]() { + graphicsScene()->setCurrentFrame(graphicsScene()->endFrame()); + }; + connect(m_toolbar, &TimelineToolBar::toLastFrameTriggered, setToLastFrame); + + auto setToPreviousFrame = [this]() { + graphicsScene()->setCurrentFrame(adjacentFrame(&previous)); + }; + connect(m_toolbar, &TimelineToolBar::previousFrameTriggered, setToPreviousFrame); + + auto setToNextFrame = [this]() { graphicsScene()->setCurrentFrame(adjacentFrame(&next)); }; + connect(m_toolbar, &TimelineToolBar::nextFrameTriggered, setToNextFrame); + + auto setCurrentFrame = [this](int frame) { graphicsScene()->setCurrentFrame(frame); }; + connect(m_toolbar, &TimelineToolBar::currentFrameChanged, setCurrentFrame); + + auto setStartFrame = [this](int start) { graphicsScene()->setStartFrame(start); }; + connect(m_toolbar, &TimelineToolBar::startFrameChanged, setStartFrame); + + auto setEndFrame = [this](int end) { graphicsScene()->setEndFrame(end); }; + connect(m_toolbar, &TimelineToolBar::endFrameChanged, setEndFrame); + + + connect(m_toolbar, &TimelineToolBar::recordToggled, + this, + &TimelineWidget::setTimelineRecording); + + connect(m_toolbar, + &TimelineToolBar::openEasingCurveEditor, + this, + &TimelineWidget::openEasingCurveEditor); + + connect(m_toolbar, + &TimelineToolBar::settingDialogClicked, + m_timelineView, + &TimelineView::openSettingsDialog); + + for (auto action : QmlDesignerPlugin::instance()->designerActionManager().designerActions()) { + if (action->menuId() == "LivePreview") { + QObject::connect(m_toolbar, + &TimelineToolBar::playTriggered, + action->action(), + [action]() { + action->action()->setChecked(false); + action->action()->triggered(true); + }); + } + } + + setTimelineActive(false); +} + +int TimelineWidget::adjacentFrame(const std::function<qreal(const QVector<qreal> &, qreal)> &fun) const +{ + auto positions = graphicsScene()->keyframePositions(); + std::sort(positions.begin(), positions.end()); + return qRound(fun(positions, graphicsScene()->currentFramePosition())); +} + +void TimelineWidget::changeScaleFactor(int factor) +{ + m_toolbar->setScaleFactor(factor); +} + +void TimelineWidget::scroll(const TimelineUtils::Side &side) +{ + if (side == TimelineUtils::Side::Left) + m_scrollbar->setValue(m_scrollbar->value() - m_scrollbar->singleStep()); + else if (side == TimelineUtils::Side::Right) + m_scrollbar->setValue(m_scrollbar->value() + m_scrollbar->singleStep()); +} + +void TimelineWidget::selectionChanged() +{ + if (graphicsScene()->hasSelection()) + m_toolbar->setActionEnabled("Curve Picker", true); + else + m_toolbar->setActionEnabled("Curve Picker", false); +} + +void TimelineWidget::openEasingCurveEditor() +{ + if (graphicsScene()->hasSelection()) { + QList<ModelNode> frames; + for (auto *item : graphicsScene()->selectedKeyframes()) + frames.append(item->frameNode()); + EasingCurveDialog::runDialog(frames); + } +} + +void TimelineWidget::setTimelineRecording(bool value) +{ + ModelNode node = timelineView()->modelNodeForId(m_toolbar->currentTimelineId()); + + if (value) { + timelineView()->activateTimelineRecording(node); + } else { + timelineView()->deactivateTimelineRecording(); + timelineView()->activateTimeline(node); + } + + graphicsScene()->invalidateRecordButtonsStatus(); +} + +void TimelineWidget::contextHelp(const Core::IContext::HelpCallback &callback) const +{ + if (timelineView()) + timelineView()->contextHelp(callback); + else + callback({}); +} + +void TimelineWidget::init() +{ + QmlTimeline currentTimeline = m_timelineView->timelineForState(m_timelineView->currentState()); + if (currentTimeline.isValid()) + setTimelineId(currentTimeline.modelNode().id()); + else + setTimelineId({}); + + invalidateTimelineDuration(graphicsScene()->currentTimeline()); + + graphicsScene()->setWidth(m_graphicsView->viewport()->width()); + + // setScaleFactor uses QSignalBlocker. + m_toolbar->setScaleFactor(0); + m_graphicsScene->setRulerScaling(0); +} + +void TimelineWidget::reset() +{ + graphicsScene()->clearTimeline(); + m_toolbar->reset(); + m_statusBar->clear(); +} + +TimelineGraphicsScene *TimelineWidget::graphicsScene() const +{ + return m_graphicsScene; +} + +TimelineToolBar *TimelineWidget::toolBar() const +{ + return m_toolbar; +} + +void TimelineWidget::invalidateTimelineDuration(const QmlTimeline &timeline) +{ + if (timelineView() && timelineView()->model()) { + QmlTimeline currentTimeline = graphicsScene()->currentTimeline(); + if (currentTimeline.isValid() && currentTimeline == timeline) { + m_toolbar->setCurrentTimeline(timeline); + graphicsScene()->setTimeline(timeline); + graphicsScene()->setCurrenFrame(timeline, getcurrentFrame(timeline)); + } + } +} + +void TimelineWidget::invalidateTimelinePosition(const QmlTimeline &timeline) +{ + if (timelineView() && timelineView()->model()) { + QmlTimeline currentTimeline = graphicsScene()->currentTimeline(); + if (currentTimeline.isValid() && currentTimeline == timeline) { + qreal frame = getcurrentFrame(timeline); + m_toolbar->setCurrentFrame(frame); + graphicsScene()->setCurrenFrame(timeline, frame); + } + } +} + +void TimelineWidget::setupScrollbar(int min, int max, int current) +{ + bool b = m_scrollbar->blockSignals(true); + m_scrollbar->setMinimum(min); + m_scrollbar->setMaximum(max); + m_scrollbar->setValue(current); + m_scrollbar->setSingleStep((max - min) / 10); + m_scrollbar->blockSignals(b); +} + +void TimelineWidget::setTimelineId(const QString &id) +{ + setTimelineActive(!m_timelineView->getTimelines().isEmpty()); + if (m_timelineView->isAttached()) { + m_toolbar->setCurrentTimeline(m_timelineView->modelNodeForId(id)); + m_toolbar->setCurrentState(m_timelineView->currentState().name()); + m_timelineView->setTimelineRecording(false); + } +} + +void TimelineWidget::setTimelineActive(bool b) +{ + if (b) { + m_toolbar->setVisible(true); + m_graphicsView->setVisible(true); + m_rulerView->setVisible(true); + m_scrollbar->setVisible(true); + m_addButton->setVisible(false); + m_graphicsView->update(); + m_rulerView->update(); + } else { + m_toolbar->setVisible(false); + m_graphicsView->setVisible(false); + m_rulerView->setVisible(false); + m_scrollbar->setVisible(false); + m_addButton->setVisible(true); + } +} + +void TimelineWidget::showEvent(QShowEvent *event) +{ + Q_UNUSED(event) + graphicsScene()->setWidth(m_graphicsView->viewport()->width()); + graphicsScene()->invalidateLayout(); + graphicsScene()->invalidate(); + graphicsScene()->onShow(); +} + +void TimelineWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + graphicsScene()->setWidth(m_graphicsView->viewport()->width()); +} + +TimelineView *TimelineWidget::timelineView() const +{ + return m_timelineView; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h new file mode 100644 index 0000000000..3fd299a88a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineutils.h" +#include <coreplugin/icontext.h> + +#include <QWidget> + +#include <functional> + +QT_FORWARD_DECLARE_CLASS(QComboBox) +QT_FORWARD_DECLARE_CLASS(QGraphicsView) +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QResizeEvent) +QT_FORWARD_DECLARE_CLASS(QScrollBar) +QT_FORWARD_DECLARE_CLASS(QShowEvent) +QT_FORWARD_DECLARE_CLASS(QString) +QT_FORWARD_DECLARE_CLASS(QPushButton) + +namespace QmlDesigner { + +class TimelineToolBar; +class TimelineView; +class TimelineGraphicsScene; +class QmlTimeline; + +class TimelineWidget : public QWidget +{ + Q_OBJECT + +public: + explicit TimelineWidget(TimelineView *view); + void contextHelp(const Core::IContext::HelpCallback &callback) const; + + TimelineGraphicsScene *graphicsScene() const; + TimelineView *timelineView() const; + TimelineToolBar *toolBar() const; + + void init(); + void reset(); + + void invalidateTimelineDuration(const QmlTimeline &timeline); + void invalidateTimelinePosition(const QmlTimeline &timeline); + void setupScrollbar(int min, int max, int current); + void setTimelineId(const QString &id); + + void setTimelineActive(bool b); + +public slots: + void selectionChanged(); + void openEasingCurveEditor(); + void setTimelineRecording(bool value); + void changeScaleFactor(int factor); + void scroll(const TimelineUtils::Side &side); + +protected: + void showEvent(QShowEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + void connectToolbar(); + + int adjacentFrame(const std::function<qreal(const QVector<qreal> &, qreal)> &fun) const; + + TimelineToolBar *m_toolbar = nullptr; + + QGraphicsView *m_rulerView = nullptr; + + QGraphicsView *m_graphicsView = nullptr; + + QScrollBar *m_scrollbar = nullptr; + + QLabel *m_statusBar = nullptr; + + TimelineView *m_timelineView = nullptr; + + TimelineGraphicsScene *m_graphicsScene; + + QPushButton *m_addButton = nullptr; +}; + +} // namespace QmlDesigner |