aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/qmldesigner/components/timelineeditor
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/qmldesigner/components/timelineeditor')
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/canvas.cpp359
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/canvas.h110
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp124
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h84
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp502
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/easingcurve.h157
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp298
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h85
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.pngbin0 -> 164 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.pngbin0 -> 162 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/animation.pngbin0 -> 200 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.pngbin0 -> 235 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.pngbin0 -> 153 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.pngbin0 -> 231 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.pngbin0 -> 190 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.pngbin0 -> 239 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.pngbin0 -> 163 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.pngbin0 -> 295 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.pngbin0 -> 147 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.pngbin0 -> 231 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.pngbin0 -> 162 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.pngbin0 -> 281 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.pngbin0 -> 237 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.pngbin0 -> 236 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.pngbin0 -> 190 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe.pngbin0 -> 185 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.pngbin0 -> 185 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.pngbin0 -> 268 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.pngbin0 -> 489 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.pngbin0 -> 241 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.pngbin0 -> 414 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.pngbin0 -> 273 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.pngbin0 -> 493 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.pngbin0 -> 327 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.pngbin0 -> 550 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.pngbin0 -> 371 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.pngbin0 -> 580 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.pngbin0 -> 328 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.pngbin0 -> 557 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.pngbin0 -> 204 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.pngbin0 -> 299 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.pngbin0 -> 201 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.pngbin0 -> 292 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.pngbin0 -> 204 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.pngbin0 -> 299 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.pngbin0 -> 336 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.pngbin0 -> 542 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.pngbin0 -> 286 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.pngbin0 -> 457 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.pngbin0 -> 407 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.pngbin0 -> 706 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.pngbin0 -> 186 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.pngbin0 -> 192 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.pngbin0 -> 171 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.pngbin0 -> 291 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.pngbin0 -> 133 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.pngbin0 -> 150 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.pngbin0 -> 150 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.pngbin0 -> 152 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/playhead.pngbin0 -> 289 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.pngbin0 -> 478 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.pngbin0 -> 132 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.pngbin0 -> 152 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.pngbin0 -> 190 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.pngbin0 -> 179 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/start_playback.pngbin0 -> 143 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.pngbin0 -> 169 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.pngbin0 -> 389 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.pngbin0 -> 135 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.pngbin0 -> 193 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.pngbin0 -> 132 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.pngbin0 -> 191 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.pngbin0 -> 186 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.pngbin0 -> 309 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.pngbin0 -> 177 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.pngbin0 -> 282 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.pngbin0 -> 202 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.pngbin0 -> 357 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.pngbin0 -> 191 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.pngbin0 -> 370 bytes
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp560
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/preseteditor.h152
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp55
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h54
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui74
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp274
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/splineeditor.h96
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo35
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timeline.qrc77
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp67
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h73
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp322
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineactions.h52
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp262
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h64
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui327
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h77
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp45
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h43
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp211
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h123
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri81
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp186
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineform.h59
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineform.ui254
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp162
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h87
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp720
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h181
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineicons.h114
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp293
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineitem.h82
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp154
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h83
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp173
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h54
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp78
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h46
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp641
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h128
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp1062
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h192
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp179
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h86
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp272
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h74
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui84
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp484
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h87
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp460
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h108
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp164
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h71
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp150
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h81
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp48
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineutils.h128
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp610
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelineview.h100
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp443
-rw-r--r--src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h106
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
new file mode 100644
index 0000000000..af651276ed
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png
new file mode 100644
index 0000000000..58a7174288
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/animation.png b/src/plugins/qmldesigner/components/timelineeditor/images/animation.png
new file mode 100644
index 0000000000..20ad0273b5
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/animation.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png
new file mode 100644
index 0000000000..1ecf1857c7
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png
new file mode 100644
index 0000000000..69c93ebe3e
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png
Binary files differ
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
new file mode 100644
index 0000000000..9bd8a52e59
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png
new file mode 100644
index 0000000000..bda4dc0095
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png
new file mode 100644
index 0000000000..3d5c3abe05
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png
new file mode 100644
index 0000000000..4842ac0738
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png
new file mode 100644
index 0000000000..0d99fc180c
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png
new file mode 100644
index 0000000000..0846f194e0
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png
Binary files differ
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
new file mode 100644
index 0000000000..8e5ddc3930
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png
new file mode 100644
index 0000000000..64a28ca075
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png
Binary files differ
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
new file mode 100644
index 0000000000..534737f385
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png
new file mode 100644
index 0000000000..5655e0b278
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png
new file mode 100644
index 0000000000..2f522c22b6
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png
new file mode 100644
index 0000000000..6e1c9f912a
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png
new file mode 100644
index 0000000000..6bf7d1ad53
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png
new file mode 100644
index 0000000000..5102e279a1
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png
new file mode 100644
index 0000000000..8a3eaa7888
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png
Binary files differ
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
new file mode 100644
index 0000000000..e0168a097a
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png
new file mode 100644
index 0000000000..2c12d98e01
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png
Binary files differ
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
new file mode 100644
index 0000000000..4bbbe6cd3f
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png
new file mode 100644
index 0000000000..58ccb7c765
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png
Binary files differ
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
new file mode 100644
index 0000000000..829dd99391
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png
new file mode 100644
index 0000000000..a195ac5fca
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png
Binary files differ
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
new file mode 100644
index 0000000000..fd879e5837
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png
new file mode 100644
index 0000000000..b84a800097
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png
Binary files differ
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
new file mode 100644
index 0000000000..0ad868dcd6
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png
new file mode 100644
index 0000000000..e840819f2d
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png
Binary files differ
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
new file mode 100644
index 0000000000..e5f63f1fc9
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png
new file mode 100644
index 0000000000..f85d3f78fd
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png
Binary files differ
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
new file mode 100644
index 0000000000..2f65f7970c
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png
new file mode 100644
index 0000000000..9798c76115
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png
Binary files differ
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
new file mode 100644
index 0000000000..b4ee45a566
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png
new file mode 100644
index 0000000000..1e39f84502
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png
Binary files differ
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
new file mode 100644
index 0000000000..b99474718c
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png
new file mode 100644
index 0000000000..4b6a7c8978
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png
Binary files differ
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
new file mode 100644
index 0000000000..fd85a10758
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png
new file mode 100644
index 0000000000..9c0a1fd550
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png
Binary files differ
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
new file mode 100644
index 0000000000..1299a370fc
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png
new file mode 100644
index 0000000000..7b5faebae0
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png
Binary files differ
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
new file mode 100644
index 0000000000..726ead7a43
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png
new file mode 100644
index 0000000000..d68aa73214
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png
Binary files differ
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
new file mode 100644
index 0000000000..f5265a2218
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png
new file mode 100644
index 0000000000..f38fbef1d4
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png
new file mode 100644
index 0000000000..b760a04133
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png
new file mode 100644
index 0000000000..415ec0127f
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png
new file mode 100644
index 0000000000..3f1e24e04a
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png
new file mode 100644
index 0000000000..001ca37b1c
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png
new file mode 100644
index 0000000000..95e8567ccb
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png b/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png
new file mode 100644
index 0000000000..518a77f404
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png
new file mode 100644
index 0000000000..7f6778556b
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png
new file mode 100644
index 0000000000..52ba668973
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png
new file mode 100644
index 0000000000..df151051fc
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png
new file mode 100644
index 0000000000..0589f982a7
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png
new file mode 100644
index 0000000000..9eed9ce3c3
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png
new file mode 100644
index 0000000000..0cf0865c48
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png
new file mode 100644
index 0000000000..f05dfcd9ed
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png b/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png
new file mode 100644
index 0000000000..d4ecf00031
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png
new file mode 100644
index 0000000000..910b856638
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png
Binary files differ
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
new file mode 100644
index 0000000000..abefa72fb3
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png
new file mode 100644
index 0000000000..d6bc429196
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png
Binary files differ
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
new file mode 100644
index 0000000000..affc3c9848
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png
Binary files differ
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
new file mode 100644
index 0000000000..83d441d64f
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png
Binary files differ
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
new file mode 100644
index 0000000000..0fec4b269e
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png
Binary files differ
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
new file mode 100644
index 0000000000..611684a7f6
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png
Binary files differ
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
new file mode 100644
index 0000000000..c1dbdbc56c
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png
new file mode 100644
index 0000000000..eec61eb86c
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png
new file mode 100644
index 0000000000..1706de0bb4
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png
new file mode 100644
index 0000000000..20433d99c4
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png
Binary files differ
diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png
new file mode 100644
index 0000000000..326ea32c25
--- /dev/null
+++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png
Binary files differ
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