diff options
Diffstat (limited to 'src/plugins/qmldesigner/components')
285 files changed, 29426 insertions, 547 deletions
diff --git a/src/plugins/qmldesigner/components/colortool/colortool.cpp b/src/plugins/qmldesigner/components/colortool/colortool.cpp new file mode 100644 index 0000000000..32a5742f9f --- /dev/null +++ b/src/plugins/qmldesigner/components/colortool/colortool.cpp @@ -0,0 +1,243 @@ +/**************************************************************************** +** +** 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 "colortool.h" + +#include "formeditorscene.h" +#include "formeditorview.h" +#include "formeditorwidget.h" +#include "itemutilfunctions.h" +#include "formeditoritem.h" + +#include "resizehandleitem.h" + +#include "nodemetainfo.h" +#include "qmlitemnode.h" +#include <qmldesignerplugin.h> +#include <abstractaction.h> +#include <designeractionmanager.h> + +#include <QApplication> +#include <QGraphicsSceneMouseEvent> +#include <QAction> +#include <QDebug> +#include <QPair> +#include <QUrl> + +namespace QmlDesigner { + +class ColorToolAction : public AbstractAction +{ +public: + ColorToolAction() : AbstractAction(QCoreApplication::translate("ColorToolAction","Edit Color")) {} + + QByteArray category() const override + { + return QByteArray(); + } + + QByteArray menuId() const override + { + return "ColorTool"; + } + + int priority() const override + { + return CustomActionsPriority; + } + + Type type() const override + { + return FormEditorAction; + } + +protected: + bool isVisible(const SelectionContext &selectionContext) const override + { + if (selectionContext.singleNodeIsSelected()) + return selectionContext.currentSingleSelectedNode().metaInfo().hasProperty("color"); + + return false; + } + + bool isEnabled(const SelectionContext &selectionContext) const override + { + return isVisible(selectionContext); + } +}; + +ColorTool::ColorTool() +{ + auto colorToolAction = new ColorToolAction; + QmlDesignerPlugin::instance()->designerActionManager().addDesignerAction(colorToolAction); + connect(colorToolAction->action(), &QAction::triggered, [=]() { + view()->changeCurrentToolTo(this); + }); +} + +ColorTool::~ColorTool() = default; + +void ColorTool::clear() +{ + if (m_colorDialog) + m_colorDialog.data()->deleteLater(); + + AbstractFormEditorTool::clear(); +} + +void ColorTool::mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) +{ + AbstractFormEditorTool::mousePressEvent(itemList, event); +} + +void ColorTool::mouseMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * /*event*/) +{ +} + +void ColorTool::hoverMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * /*event*/) +{ +} + +void ColorTool::keyPressEvent(QKeyEvent * /*keyEvent*/) +{ +} + +void ColorTool::keyReleaseEvent(QKeyEvent * /*keyEvent*/) +{ +} + +void ColorTool::dragLeaveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ +} + +void ColorTool::dragMoveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ +} + +void ColorTool::mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) +{ + AbstractFormEditorTool::mouseReleaseEvent(itemList, event); +} + + +void ColorTool::mouseDoubleClickEvent(const QList<QGraphicsItem*> &itemList, QGraphicsSceneMouseEvent *event) +{ + AbstractFormEditorTool::mouseDoubleClickEvent(itemList, event); +} + +void ColorTool::itemsAboutToRemoved(const QList<FormEditorItem*> &removedItemList) +{ + if (m_colorDialog.isNull()) + return; + + if (removedItemList.contains(m_formEditorItem)) + view()->changeToSelectionTool(); +} + +void ColorTool::selectedItemsChanged(const QList<FormEditorItem*> &itemList) +{ + if (m_colorDialog.data() + && m_oldColor.isValid()) + m_formEditorItem->qmlItemNode().setVariantProperty("color", m_oldColor); + + if (!itemList.isEmpty() + && itemList.constFirst()->qmlItemNode().modelNode().metaInfo().hasProperty("color")) { + m_formEditorItem = itemList.constFirst(); + m_oldColor = m_formEditorItem->qmlItemNode().modelValue("color").value<QColor>(); + + if (m_colorDialog.isNull()) { + m_colorDialog = new QColorDialog(view()->formEditorWidget()->parentWidget()); + m_colorDialog.data()->setCurrentColor(m_oldColor); + + connect(m_colorDialog.data(), &QDialog::accepted, this, &ColorTool::colorDialogAccepted); + connect(m_colorDialog.data(), &QDialog::rejected, this, &ColorTool::colorDialogRejected); + connect(m_colorDialog.data(), &QColorDialog::currentColorChanged, this, &ColorTool::currentColorChanged); + + m_colorDialog.data()->exec(); + } + } else { + view()->changeToSelectionTool(); + } +} + +void ColorTool::instancesCompleted(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +void ColorTool::instancesParentChanged(const QList<FormEditorItem *> & /*itemList*/) +{ +} + +void ColorTool::instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > & /*propertyList*/) +{ +} + +void ColorTool::formEditorItemsChanged(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +int ColorTool::wantHandleItem(const ModelNode &modelNode) const +{ + if (modelNode.metaInfo().hasProperty("color")) + return 10; + + return 0; +} + +QString ColorTool::name() const +{ + return tr("Color Tool"); +} + +void ColorTool::colorDialogAccepted() +{ + view()->changeToSelectionTool(); +} + +void ColorTool::colorDialogRejected() +{ + if (m_formEditorItem) { + if (m_oldColor.isValid()) + m_formEditorItem->qmlItemNode().setVariantProperty("color", m_oldColor); + else + m_formEditorItem->qmlItemNode().removeProperty("color"); + + } + + view()->changeToSelectionTool(); +} + +void ColorTool::currentColorChanged(const QColor &color) +{ + if (m_formEditorItem) { + m_formEditorItem->qmlItemNode().setVariantProperty("color", color); + } +} + +} diff --git a/src/plugins/qmldesigner/components/colortool/colortool.h b/src/plugins/qmldesigner/components/colortool/colortool.h new file mode 100644 index 0000000000..5e9e54c633 --- /dev/null +++ b/src/plugins/qmldesigner/components/colortool/colortool.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "abstractcustomtool.h" +#include "selectionindicator.h" + +#include <QHash> +#include <QPointer> +#include <QColorDialog> + +namespace QmlDesigner { + +class ColorTool : public QObject, public AbstractCustomTool +{ + Q_OBJECT +public: + ColorTool(); + ~ColorTool() override; + + void mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void hoverMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; + + void dragLeaveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + void dragMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + + void itemsAboutToRemoved(const QList<FormEditorItem*> &itemList) override; + + void selectedItemsChanged(const QList<FormEditorItem*> &itemList) override; + + void instancesCompleted(const QList<FormEditorItem*> &itemList) override; + void instancesParentChanged(const QList<FormEditorItem *> &itemList) override; + void instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > &propertyList) override; + + void clear() override; + + void formEditorItemsChanged(const QList<FormEditorItem*> &itemList) override; + + int wantHandleItem(const ModelNode &modelNode) const override; + + QString name() const override; + +private: + void colorDialogAccepted(); + void colorDialogRejected(); + void currentColorChanged(const QColor &color); + +private: + QPointer<QColorDialog> m_colorDialog; + FormEditorItem *m_formEditorItem = nullptr; + QColor m_oldColor; +}; + +} diff --git a/src/plugins/qmldesigner/components/colortool/colortool.pri b/src/plugins/qmldesigner/components/colortool/colortool.pri new file mode 100644 index 0000000000..602af91da5 --- /dev/null +++ b/src/plugins/qmldesigner/components/colortool/colortool.pri @@ -0,0 +1,3 @@ +HEADERS += $$PWD/colortool.h + +SOURCES += $$PWD/colortool.cpp diff --git a/src/plugins/qmldesigner/components/componentcore/changestyleaction.cpp b/src/plugins/qmldesigner/components/componentcore/changestyleaction.cpp index fa77acccb9..c2a17cd7b1 100644 --- a/src/plugins/qmldesigner/components/componentcore/changestyleaction.cpp +++ b/src/plugins/qmldesigner/components/componentcore/changestyleaction.cpp @@ -35,10 +35,10 @@ namespace QmlDesigner { static QString styleConfigFileName(const QString &qmlFileName) { - ProjectExplorer::Project *currentProject = ProjectExplorer::SessionManager::projectForFile(Utils::FileName::fromString(qmlFileName)); + ProjectExplorer::Project *currentProject = ProjectExplorer::SessionManager::projectForFile(Utils::FilePath::fromString(qmlFileName)); if (currentProject) - foreach (const Utils::FileName &fileName, currentProject->files(ProjectExplorer::Project::SourceFiles)) + foreach (const Utils::FilePath &fileName, currentProject->files(ProjectExplorer::Project::SourceFiles)) if (fileName.endsWith("qtquickcontrols2.conf")) return fileName.toString(); @@ -90,14 +90,14 @@ QWidget *ChangeStyleWidgetAction::createWidget(QWidget *parent) }); connect(comboBox, - static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), + QOverload<const QString &>::of(&QComboBox::activated), this, [this](const QString &style) { if (style.isEmpty()) return; - const Utils::FileName configFileName = Utils::FileName::fromString(styleConfigFileName(qmlFileName)); + const Utils::FilePath configFileName = Utils::FilePath::fromString(styleConfigFileName(qmlFileName)); if (configFileName.exists()) { QSettings infiFile(configFileName.toString(), QSettings::IniFormat); @@ -125,7 +125,7 @@ void ChangeStyleAction::currentContextChanged(const SelectionContext &selectionC const QString confFileName = styleConfigFileName(fileName); - if (Utils::FileName::fromString(confFileName).exists()) { + if (Utils::FilePath::fromString(confFileName).exists()) { QSettings infiFile(confFileName, QSettings::IniFormat); m_action->handleModelUpdate(infiFile.value("Controls/Style", "Default").toString()); } else { diff --git a/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h b/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h index 028d3a21b4..0535a8f52e 100644 --- a/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h +++ b/src/plugins/qmldesigner/components/componentcore/componentcore_constants.h @@ -165,6 +165,7 @@ const int priorityGenericToolBar = 50; const int priorityLast = 60; const char addImagesDisplayString[] = QT_TRANSLATE_NOOP("QmlDesignerAddResources", "Image Files"); +const char addFontsDisplayString[] = QT_TRANSLATE_NOOP("QmlDesignerAddResources", "Font Files"); } //ComponentCoreConstants diff --git a/src/plugins/qmldesigner/components/componentcore/crumblebar.cpp b/src/plugins/qmldesigner/components/componentcore/crumblebar.cpp index 112923c5f7..733b29ef5a 100644 --- a/src/plugins/qmldesigner/components/componentcore/crumblebar.cpp +++ b/src/plugins/qmldesigner/components/componentcore/crumblebar.cpp @@ -78,7 +78,7 @@ CrumbleBar::~CrumbleBar() delete m_crumblePath; } -void CrumbleBar::pushFile(const Utils::FileName &fileName) +void CrumbleBar::pushFile(const Utils::FilePath &fileName) { if (!m_isInternalCalled) { crumblePath()->clear(); diff --git a/src/plugins/qmldesigner/components/componentcore/crumblebar.h b/src/plugins/qmldesigner/components/componentcore/crumblebar.h index d29d8f594e..cd19e1faa2 100644 --- a/src/plugins/qmldesigner/components/componentcore/crumblebar.h +++ b/src/plugins/qmldesigner/components/componentcore/crumblebar.h @@ -39,7 +39,7 @@ public: explicit CrumbleBar(QObject *parent = nullptr); ~CrumbleBar() override; - void pushFile(const Utils::FileName &fileName); + void pushFile(const Utils::FilePath &fileName); void pushInFileComponent(const ModelNode &modelNode); void nextFileIsCalledInternally(); @@ -58,7 +58,7 @@ private: class CrumbleBarInfo { public: - Utils::FileName fileName; + Utils::FilePath fileName; QString displayName; ModelNode modelNode; }; diff --git a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp index 6edbacec36..37e1fdfd53 100644 --- a/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp +++ b/src/plugins/qmldesigner/components/componentcore/designeractionmanager.cpp @@ -1026,6 +1026,13 @@ void DesignerActionManager::createDefaultAddResourceHandler() registerAddResourceHandler(AddResourceHandler(ComponentCoreConstants::addImagesDisplayString, "*.svg", ModelNodeOperations::addImageToProject)); + + registerAddResourceHandler(AddResourceHandler(ComponentCoreConstants::addFontsDisplayString, + "*.ttf", + ModelNodeOperations::addFontToProject)); + registerAddResourceHandler(AddResourceHandler(ComponentCoreConstants::addFontsDisplayString, + "*.otf", + ModelNodeOperations::addFontToProject)); } void DesignerActionManager::addDesignerAction(ActionInterface *newAction) diff --git a/src/plugins/qmldesigner/components/componentcore/layoutingridlayout.cpp b/src/plugins/qmldesigner/components/componentcore/layoutingridlayout.cpp index 5ec3ea557b..ba520a1ca3 100644 --- a/src/plugins/qmldesigner/components/componentcore/layoutingridlayout.cpp +++ b/src/plugins/qmldesigner/components/componentcore/layoutingridlayout.cpp @@ -185,17 +185,16 @@ void LayoutInGridLayout::doIt() if (qmlItemNode.hasInstanceParentItem()) { ModelNode layoutNode; - { - RewriterTransaction transaction(m_selectionContext.view(), QByteArrayLiteral("LayoutInGridLayout1")); + + m_selectionContext.view()->executeInTransaction("LayoutInGridLayout1",[this, &layoutNode, layoutType](){ QTC_ASSERT(m_selectionContext.view()->model()->hasNodeMetaInfo(layoutType), return); NodeMetaInfo metaInfo = m_selectionContext.view()->model()->metaInfo(layoutType); layoutNode = m_selectionContext.view()->createModelNode(layoutType, metaInfo.majorVersion(), metaInfo.minorVersion()); reparentTo(layoutNode, m_parentNode); - } + }); - { - RewriterTransaction transaction(m_selectionContext.view(), QByteArrayLiteral("LayoutInGridLayout2")); + m_selectionContext.view()->executeInTransaction("LayoutInGridLayout2", [this, layoutNode](){ fillEmptyCells(); @@ -208,7 +207,7 @@ void LayoutInGridLayout::doIt() reparentToNodeAndRemovePositionForModelNodes(layoutNode, sortedSelectedNodes); setSizeAsPreferredSize(sortedSelectedNodes); setSpanning(layoutNode); - } + }); } } } diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp index f5b701e44d..9f880c453b 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp @@ -240,9 +240,7 @@ void changeOrder(const SelectionContext &selectionState, OderAction orderAction) if (!modelNode.parentProperty().isNodeListProperty()) return; - try { - RewriterTransaction transaction(selectionState.view(), QByteArrayLiteral("DesignerActionManager|raise")); - + selectionState.view()->executeInTransaction("DesignerActionManager|raise",[orderAction, selectionState, modelNode](){ ModelNode modelNode = selectionState.currentSingleSelectedNode(); NodeListProperty parentProperty = modelNode.parentProperty().toNodeListProperty(); const int index = parentProperty.indexOf(modelNode); @@ -255,11 +253,7 @@ void changeOrder(const SelectionContext &selectionState, OderAction orderAction) if (index > 0) parentProperty.slide(index, index - 1); } - - transaction.commit(); - } catch (const RewritingException &e) { //better save then sorry - e.showException(); - } + }); } void raise(const SelectionContext &selectionState) @@ -328,16 +322,13 @@ void resetSize(const SelectionContext &selectionState) if (!selectionState.view()) return; - try { - RewriterTransaction transaction(selectionState.view(), QByteArrayLiteral("DesignerActionManager|resetSize")); + selectionState.view()->executeInTransaction("DesignerActionManager|resetSize",[selectionState](){ foreach (ModelNode node, selectionState.selectedModelNodes()) { QmlItemNode itemNode(node); itemNode.removeProperty("width"); itemNode.removeProperty("height"); } - } catch (const RewritingException &e) { //better save then sorry - e.showException(); - } + }); } void resetPosition(const SelectionContext &selectionState) @@ -345,17 +336,13 @@ void resetPosition(const SelectionContext &selectionState) if (!selectionState.view()) return; - try { - RewriterTransaction transaction(selectionState.view(), QByteArrayLiteral("DesignerActionManager|resetPosition")); + selectionState.view()->executeInTransaction("DesignerActionManager|resetPosition",[selectionState](){ foreach (ModelNode node, selectionState.selectedModelNodes()) { QmlItemNode itemNode(node); itemNode.removeProperty("x"); itemNode.removeProperty("y"); } - transaction.commit(); - } catch (const RewritingException &e) { //better save then sorry - e.showException(); - } + }); } void goIntoComponentOperation(const SelectionContext &selectionState) @@ -372,11 +359,12 @@ void resetZ(const SelectionContext &selectionState) if (!selectionState.view()) return; - RewriterTransaction transaction(selectionState.view(), QByteArrayLiteral("DesignerActionManager|resetZ")); - foreach (ModelNode node, selectionState.selectedModelNodes()) { - QmlItemNode itemNode(node); - itemNode.removeProperty("z"); - } + selectionState.view()->executeInTransaction("DesignerActionManager|resetZ",[selectionState](){ + foreach (ModelNode node, selectionState.selectedModelNodes()) { + QmlItemNode itemNode(node); + itemNode.removeProperty("z"); + } + }); } static inline void backupPropertyAndRemove(const ModelNode &node, const PropertyName &propertyName) @@ -404,9 +392,7 @@ void anchorsFill(const SelectionContext &selectionState) if (!selectionState.view()) return; - try { - RewriterTransaction transaction(selectionState.view(), QByteArrayLiteral("DesignerActionManager|anchorsFill")); - + selectionState.view()->executeInTransaction("DesignerActionManager|anchorsFill",[selectionState](){ ModelNode modelNode = selectionState.currentSingleSelectedNode(); QmlItemNode node = modelNode; @@ -417,11 +403,7 @@ void anchorsFill(const SelectionContext &selectionState) backupPropertyAndRemove(modelNode, "width"); backupPropertyAndRemove(modelNode, "height"); } - - transaction.commit(); - } catch (const RewritingException &e) { //better save then sorry - e.showException(); - } + }); } void anchorsReset(const SelectionContext &selectionState) @@ -429,19 +411,19 @@ void anchorsReset(const SelectionContext &selectionState) if (!selectionState.view()) return; - RewriterTransaction transaction(selectionState.view(), QByteArrayLiteral("DesignerActionManager|anchorsReset")); - - ModelNode modelNode = selectionState.currentSingleSelectedNode(); + selectionState.view()->executeInTransaction("DesignerActionManager|anchorsReset",[selectionState](){ + ModelNode modelNode = selectionState.currentSingleSelectedNode(); - QmlItemNode node = modelNode; - if (node.isValid()) { - node.anchors().removeAnchors(); - node.anchors().removeMargins(); - restoreProperty(node, "x"); - restoreProperty(node, "y"); - restoreProperty(node, "width"); - restoreProperty(node, "height"); - } + QmlItemNode node = modelNode; + if (node.isValid()) { + node.anchors().removeAnchors(); + node.anchors().removeMargins(); + restoreProperty(node, "x"); + restoreProperty(node, "y"); + restoreProperty(node, "width"); + restoreProperty(node, "height"); + } + }); } using LessThan = std::function<bool (const ModelNode &, const ModelNode&)>; @@ -481,7 +463,7 @@ bool compareByGrid(const ModelNode &node1, const ModelNode &node2) static void layoutHelperFunction(const SelectionContext &selectionContext, const TypeName &layoutType, - LessThan lessThan) + const LessThan &lessThan) { if (!selectionContext.view() || !selectionContext.hasSingleSelectedModelNode() @@ -492,10 +474,8 @@ static void layoutHelperFunction(const SelectionContext &selectionContext, const QmlItemNode qmlItemNode = QmlItemNode(selectionContext.firstSelectedModelNode()); if (qmlItemNode.hasInstanceParentItem()) { - ModelNode layoutNode; - { - RewriterTransaction transaction(selectionContext.view(), QByteArrayLiteral("DesignerActionManager|layoutHelperFunction1")); + selectionContext.view()->executeInTransaction("DesignerActionManager|layoutHelperFunction1",[=, &layoutNode](){ QmlItemNode parentNode = qmlItemNode.instanceParentItem(); @@ -504,10 +484,9 @@ static void layoutHelperFunction(const SelectionContext &selectionContext, layoutNode = selectionContext.view()->createModelNode(layoutType, metaInfo.majorVersion(), metaInfo.minorVersion()); reparentTo(layoutNode, parentNode); - } + }); - { - RewriterTransaction transaction(selectionContext.view(), QByteArrayLiteral("DesignerActionManager|layoutHelperFunction2")); + selectionContext.view()->executeInTransaction("DesignerActionManager|layoutHelperFunction2",[=](){ QList<ModelNode> sortedSelectedNodes = selectionContext.selectedModelNodes(); Utils::sort(sortedSelectedNodes, lessThan); @@ -516,7 +495,7 @@ static void layoutHelperFunction(const SelectionContext &selectionContext, LayoutInGridLayout::reparentToNodeAndRemovePositionForModelNodes(layoutNode, sortedSelectedNodes); if (layoutType.contains("Layout")) LayoutInGridLayout::setSizeAsPreferredSize(sortedSelectedNodes); - } + }); } } } @@ -662,21 +641,14 @@ void addSignalHandlerOrGotoImplementation(const SelectionContext &selectionState if (!qmlObjectNode.isRootModelNode()) { isModelNodeRoot = false; - try { - RewriterTransaction transaction = - qmlObjectNode.view()->beginRewriterTransaction(QByteArrayLiteral("NavigatorTreeModel:exportItem")); - - QmlObjectNode qmlObjectNode(modelNode); + qmlObjectNode.view()->executeInTransaction("NavigatorTreeModel:exportItem", [&qmlObjectNode](){ qmlObjectNode.ensureAliasExport(); - transaction.commit(); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } + }); } QString itemId = modelNode.id(); - const Utils::FileName currentDesignDocument = QmlDesignerPlugin::instance()->documentManager().currentDesignDocument()->fileName(); + const Utils::FilePath currentDesignDocument = QmlDesignerPlugin::instance()->documentManager().currentDesignDocument()->fileName(); const QString fileName = currentDesignDocument.toString(); const QString typeName = currentDesignDocument.toFileInfo().baseName(); @@ -708,14 +680,10 @@ void addSignalHandlerOrGotoImplementation(const SelectionContext &selectionState if (dialog->signal().isEmpty()) return; - try { - RewriterTransaction transaction = - qmlObjectNode.view()->beginRewriterTransaction(QByteArrayLiteral("NavigatorTreeModel:exportItem")); + qmlObjectNode.view()->executeInTransaction("NavigatorTreeModel:exportItem", [=](){ addSignal(typeName, itemId, dialog->signal(), isModelNodeRoot); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } + }); addSignal(typeName, itemId, dialog->signal(), isModelNodeRoot); @@ -751,9 +719,7 @@ void removeLayout(const SelectionContext &selectionContext) if (!parent.isValid()) return; - { - RewriterTransaction transaction(selectionContext.view(), QByteArrayLiteral("DesignerActionManager|removeLayout")); - + selectionContext.view()->executeInTransaction("DesignerActionManager|removeLayout", [selectionContext, &layoutItem, parent](){ foreach (const ModelNode &modelNode, selectionContext.currentSingleSelectedNode().directSubModelNodes()) { if (QmlItemNode::isValidQmlItemNode(modelNode)) { @@ -772,7 +738,7 @@ void removeLayout(const SelectionContext &selectionContext) parent.modelNode().defaultNodeListProperty().reparentHere(modelNode); } layoutItem.destroy(); - } + }); } void removePositioner(const SelectionContext &selectionContext) @@ -826,9 +792,7 @@ void addItemToStackedContainer(const SelectionContext &selectionContext) } } - try { - RewriterTransaction transaction = - view->beginRewriterTransaction(QByteArrayLiteral("DesignerActionManager:addItemToStackedContainer")); + view->executeInTransaction("DesignerActionManager:addItemToStackedContainer", [=](){ NodeMetaInfo itemMetaInfo = view->model()->metaInfo("QtQuick.Item", -1, -1); QTC_ASSERT(itemMetaInfo.isValid(), return); @@ -853,11 +817,7 @@ void addItemToStackedContainer(const SelectionContext &selectionContext) } } - - transaction.commit(); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } + }); } PropertyName getIndexPropertyName(const ModelNode &modelNode) @@ -969,9 +929,8 @@ void addTabBarToStackedContainer(const SelectionContext &selectionContext) const PropertyName indexPropertyName = getIndexPropertyName(container); QTC_ASSERT(container.metaInfo().hasProperty(indexPropertyName), return); - try { - RewriterTransaction transaction = - view->beginRewriterTransaction(QByteArrayLiteral("DesignerActionManager:addItemToStackedContainer")); + view->executeInTransaction("DesignerActionManager:addItemToStackedContainer", + [view, container, containerItemNode, tabBarMetaInfo, tabButtonMetaInfo, indexPropertyName](){ ModelNode tabBarNode = view->createModelNode("QtQuick.Controls.TabBar", @@ -1003,13 +962,42 @@ void addTabBarToStackedContainer(const SelectionContext &selectionContext) container.removeProperty(indexPropertyName); const QString expression = id + "." + QString::fromLatin1(indexPropertyName); container.bindingProperty(indexPropertyName).setExpression(expression); + }); + +} + +bool addFontToProject(const QStringList &fileNames, const QString &defaultDirectory) +{ + QString directory = AddImagesDialog::getDirectory(fileNames, defaultDirectory); + + if (directory.isEmpty()) + return true; + + bool allSuccessful = true; + for (const QString &fileName : fileNames) { + const QString targetFile = directory + "/" + QFileInfo(fileName).fileName(); + const bool success = QFile::copy(fileName, targetFile); + + auto document = QmlDesignerPlugin::instance()->currentDesignDocument(); + + QTC_ASSERT(document, return false); - transaction.commit(); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); + if (success) { + ProjectExplorer::Node *node = ProjectExplorer::ProjectTree::nodeForFile(document->fileName()); + if (node) { + ProjectExplorer::FolderNode *containingFolder = node->parentFolderNode(); + if (containingFolder) + containingFolder->addFiles(QStringList(targetFile)); + } + } else { + allSuccessful = false; + } } + + return allSuccessful; } + bool addImageToProject(const QStringList &fileNames, const QString &defaultDirectory) { QString directory = AddImagesDialog::getDirectory(fileNames, defaultDirectory); diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h index e3396a303e..52dfaf6f1d 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h @@ -73,6 +73,7 @@ void increaseIndexOfStackedContainer(const SelectionContext &selectionContext); void decreaseIndexOfStackedContainer(const SelectionContext &selectionContext); void addTabBarToStackedContainer(const SelectionContext &selectionContext); bool addImageToProject(const QStringList &fileNames, const QString &directory); +bool addFontToProject(const QStringList &fileNames, const QString &directory); } // namespace ModelNodeOperationso } //QmlDesigner diff --git a/src/plugins/qmldesigner/components/componentcore/selectioncontext.cpp b/src/plugins/qmldesigner/components/componentcore/selectioncontext.cpp index e82b2a22dd..434f73c0f3 100644 --- a/src/plugins/qmldesigner/components/componentcore/selectioncontext.cpp +++ b/src/plugins/qmldesigner/components/componentcore/selectioncontext.cpp @@ -29,7 +29,6 @@ namespace QmlDesigner { - SelectionContext::SelectionContext() = default; SelectionContext::SelectionContext(AbstractView *view) : diff --git a/src/plugins/qmldesigner/components/componentcore/zoomaction.cpp b/src/plugins/qmldesigner/components/componentcore/zoomaction.cpp index 9a1529414a..40ad483785 100644 --- a/src/plugins/qmldesigner/components/componentcore/zoomaction.cpp +++ b/src/plugins/qmldesigner/components/componentcore/zoomaction.cpp @@ -30,7 +30,6 @@ namespace QmlDesigner { - ZoomAction::ZoomAction(QObject *parent) : QWidgetAction(parent), m_zoomLevel(1.0), @@ -105,7 +104,7 @@ QWidget *ZoomAction::createWidget(QWidget *parent) comboBox->setCurrentIndex(m_currentComboBoxIndex); blockSignals(false); }); - connect(comboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), + connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), [this, comboBox](int index) { m_currentComboBoxIndex = index; diff --git a/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.cpp b/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.cpp new file mode 100644 index 0000000000..9de928ae90 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** 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 "addnewbackenddialog.h" +#include "ui_addnewbackenddialog.h" + +#include <QPushButton> + +namespace QmlDesigner { + +AddNewBackendDialog::AddNewBackendDialog(QWidget *parent) : + QDialog(parent), + m_ui(new Ui::AddNewBackendDialog) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + m_ui->setupUi(this); + + connect(m_ui->comboBox, &QComboBox::currentTextChanged, this, &AddNewBackendDialog::invalidate); + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, [this]() { + m_applied = true; + close(); + }); + + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &AddNewBackendDialog::close); +} + +AddNewBackendDialog::~AddNewBackendDialog() +{ + delete m_ui; +} + +void AddNewBackendDialog::setupPossibleTypes(const QList<CppTypeData> &types) +{ + QSignalBlocker blocker(this); + m_typeData = types; + for (const CppTypeData &typeData : types) + m_ui->comboBox->addItem(typeData.typeName); + + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(m_ui->comboBox->count() > 0); + invalidate(); +} + +QString AddNewBackendDialog::importString() const +{ + if (m_ui->comboBox->currentIndex() < 0) + return QString(); + + CppTypeData typeData = m_typeData.at(m_ui->comboBox->currentIndex()); + + return typeData.importUrl + " " + typeData.versionString; +} + +QString AddNewBackendDialog::type() const +{ + if (m_ui->comboBox->currentIndex() < 0) + return QString(); + + return m_typeData.at(m_ui->comboBox->currentIndex()).typeName; +} + +bool AddNewBackendDialog::applied() const +{ + return m_applied; +} + +bool AddNewBackendDialog::localDefinition() const +{ + return m_ui->checkBox->isChecked(); +} + +bool AddNewBackendDialog::isSingleton() const +{ + return m_isSingleton; +} + +void AddNewBackendDialog::invalidate() +{ + if (m_ui->comboBox->currentIndex() < 0) + return; + + CppTypeData typeData = m_typeData.at(m_ui->comboBox->currentIndex()); + m_ui->importLabel->setText(importString()); + + m_ui->checkBox->setChecked(false); + if (typeData.isSingleton) + m_ui->checkBox->setEnabled(false); + + m_isSingleton = typeData.isSingleton; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.h b/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.h new file mode 100644 index 0000000000..4cfa46d66e --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QDialog> + +#include <rewriterview.h> + +namespace QmlDesigner { + +namespace Ui { +class AddNewBackendDialog; +} + +class AddNewBackendDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AddNewBackendDialog(QWidget *parent = nullptr); + ~AddNewBackendDialog() override; + void setupPossibleTypes(const QList<CppTypeData> &types); + QString importString() const; + QString type() const; + bool applied() const; + bool localDefinition() const; + bool isSingleton() const; + +private: + void invalidate(); + + Ui::AddNewBackendDialog *m_ui; + QList<CppTypeData> m_typeData; + + bool m_applied = false; + bool m_isSingleton = false; +}; + + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.ui b/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.ui new file mode 100644 index 0000000000..1da43f7526 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/addnewbackenddialog.ui @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::AddNewBackendDialog</class> + <widget class="QDialog" name="QmlDesigner::AddNewBackendDialog"> + <property name="windowModality"> + <enum>Qt::ApplicationModal</enum> + </property> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>412</width> + <height>202</height> + </rect> + </property> + <property name="windowTitle"> + <string>Add New C++ Backend</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="2" column="0"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>13</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="1"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QFrame" name="frame"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Type</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="comboBox"/> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>169</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="3"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Define object locally</string> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QCheckBox" name="checkBox"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Required import</string> + </property> + </widget> + </item> + <item row="1" column="4"> + <widget class="QLabel" name="importLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>160</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>160</width> + <height>16777215</height> + </size> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="label_4"> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Choose a type that is registered using qmlRegisterType or qmlRegisterSingletonType. The type will be available as a property in the current .qml file.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/plugins/qmldesigner/components/connectioneditor/backendmodel.cpp b/src/plugins/qmldesigner/components/connectioneditor/backendmodel.cpp new file mode 100644 index 0000000000..4a004ca575 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/backendmodel.cpp @@ -0,0 +1,333 @@ +/**************************************************************************** +** +** 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 <utils/algorithm.h> + +#include "backendmodel.h" + +#include "bindingproperty.h" +#include "connectionview.h" +#include "exception.h" +#include "nodemetainfo.h" +#include "nodeproperty.h" +#include "rewriterview.h" +#include "rewritertransaction.h" + +#include "addnewbackenddialog.h" + +#include <coreplugin/icore.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +namespace Internal { + +BackendModel::BackendModel(ConnectionView *parent) : + QStandardItemModel(parent) + ,m_connectionView(parent) +{ + connect(this, &QStandardItemModel::dataChanged, this, &BackendModel::handleDataChanged); +} + +ConnectionView *QmlDesigner::Internal::BackendModel::connectionView() const +{ + return m_connectionView; +} + +void BackendModel::resetModel() +{ + if (!m_connectionView->model()) + return; + + RewriterView *rewriterView = m_connectionView->model()->rewriterView(); + + m_lock = true; + + beginResetModel(); + clear(); + + setHorizontalHeaderLabels(QStringList({ tr("Type"), tr("Name"), tr("Singleton"), tr("Local") })); + + ModelNode rootNode = connectionView()->rootModelNode(); + + static const PropertyTypeList simpleTypes = {"int", "real", "color", "string"}; + + if (rewriterView) + for (const CppTypeData &cppTypeData : rewriterView->getCppTypes()) + if (cppTypeData.isSingleton) { + NodeMetaInfo metaInfo = m_connectionView->model()->metaInfo(cppTypeData.typeName.toUtf8()); + if (metaInfo.isValid() && !metaInfo.isSubclassOf("QtQuick.Item")) { + auto type = new QStandardItem(cppTypeData.typeName); + type->setData(cppTypeData.typeName, Qt::UserRole + 1); + type->setData(true, Qt::UserRole + 2); + type->setEditable(false); + + auto name = new QStandardItem(cppTypeData.typeName); + name->setEditable(false); + + QStandardItem *singletonItem = new QStandardItem(""); + singletonItem->setCheckState(Qt::Checked); + + singletonItem->setCheckable(true); + singletonItem->setEnabled(false); + + QStandardItem *inlineItem = new QStandardItem(""); + + inlineItem->setCheckState(Qt::Unchecked); + + inlineItem->setCheckable(true); + inlineItem->setEnabled(false); + + appendRow({ type, name, singletonItem, inlineItem }); + } + } + + if (rootNode.isValid()) + foreach (const AbstractProperty &property ,rootNode.properties()) + if (property.isDynamic() && !simpleTypes.contains(property.dynamicTypeName())) { + + NodeMetaInfo metaInfo = m_connectionView->model()->metaInfo(property.dynamicTypeName()); + if (metaInfo.isValid() && !metaInfo.isSubclassOf("QtQuick.Item")) { + QStandardItem *type = new QStandardItem(QString::fromUtf8(property.dynamicTypeName())); + type->setEditable(false); + + type->setData(QString::fromUtf8(property.name()), Qt::UserRole + 1); + type->setData(false, Qt::UserRole + 2); + QStandardItem *name = new QStandardItem(QString::fromUtf8(property.name())); + + QStandardItem *singletonItem = new QStandardItem(""); + singletonItem->setCheckState(Qt::Unchecked); + + singletonItem->setCheckable(true); + singletonItem->setEnabled(false); + + QStandardItem *inlineItem = new QStandardItem(""); + + inlineItem->setCheckState(property.isNodeProperty() ? Qt::Checked : Qt::Unchecked); + + inlineItem->setCheckable(true); + inlineItem->setEnabled(false); + + appendRow({ type, name, singletonItem, inlineItem }); + } + } + + m_lock = false; + + endResetModel(); +} + +QStringList BackendModel::possibleCppTypes() const +{ + RewriterView *rewriterView = m_connectionView->model()->rewriterView(); + + QStringList list; + + if (rewriterView) + foreach (const CppTypeData &cppTypeData, rewriterView->getCppTypes()) + list.append(cppTypeData.typeName); + + return list; +} + +CppTypeData BackendModel::cppTypeDataForType(const QString &typeName) const +{ + RewriterView *rewriterView = m_connectionView->model()->rewriterView(); + + if (!rewriterView) + return CppTypeData(); + + return Utils::findOr(rewriterView->getCppTypes(), CppTypeData(), [&typeName](const CppTypeData &data) { + return typeName == data.typeName; + }); +} + +void BackendModel::deletePropertyByRow(int rowNumber) +{ + Model *model = m_connectionView->model(); + if (!model) + return; + + /* singleton case remove the import */ + if (data(index(rowNumber, 0), Qt::UserRole + 1).toBool()) { + const QString typeName = data(index(rowNumber, 0), Qt::UserRole + 1).toString(); + CppTypeData cppTypeData = cppTypeDataForType(typeName); + + if (cppTypeData.isSingleton) { + + Import import = Import::createLibraryImport(cppTypeData.importUrl, cppTypeData.versionString); + + try { + if (model->hasImport(import)) + model->changeImports({}, {import}); + } catch (const Exception &e) { + e.showException(); + } + } + } else { + const QString propertyName = data(index(rowNumber, 0), Qt::UserRole + 1).toString(); + + ModelNode modelNode = connectionView()->rootModelNode(); + + try { + modelNode.removeProperty(propertyName.toUtf8()); + } catch (const Exception &e) { + e.showException(); + } + } + + resetModel(); +} + +void BackendModel::addNewBackend() +{ + Model *model = m_connectionView->model(); + if (!model) + return; + + AddNewBackendDialog dialog(Core::ICore::mainWindow()); + + RewriterView *rewriterView = model->rewriterView(); + + QStringList availableTypes; + + if (rewriterView) + dialog.setupPossibleTypes(Utils::filtered(rewriterView->getCppTypes(), [model](const CppTypeData &cppTypeData) { + return !cppTypeData.isSingleton || !model->metaInfo(cppTypeData.typeName.toUtf8()).isValid(); + /* Only show singletons if the import is missing */ + })); + + dialog.exec(); + + if (dialog.applied()) { + QStringList importSplit = dialog.importString().split(" "); + if (importSplit.count() != 2) { + qWarning() << Q_FUNC_INFO << "invalid import" << importSplit; + QTC_ASSERT(false, return); + } + + QString typeName = dialog.type(); + + Import import = Import::createLibraryImport(importSplit.constFirst(), importSplit.constLast()); + + /* We cannot add an import and add a node from that import in a single transaction. + * We need the import to have the meta info available. + */ + + if (!model->hasImport(import)) + model->changeImports({import}, {}); + + QString propertyName = m_connectionView->generateNewId(typeName); + + NodeMetaInfo metaInfo = model->metaInfo(typeName.toUtf8()); + + QTC_ASSERT(metaInfo.isValid(), return); + + /* Add a property for non singleton types. For singletons just adding the import is enough. */ + if (!dialog.isSingleton()) { + m_connectionView->executeInTransaction("BackendModel::addNewBackend", [=, &dialog](){ + int minorVersion = metaInfo.minorVersion(); + int majorVersion = metaInfo.majorVersion(); + + if (dialog.localDefinition()) { + ModelNode newNode = m_connectionView->createModelNode(metaInfo.typeName(), majorVersion, minorVersion); + + m_connectionView->rootModelNode().nodeProperty(propertyName.toUtf8()).setDynamicTypeNameAndsetModelNode( + typeName.toUtf8(), newNode); + } else { + m_connectionView->rootModelNode().bindingProperty( + propertyName.toUtf8()).setDynamicTypeNameAndExpression(typeName.toUtf8(), "null"); + } + }); + } + } + resetModel(); +} + +void BackendModel::updatePropertyName(int rowNumber) +{ + const PropertyName newName = data(index(rowNumber, 1)).toString().toUtf8(); + const PropertyName oldName = data(index(rowNumber, 0), Qt::UserRole + 1).toString().toUtf8(); + + m_connectionView->executeInTransaction("BackendModel::updatePropertyName", [this, newName, oldName](){ + + ModelNode rootModelNode = m_connectionView->rootModelNode(); + if (rootModelNode.property(oldName).isNodeProperty()) { + + const TypeName typeName = rootModelNode.nodeProperty(oldName).dynamicTypeName(); + const ModelNode targetModelNode = rootModelNode.nodeProperty(oldName).modelNode(); + const TypeName fullTypeName = targetModelNode.type(); + const int majorVersion = targetModelNode.majorVersion(); + const int minorVersion = targetModelNode.minorVersion(); + + rootModelNode.removeProperty(oldName); + ModelNode newNode = m_connectionView->createModelNode(fullTypeName, majorVersion, minorVersion); + m_connectionView->rootModelNode().nodeProperty(newName).setDynamicTypeNameAndsetModelNode(typeName, newNode); + + } else if (rootModelNode.property(oldName).isBindingProperty()) { + const QString expression = rootModelNode.bindingProperty(oldName).expression(); + const TypeName typeName = rootModelNode.bindingProperty(oldName).dynamicTypeName(); + + rootModelNode.removeProperty(oldName); + rootModelNode.bindingProperty(newName).setDynamicTypeNameAndExpression(typeName, expression); + } else { + qWarning() << Q_FUNC_INFO << oldName << newName << "failed..."; + QTC_ASSERT(false, return); + } + }); +} + +void BackendModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (m_lock) + return; + + if (topLeft != bottomRight) { + qWarning() << "BackendModel::handleDataChanged multi edit?"; + return; + } + + m_lock = true; + + int currentColumn = topLeft.column(); + int currentRow = topLeft.row(); + + switch (currentColumn) { + case 0: { + //updating user data + } break; + case 1: { + updatePropertyName(currentRow); + } break; + + default: qWarning() << "BindingModel::handleDataChanged column" << currentColumn; + } + + m_lock = false; +} + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/backendmodel.h b/src/plugins/qmldesigner/components/connectioneditor/backendmodel.h new file mode 100644 index 0000000000..8abbbe77fa --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/backendmodel.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once +#include <QStandardItemModel> + +#include "rewriterview.h" + +namespace QmlDesigner { + +namespace Internal { + +class ConnectionView; + +class BackendModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum ColumnRoles { + TypeNameColumn = 0, + PropertyNameColumn = 1, + IsSingletonColumn = 2, + IsLocalColumn = 3, + }; + + BackendModel(ConnectionView *parent); + + ConnectionView *connectionView() const; + + void resetModel(); + + QStringList possibleCppTypes() const; + CppTypeData cppTypeDataForType(const QString &typeName) const; + + void deletePropertyByRow(int rowNumber); + + void addNewBackend(); + +protected: + void updatePropertyName(int rowNumber); + +private: + void handleDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight); + +private: + ConnectionView *m_connectionView; + bool m_lock = false; +}; + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/bindingmodel.cpp b/src/plugins/qmldesigner/components/connectioneditor/bindingmodel.cpp new file mode 100644 index 0000000000..2cff12b044 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/bindingmodel.cpp @@ -0,0 +1,446 @@ +/**************************************************************************** +** +** 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 "bindingmodel.h" + +#include "connectionview.h" + +#include <nodemetainfo.h> +#include <nodeproperty.h> +#include <bindingproperty.h> +#include <variantproperty.h> +#include <rewritingexception.h> +#include <rewritertransaction.h> + +#include <QMessageBox> +#include <QTimer> + +namespace QmlDesigner { + +namespace Internal { + +BindingModel::BindingModel(ConnectionView *parent) + : QStandardItemModel(parent) + , m_connectionView(parent) +{ + connect(this, &QStandardItemModel::dataChanged, this, &BindingModel::handleDataChanged); +} + +void BindingModel::resetModel() +{ + beginResetModel(); + clear(); + setHorizontalHeaderLabels(QStringList({ tr("Item"), tr("Property"), tr("Source Item"), + tr("Source Property") })); + + foreach (const ModelNode modelNode, m_selectedModelNodes) + addModelNode(modelNode); + + endResetModel(); +} + +void BindingModel::bindingChanged(const BindingProperty &bindingProperty) +{ + m_handleDataChanged = false; + + QList<ModelNode> selectedNodes = connectionView()->selectedModelNodes(); + if (!selectedNodes.contains(bindingProperty.parentModelNode())) + return; + if (!m_lock) { + int rowNumber = findRowForBinding(bindingProperty); + + if (rowNumber == -1) { + addBindingProperty(bindingProperty); + } else { + updateBindingProperty(rowNumber); + } + } + + m_handleDataChanged = true; +} + +void BindingModel::bindingRemoved(const BindingProperty &bindingProperty) +{ + m_handleDataChanged = false; + + QList<ModelNode> selectedNodes = connectionView()->selectedModelNodes(); + if (!selectedNodes.contains(bindingProperty.parentModelNode())) + return; + if (!m_lock) { + int rowNumber = findRowForBinding(bindingProperty); + removeRow(rowNumber); + } + + m_handleDataChanged = true; +} + +void BindingModel::selectionChanged(const QList<ModelNode> &selectedNodes) +{ + m_handleDataChanged = false; + m_selectedModelNodes = selectedNodes; + resetModel(); + m_handleDataChanged = true; +} + +ConnectionView *BindingModel::connectionView() const +{ + return m_connectionView; +} + +BindingProperty BindingModel::bindingPropertyForRow(int rowNumber) const +{ + + const int internalId = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 1).toInt(); + const QString targetPropertyName = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 2).toString(); + + ModelNode modelNode = connectionView()->modelNodeForInternalId(internalId); + + if (modelNode.isValid()) + return modelNode.bindingProperty(targetPropertyName.toLatin1()); + + return BindingProperty(); +} + +QStringList BindingModel::possibleTargetProperties(const BindingProperty &bindingProperty) const +{ + const ModelNode modelNode = bindingProperty.parentModelNode(); + + if (!modelNode.isValid()) { + qWarning() << " BindingModel::possibleTargetPropertiesForRow invalid model node"; + return QStringList(); + } + + NodeMetaInfo metaInfo = modelNode.metaInfo(); + + if (metaInfo.isValid()) { + QStringList possibleProperties; + foreach (const PropertyName &propertyName, metaInfo.propertyNames()) { + if (metaInfo.propertyIsWritable(propertyName)) + possibleProperties << QString::fromUtf8(propertyName); + } + + return possibleProperties; + } + + return QStringList(); +} + +QStringList BindingModel::possibleSourceProperties(const BindingProperty &bindingProperty) const +{ + const QString expression = bindingProperty.expression(); + const QStringList stringlist = expression.split(QLatin1String(".")); + + TypeName typeName; + + if (bindingProperty.parentModelNode().metaInfo().isValid()) { + typeName = bindingProperty.parentModelNode().metaInfo().propertyTypeName(bindingProperty.name()); + } else { + qWarning() << " BindingModel::possibleSourcePropertiesForRow no meta info for target node"; + } + + const QString &id = stringlist.constFirst(); + + ModelNode modelNode = getNodeByIdOrParent(id, bindingProperty.parentModelNode()); + + if (!modelNode.isValid()) { + qWarning() << " BindingModel::possibleSourcePropertiesForRow invalid model node"; + return QStringList(); + } + + NodeMetaInfo metaInfo = modelNode.metaInfo(); + + QStringList possibleProperties; + + foreach (VariantProperty variantProperty, modelNode.variantProperties()) { + if (variantProperty.isDynamic()) + possibleProperties << QString::fromUtf8(variantProperty.name()); + } + + foreach (BindingProperty bindingProperty, modelNode.bindingProperties()) { + if (bindingProperty.isDynamic()) + possibleProperties << QString::fromUtf8((bindingProperty.name())); + } + + if (metaInfo.isValid()) { + foreach (const PropertyName &propertyName, metaInfo.propertyNames()) { + if (metaInfo.propertyTypeName(propertyName) == typeName) //### todo proper check + possibleProperties << QString::fromUtf8(propertyName); + } + } else { + qWarning() << " BindingModel::possibleSourcePropertiesForRow no meta info for source node"; + } + + return possibleProperties; +} + +void BindingModel::deleteBindindByRow(int rowNumber) +{ + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + + if (bindingProperty.isValid()) { + bindingProperty.parentModelNode().removeProperty(bindingProperty.name()); + } + + resetModel(); +} + +static PropertyName unusedProperty(const ModelNode &modelNode) +{ + PropertyName propertyName = "none"; + if (modelNode.metaInfo().isValid()) { + foreach (const PropertyName &propertyName, modelNode.metaInfo().propertyNames()) { + if (modelNode.metaInfo().propertyIsWritable(propertyName) && !modelNode.hasProperty(propertyName)) + return propertyName; + } + } + + return propertyName; +} + +void BindingModel::addBindingForCurrentNode() +{ + if (connectionView()->selectedModelNodes().count() == 1) { + const ModelNode modelNode = connectionView()->selectedModelNodes().constFirst(); + if (modelNode.isValid()) { + try { + modelNode.bindingProperty(unusedProperty(modelNode)).setExpression(QLatin1String("none.none")); + } catch (RewritingException &e) { + m_exceptionError = e.description(); + QTimer::singleShot(200, this, &BindingModel::handleException); + } + } + } else { + qWarning() << " BindingModel::addBindingForCurrentNode not one node selected"; + } +} + +void BindingModel::addBindingProperty(const BindingProperty &property) +{ + QStandardItem *idItem; + QStandardItem *targetPropertyNameItem; + QStandardItem *sourceIdItem; + QStandardItem *sourcePropertyNameItem; + + QString idLabel = property.parentModelNode().id(); + if (idLabel.isEmpty()) + idLabel = property.parentModelNode().simplifiedTypeName(); + idItem = new QStandardItem(idLabel); + updateCustomData(idItem, property); + targetPropertyNameItem = new QStandardItem(QString::fromUtf8(property.name())); + QList<QStandardItem*> items; + + items.append(idItem); + items.append(targetPropertyNameItem); + + QString sourceNodeName; + QString sourcePropertyName; + getExpressionStrings(property, &sourceNodeName, &sourcePropertyName); + + sourceIdItem = new QStandardItem(sourceNodeName); + sourcePropertyNameItem = new QStandardItem(sourcePropertyName); + + items.append(sourceIdItem); + items.append(sourcePropertyNameItem); + appendRow(items); +} + +void BindingModel::updateBindingProperty(int rowNumber) +{ + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + + if (bindingProperty.isValid()) { + QString targetPropertyName = QString::fromUtf8(bindingProperty.name()); + updateDisplayRole(rowNumber, TargetPropertyNameRow, targetPropertyName); + QString sourceNodeName; + QString sourcePropertyName; + getExpressionStrings(bindingProperty, &sourceNodeName, &sourcePropertyName); + updateDisplayRole(rowNumber, SourceModelNodeRow, sourceNodeName); + updateDisplayRole(rowNumber, SourcePropertyNameRow, sourcePropertyName); + } +} + +void BindingModel::addModelNode(const ModelNode &modelNode) +{ + foreach (const BindingProperty &bindingProperty, modelNode.bindingProperties()) { + addBindingProperty(bindingProperty); + } +} + +void BindingModel::updateExpression(int row) +{ + const QString sourceNode = data(index(row, SourceModelNodeRow)).toString().trimmed(); + const QString sourceProperty = data(index(row, SourcePropertyNameRow)).toString().trimmed(); + + QString expression; + if (sourceProperty.isEmpty()) { + expression = sourceNode; + } else { + expression = sourceNode + QLatin1String(".") + sourceProperty; + } + + connectionView()->executeInTransaction("BindingModel::updateExpression", [this, row, expression](){ + BindingProperty bindingProperty = bindingPropertyForRow(row); + bindingProperty.setExpression(expression.trimmed()); + }); +} + +void BindingModel::updatePropertyName(int rowNumber) +{ + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + + const PropertyName newName = data(index(rowNumber, TargetPropertyNameRow)).toString().toUtf8(); + const QString expression = bindingProperty.expression(); + const PropertyName dynamicPropertyType = bindingProperty.dynamicTypeName(); + ModelNode targetNode = bindingProperty.parentModelNode(); + + if (!newName.isEmpty()) { + RewriterTransaction transaction = + connectionView()->beginRewriterTransaction(QByteArrayLiteral("BindingModel::updatePropertyName")); + try { + if (bindingProperty.isDynamic()) { + targetNode.bindingProperty(newName).setDynamicTypeNameAndExpression(dynamicPropertyType, expression); + } else { + targetNode.bindingProperty(newName).setExpression(expression); + } + targetNode.removeProperty(bindingProperty.name()); + transaction.commit(); //committing in the try block + } catch (Exception &e) { //better save then sorry + m_exceptionError = e.description(); + QTimer::singleShot(200, this, &BindingModel::handleException); + } + + QStandardItem* idItem = item(rowNumber, 0); + BindingProperty newBindingProperty = targetNode.bindingProperty(newName); + updateCustomData(idItem, newBindingProperty); + + } else { + qWarning() << "BindingModel::updatePropertyName invalid property name"; + } +} + +ModelNode BindingModel::getNodeByIdOrParent(const QString &id, const ModelNode &targetNode) const +{ + ModelNode modelNode; + + if (id != QLatin1String("parent")) { + modelNode = connectionView()->modelNodeForId(id); + } else { + if (targetNode.hasParentProperty()) { + modelNode = targetNode.parentProperty().parentModelNode(); + } + } + return modelNode; +} + +void BindingModel::updateCustomData(QStandardItem *item, const BindingProperty &bindingProperty) +{ + item->setData(bindingProperty.parentModelNode().internalId(), Qt::UserRole + 1); + item->setData(bindingProperty.name(), Qt::UserRole + 2); +} + +int BindingModel::findRowForBinding(const BindingProperty &bindingProperty) +{ + for (int i=0; i < rowCount(); i++) { + if (compareBindingProperties(bindingPropertyForRow(i), bindingProperty)) + return i; + } + //not found + return -1; +} + +bool BindingModel::getExpressionStrings(const BindingProperty &bindingProperty, QString *sourceNode, QString *sourceProperty) +{ + //### todo we assume no expressions yet + + const QString expression = bindingProperty.expression(); + + if (true) { + const QStringList stringList = expression.split(QLatin1String(".")); + + *sourceNode = stringList.constFirst(); + + QString propertyName; + + for (int i=1; i < stringList.count(); i++) { + propertyName += stringList.at(i); + if (i != stringList.count() - 1) + propertyName += QLatin1String("."); + } + *sourceProperty = propertyName; + } + return true; +} + +void BindingModel::updateDisplayRole(int row, int columns, const QString &string) +{ + QModelIndex modelIndex = index(row, columns); + if (data(modelIndex).toString() != string) + setData(modelIndex, string); +} + +void BindingModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!m_handleDataChanged) + return; + + if (topLeft != bottomRight) { + qWarning() << "BindingModel::handleDataChanged multi edit?"; + return; + } + + m_lock = true; + + int currentColumn = topLeft.column(); + int currentRow = topLeft.row(); + + switch (currentColumn) { + case TargetModelNodeRow: { + //updating user data + } break; + case TargetPropertyNameRow: { + updatePropertyName(currentRow); + } break; + case SourceModelNodeRow: { + updateExpression(currentRow); + } break; + case SourcePropertyNameRow: { + updateExpression(currentRow); + } break; + + default: qWarning() << "BindingModel::handleDataChanged column" << currentColumn; + } + + m_lock = false; +} + +void BindingModel::handleException() +{ + QMessageBox::warning(nullptr, tr("Error"), m_exceptionError); + resetModel(); +} + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/bindingmodel.h b/src/plugins/qmldesigner/components/connectioneditor/bindingmodel.h new file mode 100644 index 0000000000..480ba254ad --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/bindingmodel.h @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <modelnode.h> +#include <bindingproperty.h> +#include <variantproperty.h> + +#include <QStandardItemModel> + +namespace QmlDesigner { + +namespace Internal { + +class ConnectionView; + +class BindingModel : public QStandardItemModel +{ + Q_OBJECT + +public: + enum ColumnRoles { + TargetModelNodeRow = 0, + TargetPropertyNameRow = 1, + SourceModelNodeRow = 2, + SourcePropertyNameRow = 3 + }; + BindingModel(ConnectionView *parent = nullptr); + void bindingChanged(const BindingProperty &bindingProperty); + void bindingRemoved(const BindingProperty &bindingProperty); + void selectionChanged(const QList<ModelNode> &selectedNodes); + + ConnectionView *connectionView() const; + BindingProperty bindingPropertyForRow(int rowNumber) const; + QStringList possibleTargetProperties(const BindingProperty &bindingProperty) const; + QStringList possibleSourceProperties(const BindingProperty &bindingProperty) const; + void deleteBindindByRow(int rowNumber); + void addBindingForCurrentNode(); + void resetModel(); + +protected: + void addBindingProperty(const BindingProperty &property); + void updateBindingProperty(int rowNumber); + void addModelNode(const ModelNode &modelNode); + void updateExpression(int row); + void updatePropertyName(int rowNumber); + ModelNode getNodeByIdOrParent(const QString &id, const ModelNode &targetNode) const; + void updateCustomData(QStandardItem *item, const BindingProperty &bindingProperty); + int findRowForBinding(const BindingProperty &bindingProperty); + + bool getExpressionStrings(const BindingProperty &bindingProperty, QString *sourceNode, QString *sourceProperty); + + void updateDisplayRole(int row, int columns, const QString &string); + +private: + void handleDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight); + void handleException(); + +private: + QList<ModelNode> m_selectedModelNodes; + ConnectionView *m_connectionView; + bool m_lock = false; + bool m_handleDataChanged = false; + QString m_exceptionError; + +}; + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectioneditor.pri b/src/plugins/qmldesigner/components/connectioneditor/connectioneditor.pri new file mode 100644 index 0000000000..5dfa3160ab --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectioneditor.pri @@ -0,0 +1,26 @@ +VPATH += $$PWD +INCLUDEPATH += $$PWD + +HEADERS += delegates.h \ + connectionview.h \ + connectionviewwidget.h \ + connectionmodel.h \ + bindingmodel.h \ + dynamicpropertiesmodel.h \ + backendmodel.h \ + $$PWD/addnewbackenddialog.h + +SOURCES += delegates.cpp \ + connectionview.cpp \ + connectionviewwidget.cpp \ + connectionmodel.cpp \ + bindingmodel.cpp \ + dynamicpropertiesmodel.cpp \ + backendmodel.cpp \ + $$PWD/addnewbackenddialog.cpp + +FORMS += \ + connectionviewwidget.ui \ + $$PWD/addnewbackenddialog.ui + +RESOURCES += connectioneditor.qrc diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectioneditor.qrc b/src/plugins/qmldesigner/components/connectioneditor/connectioneditor.qrc new file mode 100644 index 0000000000..a313b6648d --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectioneditor.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/connectionview"> + <file>stylesheet.css</file> + </qresource> +</RCC> diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp new file mode 100644 index 0000000000..80caf51ce0 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.cpp @@ -0,0 +1,357 @@ +/**************************************************************************** +** +** 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 "connectionmodel.h" +#include "connectionview.h" + +#include <bindingproperty.h> +#include <variantproperty.h> +#include <signalhandlerproperty.h> +#include <rewritertransaction.h> +#include <nodeabstractproperty.h> +#include <exception.h> +#include <nodemetainfo.h> + +#include <QStandardItemModel> +#include <QMessageBox> +#include <QTableView> +#include <QTimer> + +namespace { + +QStringList propertyNameListToStringList(const QmlDesigner::PropertyNameList &propertyNameList) +{ + QStringList stringList; + foreach (QmlDesigner::PropertyName propertyName, propertyNameList) { + stringList << QString::fromUtf8(propertyName); + } + return stringList; +} + +bool isConnection(const QmlDesigner::ModelNode &modelNode) +{ + return (modelNode.type() == "Connections" + || modelNode.type() == "QtQuick.Connections" + || modelNode.type() == "Qt.Connections"); + +} + +} //namespace + +namespace QmlDesigner { + +namespace Internal { + +ConnectionModel::ConnectionModel(ConnectionView *parent) + : QStandardItemModel(parent) + , m_connectionView(parent) +{ + connect(this, &QStandardItemModel::dataChanged, this, &ConnectionModel::handleDataChanged); +} + +void ConnectionModel::resetModel() +{ + beginResetModel(); + clear(); + setHorizontalHeaderLabels(QStringList({ tr("Target"), tr("Signal Handler"), tr("Action") })); + + if (connectionView()->isAttached()) { + foreach (const ModelNode modelNode, connectionView()->allModelNodes()) + addModelNode(modelNode); + } + + const int columnWidthTarget = connectionView()->connectionTableView()->columnWidth(0); + connectionView()->connectionTableView()->setColumnWidth(0, columnWidthTarget - 80); + + endResetModel(); +} + +SignalHandlerProperty ConnectionModel::signalHandlerPropertyForRow(int rowNumber) const +{ + const int internalId = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 1).toInt(); + const QString targetPropertyName = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 2).toString(); + + ModelNode modelNode = connectionView()->modelNodeForInternalId(internalId); + + if (modelNode.isValid()) + return modelNode.signalHandlerProperty(targetPropertyName.toUtf8()); + + return SignalHandlerProperty(); +} + +void ConnectionModel::addModelNode(const ModelNode &modelNode) +{ + if (isConnection(modelNode)) + addConnection(modelNode); +} + +void ConnectionModel::addConnection(const ModelNode &modelNode) +{ + foreach (const AbstractProperty &property, modelNode.properties()) { + if (property.isSignalHandlerProperty() && property.name() != "target") { + addSignalHandler(property.toSignalHandlerProperty()); + } + } +} + +void ConnectionModel::addSignalHandler(const SignalHandlerProperty &signalHandlerProperty) +{ + QStandardItem *targetItem; + QStandardItem *signalItem; + QStandardItem *actionItem; + + QString idLabel; + + ModelNode connectionsModelNode = signalHandlerProperty.parentModelNode(); + + if (connectionsModelNode.bindingProperty("target").isValid()) { + idLabel =connectionsModelNode.bindingProperty("target").expression(); + } + + targetItem = new QStandardItem(idLabel); + updateCustomData(targetItem, signalHandlerProperty); + const QString propertyName = QString::fromUtf8(signalHandlerProperty.name()); + const QString source = signalHandlerProperty.source(); + + signalItem = new QStandardItem(propertyName); + QList<QStandardItem*> items; + + items.append(targetItem); + items.append(signalItem); + + actionItem = new QStandardItem(source); + + items.append(actionItem); + + appendRow(items); +} + +void ConnectionModel::removeModelNode(const ModelNode &modelNode) +{ + if (isConnection(modelNode)) + removeConnection(modelNode); +} + +void ConnectionModel::removeConnection(const ModelNode & /*modelNode*/) +{ + Q_ASSERT_X(false, "not implemented", Q_FUNC_INFO); +} + +void ConnectionModel::updateSource(int row) +{ + SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row); + + const QString sourceString = data(index(row, SourceRow)).toString(); + + RewriterTransaction transaction = + connectionView()->beginRewriterTransaction(QByteArrayLiteral("ConnectionModel::updateSource")); + + try { + signalHandlerProperty.setSource(sourceString); + transaction.commit(); + } + catch (Exception &e) { + m_exceptionError = e.description(); + QTimer::singleShot(200, this, &ConnectionModel::handleException); + } + +} + +void ConnectionModel::updateSignalName(int rowNumber) +{ + SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(rowNumber); + ModelNode connectionNode = signalHandlerProperty.parentModelNode(); + + const PropertyName newName = data(index(rowNumber, TargetPropertyNameRow)).toString().toUtf8(); + if (!newName.isEmpty()) { + connectionView()->executeInTransaction("ConnectionModel::updateSignalName", [=, &connectionNode](){ + + const QString source = signalHandlerProperty.source(); + + connectionNode.signalHandlerProperty(newName).setSource(source); + connectionNode.removeProperty(signalHandlerProperty.name()); + }); + + QStandardItem* idItem = item(rowNumber, 0); + SignalHandlerProperty newSignalHandlerProperty = connectionNode.signalHandlerProperty(newName); + updateCustomData(idItem, newSignalHandlerProperty); + } else { + qWarning() << "BindingModel::updatePropertyName invalid property name"; + } +} + +void ConnectionModel::updateTargetNode(int rowNumber) +{ + SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(rowNumber); + const QString newTarget = data(index(rowNumber, TargetModelNodeRow)).toString(); + ModelNode connectionNode = signalHandlerProperty.parentModelNode(); + + if (!newTarget.isEmpty()) { + connectionView()->executeInTransaction("ConnectionModel::updateTargetNode", [= ,&connectionNode](){ + connectionNode.bindingProperty("target").setExpression(newTarget); + }); + + QStandardItem* idItem = item(rowNumber, 0); + updateCustomData(idItem, signalHandlerProperty); + + } else { + qWarning() << "BindingModel::updatePropertyName invalid target id"; + } +} + +void ConnectionModel::updateCustomData(QStandardItem *item, const SignalHandlerProperty &signalHandlerProperty) +{ + item->setData(signalHandlerProperty.parentModelNode().internalId(), Qt::UserRole + 1); + item->setData(signalHandlerProperty.name(), Qt::UserRole + 2); +} + +ModelNode ConnectionModel::getTargetNodeForConnection(const ModelNode &connection) const +{ + BindingProperty bindingProperty = connection.bindingProperty("target"); + + if (bindingProperty.isValid()) { + if (bindingProperty.expression() == QLatin1String("parent")) + return connection.parentProperty().parentModelNode(); + return connectionView()->modelNodeForId(bindingProperty.expression()); + } + + return ModelNode(); +} + +void ConnectionModel::addConnection() +{ + ModelNode rootModelNode = connectionView()->rootModelNode(); + + if (rootModelNode.isValid() && rootModelNode.metaInfo().isValid()) { + + NodeMetaInfo nodeMetaInfo = connectionView()->model()->metaInfo("QtQuick.Connections"); + + if (nodeMetaInfo.isValid()) { + connectionView()->executeInTransaction("ConnectionModel::addConnection", [=](){ + ModelNode newNode = connectionView()->createModelNode("QtQuick.Connections", + nodeMetaInfo.majorVersion(), + nodeMetaInfo.minorVersion()); + + rootModelNode.nodeAbstractProperty(rootModelNode.metaInfo().defaultPropertyName()).reparentHere(newNode); + newNode.signalHandlerProperty("onClicked").setSource(QLatin1String("print(\"clicked\")")); + + if (connectionView()->selectedModelNodes().count() == 1 + && !connectionView()->selectedModelNodes().constFirst().id().isEmpty()) { + const ModelNode selectedNode = connectionView()->selectedModelNodes().constFirst(); + newNode.bindingProperty("target").setExpression(selectedNode.id()); + } else { + newNode.bindingProperty("target").setExpression(QLatin1String("parent")); + } + }); + } + } +} + +void ConnectionModel::bindingPropertyChanged(const BindingProperty &bindingProperty) +{ + if (isConnection(bindingProperty.parentModelNode())) + resetModel(); +} + +void ConnectionModel::variantPropertyChanged(const VariantProperty &variantProperty) +{ + if (isConnection(variantProperty.parentModelNode())) + resetModel(); +} + +void ConnectionModel::deleteConnectionByRow(int currentRow) +{ + signalHandlerPropertyForRow(currentRow).parentModelNode().destroy(); +} + +void ConnectionModel::handleException() +{ + QMessageBox::warning(nullptr, tr("Error"), m_exceptionError); + resetModel(); +} + +void ConnectionModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (topLeft != bottomRight) { + qWarning() << "ConnectionModel::handleDataChanged multi edit?"; + return; + } + + m_lock = true; + + int currentColumn = topLeft.column(); + int currentRow = topLeft.row(); + + switch (currentColumn) { + case TargetModelNodeRow: { + updateTargetNode(currentRow); + } break; + case TargetPropertyNameRow: { + updateSignalName(currentRow); + } break; + case SourceRow: { + updateSource(currentRow); + } break; + + default: qWarning() << "ConnectionModel::handleDataChanged column" << currentColumn; + } + + m_lock = false; +} + +ConnectionView *ConnectionModel::connectionView() const +{ + return m_connectionView; +} + +QStringList ConnectionModel::getSignalsForRow(int row) const +{ + QStringList stringList; + SignalHandlerProperty signalHandlerProperty = signalHandlerPropertyForRow(row); + + if (signalHandlerProperty.isValid()) { + stringList.append(getPossibleSignalsForConnection(signalHandlerProperty.parentModelNode())); + } + + return stringList; +} + +QStringList ConnectionModel::getPossibleSignalsForConnection(const ModelNode &connection) const +{ + QStringList stringList; + + if (connection.isValid()) { + ModelNode targetNode = getTargetNodeForConnection(connection); + if (targetNode.isValid() && targetNode.metaInfo().isValid()) { + stringList.append(propertyNameListToStringList(targetNode.metaInfo().signalNames())); + } + } + + return stringList; +} + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h new file mode 100644 index 0000000000..2c66b2ef25 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionmodel.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QStandardItemModel> + +namespace QmlDesigner { + +class ModelNode; +class BindingProperty; +class SignalHandlerProperty; +class VariantProperty; + +namespace Internal { + +class ConnectionView; + +class ConnectionModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum ColumnRoles { + TargetModelNodeRow = 0, + TargetPropertyNameRow = 1, + SourceRow = 2 + }; + ConnectionModel(ConnectionView *parent = nullptr); + void resetModel(); + SignalHandlerProperty signalHandlerPropertyForRow(int rowNumber) const; + ConnectionView *connectionView() 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 deleteConnectionByRow(int currentRow); + +protected: + void addModelNode(const ModelNode &modelNode); + void addConnection(const ModelNode &modelNode); + void addSignalHandler(const SignalHandlerProperty &bindingProperty); + void removeModelNode(const ModelNode &modelNode); + void removeConnection(const ModelNode &modelNode); + void updateSource(int row); + void updateSignalName(int rowNumber); + void updateTargetNode(int rowNumber); + void updateCustomData(QStandardItem *item, const SignalHandlerProperty &signalHandlerProperty); + QStringList getPossibleSignalsForConnection(const ModelNode &connection) const; + +private: + void handleDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight); + void handleException(); + +private: + ConnectionView *m_connectionView; + bool m_lock = false; + QString m_exceptionError; +}; + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp b/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp new file mode 100644 index 0000000000..9aef01259e --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionview.cpp @@ -0,0 +1,222 @@ +/**************************************************************************** +** +** 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 "connectionview.h" +#include "connectionviewwidget.h" + +#include "backendmodel.h" +#include "bindingmodel.h" +#include "connectionmodel.h" +#include "dynamicpropertiesmodel.h" + +#include <bindingproperty.h> +#include <nodeabstractproperty.h> +#include <variantproperty.h> + +namespace QmlDesigner { + +namespace Internal { + +ConnectionView::ConnectionView(QObject *parent) : AbstractView(parent), + m_connectionViewWidget(new ConnectionViewWidget()), + m_connectionModel(new ConnectionModel(this)), + m_bindingModel(new BindingModel(this)), + m_dynamicPropertiesModel(new DynamicPropertiesModel(this)), + m_backendModel(new BackendModel(this)) +{ + connectionViewWidget()->setBindingModel(m_bindingModel); + connectionViewWidget()->setConnectionModel(m_connectionModel); + connectionViewWidget()->setDynamicPropertiesModel(m_dynamicPropertiesModel); + connectionViewWidget()->setBackendModel(m_backendModel); +} + +ConnectionView::~ConnectionView() = default; + +void ConnectionView::modelAttached(Model *model) +{ + AbstractView::modelAttached(model); + bindingModel()->selectionChanged(QList<ModelNode>()); + dynamicPropertiesModel()->selectionChanged(QList<ModelNode>()); + connectionModel()->resetModel(); + connectionViewWidget()->resetItemViews(); + backendModel()->resetModel(); +} + +void ConnectionView::modelAboutToBeDetached(Model *model) +{ + AbstractView::modelAboutToBeDetached(model); + bindingModel()->selectionChanged(QList<ModelNode>()); + dynamicPropertiesModel()->selectionChanged(QList<ModelNode>()); + connectionModel()->resetModel(); + connectionViewWidget()->resetItemViews(); +} + +void ConnectionView::nodeCreated(const ModelNode & /*createdNode*/) +{ +//bindings + connectionModel()->resetModel(); +} + +void ConnectionView::nodeRemoved(const ModelNode & /*removedNode*/, + const NodeAbstractProperty & /*parentProperty*/, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + connectionModel()->resetModel(); +} + +void ConnectionView::nodeReparented(const ModelNode & /*node*/, const NodeAbstractProperty & /*newPropertyParent*/, + const NodeAbstractProperty & /*oldPropertyParent*/, AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + connectionModel()->resetModel(); +} + +void ConnectionView::nodeIdChanged(const ModelNode & /*node*/, const QString & /*newId*/, const QString & /*oldId*/) +{ + connectionModel()->resetModel(); + bindingModel()->resetModel(); + dynamicPropertiesModel()->resetModel(); +} + +void ConnectionView::propertiesAboutToBeRemoved(const QList<AbstractProperty> & propertyList) +{ + foreach (const AbstractProperty &property, propertyList) { + if (property.isBindingProperty()) { + bindingModel()->bindingRemoved(property.toBindingProperty()); + dynamicPropertiesModel()->bindingRemoved(property.toBindingProperty()); + } else if (property.isVariantProperty()) { + //### dynamicPropertiesModel->bindingRemoved(property.toVariantProperty()); + } + } +} + +void ConnectionView::variantPropertiesChanged(const QList<VariantProperty> &propertyList, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + foreach (const VariantProperty &variantProperty, propertyList) { + if (variantProperty.isDynamic()) + dynamicPropertiesModel()->variantPropertyChanged(variantProperty); + if (variantProperty.isDynamic() && variantProperty.parentModelNode().isRootNode()) + backendModel()->resetModel(); + + connectionModel()->variantPropertyChanged(variantProperty); + } + +} + +void ConnectionView::bindingPropertiesChanged(const QList<BindingProperty> &propertyList, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + foreach (const BindingProperty &bindingProperty, propertyList) { + bindingModel()->bindingChanged(bindingProperty); + if (bindingProperty.isDynamic()) + dynamicPropertiesModel()->bindingPropertyChanged(bindingProperty); + if (bindingProperty.isDynamic() && bindingProperty.parentModelNode().isRootNode()) + backendModel()->resetModel(); + + connectionModel()->bindingPropertyChanged(bindingProperty); + } +} + +void ConnectionView::selectedNodesChanged(const QList<ModelNode> & selectedNodeList, + const QList<ModelNode> & /*lastSelectedNodeList*/) +{ + bindingModel()->selectionChanged(selectedNodeList); + dynamicPropertiesModel()->selectionChanged(selectedNodeList); + connectionViewWidget()->bindingTableViewSelectionChanged(QModelIndex(), QModelIndex()); + connectionViewWidget()->dynamicPropertiesTableViewSelectionChanged(QModelIndex(), QModelIndex()); + + if (connectionViewWidget()->currentTab() == ConnectionViewWidget::BindingTab + || connectionViewWidget()->currentTab() == ConnectionViewWidget::DynamicPropertiesTab) + emit connectionViewWidget()->setEnabledAddButton(selectedNodeList.count() == 1); +} + +void ConnectionView::importsChanged(const QList<Import> & /*addedImports*/, const QList<Import> & /*removedImports*/) +{ + backendModel()->resetModel(); +} + +WidgetInfo ConnectionView::widgetInfo() +{ + return createWidgetInfo(m_connectionViewWidget.data(), + new WidgetInfo::ToolBarWidgetDefaultFactory<ConnectionViewWidget>(connectionViewWidget()), + QLatin1String("ConnectionView"), + WidgetInfo::LeftPane, + 0, + tr("Connection View")); +} + +bool ConnectionView::hasWidget() const +{ + return true; +} + +QTableView *ConnectionView::connectionTableView() const +{ + return connectionViewWidget()->connectionTableView(); +} + +QTableView *ConnectionView::bindingTableView() const +{ + return connectionViewWidget()->bindingTableView(); +} + +QTableView *ConnectionView::dynamicPropertiesTableView() const +{ + return connectionViewWidget()->dynamicPropertiesTableView(); +} + +QTableView *ConnectionView::backendView() const +{ + return connectionViewWidget()->backendView(); +} + +ConnectionViewWidget *ConnectionView::connectionViewWidget() const +{ + return m_connectionViewWidget.data(); +} + +ConnectionModel *ConnectionView::connectionModel() const +{ + return m_connectionModel; +} + +BindingModel *ConnectionView::bindingModel() const +{ + return m_bindingModel; +} + +DynamicPropertiesModel *ConnectionView::dynamicPropertiesModel() const +{ + return m_dynamicPropertiesModel; +} + +BackendModel *ConnectionView::backendModel() const +{ + return m_backendModel; +} + +} // namesapce Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionview.h b/src/plugins/qmldesigner/components/connectioneditor/connectionview.h new file mode 100644 index 0000000000..da7623375a --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionview.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <abstractview.h> +#include <qmlitemnode.h> + +#include <QPointer> + +QT_BEGIN_NAMESPACE +class QTableView; +class QListView; +QT_END_NAMESPACE + +namespace QmlDesigner { + +namespace Internal { + +class ConnectionViewWidget; +class BindingModel; +class ConnectionModel; +class DynamicPropertiesModel; +class BackendModel; + +class ConnectionView : public AbstractView +{ + Q_OBJECT + +public: + ConnectionView(QObject *parent = nullptr); + ~ConnectionView() override; + + // AbstractView + void modelAttached(Model *model) override; + void modelAboutToBeDetached(Model *model) override; + + void nodeCreated(const ModelNode &createdNode) override; + void nodeRemoved(const ModelNode &removedNode, const NodeAbstractProperty &parentProperty, PropertyChangeFlags propertyChange) override; + void nodeReparented(const ModelNode &node, const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, AbstractView::PropertyChangeFlags propertyChange) override; + void nodeIdChanged(const ModelNode& node, const QString& newId, const QString& oldId) override; + void propertiesAboutToBeRemoved(const QList<AbstractProperty>& propertyList) override; + void variantPropertiesChanged(const QList<VariantProperty>& propertyList, PropertyChangeFlags propertyChange) override; + void bindingPropertiesChanged(const QList<BindingProperty>& propertyList, PropertyChangeFlags propertyChange) override; + + void selectedNodesChanged(const QList<ModelNode> &selectedNodeList, + const QList<ModelNode> &lastSelectedNodeList) override; + + void importsChanged(const QList<Import> &addedImports, const QList<Import> &removedImports) override; + + WidgetInfo widgetInfo() override; + bool hasWidget() const override; + + QTableView *connectionTableView() const; + QTableView *bindingTableView() const; + QTableView *dynamicPropertiesTableView() const; + QTableView *backendView() const; + +protected: + ConnectionViewWidget *connectionViewWidget() const; + ConnectionModel *connectionModel() const; + BindingModel *bindingModel() const; + DynamicPropertiesModel *dynamicPropertiesModel() const; + BackendModel *backendModel() const; + + +private: //variables + QPointer<ConnectionViewWidget> m_connectionViewWidget; + ConnectionModel *m_connectionModel; + BindingModel *m_bindingModel; + DynamicPropertiesModel *m_dynamicPropertiesModel; + BackendModel *m_backendModel; +}; + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.cpp b/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.cpp new file mode 100644 index 0000000000..c9b1277ce4 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.cpp @@ -0,0 +1,348 @@ +/**************************************************************************** +** +** 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 "connectionviewwidget.h" +#include "connectionview.h" +#include "ui_connectionviewwidget.h" + +#include "delegates.h" +#include "backendmodel.h" +#include "bindingmodel.h" +#include "connectionmodel.h" +#include "dynamicpropertiesmodel.h" +#include "theme.h" + +#include <designersettings.h> +#include <qmldesignerplugin.h> + +#include <coreplugin/coreconstants.h> +#include <utils/fileutils.h> +#include <utils/utilsicons.h> + +#include <QToolButton> +#include <QStyleFactory> + +namespace QmlDesigner { + +namespace Internal { + +ConnectionViewWidget::ConnectionViewWidget(QWidget *parent) : + QFrame(parent), + ui(new Ui::ConnectionViewWidget) +{ + + setWindowTitle(tr("Connections", "Title of connection view")); + ui->setupUi(this); + + QStyle *style = QStyleFactory::create("fusion"); + setStyle(style); + + setStyleSheet(Theme::replaceCssColors(QLatin1String(Utils::FileReader::fetchQrc(QLatin1String(":/connectionview/stylesheet.css"))))); + + //ui->tabWidget->tabBar()->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + ui->tabBar->setUsesScrollButtons(true); + ui->tabBar->setElideMode(Qt::ElideRight); + + ui->tabBar->addTab(tr("Connections", "Title of connection view")); + ui->tabBar->addTab(tr("Bindings", "Title of connection view")); + ui->tabBar->addTab(tr("Properties", "Title of dynamic properties view")); + + auto settings = QmlDesignerPlugin::instance()->settings(); + + if (!settings.value(DesignerSettingsKey::STANDALONE_MODE).toBool()) + ui->tabBar->addTab(tr("Backends", "Title of dynamic properties view")); + + ui->tabBar->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); + + const QString themedScrollBarCss = Theme::replaceCssColors( + QLatin1String(Utils::FileReader::fetchQrc(QLatin1String(":/qmldesigner/scrollbar.css")))); + + ui->connectionView->setStyleSheet(themedScrollBarCss); + ui->bindingView->setStyleSheet(themedScrollBarCss); + ui->dynamicPropertiesView->setStyleSheet(themedScrollBarCss); + ui->backendView->setStyleSheet(themedScrollBarCss); + + connect(ui->tabBar, &QTabBar::currentChanged, + ui->stackedWidget, &QStackedWidget::setCurrentIndex); + + connect(ui->tabBar, &QTabBar::currentChanged, + this, &ConnectionViewWidget::handleTabChanged); + + ui->stackedWidget->setCurrentIndex(0); +} + +ConnectionViewWidget::~ConnectionViewWidget() +{ + delete ui; +} + +void ConnectionViewWidget::setBindingModel(BindingModel *model) +{ + ui->bindingView->setModel(model); + ui->bindingView->verticalHeader()->hide(); + ui->bindingView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->bindingView->setItemDelegate(new BindingDelegate); + connect(ui->bindingView->selectionModel(), &QItemSelectionModel::currentRowChanged, + this, &ConnectionViewWidget::bindingTableViewSelectionChanged); +} + +void ConnectionViewWidget::setConnectionModel(ConnectionModel *model) +{ + ui->connectionView->setModel(model); + ui->connectionView->verticalHeader()->hide(); + ui->connectionView->horizontalHeader()->setDefaultSectionSize(160); + ui->connectionView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->connectionView->setItemDelegate(new ConnectionDelegate); + connect(ui->connectionView->selectionModel(), &QItemSelectionModel::currentRowChanged, + this, &ConnectionViewWidget::connectionTableViewSelectionChanged); + +} + +void ConnectionViewWidget::setDynamicPropertiesModel(DynamicPropertiesModel *model) +{ + ui->dynamicPropertiesView->setModel(model); + ui->dynamicPropertiesView->verticalHeader()->hide(); + ui->dynamicPropertiesView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->dynamicPropertiesView->setItemDelegate(new DynamicPropertiesDelegate); + connect(ui->dynamicPropertiesView->selectionModel(), &QItemSelectionModel::currentRowChanged, + this, &ConnectionViewWidget::dynamicPropertiesTableViewSelectionChanged); +} + +void ConnectionViewWidget::setBackendModel(BackendModel *model) +{ + ui->backendView->setModel(model); + ui->backendView->verticalHeader()->hide(); + ui->backendView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->backendView->setItemDelegate(new BackendDelegate); + model->resetModel(); + connect(ui->backendView->selectionModel(), &QItemSelectionModel::currentRowChanged, + this, &ConnectionViewWidget::backendTableViewSelectionChanged); +} + +QList<QToolButton *> ConnectionViewWidget::createToolBarWidgets() +{ + QList<QToolButton *> buttons; + + buttons << new QToolButton(); + buttons.constLast()->setIcon(Utils::Icons::PLUS_TOOLBAR.icon()); + buttons.constLast()->setToolTip(tr("Add binding or connection.")); + connect(buttons.constLast(), &QAbstractButton::clicked, this, &ConnectionViewWidget::addButtonClicked); + connect(this, &ConnectionViewWidget::setEnabledAddButton, buttons.constLast(), &QWidget::setEnabled); + + buttons << new QToolButton(); + buttons.constLast()->setIcon(Utils::Icons::MINUS.icon()); + buttons.constLast()->setToolTip(tr("Remove selected binding or connection.")); + buttons.constLast()->setShortcut(QKeySequence(Qt::Key_Delete)); + connect(buttons.constLast(), &QAbstractButton::clicked, this, &ConnectionViewWidget::removeButtonClicked); + connect(this, &ConnectionViewWidget::setEnabledRemoveButton, buttons.constLast(), &QWidget::setEnabled); + + return buttons; +} + +ConnectionViewWidget::TabStatus ConnectionViewWidget::currentTab() const +{ + switch (ui->stackedWidget->currentIndex()) { + case 0: return ConnectionTab; + case 1: return BindingTab; + case 2: return DynamicPropertiesTab; + case 3: return BackendTab; + default: return InvalidTab; + } +} + +void ConnectionViewWidget::resetItemViews() +{ + if (currentTab() == ConnectionTab) { + ui->connectionView->selectionModel()->clear(); + + } else if (currentTab() == BindingTab) { + ui->bindingView->selectionModel()->clear(); + + } else if (currentTab() == DynamicPropertiesTab) { + ui->dynamicPropertiesView->selectionModel()->clear(); + } else if (currentTab() == BackendTab) { + ui->backendView->selectionModel()->clear(); + } + invalidateButtonStatus(); +} + +void ConnectionViewWidget::invalidateButtonStatus() +{ + if (currentTab() == ConnectionTab) { + emit setEnabledRemoveButton(ui->connectionView->selectionModel()->hasSelection()); + emit setEnabledAddButton(true); + } else if (currentTab() == BindingTab) { + emit setEnabledRemoveButton(ui->bindingView->selectionModel()->hasSelection()); + auto bindingModel = qobject_cast<BindingModel*>(ui->bindingView->model()); + emit setEnabledAddButton(bindingModel->connectionView()->model() && + bindingModel->connectionView()->selectedModelNodes().count() == 1); + + } else if (currentTab() == DynamicPropertiesTab) { + emit setEnabledRemoveButton(ui->dynamicPropertiesView->selectionModel()->hasSelection()); + auto dynamicPropertiesModel = qobject_cast<DynamicPropertiesModel*>(ui->dynamicPropertiesView->model()); + emit setEnabledAddButton(dynamicPropertiesModel->connectionView()->model() && + dynamicPropertiesModel->connectionView()->selectedModelNodes().count() == 1); + } else if (currentTab() == BackendTab) { + emit setEnabledAddButton(true); + emit setEnabledRemoveButton(ui->backendView->selectionModel()->hasSelection()); + } +} + +QTableView *ConnectionViewWidget::connectionTableView() const +{ + return ui->connectionView; +} + +QTableView *ConnectionViewWidget::bindingTableView() const +{ + return ui->bindingView; +} + +QTableView *ConnectionViewWidget::dynamicPropertiesTableView() const +{ + return ui->dynamicPropertiesView; +} + +QTableView *ConnectionViewWidget::backendView() const +{ + return ui->backendView; +} + +void ConnectionViewWidget::handleTabChanged(int) +{ + invalidateButtonStatus(); +} + +void ConnectionViewWidget::removeButtonClicked() +{ + if (currentTab() == ConnectionTab) { + if (ui->connectionView->selectionModel()->selectedRows().isEmpty()) + return; + int currentRow = ui->connectionView->selectionModel()->selectedRows().constFirst().row(); + auto connectionModel = qobject_cast<ConnectionModel*>(ui->connectionView->model()); + if (connectionModel) { + connectionModel->deleteConnectionByRow(currentRow); + } + } else if (currentTab() == BindingTab) { + if (ui->bindingView->selectionModel()->selectedRows().isEmpty()) + return; + int currentRow = ui->bindingView->selectionModel()->selectedRows().constFirst().row(); + auto bindingModel = qobject_cast<BindingModel*>(ui->bindingView->model()); + if (bindingModel) { + bindingModel->deleteBindindByRow(currentRow); + } + } else if (currentTab() == DynamicPropertiesTab) { + if (ui->dynamicPropertiesView->selectionModel()->selectedRows().isEmpty()) + return; + int currentRow = ui->dynamicPropertiesView->selectionModel()->selectedRows().constFirst().row(); + auto dynamicPropertiesModel = qobject_cast<DynamicPropertiesModel*>(ui->dynamicPropertiesView->model()); + if (dynamicPropertiesModel) + dynamicPropertiesModel->deleteDynamicPropertyByRow(currentRow); + } else if (currentTab() == BackendTab) { + int currentRow = ui->backendView->selectionModel()->selectedRows().constFirst().row(); + auto backendModel = qobject_cast<BackendModel*>(ui->backendView->model()); + if (backendModel) + backendModel->deletePropertyByRow(currentRow); + } + + invalidateButtonStatus(); +} + +void ConnectionViewWidget::addButtonClicked() +{ + + if (currentTab() == ConnectionTab) { + auto connectionModel = qobject_cast<ConnectionModel*>(ui->connectionView->model()); + if (connectionModel) { + connectionModel->addConnection(); + } + } else if (currentTab() == BindingTab) { + auto bindingModel = qobject_cast<BindingModel*>(ui->bindingView->model()); + if (bindingModel) { + bindingModel->addBindingForCurrentNode(); + } + + } else if (currentTab() == DynamicPropertiesTab) { + auto dynamicPropertiesModel = qobject_cast<DynamicPropertiesModel*>(ui->dynamicPropertiesView->model()); + if (dynamicPropertiesModel) + dynamicPropertiesModel->addDynamicPropertyForCurrentNode(); + } else if (currentTab() == BackendTab) { + auto backendModel = qobject_cast<BackendModel*>(ui->backendView->model()); + if (backendModel) + backendModel->addNewBackend(); + } + + invalidateButtonStatus(); +} + +void ConnectionViewWidget::bindingTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + if (currentTab() == BindingTab) { + if (current.isValid()) { + emit setEnabledRemoveButton(true); + } else { + emit setEnabledRemoveButton(false); + } + } +} + +void ConnectionViewWidget::connectionTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + if (currentTab() == ConnectionTab) { + if (current.isValid()) { + emit setEnabledRemoveButton(true); + } else { + emit setEnabledRemoveButton(false); + } + } +} + +void ConnectionViewWidget::dynamicPropertiesTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*previous*/) +{ + if (currentTab() == DynamicPropertiesTab) { + if (current.isValid()) { + emit setEnabledRemoveButton(true); + } else { + emit setEnabledRemoveButton(false); + } + } +} + +void ConnectionViewWidget::backendTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex & /*revious*/) +{ + if (currentTab() == BackendTab) { + if (current.isValid()) { + emit setEnabledRemoveButton(true); + } else { + emit setEnabledRemoveButton(false); + } + } + +} + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.h b/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.h new file mode 100644 index 0000000000..2bcc3932c1 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.h @@ -0,0 +1,102 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QFrame> +#include <QAbstractItemView> + +QT_BEGIN_NAMESPACE +class QToolButton; +class QTableView; +class QListView; +QT_END_NAMESPACE + +namespace QmlDesigner { + +namespace Ui { class ConnectionViewWidget; } + +namespace Internal { + +class BindingModel; +class ConnectionModel; +class DynamicPropertiesModel; +class BackendModel; + +class ConnectionViewWidget : public QFrame +{ + Q_OBJECT + +public: + + enum TabStatus { + ConnectionTab, + BindingTab, + DynamicPropertiesTab, + BackendTab, + InvalidTab + }; + + explicit ConnectionViewWidget(QWidget *parent = nullptr); + ~ConnectionViewWidget() override; + + void setBindingModel(BindingModel *model); + void setConnectionModel(ConnectionModel *model); + void setDynamicPropertiesModel(DynamicPropertiesModel *model); + void setBackendModel(BackendModel *model); + + QList<QToolButton*> createToolBarWidgets(); + + TabStatus currentTab() const; + + void resetItemViews(); + void invalidateButtonStatus(); + + QTableView *connectionTableView() const; + QTableView *bindingTableView() const; + QTableView *dynamicPropertiesTableView() const; + QTableView *backendView() const; + + void bindingTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); + void connectionTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); + void dynamicPropertiesTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); + void backendTableViewSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); + +signals: + void setEnabledAddButton(bool enabled); + void setEnabledRemoveButton(bool enabled); + +private: + void handleTabChanged(int i); + void removeButtonClicked(); + void addButtonClicked(); + +private: + Ui::ConnectionViewWidget *ui; +}; + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.ui b/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.ui new file mode 100644 index 0000000000..18df078ec6 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/connectionviewwidget.ui @@ -0,0 +1,273 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::ConnectionViewWidget</class> + <widget class="QWidget" name="QmlDesigner::ConnectionViewWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>994</width> + <height>611</height> + </rect> + </property> + <property name="windowTitle"> + <string>Connections</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <property name="spacing"> + <number>0</number> + </property> + <item row="1" column="0"> + <widget class="QWidget" name="widgetSpacer" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>0</width> + <height>4</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>16777215</width> + <height>4</height> + </size> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QTabBar" name="tabBar" native="true"/> + </item> + <item row="2" column="0"> + <widget class="QStackedWidget" name="stackedWidget"> + <property name="currentIndex"> + <number>3</number> + </property> + <widget class="QWidget" name="connectionViewPage"> + <layout class="QGridLayout" name="gridLayout_3"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="3" column="0" colspan="5"> + <widget class="QTableView" name="connectionView"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="cornerButtonEnabled"> + <bool>false</bool> + </property> + <attribute name="horizontalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="bindingViewPage"> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="2" column="0" colspan="3"> + <widget class="QTableView" name="bindingView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="cornerButtonEnabled"> + <bool>false</bool> + </property> + <attribute name="horizontalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="dynamicPropertiesPage"> + <layout class="QGridLayout" name="gridLayout_4"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QTableView" name="dynamicPropertiesView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="cornerButtonEnabled"> + <bool>false</bool> + </property> + <attribute name="horizontalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="backendViewPage"> + <layout class="QGridLayout" name="gridLayout_5"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item row="0" column="0"> + <widget class="QTableView" name="backendView"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="showGrid"> + <bool>false</bool> + </property> + <property name="cornerButtonEnabled"> + <bool>false</bool> + </property> + <attribute name="horizontalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + <attribute name="verticalHeaderHighlightSections"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + </widget> + </item> + </layout> + <zorder>stackedWidget</zorder> + <zorder>tabBar</zorder> + <zorder>widgetSpacer</zorder> + </widget> + <customwidgets> + <customwidget> + <class>QTabBar</class> + <extends>QWidget</extends> + <header location="global">qtabbar.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <resources/> + <connections/> +</ui> diff --git a/src/plugins/qmldesigner/components/connectioneditor/delegates.cpp b/src/plugins/qmldesigner/components/connectioneditor/delegates.cpp new file mode 100644 index 0000000000..555f448858 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/delegates.cpp @@ -0,0 +1,361 @@ +/**************************************************************************** +** +** 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 "delegates.h" + +#include "backendmodel.h" +#include "connectionmodel.h" +#include "bindingmodel.h" +#include "dynamicpropertiesmodel.h" +#include "connectionview.h" + +#include <bindingproperty.h> + +#include <utils/qtcassert.h> + +#include <QStyleFactory> +#include <QItemEditorFactory> +#include <QDebug> + +namespace QmlDesigner { + +namespace Internal { + +QStringList prependOnForSignalHandler(const QStringList &signalNames) +{ + QStringList signalHandlerNames; + foreach (const QString &signalName, signalNames) { + QString signalHandlerName = signalName; + if (!signalHandlerName.isEmpty()) { + QChar firstChar = signalHandlerName.at(0).toUpper(); + signalHandlerName[0] = firstChar; + signalHandlerName.prepend(QLatin1String("on")); + signalHandlerNames.append(signalHandlerName); + } + } + return signalHandlerNames; +} + +PropertiesComboBox::PropertiesComboBox(QWidget *parent) : QComboBox(parent) +{ + setEditable(true); + setValidator(new QRegularExpressionValidator(QRegularExpression(QLatin1String("[a-z|A-Z|0-9|._-]*")), this)); +} + +QString PropertiesComboBox::text() const +{ + return currentText(); +} + +void PropertiesComboBox::setText(const QString &text) +{ + setEditText(text); +} + +void PropertiesComboBox::disableValidator() +{ + setValidator(nullptr); +} + +ConnectionComboBox::ConnectionComboBox(QWidget *parent) : PropertiesComboBox(parent) +{ +} + +QString ConnectionComboBox::text() const +{ + int index = findText(currentText()); + if (index > -1) { + QVariant variantData = itemData(index); + if (variantData.isValid()) + return variantData.toString(); + } + + return currentText(); +} + +ConnectionEditorDelegate::ConnectionEditorDelegate(QWidget *parent) + : QStyledItemDelegate(parent) +{ +} + +void ConnectionEditorDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + opt.state &= ~QStyle::State_HasFocus; + QStyledItemDelegate::paint(painter, opt, index); +} + +BindingDelegate::BindingDelegate(QWidget *parent) : ConnectionEditorDelegate(parent) +{ + static QItemEditorFactory *factory = nullptr; + if (factory == nullptr) { + factory = new QItemEditorFactory; + QItemEditorCreatorBase *creator + = new QItemEditorCreator<PropertiesComboBox>("text"); + factory->registerEditor(QVariant::String, creator); + } + + setItemEditorFactory(factory); +} + +QWidget *BindingDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QWidget *widget = QStyledItemDelegate::createEditor(parent, option, index); + + const auto model = qobject_cast<const BindingModel*>(index.model()); + if (!model) { + qWarning() << "BindingDelegate::createEditor no model"; + return widget; + } + if (!model->connectionView()) { + qWarning() << "BindingDelegate::createEditor no connection view"; + return widget; + } + + model->connectionView()->allModelNodes(); + + auto bindingComboBox = qobject_cast<PropertiesComboBox*>(widget); + if (!bindingComboBox) { + qWarning() << "BindingDelegate::createEditor no bindingComboBox"; + return widget; + } + + BindingProperty bindingProperty = model->bindingPropertyForRow(index.row()); + + switch (index.column()) { + case BindingModel::TargetModelNodeRow: + return nullptr; //no editor + case BindingModel::TargetPropertyNameRow: { + bindingComboBox->addItems(model->possibleTargetProperties(bindingProperty)); + } break; + case BindingModel::SourceModelNodeRow: { + foreach (const ModelNode &modelNode, model->connectionView()->allModelNodes()) { + if (!modelNode.id().isEmpty()) { + bindingComboBox->addItem(modelNode.id()); + } + } + if (!bindingProperty.parentModelNode().isRootNode()) + bindingComboBox->addItem(QLatin1String("parent")); + } break; + case BindingModel::SourcePropertyNameRow: { + bindingComboBox->addItems(model->possibleSourceProperties(bindingProperty)); + bindingComboBox->disableValidator(); + } break; + default: qWarning() << "BindingDelegate::createEditor column" << index.column(); + } + + connect(bindingComboBox, QOverload<int>::of(&QComboBox::activated), this, [=]() { + auto delegate = const_cast<BindingDelegate*>(this); + emit delegate->commitData(bindingComboBox); + }); + + return widget; +} + +DynamicPropertiesDelegate::DynamicPropertiesDelegate(QWidget *parent) : ConnectionEditorDelegate(parent) +{ +// static QItemEditorFactory *factory = 0; +// if (factory == 0) { +// factory = new QItemEditorFactory; +// QItemEditorCreatorBase *creator +// = new QItemEditorCreator<DynamicPropertiesComboBox>("text"); +// factory->registerEditor(QVariant::String, creator); +// } + +// setItemEditorFactory(factory); +} + +QWidget *DynamicPropertiesDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QWidget *widget = QStyledItemDelegate::createEditor(parent, option, index); + + const auto model = qobject_cast<const DynamicPropertiesModel*>(index.model()); + if (!model) { + qWarning() << "BindingDelegate::createEditor no model"; + return widget; + } + + if (!model->connectionView()) { + qWarning() << "BindingDelegate::createEditor no connection view"; + return widget; + } + model->connectionView()->allModelNodes(); + + switch (index.column()) { + case DynamicPropertiesModel::TargetModelNodeRow: { + return nullptr; //no editor + }; + case DynamicPropertiesModel::PropertyNameRow: { + return QStyledItemDelegate::createEditor(parent, option, index); + }; + case DynamicPropertiesModel::PropertyTypeRow: { + + auto dynamicPropertiesComboBox = new PropertiesComboBox(parent); + connect(dynamicPropertiesComboBox, QOverload<int>::of(&QComboBox::activated), this, [=]() { + auto delegate = const_cast<DynamicPropertiesDelegate*>(this); + emit delegate->commitData(dynamicPropertiesComboBox); + }); + + dynamicPropertiesComboBox->addItem(QLatin1String("alias")); + //dynamicPropertiesComboBox->addItem(QLatin1String("Item")); + dynamicPropertiesComboBox->addItem(QLatin1String("real")); + dynamicPropertiesComboBox->addItem(QLatin1String("int")); + dynamicPropertiesComboBox->addItem(QLatin1String("string")); + dynamicPropertiesComboBox->addItem(QLatin1String("bool")); + dynamicPropertiesComboBox->addItem(QLatin1String("url")); + dynamicPropertiesComboBox->addItem(QLatin1String("color")); + dynamicPropertiesComboBox->addItem(QLatin1String("variant")); + return dynamicPropertiesComboBox; + }; + case DynamicPropertiesModel::PropertyValueRow: { + return QStyledItemDelegate::createEditor(parent, option, index); + }; + default: qWarning() << "BindingDelegate::createEditor column" << index.column(); + } + + return nullptr; +} + +ConnectionDelegate::ConnectionDelegate(QWidget *parent) : ConnectionEditorDelegate(parent) +{ + static QItemEditorFactory *factory = nullptr; + if (factory == nullptr) { + factory = new QItemEditorFactory; + QItemEditorCreatorBase *creator + = new QItemEditorCreator<ConnectionComboBox>("text"); + factory->registerEditor(QVariant::String, creator); + } + + setItemEditorFactory(factory); +} + +QWidget *ConnectionDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + + QWidget *widget = QStyledItemDelegate::createEditor(parent, option, index); + + const auto connectionModel = qobject_cast<const ConnectionModel*>(index.model()); + + auto connectionComboBox = qobject_cast<ConnectionComboBox*>(widget); + + if (!connectionModel) { + qWarning() << "ConnectionDelegate::createEditor no model"; + return widget; + } + + if (!connectionModel->connectionView()) { + qWarning() << "ConnectionDelegate::createEditor no connection view"; + return widget; + } + + if (!connectionComboBox) { + qWarning() << "ConnectionDelegate::createEditor no bindingComboBox"; + return widget; + } + + switch (index.column()) { + case ConnectionModel::TargetModelNodeRow: { + foreach (const ModelNode &modelNode, connectionModel->connectionView()->allModelNodes()) { + if (!modelNode.id().isEmpty()) { + connectionComboBox->addItem(modelNode.id()); + } + } + } break; + case ConnectionModel::TargetPropertyNameRow: { + connectionComboBox->addItems(prependOnForSignalHandler(connectionModel->getSignalsForRow(index.row()))); + } break; + case ConnectionModel::SourceRow: { + ModelNode rootModelNode = connectionModel->connectionView()->rootModelNode(); + if (QmlItemNode::isValidQmlItemNode(rootModelNode) && !rootModelNode.id().isEmpty()) { + + QString itemText = tr("Change to default state"); + QString source = QString::fromLatin1("{ %1.state = \"\" }").arg(rootModelNode.id()); + connectionComboBox->addItem(itemText, source); + connectionComboBox->disableValidator(); + + foreach (const QmlModelState &state, QmlItemNode(rootModelNode).states().allStates()) { + QString itemText = tr("Change state to %1").arg(state.name()); + QString source = QString::fromLatin1("{ %1.state = \"%2\" }").arg(rootModelNode.id()).arg(state.name()); + connectionComboBox->addItem(itemText, source); + } + } + connectionComboBox->disableValidator(); + } break; + + default: qWarning() << "ConnectionDelegate::createEditor column" << index.column(); + } + + connect(connectionComboBox, QOverload<int>::of(&QComboBox::activated), this, [=]() { + auto delegate = const_cast<ConnectionDelegate*>(this); + emit delegate->commitData(connectionComboBox); + }); + + return widget; +} + +BackendDelegate::BackendDelegate(QWidget *parent) : ConnectionEditorDelegate(parent) +{ +} + +QWidget *BackendDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + const auto model = qobject_cast<const BackendModel*>(index.model()); + + model->connectionView()->allModelNodes(); + + QWidget *widget = QStyledItemDelegate::createEditor(parent, option, index); + + QTC_ASSERT(model, return widget); + QTC_ASSERT(model->connectionView(), return widget); + + switch (index.column()) { + case BackendModel::TypeNameColumn: { + auto backendComboBox = new PropertiesComboBox(parent); + backendComboBox->addItems(model->possibleCppTypes()); + connect(backendComboBox, QOverload<int>::of(&QComboBox::activated), this, [=]() { + auto delegate = const_cast<BackendDelegate*>(this); + emit delegate->commitData(backendComboBox); + }); + return backendComboBox; + }; + case BackendModel::PropertyNameColumn: { + return widget; + }; + case BackendModel::IsSingletonColumn: { + return nullptr; //no editor + }; + case BackendModel::IsLocalColumn: { + return nullptr; //no editor + }; + default: qWarning() << "BackendDelegate::createEditor column" << index.column(); + } + + return widget; +} + +} // namesapce Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/delegates.h b/src/plugins/qmldesigner/components/connectioneditor/delegates.h new file mode 100644 index 0000000000..b9792293ac --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/delegates.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QStandardItem> +#include <QStyledItemDelegate> +#include <QComboBox> + +namespace QmlDesigner { + +namespace Internal { + +class PropertiesComboBox : public QComboBox +{ + Q_OBJECT + Q_PROPERTY(QString text READ text WRITE setText USER true) +public: + PropertiesComboBox(QWidget *parent = nullptr); + + virtual QString text() const; + void setText(const QString &text); + void disableValidator(); +}; + +class ConnectionComboBox : public PropertiesComboBox +{ + Q_OBJECT +public: + ConnectionComboBox(QWidget *parent = nullptr); + QString text() const override; +}; + +class ConnectionEditorDelegate : public QStyledItemDelegate +{ +public: + ConnectionEditorDelegate(QWidget *parent = nullptr); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +class BindingDelegate : public ConnectionEditorDelegate +{ +public: + BindingDelegate(QWidget *parent = nullptr); + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +class DynamicPropertiesDelegate : public ConnectionEditorDelegate +{ +public: + DynamicPropertiesDelegate(QWidget *parent = nullptr); + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + + +class ConnectionDelegate : public ConnectionEditorDelegate +{ + Q_OBJECT +public: + ConnectionDelegate(QWidget *parent = nullptr); + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +class BackendDelegate : public ConnectionEditorDelegate +{ + Q_OBJECT +public: + BackendDelegate(QWidget *parent = nullptr); + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/dynamicpropertiesmodel.cpp b/src/plugins/qmldesigner/components/connectioneditor/dynamicpropertiesmodel.cpp new file mode 100644 index 0000000000..0a08e5c883 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/dynamicpropertiesmodel.cpp @@ -0,0 +1,677 @@ +/**************************************************************************** +** +** 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 "dynamicpropertiesmodel.h" + +#include "connectionview.h" + +#include <nodemetainfo.h> +#include <nodeproperty.h> +#include <variantproperty.h> +#include <bindingproperty.h> +#include <rewritingexception.h> +#include <rewritertransaction.h> + +#include <utils/fileutils.h> + +#include <QMessageBox> +#include <QTimer> +#include <QUrl> + +namespace { + +bool compareVariantProperties(const QmlDesigner::VariantProperty &variantProperty01, const QmlDesigner::VariantProperty &variantProperty02) +{ + if (variantProperty01.parentModelNode() != variantProperty02.parentModelNode()) + return false; + if (variantProperty01.name() != variantProperty02.name()) + return false; + return true; +} + +QString idOrTypeNameForNode(const QmlDesigner::ModelNode &modelNode) +{ + QString idLabel = modelNode.id(); + if (idLabel.isEmpty()) + idLabel = modelNode.simplifiedTypeName(); + + return idLabel; +} + +QmlDesigner::PropertyName unusedProperty(const QmlDesigner::ModelNode &modelNode) +{ + QmlDesigner::PropertyName propertyName = "property"; + int i = 0; + if (modelNode.metaInfo().isValid()) { + while (true) { + const QmlDesigner::PropertyName currentPropertyName = propertyName + QString::number(i).toLatin1(); + if (!modelNode.hasProperty(currentPropertyName) && !modelNode.metaInfo().hasProperty(currentPropertyName)) + return currentPropertyName; + i++; + } + } + + return propertyName; +} + +QVariant convertVariantForTypeName(const QVariant &variant, const QmlDesigner::TypeName &typeName) +{ + QVariant returnValue = variant; + + if (typeName == "int") { + bool ok; + returnValue = variant.toInt(&ok); + if (!ok) + returnValue = 0; + } else if (typeName == "real") { + bool ok; + returnValue = variant.toReal(&ok); + if (!ok) + returnValue = 0.0; + + } else if (typeName == "string") { + returnValue = variant.toString(); + + } else if (typeName == "bool") { + returnValue = variant.toBool(); + } else if (typeName == "url") { + returnValue = variant.toUrl(); + } else if (typeName == "color") { + if (QColor::isValidColor(variant.toString())) { + returnValue = variant.toString(); + } else { + returnValue = QColor(Qt::black); + } + } else if (typeName == "Item") { + returnValue = 0; + } + + return returnValue; +} + +} //internal namespace + +namespace QmlDesigner { + +namespace Internal { + +DynamicPropertiesModel::DynamicPropertiesModel(ConnectionView *parent) + : QStandardItemModel(parent) + , m_connectionView(parent) +{ + connect(this, &QStandardItemModel::dataChanged, this, &DynamicPropertiesModel::handleDataChanged); +} + +void DynamicPropertiesModel::resetModel() +{ + beginResetModel(); + clear(); + setHorizontalHeaderLabels(QStringList({ tr("Item"), tr("Property"), tr("Property Type"), + tr("Property Value") })); + + foreach (const ModelNode modelNode, m_selectedModelNodes) + addModelNode(modelNode); + + endResetModel(); +} + +void DynamicPropertiesModel::bindingPropertyChanged(const BindingProperty &bindingProperty) +{ + if (!bindingProperty.isDynamic()) + return; + + m_handleDataChanged = false; + + QList<ModelNode> selectedNodes = connectionView()->selectedModelNodes(); + if (!selectedNodes.contains(bindingProperty.parentModelNode())) + return; + if (!m_lock) { + int rowNumber = findRowForBindingProperty(bindingProperty); + + if (rowNumber == -1) { + addBindingProperty(bindingProperty); + } else { + updateBindingProperty(rowNumber); + } + } + + m_handleDataChanged = true; +} + +void DynamicPropertiesModel::variantPropertyChanged(const VariantProperty &variantProperty) +{ + if (!variantProperty.isDynamic()) + return; + + m_handleDataChanged = false; + + QList<ModelNode> selectedNodes = connectionView()->selectedModelNodes(); + if (!selectedNodes.contains(variantProperty.parentModelNode())) + return; + if (!m_lock) { + int rowNumber = findRowForVariantProperty(variantProperty); + + if (rowNumber == -1) { + addVariantProperty(variantProperty); + } else { + updateVariantProperty(rowNumber); + } + } + + m_handleDataChanged = true; +} + +void DynamicPropertiesModel::bindingRemoved(const BindingProperty &bindingProperty) +{ + m_handleDataChanged = false; + + QList<ModelNode> selectedNodes = connectionView()->selectedModelNodes(); + if (!selectedNodes.contains(bindingProperty.parentModelNode())) + return; + if (!m_lock) { + int rowNumber = findRowForBindingProperty(bindingProperty); + removeRow(rowNumber); + } + + m_handleDataChanged = true; +} + +void DynamicPropertiesModel::selectionChanged(const QList<ModelNode> &selectedNodes) +{ + m_handleDataChanged = false; + m_selectedModelNodes = selectedNodes; + resetModel(); + m_handleDataChanged = true; +} + +ConnectionView *DynamicPropertiesModel::connectionView() const +{ + return m_connectionView; +} + +BindingProperty DynamicPropertiesModel::bindingPropertyForRow(int rowNumber) const +{ + + const int internalId = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 1).toInt(); + const QString targetPropertyName = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 2).toString(); + + ModelNode modelNode = connectionView()->modelNodeForInternalId(internalId); + + if (modelNode.isValid()) + return modelNode.bindingProperty(targetPropertyName.toUtf8()); + + return BindingProperty(); +} + +VariantProperty DynamicPropertiesModel::variantPropertyForRow(int rowNumber) const +{ + const int internalId = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 1).toInt(); + const QString targetPropertyName = data(index(rowNumber, TargetModelNodeRow), Qt::UserRole + 2).toString(); + + ModelNode modelNode = connectionView()->modelNodeForInternalId(internalId); + + if (modelNode.isValid()) + return modelNode.variantProperty(targetPropertyName.toUtf8()); + + return VariantProperty(); +} + +QStringList DynamicPropertiesModel::possibleTargetProperties(const BindingProperty &bindingProperty) const +{ + const ModelNode modelNode = bindingProperty.parentModelNode(); + + if (!modelNode.isValid()) { + qWarning() << " BindingModel::possibleTargetPropertiesForRow invalid model node"; + return QStringList(); + } + + NodeMetaInfo metaInfo = modelNode.metaInfo(); + + if (metaInfo.isValid()) { + QStringList possibleProperties; + foreach (const PropertyName &propertyName, metaInfo.propertyNames()) { + if (metaInfo.propertyIsWritable(propertyName)) + possibleProperties << QString::fromUtf8(propertyName); + } + + return possibleProperties; + } + + return QStringList(); +} + +void DynamicPropertiesModel::addDynamicPropertyForCurrentNode() +{ + if (connectionView()->selectedModelNodes().count() == 1) { + const ModelNode modelNode = connectionView()->selectedModelNodes().constFirst(); + if (modelNode.isValid()) { + try { + modelNode.variantProperty(unusedProperty(modelNode)).setDynamicTypeNameAndValue("string", QLatin1String("none.none")); + } catch (RewritingException &e) { + m_exceptionError = e.description(); + QTimer::singleShot(200, this, &DynamicPropertiesModel::handleException); + } + } + } else { + qWarning() << " BindingModel::addBindingForCurrentNode not one node selected"; + } +} + +QStringList DynamicPropertiesModel::possibleSourceProperties(const BindingProperty &bindingProperty) const +{ + const QString expression = bindingProperty.expression(); + const QStringList stringlist = expression.split(QLatin1String(".")); + + PropertyName typeName; + + if (bindingProperty.parentModelNode().metaInfo().isValid()) { + typeName = bindingProperty.parentModelNode().metaInfo().propertyTypeName(bindingProperty.name()); + } else { + qWarning() << " BindingModel::possibleSourcePropertiesForRow no meta info for target node"; + } + + const QString &id = stringlist.constFirst(); + + ModelNode modelNode = getNodeByIdOrParent(id, bindingProperty.parentModelNode()); + + if (!modelNode.isValid()) { + qWarning() << " BindingModel::possibleSourcePropertiesForRow invalid model node"; + return QStringList(); + } + + NodeMetaInfo metaInfo = modelNode.metaInfo(); + + if (metaInfo.isValid()) { + QStringList possibleProperties; + foreach (const PropertyName &propertyName, metaInfo.propertyNames()) { + if (metaInfo.propertyTypeName(propertyName) == typeName) //### todo proper check + possibleProperties << QString::fromUtf8(propertyName); + } + + return possibleProperties; + } else { + qWarning() << " BindingModel::possibleSourcePropertiesForRow no meta info for source node"; + } + + return QStringList(); +} + +void DynamicPropertiesModel::deleteDynamicPropertyByRow(int rowNumber) +{ + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + if (bindingProperty.isValid()) { + bindingProperty.parentModelNode().removeProperty(bindingProperty.name()); + } + + VariantProperty variantProperty = variantPropertyForRow(rowNumber); + + if (variantProperty.isValid()) { + variantProperty.parentModelNode().removeProperty(variantProperty.name()); + } + + resetModel(); +} + +void DynamicPropertiesModel::addProperty(const QVariant &propertyValue, + const QString &propertyType, + const AbstractProperty &abstractProperty) +{ + QList<QStandardItem*> items; + + QStandardItem *idItem; + QStandardItem *propertyNameItem; + QStandardItem *propertyTypeItem; + QStandardItem *propertyValueItem; + + idItem = new QStandardItem(idOrTypeNameForNode(abstractProperty.parentModelNode())); + updateCustomData(idItem, abstractProperty); + + propertyNameItem = new QStandardItem(QString::fromUtf8(abstractProperty.name())); + + items.append(idItem); + items.append(propertyNameItem); + + + propertyTypeItem = new QStandardItem(propertyType); + items.append(propertyTypeItem); + + propertyValueItem = new QStandardItem(); + propertyValueItem->setData(propertyValue, Qt::DisplayRole); + items.append(propertyValueItem); + + appendRow(items); +} + +void DynamicPropertiesModel::addBindingProperty(const BindingProperty &property) +{ + QVariant value = property.expression(); + QString type = QString::fromLatin1(property.dynamicTypeName()); + addProperty(value, type, property); +} + +void DynamicPropertiesModel::addVariantProperty(const VariantProperty &property) +{ + QVariant value = property.value(); + QString type = QString::fromLatin1(property.dynamicTypeName()); + addProperty(value, type, property); +} + +void DynamicPropertiesModel::updateBindingProperty(int rowNumber) +{ + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + + if (bindingProperty.isValid()) { + QString propertyName = QString::fromUtf8(bindingProperty.name()); + updateDisplayRole(rowNumber, PropertyNameRow, propertyName); + QString value = bindingProperty.expression(); + QString type = QString::fromUtf8(bindingProperty.dynamicTypeName()); + updateDisplayRole(rowNumber, PropertyTypeRow, type); + updateDisplayRole(rowNumber, PropertyValueRow, value); + } +} + +void DynamicPropertiesModel::updateVariantProperty(int rowNumber) +{ + VariantProperty variantProperty = variantPropertyForRow(rowNumber); + + if (variantProperty.isValid()) { + QString propertyName = QString::fromUtf8(variantProperty.name()); + updateDisplayRole(rowNumber, PropertyNameRow, propertyName); + QVariant value = variantProperty.value(); + QString type = QString::fromUtf8(variantProperty.dynamicTypeName()); + updateDisplayRole(rowNumber, PropertyTypeRow, type); + + updateDisplayRoleFromVariant(rowNumber, PropertyValueRow, value); + } +} + +void DynamicPropertiesModel::addModelNode(const ModelNode &modelNode) +{ + foreach (const BindingProperty &bindingProperty, modelNode.bindingProperties()) { + if (bindingProperty.isDynamic()) + addBindingProperty(bindingProperty); + } + + foreach (const VariantProperty &variantProperty, modelNode.variantProperties()) { + if (variantProperty.isDynamic()) + addVariantProperty(variantProperty); + } +} + +void DynamicPropertiesModel::updateValue(int row) +{ + BindingProperty bindingProperty = bindingPropertyForRow(row); + + if (bindingProperty.isBindingProperty()) { + const QString expression = data(index(row, PropertyValueRow)).toString(); + + RewriterTransaction transaction = connectionView()->beginRewriterTransaction(QByteArrayLiteral("DynamicPropertiesModel::updateValue")); + try { + bindingProperty.setDynamicTypeNameAndExpression(bindingProperty.dynamicTypeName(), expression); + transaction.commit(); //committing in the try block + } catch (Exception &e) { + m_exceptionError = e.description(); + QTimer::singleShot(200, this, &DynamicPropertiesModel::handleException); + } + return; + } + + VariantProperty variantProperty = variantPropertyForRow(row); + + if (variantProperty.isVariantProperty()) { + const QVariant value = data(index(row, PropertyValueRow)); + + RewriterTransaction transaction = connectionView()->beginRewriterTransaction(QByteArrayLiteral("DynamicPropertiesModel::updateValue")); + try { + variantProperty.setDynamicTypeNameAndValue(variantProperty.dynamicTypeName(), value); + transaction.commit(); //committing in the try block + } catch (Exception &e) { + m_exceptionError = e.description(); + QTimer::singleShot(200, this, &DynamicPropertiesModel::handleException); + } + } +} + +void DynamicPropertiesModel::updatePropertyName(int rowNumber) +{ + const PropertyName newName = data(index(rowNumber, PropertyNameRow)).toString().toUtf8(); + if (newName.isEmpty()) { + qWarning() << "DynamicPropertiesModel::updatePropertyName invalid property name"; + return; + } + + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + + ModelNode targetNode = bindingProperty.parentModelNode(); + + if (bindingProperty.isBindingProperty()) { + connectionView()->executeInTransaction("DynamicPropertiesModel::updatePropertyName", [bindingProperty, newName, &targetNode](){ + const QString expression = bindingProperty.expression(); + const PropertyName dynamicPropertyType = bindingProperty.dynamicTypeName(); + + targetNode.bindingProperty(newName).setDynamicTypeNameAndExpression(dynamicPropertyType, expression); + targetNode.removeProperty(bindingProperty.name()); + }); + + updateCustomData(rowNumber, targetNode.bindingProperty(newName)); + return; + } + + VariantProperty variantProperty = variantPropertyForRow(rowNumber); + + if (variantProperty.isVariantProperty()) { + const QVariant value = variantProperty.value(); + const PropertyName dynamicPropertyType = variantProperty.dynamicTypeName(); + ModelNode targetNode = variantProperty.parentModelNode(); + + connectionView()->executeInTransaction("DynamicPropertiesModel::updatePropertyName", [=](){ + targetNode.variantProperty(newName).setDynamicTypeNameAndValue(dynamicPropertyType, value); + targetNode.removeProperty(variantProperty.name()); + }); + + updateCustomData(rowNumber, targetNode.variantProperty(newName)); + } +} + +void DynamicPropertiesModel::updatePropertyType(int rowNumber) +{ + + const TypeName newType = data(index(rowNumber, PropertyTypeRow)).toString().toLatin1(); + + if (newType.isEmpty()) { + qWarning() << "DynamicPropertiesModel::updatePropertyName invalid property type"; + return; + } + + BindingProperty bindingProperty = bindingPropertyForRow(rowNumber); + + if (bindingProperty.isBindingProperty()) { + const QString expression = bindingProperty.expression(); + const PropertyName propertyName = bindingProperty.name(); + ModelNode targetNode = bindingProperty.parentModelNode(); + + connectionView()->executeInTransaction("DynamicPropertiesModel::updatePropertyType", [=](){ + targetNode.removeProperty(bindingProperty.name()); + targetNode.bindingProperty(propertyName).setDynamicTypeNameAndExpression(newType, expression); + }); + + updateCustomData(rowNumber, targetNode.bindingProperty(propertyName)); + return; + } + + VariantProperty variantProperty = variantPropertyForRow(rowNumber); + + if (variantProperty.isVariantProperty()) { + const QVariant value = variantProperty.value(); + ModelNode targetNode = variantProperty.parentModelNode(); + const PropertyName propertyName = variantProperty.name(); + + connectionView()->executeInTransaction("DynamicPropertiesModel::updatePropertyType", [=](){ + targetNode.removeProperty(variantProperty.name()); + if (newType == "alias") { //alias properties have to be bindings + targetNode.bindingProperty(propertyName).setDynamicTypeNameAndExpression(newType, QLatin1String("none.none")); + } else { + targetNode.variantProperty(propertyName).setDynamicTypeNameAndValue(newType, convertVariantForTypeName(value, newType)); + } + }); + + updateCustomData(rowNumber, targetNode.variantProperty(propertyName)); + + if (variantProperty.isVariantProperty()) { + updateVariantProperty(rowNumber); + } else if (bindingProperty.isBindingProperty()) { + updateBindingProperty(rowNumber); + } + } +} + +ModelNode DynamicPropertiesModel::getNodeByIdOrParent(const QString &id, const ModelNode &targetNode) const +{ + ModelNode modelNode; + + if (id != QLatin1String("parent")) { + modelNode = connectionView()->modelNodeForId(id); + } else { + if (targetNode.hasParentProperty()) { + modelNode = targetNode.parentProperty().parentModelNode(); + } + } + return modelNode; +} + +void DynamicPropertiesModel::updateCustomData(QStandardItem *item, const AbstractProperty &property) +{ + item->setData(property.parentModelNode().internalId(), Qt::UserRole + 1); + item->setData(property.name(), Qt::UserRole + 2); +} + +void DynamicPropertiesModel::updateCustomData(int row, const AbstractProperty &property) +{ + QStandardItem* idItem = item(row, 0); + updateCustomData(idItem, property); +} + +int DynamicPropertiesModel::findRowForBindingProperty(const BindingProperty &bindingProperty) const +{ + for (int i=0; i < rowCount(); i++) { + if (compareBindingProperties(bindingPropertyForRow(i), bindingProperty)) + return i; + } + //not found + return -1; +} + +int DynamicPropertiesModel::findRowForVariantProperty(const VariantProperty &variantProperty) const +{ + for (int i=0; i < rowCount(); i++) { + if (compareVariantProperties(variantPropertyForRow(i), variantProperty)) + return i; + } + //not found + return -1; +} + +bool DynamicPropertiesModel::getExpressionStrings(const BindingProperty &bindingProperty, QString *sourceNode, QString *sourceProperty) +{ + //### todo we assume no expressions yet + + const QString expression = bindingProperty.expression(); + + if (true) { + const QStringList stringList = expression.split(QLatin1String(".")); + + *sourceNode = stringList.constFirst(); + + QString propertyName; + + for (int i=1; i < stringList.count(); i++) { + propertyName += stringList.at(i); + if (i != stringList.count() - 1) + propertyName += QLatin1String("."); + } + *sourceProperty = propertyName; + } + return true; +} + +void DynamicPropertiesModel::updateDisplayRole(int row, int columns, const QString &string) +{ + QModelIndex modelIndex = index(row, columns); + if (data(modelIndex).toString() != string) + setData(modelIndex, string); +} + +void DynamicPropertiesModel::updateDisplayRoleFromVariant(int row, int columns, const QVariant &variant) +{ + QModelIndex modelIndex = index(row, columns); + if (data(modelIndex) != variant) + setData(modelIndex, variant); +} + + +void DynamicPropertiesModel::handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + if (!m_handleDataChanged) + return; + + if (topLeft != bottomRight) { + qWarning() << "BindingModel::handleDataChanged multi edit?"; + return; + } + + m_lock = true; + + int currentColumn = topLeft.column(); + int currentRow = topLeft.row(); + + switch (currentColumn) { + case TargetModelNodeRow: { + //updating user data + } break; + case PropertyNameRow: { + updatePropertyName(currentRow); + } break; + case PropertyTypeRow: { + updatePropertyType(currentRow); + } break; + case PropertyValueRow: { + updateValue(currentRow); + } break; + + default: qWarning() << "BindingModel::handleDataChanged column" << currentColumn; + } + + m_lock = false; +} + +void DynamicPropertiesModel::handleException() +{ + QMessageBox::warning(nullptr, tr("Error"), m_exceptionError); + resetModel(); +} + +} // namespace Internal + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/dynamicpropertiesmodel.h b/src/plugins/qmldesigner/components/connectioneditor/dynamicpropertiesmodel.h new file mode 100644 index 0000000000..e0c9617fed --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/dynamicpropertiesmodel.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <modelnode.h> +#include <bindingproperty.h> +#include <variantproperty.h> + +#include <QStandardItemModel> + +namespace QmlDesigner { + +namespace Internal { + +class ConnectionView; + +class DynamicPropertiesModel : public QStandardItemModel +{ + Q_OBJECT + +public: + enum ColumnRoles { + TargetModelNodeRow = 0, + PropertyNameRow = 1, + PropertyTypeRow = 2, + PropertyValueRow = 3 + }; + DynamicPropertiesModel(ConnectionView *parent = nullptr); + void bindingPropertyChanged(const BindingProperty &bindingProperty); + void variantPropertyChanged(const VariantProperty &variantProperty); + void bindingRemoved(const BindingProperty &bindingProperty); + void selectionChanged(const QList<ModelNode> &selectedNodes); + + ConnectionView *connectionView() const; + BindingProperty bindingPropertyForRow(int rowNumber) const; + VariantProperty variantPropertyForRow(int rowNumber) const; + QStringList possibleTargetProperties(const BindingProperty &bindingProperty) const; + QStringList possibleSourceProperties(const BindingProperty &bindingProperty) const; + void deleteDynamicPropertyByRow(int rowNumber); + + void updateDisplayRoleFromVariant(int row, int columns, const QVariant &variant); + void addDynamicPropertyForCurrentNode(); + void resetModel(); + +protected: + void addProperty(const QVariant &propertyValue, + const QString &propertyType, + const AbstractProperty &abstractProperty); + void addBindingProperty(const BindingProperty &property); + void addVariantProperty(const VariantProperty &property); + void updateBindingProperty(int rowNumber); + void updateVariantProperty(int rowNumber); + void addModelNode(const ModelNode &modelNode); + void updateValue(int row); + void updatePropertyName(int rowNumber); + void updatePropertyType(int rowNumber); + ModelNode getNodeByIdOrParent(const QString &id, const ModelNode &targetNode) const; + void updateCustomData(QStandardItem *item, const AbstractProperty &property); + void updateCustomData(int row, const AbstractProperty &property); + int findRowForBindingProperty(const BindingProperty &bindingProperty) const; + int findRowForVariantProperty(const VariantProperty &variantProperty) const; + + bool getExpressionStrings(const BindingProperty &bindingProperty, QString *sourceNode, QString *sourceProperty); + + void updateDisplayRole(int row, int columns, const QString &string); + +private: + void handleDataChanged(const QModelIndex &topLeft, const QModelIndex& bottomRight); + void handleException(); + +private: + QList<ModelNode> m_selectedModelNodes; + ConnectionView *m_connectionView; + bool m_lock = false; + bool m_handleDataChanged = false; + QString m_exceptionError; + +}; + +} // namespace Internal +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/connectioneditor/stylesheet.css b/src/plugins/qmldesigner/components/connectioneditor/stylesheet.css new file mode 100644 index 0000000000..aeacc63733 --- /dev/null +++ b/src/plugins/qmldesigner/components/connectioneditor/stylesheet.css @@ -0,0 +1,123 @@ +QFrame +{ + background-color: creatorTheme.QmlDesigner_BackgroundColorDarkAlternate; + color: creatorTheme.PanelTextColorLight; + font-size: creatorTheme.captionFontPixelSize; + border-radius: 0px; +} + +QTableView { + color: creatorTheme.PanelTextColorLight; + selection-color: creatorTheme.PanelTextColorLight; + selection-background-color: creatorTheme.QmlDesigner_HighlightColor; + +} + +QTabBar QToolButton { + background-color: creatorTheme.QmlDesigner_BackgroundColorDarkAlternate; + border: 1px solid creatorTheme.QmlDesigner_BackgroundColorDarker; + border-radius: 0px; +} + +QTableView::item +{ + border: 0px; + padding-left: 4px; +} + +QTableView::item:focus +{ + border: none; + background-color: transparent; +} + +QTableView::item:selected +{ + border: none +} + +QHeaderView::section { + background-color: #494949; + padding: 4px; + border: 1px solid black; + color: #cacaca; + margin: 2px +} + +QTableView { + alternate-background-color: #414141; +} + +QWidget#widgetSpacer { + background-color: creatorTheme.QmlDesigner_TabLight; +} + +QStackedWidget { + border: 0px; + background-color: #4f4f4f; +} + +QTabBar::tab:selected { + border: none; + border-image: none; + image: none; + + background-color: creatorTheme.QmlDesigner_TabLight; + color: creatorTheme.QmlDesigner_TabDark; +} + + +QTabBar::tab { + width: 92px; + height: 22px; + margin-top: 0x; + margin-bottom: 0px; + margin-left: 0px; + margin-right: 0px; + font: bold; + font-size: creatorTheme.captionFontPixelSize; + background-color: creatorTheme.QmlDesigner_TabDark; + color: creatorTheme.QmlDesigner_TabLight; +} + +QSpinBox +{ + font-size: creatorTheme.captionFontPixelSize; + color: white; + padding-right: 2px; + padding-top: 2px; + padding-bottom: 2px; + padding-left: 12px; + border: 2px solid #0F0F0F; + border-width: 1; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #2c2c2c, stop: 1 #333333); + + min-height: 22px; +} + + QDoubleSpinBox + { + font-size: creatorTheme.captionFontPixelSize; + color: white; + padding-right: 2px; + padding-top: 2px; + padding-bottom: 2px; + padding-left: 12px; + border: 2px solid #0F0F0F; + border-width: 1; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #2c2c2c, stop: 1 #333333); + min-height: 22px; + } + +QLineEdit +{ + color: white; + font-size: creatorTheme.captionFontPixelSize; + border: 2px solid #0F0F0F; + border-width: 1; + min-height: 26px; + background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #2c2c2c, stop: 1 #333333); +} diff --git a/src/plugins/qmldesigner/components/curveeditor/animationcurve.cpp b/src/plugins/qmldesigner/components/curveeditor/animationcurve.cpp new file mode 100644 index 0000000000..8e2d5224e1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/animationcurve.cpp @@ -0,0 +1,187 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "animationcurve.h" +#include "detail/curvesegment.h" + +#include <QLineF> + +namespace DesignTools { + +AnimationCurve::AnimationCurve() + : m_frames() +{} + +AnimationCurve::AnimationCurve(const std::vector<Keyframe> &frames) + : m_frames(frames) + , m_minY(std::numeric_limits<double>::max()) + , m_maxY(std::numeric_limits<double>::lowest()) +{ + if (isValid()) { + + for (auto e : extrema()) { + + if (m_minY > e.y()) + m_minY = e.y(); + + if (m_maxY < e.y()) + m_maxY = e.y(); + } + + for (auto &frame : qAsConst(m_frames)) { + if (frame.position().y() < m_minY) + m_minY = frame.position().y(); + + if (frame.position().y() > m_maxY) + m_maxY = frame.position().y(); + } + } +} + +bool AnimationCurve::isValid() const +{ + return m_frames.size() >= 2; +} + +double AnimationCurve::minimumTime() const +{ + if (!m_frames.empty()) + return m_frames.front().position().x(); + + return std::numeric_limits<double>::max(); +} + +double AnimationCurve::maximumTime() const +{ + if (!m_frames.empty()) + return m_frames.back().position().x(); + + return std::numeric_limits<double>::lowest(); +} + +double AnimationCurve::minimumValue() const +{ + return m_minY; +} + +double AnimationCurve::maximumValue() const +{ + return m_maxY; +} + +std::vector<Keyframe> AnimationCurve::keyframes() const +{ + return m_frames; +} + +std::vector<QPointF> AnimationCurve::extrema() const +{ + std::vector<QPointF> out; + + CurveSegment segment; + segment.setLeft(m_frames.at(0)); + + for (size_t i = 1; i < m_frames.size(); ++i) { + + segment.setRight(m_frames[i]); + + const auto es = segment.extrema(); + out.insert(std::end(out), std::begin(es), std::end(es)); + + segment.setLeft(m_frames[i]); + } + + return out; +} + +std::vector<double> AnimationCurve::yForX(double x) const +{ + if (m_frames.front().position().x() > x) + return std::vector<double>(); + + CurveSegment segment; + for (auto &frame : m_frames) { + if (frame.position().x() > x) { + segment.setRight(frame); + return segment.yForX(x); + } + segment.setLeft(frame); + } + return std::vector<double>(); +} + +std::vector<double> AnimationCurve::xForY(double y, uint segment) const +{ + if (m_frames.size() > segment + 1) { + CurveSegment seg(m_frames[segment], m_frames[segment + 1]); + return seg.xForY(y); + } + return std::vector<double>(); +} + +bool AnimationCurve::intersects(const QPointF &coord, double radius) +{ + if (m_frames.size() < 2) + return false; + + std::vector<CurveSegment> influencer; + + CurveSegment current; + current.setLeft(m_frames.at(0)); + + for (size_t i = 1; i < m_frames.size(); ++i) { + Keyframe &frame = m_frames.at(i); + + current.setRight(frame); + + if (current.containsX(coord.x() - radius) || + current.containsX(coord.x()) || + current.containsX(coord.x() + radius)) { + influencer.push_back(current); + } + + if (frame.position().x() > coord.x() + radius) + break; + + current.setLeft(frame); + } + + for (auto &segment : influencer) { + for (auto &y : segment.yForX(coord.x())) { + QLineF line(coord.x(), y, coord.x(), coord.y()); + if (line.length() < radius) + return true; + } + + for (auto &x : segment.xForY(coord.y())) { + QLineF line(x, coord.y(), coord.x(), coord.y()); + if (line.length() < radius) + return true; + } + } + return false; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/animationcurve.h b/src/plugins/qmldesigner/components/curveeditor/animationcurve.h new file mode 100644 index 0000000000..0533e479a1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/animationcurve.h @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "keyframe.h" + +#include <vector> + +namespace DesignTools { + +class AnimationCurve +{ +public: + AnimationCurve(); + + AnimationCurve(const std::vector<Keyframe> &frames); + + bool isValid() const; + + double minimumTime() const; + + double maximumTime() const; + + double minimumValue() const; + + double maximumValue() const; + + std::vector<Keyframe> keyframes() const; + + std::vector<QPointF> extrema() const; + + std::vector<double> yForX(double x) const; + + std::vector<double> xForY(double y, uint segment) const; + + bool intersects(const QPointF &coord, double radius); + +private: + std::vector<Keyframe> m_frames; + + double m_minY; + + double m_maxY; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/curveeditor.cpp b/src/plugins/qmldesigner/components/curveeditor/curveeditor.cpp new file mode 100644 index 0000000000..4eba31c6bd --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/curveeditor.cpp @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditor.h" +#include "curveeditormodel.h" +#include "detail/curveitem.h" +#include "detail/graphicsview.h" +#include "detail/treeview.h" + +#include <QHBoxLayout> +#include <QSplitter> + +namespace DesignTools { + +CurveEditor::CurveEditor(CurveEditorModel *model, QWidget *parent) + : QWidget(parent) + , m_tree(new TreeView(model, this)) + , m_view(new GraphicsView(model)) +{ + QSplitter *splitter = new QSplitter; + splitter->addWidget(m_tree); + splitter->addWidget(m_view); + splitter->setStretchFactor(1, 2); + + QHBoxLayout *box = new QHBoxLayout; + box->addWidget(splitter); + setLayout(box); + + connect(m_tree, &TreeView::curvesSelected, m_view, &GraphicsView::reset); +} + +void CurveEditor::zoomX(double zoom) +{ + m_view->setZoomX(zoom); +} + +void CurveEditor::zoomY(double zoom) +{ + m_view->setZoomY(zoom); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/curveeditor.h b/src/plugins/qmldesigner/components/curveeditor/curveeditor.h new file mode 100644 index 0000000000..a2c5873be0 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/curveeditor.h @@ -0,0 +1,53 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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> + +namespace DesignTools { + +class CurveEditorModel; +class GraphicsView; +class TreeView; + +class CurveEditor : public QWidget +{ + Q_OBJECT + +public: + CurveEditor(CurveEditorModel *model, QWidget *parent = nullptr); + + void zoomX(double zoom); + + void zoomY(double zoom); + +private: + TreeView *m_tree; + + GraphicsView *m_view; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/curveeditor.pri b/src/plugins/qmldesigner/components/curveeditor/curveeditor.pri new file mode 100644 index 0000000000..31ffe5d818 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/curveeditor.pri @@ -0,0 +1,46 @@ +INCLUDEPATH += $$PWD + +HEADERS += \ + $$PWD/animationcurve.h \ + $$PWD/curveeditor.h \ + $$PWD/curveeditormodel.h \ + $$PWD/detail/colorcontrol.h \ + $$PWD/detail/curveeditorstyledialog.h \ + $$PWD/detail/curveitem.h \ + $$PWD/detail/curvesegment.h \ + $$PWD/detail/graphicsscene.h \ + $$PWD/detail/graphicsview.h \ + $$PWD/detail/handleitem.h \ + $$PWD/detail/keyframeitem.h \ + $$PWD/detail/playhead.h \ + $$PWD/detail/selectableitem.h \ + $$PWD/detail/selector.h \ + $$PWD/detail/shortcut.h \ + $$PWD/detail/treeitemdelegate.h \ + $$PWD/detail/treemodel.h \ + $$PWD/detail/treeview.h \ + $$PWD/keyframe.h \ + $$PWD/treeitem.h + +SOURCES += \ + $$PWD/animationcurve.cpp \ + $$PWD/curveeditor.cpp \ + $$PWD/curveeditormodel.cpp \ + $$PWD/detail/colorcontrol.cpp \ + $$PWD/detail/curveeditorstyledialog.cpp \ + $$PWD/detail/curveitem.cpp \ + $$PWD/detail/curvesegment.cpp \ + $$PWD/detail/graphicsscene.cpp \ + $$PWD/detail/graphicsview.cpp \ + $$PWD/detail/handleitem.cpp \ + $$PWD/detail/keyframeitem.cpp \ + $$PWD/detail/playhead.cpp \ + $$PWD/detail/selectableitem.cpp \ + $$PWD/detail/selector.cpp \ + $$PWD/detail/shortcut.cpp \ + $$PWD/detail/treeitemdelegate.cpp \ + $$PWD/detail/treemodel.cpp \ + $$PWD/detail/treeview.cpp \ + $$PWD/detail/utils.cpp \ + $$PWD/keyframe.cpp \ + $$PWD/treeitem.cpp diff --git a/src/plugins/qmldesigner/components/curveeditor/curveeditormodel.cpp b/src/plugins/qmldesigner/components/curveeditor/curveeditormodel.cpp new file mode 100644 index 0000000000..3b8b26b763 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/curveeditormodel.cpp @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditormodel.h" +#include "treeitem.h" +#include "detail/graphicsview.h" + +namespace DesignTools { + +CurveEditorModel::CurveEditorModel(QObject *parent) + : TreeModel(parent) +{} + +CurveEditorModel::~CurveEditorModel() {} + + +void CurveEditorModel::setCurrentFrame(int frame) +{ + if (graphicsView()) + graphicsView()->setCurrentFrame(frame); +} + +void CurveEditorModel::setCurve(unsigned int id, const AnimationCurve &curve) +{ + if (TreeItem *item = find(id)) { + if (PropertyTreeItem *propertyItem = item->asPropertyItem()) { + propertyItem->setCurve(curve); + emit curveChanged(propertyItem); + } + } +} + +void CurveEditorModel::reset(const std::vector<TreeItem *> &items) +{ + beginResetModel(); + + initialize(); + + unsigned int counter = 0; + for (auto *item : items) { + item->setId(++counter); + root()->addChild(item); + } + + endResetModel(); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/curveeditormodel.h b/src/plugins/qmldesigner/components/curveeditor/curveeditormodel.h new file mode 100644 index 0000000000..6e212d4c6a --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/curveeditormodel.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "detail/treemodel.h" + +#include <vector> + +QT_BEGIN_NAMESPACE +class QPointF; +QT_END_NAMESPACE + +namespace DesignTools { + +struct CurveEditorStyle; + +class AnimationCurve; +class PropertyTreeItem; +class TreeItem; + +class CurveEditorModel : public TreeModel +{ + Q_OBJECT + +signals: + void currentFrameChanged(int frame); + + void curveChanged(PropertyTreeItem *item); + +public: + virtual double minimumTime() const = 0; + + virtual double maximumTime() const = 0; + + virtual CurveEditorStyle style() const = 0; + +public: + CurveEditorModel(QObject *parent = nullptr); + + ~CurveEditorModel() override; + + void setCurrentFrame(int frame); + + void setCurve(unsigned int id, const AnimationCurve &curve); + + void reset(const std::vector<TreeItem *> &items); +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/curveeditorstyle.h b/src/plugins/qmldesigner/components/curveeditor/curveeditorstyle.h new file mode 100644 index 0000000000..03ea11c8c1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/curveeditorstyle.h @@ -0,0 +1,125 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "detail/shortcut.h" + +#include <QBitmap> +#include <QBrush> +#include <QColor> +#include <QDialog> +#include <QIcon> +#include <QKeySequence> + +namespace DesignTools { + +struct TreeItemStyleOption +{ + double margins; + QIcon pinnedIcon = QIcon(":/ICON_PINNED"); + QIcon unpinnedIcon = QIcon(":/ICON_UNPINNED"); + QIcon lockedIcon = QIcon(":/ICON_LOCKED"); + QIcon unlockedIcon = QIcon(":/ICON_UNLOCKED"); +}; + +struct HandleItemStyleOption +{ + double size = 10.0; + double lineWidth = 1.0; + QColor color = QColor(200, 0, 0); + QColor selectionColor = QColor(200, 200, 200); +}; + +struct KeyframeItemStyleOption +{ + double size = 10.0; + QColor color = QColor(200, 200, 0); + QColor selectionColor = QColor(200, 200, 200); +}; + +struct CurveItemStyleOption +{ + double width = 1.0; + QColor color = QColor(0, 200, 0); + QColor selectionColor = QColor(200, 200, 200); +}; + +struct PlayheadStyleOption +{ + double width = 20.0; + double radius = 4.0; + QColor color = QColor(200, 200, 0); +}; + +struct Shortcuts +{ + Shortcut newSelection = Shortcut(Qt::LeftButton); + Shortcut addToSelection = Shortcut(Qt::LeftButton, Qt::ControlModifier | Qt::ShiftModifier); + Shortcut removeFromSelection = Shortcut(Qt::LeftButton, Qt::ShiftModifier); + Shortcut toggleSelection = Shortcut(Qt::LeftButton, Qt::ControlModifier); + + Shortcut zoom = Shortcut(Qt::RightButton, Qt::AltModifier); + Shortcut pan = Shortcut(Qt::MiddleButton, Qt::AltModifier); + Shortcut frameAll = Shortcut(Qt::NoModifier, Qt::Key_A); +}; + +struct CurveEditorStyle +{ + Shortcuts shortcuts; + + QBrush backgroundBrush = QBrush(QColor(5, 0, 100)); + QBrush backgroundAlternateBrush = QBrush(QColor(0, 0, 50)); + QColor fontColor = QColor(200, 200, 200); + QColor gridColor = QColor(128, 128, 128); + double canvasMargin = 5.0; + int zoomInWidth = 100; + int zoomInHeight = 100; + double timeAxisHeight = 40.0; + double timeOffsetLeft = 10.0; + double timeOffsetRight = 10.0; + QColor rangeBarColor = QColor(128, 128, 128); + QColor rangeBarCapsColor = QColor(50, 50, 255); + double valueAxisWidth = 60.0; + double valueOffsetTop = 10.0; + double valueOffsetBottom = 10.0; + + HandleItemStyleOption handleStyle; + KeyframeItemStyleOption keyframeStyle; + CurveItemStyleOption curveStyle; + TreeItemStyleOption treeItemStyle; + PlayheadStyleOption playhead; +}; + +inline QPixmap pixmapFromIcon(const QIcon &icon, const QSize &size, const QColor &color) +{ + QPixmap pixmap = icon.pixmap(size); + QPixmap mask(pixmap.size()); + mask.fill(color); + mask.setMask(pixmap.createMaskFromColor(Qt::transparent)); + return mask; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/colorcontrol.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/colorcontrol.cpp new file mode 100644 index 0000000000..a833a4c6ff --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/colorcontrol.cpp @@ -0,0 +1,101 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "colorcontrol.h" + +#include <QColorDialog> +#include <QEvent> +#include <QHelpEvent> +#include <QPainter> +#include <QToolTip> + +namespace DesignTools { + +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; + +QColor ColorControl::value() const +{ + return m_color; +} + +void ColorControl::setValue(const QColor &val) +{ + m_color = val; +} + +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, this); + + event->accept(); + + if (color != m_color) { + m_color = color; + update(); + emit valueChanged(); + } +} + +void ColorControl::mousePressEvent(QMouseEvent *event) +{ + // Required if embedded in a QGraphicsProxywidget + // in order to call mouseRelease properly. + QWidget::mousePressEvent(event); + event->accept(); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/colorcontrol.h b/src/plugins/qmldesigner/components/curveeditor/detail/colorcontrol.h new file mode 100644 index 0000000000..54dfe194f8 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/colorcontrol.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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> + +namespace DesignTools { + +class ColorControl : public QWidget +{ + Q_OBJECT + +public: + ColorControl(); + + ColorControl(const QColor &color, QWidget *parent = nullptr); + + ~ColorControl() override; + + QColor value() const; + + void setValue(const QColor &val); + +protected: + bool event(QEvent *event) override; + + void paintEvent(QPaintEvent *event) override; + + void mouseReleaseEvent(QMouseEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + +signals: + void valueChanged(); + +private: + QColor m_color; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/curveeditorstyledialog.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/curveeditorstyledialog.cpp new file mode 100644 index 0000000000..a8b653a74d --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/curveeditorstyledialog.cpp @@ -0,0 +1,267 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyledialog.h" +#include "colorcontrol.h" +#include "curveeditorstyle.h" + +#include <QDebug> +#include <QDoubleSpinBox> +#include <QLabel> +#include <QPushButton> +#include <QSpinBox> +#include <QVBoxLayout> + +namespace DesignTools { + +QHBoxLayout *createRow(const QString &title, QWidget *widget) +{ + auto *label = new QLabel(title); + label->setFixedWidth(200); + label->setAlignment(Qt::AlignRight); + + auto *box = new QHBoxLayout; + box->addWidget(label); + box->addWidget(widget); + return box; +} + +CurveEditorStyleDialog::CurveEditorStyleDialog(CurveEditorStyle &style, QWidget *parent) + : QDialog(parent) + , m_printButton(new QPushButton("Print")) + , m_background(new ColorControl(style.backgroundBrush.color())) + , m_backgroundAlternate(new ColorControl(style.backgroundAlternateBrush.color())) + , m_fontColor(new ColorControl(style.fontColor)) + , m_gridColor(new ColorControl(style.gridColor)) + , m_canvasMargin(new QDoubleSpinBox()) + , m_zoomInWidth(new QSpinBox()) + , m_zoomInHeight(new QSpinBox()) + , m_timeAxisHeight(new QDoubleSpinBox()) + , m_timeOffsetLeft(new QDoubleSpinBox()) + , m_timeOffsetRight(new QDoubleSpinBox()) + , m_rangeBarColor(new ColorControl(style.rangeBarCapsColor)) + , m_rangeBarCapsColor(new ColorControl(style.rangeBarCapsColor)) + , m_valueAxisWidth(new QDoubleSpinBox()) + , m_valueOffsetTop(new QDoubleSpinBox()) + , m_valueOffsetBottom(new QDoubleSpinBox()) + , m_handleSize(new QDoubleSpinBox()) + , m_handleLineWidth(new QDoubleSpinBox()) + , m_handleColor(new ColorControl(style.handleStyle.color)) + , m_handleSelectionColor(new ColorControl(style.handleStyle.selectionColor)) + , m_keyframeSize(new QDoubleSpinBox()) + , m_keyframeColor(new ColorControl(style.keyframeStyle.color)) + , m_keyframeSelectionColor(new ColorControl(style.keyframeStyle.selectionColor)) + , m_curveWidth(new QDoubleSpinBox()) + , m_curveColor(new ColorControl(style.curveStyle.color)) + , m_curveSelectionColor(new ColorControl(style.curveStyle.selectionColor)) + , m_treeMargins(new QDoubleSpinBox()) + , m_playheadWidth(new QDoubleSpinBox()) + , m_playheadRadius(new QDoubleSpinBox()) + , m_playheadColor(new ColorControl(style.playhead.color)) + +{ + m_canvasMargin->setValue(style.canvasMargin); + m_zoomInWidth->setValue(style.zoomInWidth); + m_zoomInHeight->setValue(style.zoomInHeight); + m_zoomInHeight->setMaximum(9000); + + m_timeAxisHeight->setValue(style.timeAxisHeight); + m_timeOffsetLeft->setValue(style.timeOffsetLeft); + m_timeOffsetRight->setValue(style.timeOffsetRight); + m_valueAxisWidth->setValue(style.valueAxisWidth); + m_valueOffsetTop->setValue(style.valueOffsetTop); + m_valueOffsetBottom->setValue(style.valueOffsetBottom); + m_handleSize->setValue(style.handleStyle.size); + m_handleLineWidth->setValue(style.handleStyle.lineWidth); + m_keyframeSize->setValue(style.keyframeStyle.size); + m_curveWidth->setValue(style.curveStyle.width); + m_treeMargins->setValue(style.treeItemStyle.margins); + m_playheadWidth->setValue(style.playhead.width); + m_playheadRadius->setValue(style.playhead.radius); + + connect(m_printButton, &QPushButton::released, this, &CurveEditorStyleDialog::printStyle); + + auto intChanged = [this](int) { emitStyleChanged(); }; + auto doubleChanged = [this](double) { emitStyleChanged(); }; + auto colorChanged = [this]() { emitStyleChanged(); }; + + auto intSignal = static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged); + auto doubleSignal = static_cast<void (QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged); + + connect(m_background, &ColorControl::valueChanged, colorChanged); + connect(m_backgroundAlternate, &ColorControl::valueChanged, colorChanged); + connect(m_fontColor, &ColorControl::valueChanged, colorChanged); + connect(m_gridColor, &ColorControl::valueChanged, colorChanged); + connect(m_canvasMargin, doubleSignal, doubleChanged); + connect(m_zoomInWidth, intSignal, intChanged); + connect(m_zoomInHeight, intSignal, intChanged); + connect(m_timeAxisHeight, doubleSignal, doubleChanged); + connect(m_timeOffsetLeft, doubleSignal, doubleChanged); + connect(m_timeOffsetRight, doubleSignal, doubleChanged); + connect(m_rangeBarColor, &ColorControl::valueChanged, colorChanged); + connect(m_rangeBarCapsColor, &ColorControl::valueChanged, colorChanged); + connect(m_valueAxisWidth, doubleSignal, doubleChanged); + connect(m_valueOffsetTop, doubleSignal, doubleChanged); + connect(m_valueOffsetBottom, doubleSignal, doubleChanged); + connect(m_handleSize, doubleSignal, doubleChanged); + connect(m_handleLineWidth, doubleSignal, doubleChanged); + connect(m_handleColor, &ColorControl::valueChanged, colorChanged); + connect(m_handleSelectionColor, &ColorControl::valueChanged, colorChanged); + connect(m_keyframeSize, doubleSignal, doubleChanged); + connect(m_keyframeColor, &ColorControl::valueChanged, colorChanged); + connect(m_keyframeSelectionColor, &ColorControl::valueChanged, colorChanged); + connect(m_curveWidth, doubleSignal, doubleChanged); + connect(m_curveColor, &ColorControl::valueChanged, colorChanged); + connect(m_curveSelectionColor, &ColorControl::valueChanged, colorChanged); + connect(m_treeMargins, doubleSignal, doubleChanged); + connect(m_playheadWidth, doubleSignal, doubleChanged); + connect(m_playheadRadius, doubleSignal, doubleChanged); + connect(m_playheadColor, &ColorControl::valueChanged, colorChanged); + + auto *box = new QVBoxLayout; + box->addLayout(createRow("Background Color", m_background)); + box->addLayout(createRow("Alternate Background Color", m_backgroundAlternate)); + box->addLayout(createRow("Font Color", m_fontColor)); + box->addLayout(createRow("Grid Color", m_gridColor)); + box->addLayout(createRow("Canvas Margin", m_canvasMargin)); + box->addLayout(createRow("Zoom In Width", m_zoomInWidth)); + box->addLayout(createRow("Zoom In Height", m_zoomInHeight)); + box->addLayout(createRow("Time Axis Height", m_timeAxisHeight)); + box->addLayout(createRow("Time Axis Left Offset", m_timeOffsetLeft)); + box->addLayout(createRow("Time Axis Right Offset", m_timeOffsetRight)); + box->addLayout(createRow("Range Bar Color", m_rangeBarColor)); + box->addLayout(createRow("Range Bar Caps Color", m_rangeBarCapsColor)); + box->addLayout(createRow("Value Axis Width", m_valueAxisWidth)); + box->addLayout(createRow("Value Axis Top Offset", m_valueOffsetTop)); + box->addLayout(createRow("Value Axis Bottom Offset", m_valueOffsetBottom)); + box->addLayout(createRow("Handle Size", m_handleSize)); + box->addLayout(createRow("Handle Line Width", m_handleLineWidth)); + box->addLayout(createRow("Handle Color", m_handleColor)); + box->addLayout(createRow("Handle Selection Color", m_handleSelectionColor)); + box->addLayout(createRow("Keyframe Size", m_keyframeSize)); + box->addLayout(createRow("Keyframe Color", m_keyframeColor)); + box->addLayout(createRow("Keyframe Selection Color", m_keyframeSelectionColor)); + box->addLayout(createRow("Curve Width", m_curveWidth)); + box->addLayout(createRow("Curve Color", m_curveColor)); + box->addLayout(createRow("Curve Selection Color", m_curveSelectionColor)); + box->addLayout(createRow("Treeview margins", m_treeMargins)); + box->addLayout(createRow("Playhead width", m_playheadWidth)); + box->addLayout(createRow("Playhead radius", m_playheadRadius)); + box->addLayout(createRow("Playhead color", m_playheadColor)); + + box->addWidget(m_printButton); + setLayout(box); +} + +CurveEditorStyle CurveEditorStyleDialog::style() const +{ + CurveEditorStyle style; + style.backgroundBrush = QBrush(m_background->value()); + style.backgroundAlternateBrush = QBrush(m_backgroundAlternate->value()); + style.fontColor = m_fontColor->value(); + style.gridColor = m_gridColor->value(); + style.canvasMargin = m_canvasMargin->value(); + style.zoomInWidth = m_zoomInWidth->value(); + style.zoomInHeight = m_zoomInHeight->value(); + style.timeAxisHeight = m_timeAxisHeight->value(); + style.timeOffsetLeft = m_timeOffsetLeft->value(); + style.timeOffsetRight = m_timeOffsetRight->value(); + style.rangeBarColor = m_rangeBarColor->value(); + style.rangeBarCapsColor = m_rangeBarCapsColor->value(); + style.valueAxisWidth = m_valueAxisWidth->value(); + style.valueOffsetTop = m_valueOffsetTop->value(); + style.valueOffsetBottom = m_valueOffsetBottom->value(); + style.handleStyle.size = m_handleSize->value(); + style.handleStyle.lineWidth = m_handleLineWidth->value(); + style.handleStyle.color = m_handleColor->value(); + style.handleStyle.selectionColor = m_handleSelectionColor->value(); + style.keyframeStyle.size = m_keyframeSize->value(); + style.keyframeStyle.color = m_keyframeColor->value(); + style.keyframeStyle.selectionColor = m_keyframeSelectionColor->value(); + style.curveStyle.width = m_curveWidth->value(); + style.curveStyle.color = m_curveColor->value(); + style.curveStyle.selectionColor = m_curveSelectionColor->value(); + style.treeItemStyle.margins = m_treeMargins->value(); + style.playhead.width = m_playheadWidth->value(); + style.playhead.radius = m_playheadRadius->value(); + style.playhead.color = m_playheadColor->value(); + + return style; +} + +void CurveEditorStyleDialog::emitStyleChanged() +{ + emit styleChanged(style()); +} + +void CurveEditorStyleDialog::printStyle() +{ + auto toString = [](const QColor &color) { + QString tmp + = QString("QColor(%1, %2, %3)").arg(color.red()).arg(color.green()).arg(color.blue()); + return qPrintable(tmp); + }; + + CurveEditorStyle s = style(); + qDebug() << ""; + qDebug().nospace() << "CurveEditorStyle out;"; + qDebug().nospace() << "out.backgroundBrush = QBrush(" << toString(s.backgroundBrush.color()) + << ");"; + qDebug().nospace() << "out.backgroundAlternateBrush = QBrush(" + << toString(s.backgroundAlternateBrush.color()) << ");"; + qDebug().nospace() << "out.fontColor = " << toString(s.fontColor) << ";"; + qDebug().nospace() << "out.gridColor = " << toString(s.gridColor) << ";"; + qDebug().nospace() << "out.canvasMargin = " << s.canvasMargin << ";"; + qDebug().nospace() << "out.zoomInWidth = " << s.zoomInWidth << ";"; + qDebug().nospace() << "out.zoomInHeight = " << s.zoomInHeight << ";"; + qDebug().nospace() << "out.timeAxisHeight = " << s.timeAxisHeight << ";"; + qDebug().nospace() << "out.timeOffsetLeft = " << s.timeOffsetLeft << ";"; + qDebug().nospace() << "out.timeOffsetRight = " << s.timeOffsetRight << ";"; + qDebug().nospace() << "out.rangeBarColor = " << toString(s.rangeBarColor) << ";"; + qDebug().nospace() << "out.rangeBarCapsColor = " << toString(s.rangeBarCapsColor) << ";"; + qDebug().nospace() << "out.valueAxisWidth = " << s.valueAxisWidth << ";"; + qDebug().nospace() << "out.valueOffsetTop = " << s.valueOffsetTop << ";"; + qDebug().nospace() << "out.valueOffsetBottom = " << s.valueOffsetBottom << ";"; + qDebug().nospace() << "out.handleStyle.size = " << s.handleStyle.size << ";"; + qDebug().nospace() << "out.handleStyle.lineWidth = " << s.handleStyle.lineWidth << ";"; + qDebug().nospace() << "out.handleStyle.color = " << toString(s.handleStyle.color) << ";"; + qDebug().nospace() << "out.handleStyle.selectionColor = " + << toString(s.handleStyle.selectionColor) << ";"; + qDebug().nospace() << "out.keyframeStyle.size = " << s.keyframeStyle.size << ";"; + qDebug().nospace() << "out.keyframeStyle.color = " << toString(s.keyframeStyle.color) << ";"; + qDebug().nospace() << "out.keyframeStyle.selectionColor = " + << toString(s.keyframeStyle.selectionColor) << ";"; + qDebug().nospace() << "out.curveStyle.width = " << s.curveStyle.width << ";"; + qDebug().nospace() << "out.curveStyle.color = " << toString(s.curveStyle.color) << ";"; + qDebug().nospace() << "out.curveStyle.selectionColor = " + << toString(s.curveStyle.selectionColor) << ";"; + qDebug().nospace() << "out.treeItemStyle.margins = " << s.treeItemStyle.margins << ";"; + qDebug().nospace() << "out.playheadStyle.width = " << s.playhead.width << ";"; + qDebug().nospace() << "out.playheadStyle.radius = " << s.playhead.radius << ";"; + qDebug().nospace() << "out.playheadStyle.color = " << toString(s.playhead.color) << ";"; + qDebug().nospace() << "return out;"; + qDebug() << ""; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/curveeditorstyledialog.h b/src/plugins/qmldesigner/components/curveeditor/detail/curveeditorstyledialog.h new file mode 100644 index 0000000000..f1dc3bc372 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/curveeditorstyledialog.h @@ -0,0 +1,127 @@ + +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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_BEGIN_NAMESPACE +class QPushButton; +class QSpinBox; +class QDoubleSpinBox; +QT_END_NAMESPACE + +namespace DesignTools { + +class ColorControl; + +struct CurveEditorStyle; + +class CurveEditorStyleDialog : public QDialog +{ + Q_OBJECT + +signals: + void styleChanged(const CurveEditorStyle &style); + +public: + CurveEditorStyleDialog(CurveEditorStyle &style, QWidget *parent = nullptr); + + CurveEditorStyle style() const; + +private: + void emitStyleChanged(); + + void printStyle(); + +private: + QPushButton *m_printButton; + + ColorControl *m_background; + + ColorControl *m_backgroundAlternate; + + ColorControl *m_fontColor; + + ColorControl *m_gridColor; + + QDoubleSpinBox *m_canvasMargin; + + QSpinBox *m_zoomInWidth; + + QSpinBox *m_zoomInHeight; + + QDoubleSpinBox *m_timeAxisHeight; + + QDoubleSpinBox *m_timeOffsetLeft; + + QDoubleSpinBox *m_timeOffsetRight; + + ColorControl *m_rangeBarColor; + + ColorControl *m_rangeBarCapsColor; + + QDoubleSpinBox *m_valueAxisWidth; + + QDoubleSpinBox *m_valueOffsetTop; + + QDoubleSpinBox *m_valueOffsetBottom; + + // HandleItem + QDoubleSpinBox *m_handleSize; + + QDoubleSpinBox *m_handleLineWidth; + + ColorControl *m_handleColor; + + ColorControl *m_handleSelectionColor; + + // KeyframeItem + QDoubleSpinBox *m_keyframeSize; + + ColorControl *m_keyframeColor; + + ColorControl *m_keyframeSelectionColor; + + // CurveItem + QDoubleSpinBox *m_curveWidth; + + ColorControl *m_curveColor; + + ColorControl *m_curveSelectionColor; + + // TreeItem + QDoubleSpinBox *m_treeMargins; + + // Playhead + QDoubleSpinBox *m_playheadWidth; + + QDoubleSpinBox *m_playheadRadius; + + ColorControl *m_playheadColor; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/curveitem.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/curveitem.cpp new file mode 100644 index 0000000000..38efd29571 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/curveitem.cpp @@ -0,0 +1,226 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveitem.h" +#include "animationcurve.h" +#include "graphicsscene.h" +#include "keyframeitem.h" +#include "utils.h" + +#include <QPainter> +#include <QPainterPath> + +#include <cmath> + +namespace DesignTools { + +CurveItem::CurveItem(QGraphicsItem *parent) + : QGraphicsObject(parent) + , m_id(0) + , m_style() + , m_transform() + , m_keyframes() + , m_underMouse(false) + , m_itemDirty(false) + , m_pathDirty(true) +{} + +CurveItem::CurveItem(unsigned int id, const AnimationCurve &curve, QGraphicsItem *parent) + : QGraphicsObject(parent) + , m_id(id) + , m_style() + , m_transform() + , m_keyframes() + , m_underMouse(false) + , m_itemDirty(false) + , m_pathDirty(true) +{ + setAcceptHoverEvents(true); + + setFlag(QGraphicsItem::ItemIsMovable, false); + + auto emitCurveChanged = [this]() { + m_itemDirty = true; + m_pathDirty = true; + update(); + }; + + for (auto frame : curve.keyframes()) { + auto *item = new KeyframeItem(frame, this); + QObject::connect(item, &KeyframeItem::redrawCurve, emitCurveChanged); + m_keyframes.push_back(item); + } +} + +CurveItem::~CurveItem() {} + +int CurveItem::type() const +{ + return Type; +} + +QRectF CurveItem::boundingRect() const +{ + auto bbox = [](QRectF &bounds, const Keyframe &frame) { + grow(bounds, frame.position()); + grow(bounds, frame.leftHandle()); + grow(bounds, frame.rightHandle()); + }; + + QRectF bounds; + for (auto *item : m_keyframes) + bbox(bounds, item->keyframe()); + + return m_transform.mapRect(bounds); +} + +bool CurveItem::contains(const QPointF &point) const +{ + bool valid = false; + QPointF transformed(m_transform.inverted(&valid).map(point)); + + double width = std::abs(20.0 / scaleY(m_transform)); + + if (valid) + return curve().intersects(transformed, width); + + return false; +} + +void CurveItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + if (m_keyframes.size() > 1) { + QPen pen = painter->pen(); + QColor col = m_underMouse ? Qt::red : m_style.color; + + pen.setWidthF(m_style.width); + pen.setColor(hasSelection() ? m_style.selectionColor : col); + + painter->save(); + painter->setPen(pen); + painter->drawPath(path()); + + painter->restore(); + } +} + +bool CurveItem::isDirty() const +{ + return m_itemDirty; +} + +bool CurveItem::hasSelection() const +{ + for (auto *frame : m_keyframes) { + if (frame->selected()) + return true; + } + + return false; +} + +unsigned int CurveItem::id() const +{ + return m_id; +} + +QPainterPath CurveItem::path() const +{ + if (m_pathDirty) { + Keyframe previous = m_keyframes.front()->keyframe(); + Keyframe current; + + m_path = QPainterPath(m_transform.map(previous.position())); + for (size_t i = 1; i < m_keyframes.size(); ++i) { + current = m_keyframes[i]->keyframe(); + + if (previous.rightHandle().isNull() || current.leftHandle().isNull()) { + m_path.lineTo(m_transform.map(current.position())); + } else { + m_path.cubicTo( + m_transform.map(previous.rightHandle()), + m_transform.map(current.leftHandle()), + m_transform.map(current.position())); + } + + previous = current; + } + m_pathDirty = false; + } + + return m_path; +} + +AnimationCurve CurveItem::curve() const +{ + std::vector<Keyframe> out; + out.reserve(m_keyframes.size()); + for (auto item : m_keyframes) + out.push_back(item->keyframe()); + + return out; +} + +void CurveItem::setDirty(bool dirty) +{ + m_itemDirty = dirty; +} + +QRectF CurveItem::setComponentTransform(const QTransform &transform) +{ + m_pathDirty = true; + + prepareGeometryChange(); + m_transform = transform; + for (auto frame : m_keyframes) + frame->setComponentTransform(transform); + + return boundingRect(); +} + +void CurveItem::setStyle(const CurveEditorStyle &style) +{ + m_style = style.curveStyle; + + for (auto *frame : m_keyframes) + frame->setStyle(style); +} + +void CurveItem::connect(GraphicsScene *scene) +{ + for (auto *frame : m_keyframes) { + QObject::connect(frame, &KeyframeItem::keyframeMoved, scene, &GraphicsScene::keyframeMoved); + QObject::connect(frame, &KeyframeItem::handleMoved, scene, &GraphicsScene::handleMoved); + } +} + +void CurveItem::setIsUnderMouse(bool under) +{ + if (under != m_underMouse) { + m_underMouse = under; + update(); + } +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/curveitem.h b/src/plugins/qmldesigner/components/curveeditor/detail/curveitem.h new file mode 100644 index 0000000000..90e68e20f1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/curveitem.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyle.h" +#include "selectableitem.h" + +#include <QGraphicsObject> + +namespace DesignTools { + +class AnimationCurve; +class KeyframeItem; +class GraphicsScene; + +class CurveItem : public QGraphicsObject +{ + Q_OBJECT + +public: + CurveItem(QGraphicsItem *parent = nullptr); + + CurveItem(unsigned int id, const AnimationCurve &curve, QGraphicsItem *parent = nullptr); + + ~CurveItem() override; + + enum { Type = ItemTypeCurve }; + + int type() const override; + + QRectF boundingRect() const override; + + bool contains(const QPointF &point) const override; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + bool isDirty() const; + + bool hasSelection() const; + + unsigned int id() const; + + AnimationCurve curve() const; + + void setDirty(bool dirty); + + QRectF setComponentTransform(const QTransform &transform); + + void setStyle(const CurveEditorStyle &style); + + void connect(GraphicsScene *scene); + + void setIsUnderMouse(bool under); + +private: + QPainterPath path() const; + + unsigned int m_id; + + CurveItemStyleOption m_style; + + QTransform m_transform; + + std::vector<KeyframeItem *> m_keyframes; + + bool m_underMouse; + + bool m_itemDirty; + + mutable bool m_pathDirty; + + mutable QPainterPath m_path; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/curvesegment.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/curvesegment.cpp new file mode 100644 index 0000000000..40f675f3ec --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/curvesegment.cpp @@ -0,0 +1,277 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curvesegment.h" +#include "utils.h" + +#include <qmath.h> + +#include <assert.h> + +namespace DesignTools { + +class CubicPolynomial +{ +public: + CubicPolynomial(double p0, double p1, double p2, double p3); + + std::vector<double> extrema() const; + + std::vector<double> roots() const; + +private: + double m_a; + double m_b; + double m_c; + double m_d; +}; + +CubicPolynomial::CubicPolynomial(double p0, double p1, double p2, double p3) + : m_a(p3 - 3.0 * p2 + 3.0 * p1 - p0) + , m_b(3.0 * p2 - 6.0 * p1 + 3.0 * p0) + , m_c(3.0 * p1 - 3.0 * p0) + , m_d(p0) +{} + +std::vector<double> CubicPolynomial::extrema() const +{ + std::vector<double> out; + + auto addValidValue = [&out](double value) { + if (!std::isnan(value) && !std::isinf(value)) + out.push_back(clamp(value, 0.0, 1.0)); + }; + + // Find the roots of the first derivative of y. + auto pd2 = (2.0 * m_b) / (3.0 * m_a) / 2.0; + auto q = m_c / (3.0 * m_a); + + auto radi = std::pow(pd2, 2.0) - q; + + auto x1 = -pd2 + std::sqrt(radi); + auto x2 = -pd2 - std::sqrt(radi); + + addValidValue(x1); + addValidValue(x2); + + return out; +} + +std::vector<double> CubicPolynomial::roots() const +{ + std::vector<double> out; + + auto addValidValue = [&out](double value) { + if (!(std::isnan(value) || std::isinf(value))) + out.push_back(value); + }; + + if (m_a == 0.0) { + // Linear + if (m_b == 0.0) { + if (m_c != 0.0) + out.push_back(-m_d / m_c); + // Quadratic + } else { + const double p = m_c / m_b / 2.0; + const double q = m_d / m_b; + addValidValue(-p + std::sqrt(std::pow(p, 2.0) - q)); + addValidValue(-p - std::sqrt(std::pow(p, 2.0) - q)); + } + // Cubic + } else { + const double p = 3.0 * m_a * m_c - std::pow(m_b, 2.0); + const double q = 2.0 * std::pow(m_b, 3.0) - 9.0 * m_a * m_b * m_c + + 27.0 * std::pow(m_a, 2.0) * m_d; + + auto disc = std::pow(q, 2.0) + 4.0 * std::pow(p, 3.0); + + auto toX = [&](double y) { return (y - m_b) / (3.0 * m_a); }; + + // One real solution. + if (disc >= 0) { + auto u = (1.0 / 2.0) + * std::cbrt(-4.0 * q + + 4.0 * std::sqrt(std::pow(q, 2.0) + 4.0 * std::pow(p, 3.0))); + auto v = (1.0 / 2.0) + * std::cbrt(-4.0 * q + - 4.0 * std::sqrt(std::pow(q, 2.0) + 4.0 * std::pow(p, 3.0))); + + addValidValue(toX(u + v)); + // Three real solutions. + } else { + auto phi = acos(-q / (2 * std::sqrt(-std::pow(p, 3.0)))); + auto y1 = std::sqrt(-p) * 2.0 * cos(phi / 3.0); + auto y2 = std::sqrt(-p) * 2.0 * cos((phi / 3.0) + (2.0 * M_PI / 3.0)); + auto y3 = std::sqrt(-p) * 2.0 * cos((phi / 3.0) + (4.0 * M_PI / 3.0)); + + addValidValue(toX(y1)); + addValidValue(toX(y2)); + addValidValue(toX(y3)); + } + } + return out; +} + +CurveSegment::CurveSegment() + : m_left() + , m_right() +{} + +CurveSegment::CurveSegment(const Keyframe &left, const Keyframe &right) + : m_left(left) + , m_right(right) +{} + +bool CurveSegment::containsX(double x) const +{ + return m_left.position().x() <= x && m_right.position().x() >= x; +} + +double evaluateForT(double t, double p0, double p1, double p2, double p3) +{ + assert(t >= 0. && t <= 1.); + + const double it = 1.0 - t; + + return p0 * std::pow(it, 3.0) + p1 * 3.0 * std::pow(it, 2.0) * t + + p2 * 3.0 * it * std::pow(t, 2.0) + p3 * std::pow(t, 3.0); +} + +QPointF CurveSegment::evaluate(double t) const +{ + const double x = evaluateForT( + t, + m_left.position().x(), + m_left.rightHandle().x(), + m_right.leftHandle().x(), + m_right.position().x()); + + const double y = evaluateForT( + t, + m_left.position().y(), + m_left.rightHandle().y(), + m_right.leftHandle().y(), + m_right.position().y()); + + return QPointF(x, y); +} + +std::vector<QPointF> CurveSegment::extrema() const +{ + std::vector<QPointF> out; + + auto polynomial = CubicPolynomial( + m_left.position().y(), + m_left.rightHandle().y(), + m_right.leftHandle().y(), + m_right.position().y()); + + for (double t : polynomial.extrema()) { + + const double x = evaluateForT( + t, + m_left.position().x(), + m_left.rightHandle().x(), + m_right.leftHandle().x(), + m_right.position().x()); + + const double y = evaluateForT( + t, + m_left.position().y(), + m_left.rightHandle().y(), + m_right.leftHandle().y(), + m_right.position().y()); + + out.push_back(QPointF(x, y)); + } + return out; +} + +std::vector<double> CurveSegment::yForX(double x) const +{ + std::vector<double> out; + + auto polynomial = CubicPolynomial( + m_left.position().x() - x, + m_left.rightHandle().x() - x, + m_right.leftHandle().x() - x, + m_right.position().x() - x); + + for (double t : polynomial.roots()) { + if (t < 0.0 || t > 1.0) + continue; + + const double y = evaluateForT( + t, + m_left.position().y(), + m_left.rightHandle().y(), + m_right.leftHandle().y(), + m_right.position().y()); + + out.push_back(y); + } + + return out; +} + +std::vector<double> CurveSegment::xForY(double y) const +{ + std::vector<double> out; + + auto polynomial = CubicPolynomial( + m_left.position().y() - y, + m_left.rightHandle().y() - y, + m_right.leftHandle().y() - y, + m_right.position().y() - y); + + for (double t : polynomial.roots()) { + if (t < 0.0 || t > 1.0) + continue; + + const double x = evaluateForT( + t, + m_left.position().x(), + m_left.rightHandle().x(), + m_right.leftHandle().x(), + m_right.position().x()); + + out.push_back(x); + } + + return out; +} + +void CurveSegment::setLeft(const Keyframe &frame) +{ + m_left = frame; +} + +void CurveSegment::setRight(const Keyframe &frame) +{ + m_right = frame; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/curvesegment.h b/src/plugins/qmldesigner/components/curveeditor/detail/curvesegment.h new file mode 100644 index 0000000000..5dbce58bcc --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/curvesegment.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "keyframe.h" + +#include <vector> + +QT_BEGIN_NAMESPACE +class QPointF; +QT_END_NAMESPACE + +namespace DesignTools { + +class CurveSegment +{ +public: + CurveSegment(); + + CurveSegment(const Keyframe &first, const Keyframe &last); + + bool containsX(double x) const; + + QPointF evaluate(double t) const; + + std::vector<QPointF> extrema() const; + + std::vector<double> yForX(double x) const; + + std::vector<double> xForY(double y) const; + + void setLeft(const Keyframe &frame); + + void setRight(const Keyframe &frame); + +private: + Keyframe m_left; + + Keyframe m_right; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/graphicsscene.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsscene.cpp new file mode 100644 index 0000000000..096e57aafa --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsscene.cpp @@ -0,0 +1,225 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "graphicsscene.h" +#include "animationcurve.h" +#include "curveitem.h" +#include "graphicsview.h" +#include "handleitem.h" + +#include <QGraphicsSceneMouseEvent> + +namespace DesignTools { + +GraphicsScene::GraphicsScene(QObject *parent) + : QGraphicsScene(parent) + , m_dirty(true) + , m_limits() +{} + +bool GraphicsScene::empty() const +{ + return items().empty(); +} + +double GraphicsScene::minimumTime() const +{ + return limits().left(); +} + +double GraphicsScene::maximumTime() const +{ + return limits().right(); +} + +double GraphicsScene::minimumValue() const +{ + return limits().bottom(); +} + +double GraphicsScene::maximumValue() const +{ + return limits().top(); +} + +void GraphicsScene::addCurveItem(CurveItem *item) +{ + m_dirty = true; + addItem(item); + item->connect(this); +} + +void GraphicsScene::setComponentTransform(const QTransform &transform) +{ + QRectF bounds; + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(item)) + bounds = bounds.united(curveItem->setComponentTransform(transform)); + } + + if (bounds.isNull()) { + if (GraphicsView *gview = graphicsView()) + bounds = gview->defaultRasterRect(); + } + + if (bounds.isValid()) + setSceneRect(bounds); +} + +void GraphicsScene::keyframeMoved(KeyframeItem *movedItem, const QPointF &direction) +{ + const auto itemList = items(); + for (auto *item : itemList) { + if (item == movedItem) + continue; + + if (auto *frameItem = qgraphicsitem_cast<KeyframeItem *>(item)) { + if (frameItem->selected()) + frameItem->moveKeyframe(direction); + } + } +} + +void GraphicsScene::handleMoved(KeyframeItem *frame, + HandleSlot handle, + double angle, + double deltaLength) +{ + const auto itemList = items(); + for (auto *item : itemList) { + if (item == frame) + continue; + + if (auto *frameItem = qgraphicsitem_cast<KeyframeItem *>(item)) { + if (frameItem->selected()) + frameItem->moveHandle(handle, angle, deltaLength); + } + } +} + +void GraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) +{ + QGraphicsScene::mouseMoveEvent(mouseEvent); + + if (hasActiveItem()) + return; + + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(item)) + curveItem->setIsUnderMouse(curveItem->contains(mouseEvent->scenePos())); + } +} + +void GraphicsScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) +{ + QGraphicsScene::mouseReleaseEvent(mouseEvent); + + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(item)) { + if (curveItem->contains(mouseEvent->scenePos())) + curveItem->setSelected(true); + + if (curveItem->isDirty()) { + emit curveChanged(curveItem->id(), curveItem->curve()); + curveItem->setDirty(false); + m_dirty = true; + } + } + } +} + +bool GraphicsScene::hasActiveKeyframe() const +{ + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *kitem = qgraphicsitem_cast<KeyframeItem *>(item)) { + if (kitem->activated()) + return true; + } + } + return false; +} + +bool GraphicsScene::hasActiveHandle() const +{ + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *hitem = qgraphicsitem_cast<HandleItem *>(item)) { + if (hitem->activated()) + return true; + } + } + return false; +} + +bool GraphicsScene::hasActiveItem() const +{ + return hasActiveKeyframe() || hasActiveHandle(); +} + +GraphicsView *GraphicsScene::graphicsView() const +{ + const QList<QGraphicsView *> viewList = views(); + for (auto &&view : viewList) { + if (GraphicsView *gview = qobject_cast<GraphicsView *>(view)) + return gview; + } + return nullptr; +} + +QRectF GraphicsScene::limits() const +{ + if (m_dirty) { + QPointF min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max()); + QPointF max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest()); + + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(item)) { + auto curve = curveItem->curve(); + if (min.x() > curve.minimumTime()) + min.rx() = curve.minimumTime(); + + if (min.y() > curve.minimumValue()) + min.ry() = curve.minimumValue(); + + if (max.x() < curve.maximumTime()) + max.rx() = curve.maximumTime(); + + if (max.y() < curve.maximumValue()) + max.ry() = curve.maximumValue(); + } + } + + m_limits = QRectF(QPointF(min.x(), max.y()), QPointF(max.x(), min.y())); + m_dirty = false; + } + return m_limits; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/graphicsscene.h b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsscene.h new file mode 100644 index 0000000000..981c326b5a --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsscene.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "keyframeitem.h" + +#include <QGraphicsScene> + +namespace DesignTools { + +class AnimationCurve; +class CurveItem; +class GraphicsView; + +class GraphicsScene : public QGraphicsScene +{ + Q_OBJECT + +signals: + void curveChanged(unsigned int id, const AnimationCurve &curve); + +public: + GraphicsScene(QObject *parent = nullptr); + + bool empty() const; + + bool hasActiveKeyframe() const; + + bool hasActiveHandle() const; + + bool hasActiveItem() const; + + double minimumTime() const; + + double maximumTime() const; + + double minimumValue() const; + + double maximumValue() const; + + void addCurveItem(CurveItem *item); + + void setComponentTransform(const QTransform &transform); + + void keyframeMoved(KeyframeItem *item, const QPointF &direction); + + void handleMoved(KeyframeItem *frame, HandleSlot handle, double angle, double deltaLength); + +protected: + void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override; + + void mouseReleaseEvent(QGraphicsSceneMouseEvent *mouseEvent) override; + +private: + using QGraphicsScene::addItem; + + GraphicsView *graphicsView() const; + + QRectF limits() const; + + mutable bool m_dirty; + + mutable QRectF m_limits; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/graphicsview.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsview.cpp new file mode 100644 index 0000000000..ae60888d1f --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsview.cpp @@ -0,0 +1,523 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "graphicsview.h" +#include "curveeditormodel.h" +#include "curveitem.h" +#include "utils.h" + +#include <QAction> +#include <QMenu> +#include <QResizeEvent> +#include <QScrollBar> + +#include <cmath> + +namespace DesignTools { + +GraphicsView::GraphicsView(CurveEditorModel *model, QWidget *parent) + : QGraphicsView(parent) + , m_zoomX(0.0) + , m_zoomY(0.0) + , m_transform() + , m_scene() + , m_model(model) + , m_playhead(this) + , m_selector() + , m_style(model->style()) + , m_dialog(m_style) +{ + model->setGraphicsView(this); + + setScene(&m_scene); + setAlignment(Qt::AlignLeft | Qt::AlignVCenter); + setResizeAnchor(QGraphicsView::NoAnchor); + setTransformationAnchor(QGraphicsView::NoAnchor); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + connect(&m_dialog, &CurveEditorStyleDialog::styleChanged, this, &GraphicsView::setStyle); + + auto itemSlot = [this](unsigned int id, const AnimationCurve &curve) { + applyZoom(m_zoomX, m_zoomY); + m_model->setCurve(id, curve); + }; + + connect(&m_scene, &GraphicsScene::curveChanged, itemSlot); + + applyZoom(m_zoomX, m_zoomY); + update(); +} + +CurveEditorModel *GraphicsView::model() const +{ + return m_model; +} + +CurveEditorStyle GraphicsView::editorStyle() const +{ + return m_style; +} + +bool GraphicsView::hasActiveItem() const +{ + return m_scene.hasActiveItem(); +} + +bool GraphicsView::hasActiveHandle() const +{ + return m_scene.hasActiveHandle(); +} + +double GraphicsView::minimumTime() const +{ + bool check = m_model->minimumTime() < m_scene.minimumTime(); + return check ? m_model->minimumTime() : m_scene.minimumTime(); +} + +double GraphicsView::maximumTime() const +{ + bool check = m_model->maximumTime() > m_scene.maximumTime(); + return check ? m_model->maximumTime() : m_scene.maximumTime(); +} + +double GraphicsView::minimumValue() const +{ + return m_scene.empty() ? -1.0 : m_scene.minimumValue(); +} + +double GraphicsView::maximumValue() const +{ + return m_scene.empty() ? 1.0 : m_scene.maximumValue(); +} + +double GraphicsView::zoomX() const +{ + return m_zoomX; +} + +double GraphicsView::zoomY() const +{ + return m_zoomY; +} + +QRectF GraphicsView::canvasRect() const +{ + QRect r = viewport()->rect().adjusted( + m_style.valueAxisWidth + m_style.canvasMargin, + m_style.timeAxisHeight + m_style.canvasMargin, + -m_style.canvasMargin, + -m_style.canvasMargin); + + return mapToScene(r).boundingRect(); +} + +QRectF GraphicsView::timeScaleRect() const +{ + QRect vp(viewport()->rect()); + QPoint tl = vp.topLeft() + QPoint(m_style.valueAxisWidth, 0); + QPoint br = vp.topRight() + QPoint(0, m_style.timeAxisHeight); + return mapToScene(QRect(tl, br)).boundingRect(); +} + +QRectF GraphicsView::valueScaleRect() const +{ + QRect vp(viewport()->rect()); + QPoint tl = vp.topLeft() + QPoint(0, m_style.timeAxisHeight); + QPoint br = vp.bottomLeft() + QPoint(m_style.valueAxisWidth, 0); + return mapToScene(QRect(tl, br)).boundingRect(); +} + +QRectF GraphicsView::defaultRasterRect() const +{ + QPointF topLeft(mapTimeToX(minimumTime()), mapValueToY(maximumValue())); + QPointF bottomRight(mapTimeToX(maximumTime()), mapValueToY(minimumValue())); + return QRectF(topLeft, bottomRight); +} + +void GraphicsView::setStyle(const CurveEditorStyle &style) +{ + m_style = style; + + const auto itemList = items(); + for (auto *item : itemList) { + if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(item)) + curveItem->setStyle(style); + } + + applyZoom(m_zoomX, m_zoomY); + viewport()->update(); +} + +void GraphicsView::setZoomX(double zoom, const QPoint &pivot) +{ + applyZoom(zoom, m_zoomY, pivot); + viewport()->update(); +} + +void GraphicsView::setZoomY(double zoom, const QPoint &pivot) +{ + applyZoom(m_zoomX, zoom, pivot); + viewport()->update(); +} + +void GraphicsView::setCurrentFrame(int frame) +{ + int clampedFrame = clamp(frame, m_model->minimumTime(), m_model->maximumTime()); + m_playhead.moveToFrame(clampedFrame, this); + viewport()->update(); +} + +void GraphicsView::scrollContent(double x, double y) +{ + QScrollBar *hs = horizontalScrollBar(); + QScrollBar *vs = verticalScrollBar(); + hs->setValue(hs->value() + x); + vs->setValue(vs->value() + y); +} + +void GraphicsView::reset(const std::vector<CurveItem *> &items) +{ + m_scene.clear(); + for (auto *item : items) + m_scene.addCurveItem(item); + + applyZoom(m_zoomX, m_zoomY); + viewport()->update(); +} + +void GraphicsView::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + applyZoom(m_zoomX, m_zoomY); +} + +void GraphicsView::keyPressEvent(QKeyEvent *event) +{ + Shortcut shortcut(event->modifiers(), static_cast<Qt::Key>(event->key())); + if (shortcut == m_style.shortcuts.frameAll) + applyZoom(0.0, 0.0); +} + +void GraphicsView::mousePressEvent(QMouseEvent *event) +{ + if (m_playhead.mousePress(globalToScene(event->globalPos()))) + return; + + Shortcut shortcut(event); + if (shortcut == Shortcut(Qt::LeftButton)) { + QPointF pos = mapToScene(event->pos()); + if (timeScaleRect().contains(pos)) { + setCurrentFrame(std::round(mapXtoTime(pos.x()))); + event->accept(); + return; + } + } + + QGraphicsView::mousePressEvent(event); + + m_selector.mousePress(event, this); +} + +void GraphicsView::mouseMoveEvent(QMouseEvent *event) +{ + if (m_playhead.mouseMove(globalToScene(event->globalPos()), this)) + return; + + QGraphicsView::mouseMoveEvent(event); + + m_selector.mouseMove(event, this, m_playhead); +} + +void GraphicsView::mouseReleaseEvent(QMouseEvent *event) +{ + QGraphicsView::mouseReleaseEvent(event); + + m_playhead.mouseRelease(this); + m_selector.mouseRelease(event, this); + this->viewport()->update(); +} + +void GraphicsView::wheelEvent(QWheelEvent *event) +{ + if (event->modifiers().testFlag(Qt::AltModifier)) + return; + + QGraphicsView::wheelEvent(event); +} + +void GraphicsView::contextMenuEvent(QContextMenuEvent *event) +{ + if (event->modifiers() != Qt::NoModifier) + return; + + auto openStyleEditor = [this]() { m_dialog.show(); }; + + QMenu menu; + QAction *openEditorAction = menu.addAction(tr("Open Style Editor")); + connect(openEditorAction, &QAction::triggered, openStyleEditor); + + menu.exec(event->globalPos()); +} + +void GraphicsView::drawForeground(QPainter *painter, const QRectF &rect) +{ + QRectF abscissa = timeScaleRect(); + if (abscissa.isValid()) + drawTimeScale(painter, abscissa); + + auto ordinate = valueScaleRect(); + if (ordinate.isValid()) + drawValueScale(painter, ordinate); + + m_playhead.paint(painter, this); + + painter->fillRect(QRectF(rect.topLeft(), abscissa.bottomLeft()), + m_style.backgroundAlternateBrush); + + m_selector.paint(painter); +} + +void GraphicsView::drawBackground(QPainter *painter, const QRectF &rect) +{ + painter->fillRect(rect, m_style.backgroundBrush); + painter->fillRect(scene()->sceneRect(), m_style.backgroundAlternateBrush); + + drawGrid(painter, rect); + drawExtremaX(painter, rect); + drawExtremaY(painter, rect); +} + +int GraphicsView::mapTimeToX(double time) const +{ + return std::round(time * scaleX(m_transform)); +} + +int GraphicsView::mapValueToY(double y) const +{ + return std::round(y * scaleY(m_transform)); +} + +double GraphicsView::mapXtoTime(int x) const +{ + return static_cast<double>(x) / scaleX(m_transform); +} + +double GraphicsView::mapYtoValue(int y) const +{ + return static_cast<double>(y) / scaleY(m_transform); +} + +QPointF GraphicsView::globalToScene(const QPoint &point) const +{ + return mapToScene(viewport()->mapFromGlobal(point)); +} + +QPointF GraphicsView::globalToRaster(const QPoint &point) const +{ + QPointF scene = globalToScene(point); + return QPointF(mapXtoTime(scene.x()), mapYtoValue(scene.y())); +} + +void GraphicsView::applyZoom(double x, double y, const QPoint &pivot) +{ + QPointF pivotRaster(globalToRaster(pivot)); + + m_zoomX = clamp(x, 0.0, 1.0); + m_zoomY = clamp(y, 0.0, 1.0); + + double minTime = minimumTime(); + double maxTime = maximumTime(); + + double minValue = minimumValue(); + double maxValue = maximumValue(); + + QRectF canvas = canvasRect(); + + double xZoomedOut = canvas.width() / (maxTime - minTime); + double xZoomedIn = m_style.zoomInWidth; + double scaleX = lerp(clamp(m_zoomX, 0.0, 1.0), xZoomedOut, xZoomedIn); + + double yZoomedOut = canvas.height() / (maxValue - minValue); + double yZoomedIn = m_style.zoomInHeight; + double scaleY = lerp(clamp(m_zoomY, 0.0, 1.0), -yZoomedOut, -yZoomedIn); + + m_transform = QTransform::fromScale(scaleX, scaleY); + + m_scene.setComponentTransform(m_transform); + + QRectF sr = m_scene.sceneRect().adjusted( + -m_style.valueAxisWidth - m_style.canvasMargin, + -m_style.timeAxisHeight - m_style.canvasMargin, + m_style.canvasMargin, + m_style.canvasMargin); + + setSceneRect(sr); + + m_playhead.resize(this); + + if (!pivot.isNull()) { + QPointF deltaTransformed = pivotRaster - globalToRaster(pivot); + scrollContent(mapTimeToX(deltaTransformed.x()), mapValueToY(deltaTransformed.y())); + } +} + +void GraphicsView::drawGrid(QPainter *painter, const QRectF &rect) +{ + QRectF gridRect = rect.adjusted( + m_style.valueAxisWidth + m_style.canvasMargin, + m_style.timeAxisHeight + m_style.canvasMargin, + -m_style.canvasMargin, + -m_style.canvasMargin); + + if (!gridRect.isValid()) + return; + + auto drawVerticalLine = [painter, gridRect](double position) { + painter->drawLine(position, gridRect.top(), position, gridRect.bottom()); + }; + + painter->save(); + painter->setPen(m_style.gridColor); + + double timeIncrement = timeLabelInterval(painter, m_model->maximumTime()); + for (double i = minimumTime(); i <= maximumTime(); i += timeIncrement) + drawVerticalLine(mapTimeToX(i)); + + painter->restore(); +} + +void GraphicsView::drawExtremaX(QPainter *painter, const QRectF &rect) +{ + auto drawVerticalLine = [rect, painter](double position) { + painter->drawLine(position, rect.top(), position, rect.bottom()); + }; + + painter->save(); + painter->setPen(Qt::red); + drawVerticalLine(mapTimeToX(m_model->minimumTime())); + drawVerticalLine(mapTimeToX(m_model->maximumTime())); + painter->restore(); +} + +void GraphicsView::drawExtremaY(QPainter *painter, const QRectF &rect) +{ + if (m_scene.empty()) + return; + + auto drawHorizontalLine = [rect, painter](double position) { + painter->drawLine(rect.left(), position, rect.right(), position); + }; + + painter->save(); + painter->setPen(Qt::blue); + drawHorizontalLine(mapValueToY(m_scene.minimumValue())); + drawHorizontalLine(mapValueToY(m_scene.maximumValue())); + + drawHorizontalLine(mapValueToY(0.0)); + + painter->restore(); +} + +void GraphicsView::drawTimeScale(QPainter *painter, const QRectF &rect) +{ + painter->save(); + painter->setPen(m_style.fontColor); + painter->fillRect(rect, m_style.backgroundAlternateBrush); + + QFontMetrics fm(painter->font()); + + auto paintLabeledTick = [this, painter, rect, fm](double time) { + QString timeText = QString("%1").arg(time); + + int position = mapTimeToX(time); + + QRect textRect = fm.boundingRect(timeText); + textRect.moveCenter(QPoint(position, rect.center().y())); + + painter->drawText(textRect, Qt::AlignCenter, timeText); + painter->drawLine(position, rect.bottom() - 2, position, textRect.bottom() + 2); + }; + + double timeIncrement = timeLabelInterval(painter, maximumTime()); + for (double i = minimumTime(); i <= maximumTime(); i += timeIncrement) + paintLabeledTick(i); + + painter->restore(); +} + +void GraphicsView::drawValueScale(QPainter *painter, const QRectF &rect) +{ + painter->save(); + painter->setPen(m_style.fontColor); + painter->fillRect(rect, m_style.backgroundAlternateBrush); + + QFontMetrics fm(painter->font()); + auto paintLabeledTick = [this, painter, rect, fm](double value) { + QString valueText = QString("%1").arg(value); + + int position = mapValueToY(value); + + QRect textRect = fm.boundingRect(valueText); + textRect.moveCenter(QPoint(rect.center().x(), position)); + painter->drawText(textRect, Qt::AlignCenter, valueText); + }; + + paintLabeledTick(minimumValue()); + paintLabeledTick(maximumValue()); + painter->restore(); +} + +double GraphicsView::timeLabelInterval(QPainter *painter, double maxTime) +{ + QFontMetrics fm(painter->font()); + int minTextSpacing = fm.width(QString("X%1X").arg(maxTime)); + + int deltaTime = 1; + int nextFactor = 5; + + double tickDistance = mapTimeToX(deltaTime); + + while (true) { + if (tickDistance == 0 && deltaTime > maxTime) + return maxTime; + + if (tickDistance > minTextSpacing) + break; + + deltaTime *= nextFactor; + tickDistance = mapTimeToX(deltaTime); + + if (nextFactor == 5) + nextFactor = 2; + else + nextFactor = 5; + } + + return deltaTime; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/graphicsview.h b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsview.h new file mode 100644 index 0000000000..1975e3696d --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/graphicsview.h @@ -0,0 +1,157 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyle.h" +#include "curveeditorstyledialog.h" +#include "graphicsscene.h" +#include "playhead.h" +#include "selector.h" + +#include <QGraphicsView> + +namespace DesignTools { + +class CurveItem; +class CurveEditorModel; +class Playhead; + +class GraphicsView : public QGraphicsView +{ + Q_OBJECT + + friend class Playhead; + +public: + GraphicsView(CurveEditorModel *model, QWidget *parent = nullptr); + + CurveEditorModel *model() const; + + CurveEditorStyle editorStyle() const; + + bool hasActiveItem() const; + + bool hasActiveHandle() const; + + int mapTimeToX(double time) const; + + int mapValueToY(double value) const; + + double mapXtoTime(int x) const; + + double mapYtoValue(int y) const; + + QPointF globalToScene(const QPoint &point) const; + + QPointF globalToRaster(const QPoint &point) const; + + double minimumTime() const; + + double maximumTime() const; + + double minimumValue() const; + + double maximumValue() const; + + double zoomX() const; + + double zoomY() const; + + QRectF canvasRect() const; + + QRectF timeScaleRect() const; + + QRectF valueScaleRect() const; + + QRectF defaultRasterRect() const; + + void setStyle(const CurveEditorStyle &style); + + void setZoomX(double zoom, const QPoint &pivot = QPoint()); + + void setZoomY(double zoom, const QPoint &pivot = QPoint()); + + void setCurrentFrame(int frame); + + void scrollContent(double x, double y); + + void reset(const std::vector<CurveItem *> &items); + +protected: + void resizeEvent(QResizeEvent *event) override; + + void keyPressEvent(QKeyEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + + void mouseMoveEvent(QMouseEvent *event) override; + + void mouseReleaseEvent(QMouseEvent *event) override; + + void wheelEvent(QWheelEvent *event) override; + + void contextMenuEvent(QContextMenuEvent *event) override; + + void drawForeground(QPainter *painter, const QRectF &rect) override; + + void drawBackground(QPainter *painter, const QRectF &rect) override; + +private: + void applyZoom(double x, double y, const QPoint &pivot = QPoint()); + + void drawGrid(QPainter *painter, const QRectF &rect); + + void drawExtremaX(QPainter *painter, const QRectF &rect); + + void drawExtremaY(QPainter *painter, const QRectF &rect); + + void drawTimeScale(QPainter *painter, const QRectF &rect); + + void drawValueScale(QPainter *painter, const QRectF &rect); + + double timeLabelInterval(QPainter *painter, double maxTime); + +private: + double m_zoomX; + + double m_zoomY; + + QTransform m_transform; + + GraphicsScene m_scene; + + CurveEditorModel *m_model; + + Playhead m_playhead; + + Selector m_selector; + + CurveEditorStyle m_style; + + CurveEditorStyleDialog m_dialog; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/handleitem.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/handleitem.cpp new file mode 100644 index 0000000000..c54b26e279 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/handleitem.cpp @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "handleitem.h" +#include "utils.h" + +#include <QPainter> + +namespace DesignTools { + +struct HandleGeometry +{ + HandleGeometry(const QPointF &pos, const HandleItemStyleOption &style) + { + QPointF topLeft(-style.size / 2.0, -style.size / 2.0); + handle = QRectF(topLeft, -topLeft); + toKeyframe = QLineF(QPointF(0.0, 0.0), -pos); + angle = -toKeyframe.angle() + 45.0; + } + + QRectF handle; + + QLineF toKeyframe; + + double angle; +}; + +HandleItem::HandleItem(QGraphicsItem *parent) + : SelectableItem(parent) + , m_style() +{ + setFlag(QGraphicsItem::ItemStacksBehindParent, true); +} + +HandleItem::~HandleItem() {} + +int HandleItem::type() const +{ + return Type; +} + +QRectF HandleItem::boundingRect() const +{ + HandleGeometry geom(pos(), m_style); + + QTransform transform; + transform.rotate(geom.angle); + + QRectF bounds = bbox(geom.handle, transform); + grow(bounds, -pos()); + return bounds; +} + +void HandleItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + QColor handleColor(isSelected() ? m_style.selectionColor : m_style.color); + + HandleGeometry geom(pos(), m_style); + + QPen pen = painter->pen(); + pen.setWidthF(m_style.lineWidth); + pen.setColor(handleColor); + + painter->save(); + painter->setPen(pen); + + painter->drawLine(geom.toKeyframe); + painter->rotate(geom.angle); + painter->fillRect(geom.handle, handleColor); + + painter->restore(); +} + +void HandleItem::setStyle(const CurveEditorStyle &style) +{ + m_style = style.handleStyle; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/handleitem.h b/src/plugins/qmldesigner/components/curveeditor/detail/handleitem.h new file mode 100644 index 0000000000..4c9126c629 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/handleitem.h @@ -0,0 +1,56 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyle.h" +#include "selectableitem.h" + +namespace DesignTools { + +class HandleItem : public SelectableItem +{ + Q_OBJECT + +public: + HandleItem(QGraphicsItem *parent); + + ~HandleItem() override; + + enum { Type = ItemTypeHandle }; + + int type() const override; + + QRectF boundingRect() const override; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + void setStyle(const CurveEditorStyle &style); + +private: + HandleItemStyleOption m_style; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/keyframeitem.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/keyframeitem.cpp new file mode 100644 index 0000000000..94cdbbcbb1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/keyframeitem.cpp @@ -0,0 +1,251 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "keyframeitem.h" +#include "handleitem.h" + +#include <QPainter> + +#include <cmath> + +namespace DesignTools { + +KeyframeItem::KeyframeItem(QGraphicsItem *parent) + : SelectableItem(parent) + , m_frame() +{} + +KeyframeItem::KeyframeItem(const Keyframe &keyframe, QGraphicsItem *parent) + : SelectableItem(parent) + , m_transform() + , m_frame(keyframe) + , m_left(keyframe.hasLeftHandle() ? new HandleItem(this) : nullptr) + , m_right(keyframe.hasRightHandle() ? new HandleItem(this) : nullptr) +{ + auto updatePosition = [this]() { this->updatePosition(true); }; + connect(this, &QGraphicsObject::xChanged, updatePosition); + connect(this, &QGraphicsObject::yChanged, updatePosition); + + if (m_left) { + m_left->setPos(m_frame.leftHandle() - m_frame.position()); + auto updateLeftHandle = [this]() { updateHandle(m_left); }; + connect(m_left, &QGraphicsObject::xChanged, updateLeftHandle); + connect(m_left, &QGraphicsObject::yChanged, updateLeftHandle); + m_left->hide(); + } + + if (m_right) { + m_right->setPos(m_frame.rightHandle() - m_frame.position()); + auto updateRightHandle = [this]() { updateHandle(m_right); }; + connect(m_right, &QGraphicsObject::xChanged, updateRightHandle); + connect(m_right, &QGraphicsObject::yChanged, updateRightHandle); + m_right->hide(); + } + + setPos(m_frame.position()); +} + +int KeyframeItem::type() const +{ + return Type; +} + +QRectF KeyframeItem::boundingRect() const +{ + QPointF topLeft(-m_style.size / 2.0, -m_style.size / 2.0); + return QRectF(topLeft, -topLeft); +} + +void KeyframeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + QPen pen = painter->pen(); + pen.setColor(Qt::black); + + painter->save(); + painter->setPen(pen); + painter->setBrush(selected() ? Qt::red : m_style.color); + painter->drawEllipse(boundingRect()); + + painter->restore(); +} + +KeyframeItem::~KeyframeItem() {} + +Keyframe KeyframeItem::keyframe() const +{ + return m_frame; +} + +void KeyframeItem::setComponentTransform(const QTransform &transform) +{ + m_transform = transform; + + if (m_left) + m_left->setPos(m_transform.map(m_frame.leftHandle() - m_frame.position())); + + if (m_right) + m_right->setPos(m_transform.map(m_frame.rightHandle() - m_frame.position())); + + setPos(m_transform.map(m_frame.position())); +} + +void KeyframeItem::setStyle(const CurveEditorStyle &style) +{ + m_style = style.keyframeStyle; + + if (m_left) + m_left->setStyle(style); + + if (m_right) + m_right->setStyle(style); +} + +void KeyframeItem::updatePosition(bool update) +{ + bool ok = false; + QPointF position = m_transform.inverted(&ok).map(pos()); + + if (!ok) + return; + + QPointF oldPosition = m_frame.position(); + m_frame.setPosition(position); + + if (m_left) + updateHandle(m_left, false); + + if (m_right) + updateHandle(m_right, false); + + if (update) { + emit redrawCurve(); + emit keyframeMoved(this, position - oldPosition); + } +} + +void KeyframeItem::moveKeyframe(const QPointF &direction) +{ + this->blockSignals(true); + setPos(m_transform.map(m_frame.position() + direction)); + updatePosition(false); + this->blockSignals(false); + emit redrawCurve(); +} + +void KeyframeItem::moveHandle(HandleSlot handle, double deltaAngle, double deltaLength) +{ + auto move = [this, deltaAngle, deltaLength](HandleItem *item) { + QLineF current(QPointF(0.0, 0.0), item->pos()); + current.setAngle(current.angle() + deltaAngle); + current.setLength(current.length() + deltaLength); + item->setPos(current.p2()); + updateHandle(item, false); + }; + + this->blockSignals(true); + + if (handle == HandleSlot::Left) + move(m_left); + else if (handle == HandleSlot::Right) + move(m_right); + + this->blockSignals(false); + + emit redrawCurve(); +} + +void KeyframeItem::updateHandle(HandleItem *handle, bool emitChanged) +{ + bool ok = false; + + QPointF handlePosition = m_transform.inverted(&ok).map(handle->pos()); + + if (!ok) + return; + + QPointF oldPosition; + QPointF newPosition; + HandleSlot slot = HandleSlot::Undefined; + if (handle == m_left) { + slot = HandleSlot::Left; + oldPosition = m_frame.leftHandle(); + m_frame.setLeftHandle(m_frame.position() + handlePosition); + newPosition = m_frame.leftHandle(); + } else { + slot = HandleSlot::Right; + oldPosition = m_frame.rightHandle(); + m_frame.setRightHandle(m_frame.position() + handlePosition); + newPosition = m_frame.rightHandle(); + } + + if (emitChanged) { + QLineF oldLine(m_frame.position(), oldPosition); + QLineF newLine(m_frame.position(), newPosition); + + QLineF mappedOld = m_transform.map(oldLine); + QLineF mappedNew = m_transform.map(newLine); + + auto angle = mappedOld.angleTo(mappedNew); + auto deltaLength = mappedNew.length() - mappedOld.length(); + + emit redrawCurve(); + emit handleMoved(this, slot, angle, deltaLength); + } +} + +QVariant KeyframeItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionChange) { + bool ok; + QPointF position = m_transform.inverted(&ok).map(value.toPointF()); + if (ok) { + position.setX(std::round(position.x())); + return QVariant(m_transform.map(position)); + } + } + + return QGraphicsItem::itemChange(change, value); +} + +void KeyframeItem::selectionCallback() +{ + auto setHandleVisibility = [](HandleItem *handle, bool visible) { + if (handle) + handle->setVisible(visible); + }; + + if (selected()) { + setHandleVisibility(m_left, true); + setHandleVisibility(m_right, true); + } else { + setHandleVisibility(m_left, false); + setHandleVisibility(m_right, false); + } +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/keyframeitem.h b/src/plugins/qmldesigner/components/curveeditor/detail/keyframeitem.h new file mode 100644 index 0000000000..ae65be1e9b --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/keyframeitem.h @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyle.h" +#include "keyframe.h" +#include "selectableitem.h" + +#include <QGraphicsObject> + +namespace DesignTools { + +class HandleItem; + +enum class HandleSlot { Undefined, Left, Right }; + +class KeyframeItem : public SelectableItem +{ + Q_OBJECT + +signals: + void redrawCurve(); + + void keyframeMoved(KeyframeItem *item, const QPointF &direction); + + void handleMoved(KeyframeItem *frame, HandleSlot handle, double angle, double deltaLength); + +public: + KeyframeItem(QGraphicsItem *parent = nullptr); + + KeyframeItem(const Keyframe &keyframe, QGraphicsItem *parent = nullptr); + + ~KeyframeItem() override; + + enum { Type = ItemTypeKeyframe }; + + int type() const override; + + QRectF boundingRect() const override; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + Keyframe keyframe() const; + + void setComponentTransform(const QTransform &transform); + + void setStyle(const CurveEditorStyle &style); + + void moveKeyframe(const QPointF &direction); + + void moveHandle(HandleSlot handle, double deltaAngle, double deltaLength); + +protected: + QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override; + + void selectionCallback() override; + +private: + void updatePosition(bool emit = true); + + void updateHandle(HandleItem *handle, bool emit = true); + +private: + QTransform m_transform; + + KeyframeItemStyleOption m_style; + + Keyframe m_frame; + + HandleItem *m_left; + + HandleItem *m_right; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/playhead.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/playhead.cpp new file mode 100644 index 0000000000..e2e6d21274 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/playhead.cpp @@ -0,0 +1,187 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "playhead.h" +#include "curveeditormodel.h" +#include "graphicsview.h" + +#include <QApplication> +#include <QGraphicsScene> +#include <QPainter> + +#include <cmath> + +namespace DesignTools { + +constexpr double g_playheadMargin = 5.0; + +Playhead::Playhead(GraphicsView *view) + : m_frame(0) + , m_moving(false) + , m_rect() + , m_timer() +{ + m_timer.setSingleShot(true); + m_timer.setInterval(30); + QObject::connect(&m_timer, &QTimer::timeout, view, [this, view]() { + if (QApplication::mouseButtons() == Qt::LeftButton) + mouseMoveOutOfBounds(view); + }); +} + +int Playhead::currentFrame() const +{ + return m_frame; +} + +void Playhead::moveToFrame(int frame, GraphicsView *view) +{ + m_frame = frame; + m_rect.moveCenter(QPointF(view->mapTimeToX(m_frame), m_rect.center().y())); +} + +void Playhead::resize(GraphicsView *view) +{ + QRectF viewRect = view->mapToScene(view->viewport()->rect()).boundingRect(); + + CurveEditorStyle style = view->editorStyle(); + + QPointF tlr(style.valueAxisWidth, style.timeAxisHeight - style.playhead.width); + QPointF brr(style.valueAxisWidth + style.playhead.width, -g_playheadMargin); + + QPointF tl = viewRect.topLeft() + tlr; + QPointF br = viewRect.bottomLeft() + brr; + + m_rect = QRectF(tl, br); + + moveToFrame(m_frame, view); +} + +bool Playhead::mousePress(const QPointF &pos) +{ + QRectF hitRect = m_rect; + hitRect.setBottom(hitRect.top() + hitRect.width()); + + m_moving = hitRect.contains(pos); + + return m_moving; +} + +bool Playhead::mouseMove(const QPointF &pos, GraphicsView *view) +{ + if (m_moving) { + CurveEditorStyle style = view->editorStyle(); + + QRectF canvas = view->canvasRect().adjusted(0.0, -style.timeAxisHeight, 0.0, 0.0); + + if (canvas.contains(pos)) + view->setCurrentFrame(std::round(view->mapXtoTime(pos.x()))); + else if (!m_timer.isActive()) + m_timer.start(); + } + + return m_moving; +} + +void Playhead::mouseMoveOutOfBounds(GraphicsView *view) +{ + if (QApplication::mouseButtons() != Qt::LeftButton) + return; + + CurveEditorStyle style = view->editorStyle(); + QRectF canvas = view->canvasRect(); + QPointF pos = view->globalToScene(QCursor::pos()); + + if (pos.x() > canvas.right()) { + double speed = (pos.x() - canvas.right()); + double nextCenter = m_rect.center().x() + speed; + double frame = std::round(view->mapXtoTime(nextCenter)); + view->setCurrentFrame(frame); + + double framePosition = view->mapTimeToX(frame); + double rightSideOut = framePosition + style.playhead.width / 2.0 + style.canvasMargin; + double overshoot = rightSideOut - canvas.right(); + view->scrollContent(overshoot, 0.0); + + } else if (pos.x() < canvas.left()) { + double speed = (canvas.left() - pos.x()); + double nextCenter = m_rect.center().x() - speed; + double frame = std::round(view->mapXtoTime(nextCenter)); + view->setCurrentFrame(frame); + + double framePosition = view->mapTimeToX(frame); + double leftSideOut = framePosition - style.playhead.width / 2.0 - style.canvasMargin; + double undershoot = canvas.left() - leftSideOut; + view->scrollContent(-undershoot, 0.0); + } + + m_timer.start(); +} + +void Playhead::mouseRelease(GraphicsView *view) +{ + if (m_moving) + emit view->model()->currentFrameChanged(m_frame); + + m_moving = false; +} + +void Playhead::paint(QPainter *painter, GraphicsView *view) const +{ + CurveEditorStyle style = view->editorStyle(); + + painter->save(); + painter->setPen(style.playhead.color); + painter->setRenderHint(QPainter::Antialiasing, true); + + QRectF rect = m_rect; + rect.setBottom(m_rect.top() + m_rect.width()); + + QPointF top(rect.center().x(), rect.top()); + QPointF right(rect.right(), rect.top()); + QPointF bottom(rect.center().x(), rect.bottom()); + QPointF left(rect.left(), rect.top()); + + QLineF rightToBottom(right, bottom); + rightToBottom.setLength(style.playhead.radius); + + QLineF leftToBottom(left, bottom); + leftToBottom.setLength(style.playhead.radius); + + QPainterPath path(top); + path.lineTo(right - QPointF(style.playhead.radius, 0.)); + path.quadTo(right, rightToBottom.p2()); + path.lineTo(bottom); + path.lineTo(leftToBottom.p2()); + path.quadTo(left, left + QPointF(style.playhead.radius, 0.)); + path.closeSubpath(); + + painter->fillPath(path, style.playhead.color); + painter->drawLine(top + QPointF(0., 5.), QPointF(m_rect.center().x(), m_rect.bottom())); + + painter->restore(); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/playhead.h b/src/plugins/qmldesigner/components/curveeditor/detail/playhead.h new file mode 100644 index 0000000000..28d2a21a5c --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/playhead.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QRectF> +#include <QTimer> + +QT_BEGIN_NAMESPACE +class QPainter; +QT_END_NAMESPACE + +namespace DesignTools { + +class GraphicsView; + +class Playhead +{ +public: + Playhead(GraphicsView *view); + + int currentFrame() const; + + void paint(QPainter *painter, GraphicsView *view) const; + + void moveToFrame(int frame, GraphicsView *view); + + void resize(GraphicsView *view); + + bool mousePress(const QPointF &pos); + + bool mouseMove(const QPointF &pos, GraphicsView *view); + + void mouseRelease(GraphicsView *view); + +private: + void mouseMoveOutOfBounds(GraphicsView *view); + + int m_frame; + + bool m_moving; + + QRectF m_rect; + + QTimer m_timer; + + GraphicsView *m_view; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/selectableitem.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/selectableitem.cpp new file mode 100644 index 0000000000..fb61682090 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/selectableitem.cpp @@ -0,0 +1,112 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "selectableitem.h" +#include "keyframeitem.h" + +#include <QDebug> + +namespace DesignTools { + +SelectableItem::SelectableItem(QGraphicsItem *parent) + : QGraphicsObject(parent) + , m_active(false) + , m_selected(false) + , m_preSelected(SelectionMode::Undefined) +{ + setFlag(QGraphicsItem::ItemIsSelectable, false); + + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIgnoresTransformations, true); + setFlag(QGraphicsItem::ItemSendsGeometryChanges, true); +} + +SelectableItem::~SelectableItem() {} + +bool SelectableItem::activated() const +{ + return m_active; +} + +bool SelectableItem::selected() const +{ + switch (m_preSelected) { + case SelectionMode::Clear: + return false; + case SelectionMode::New: + return true; + case SelectionMode::Add: + return true; + case SelectionMode::Remove: + return false; + case SelectionMode::Toggle: + return !m_selected; + default: + return m_selected; + } + + return false; +} + +void SelectableItem::setActivated(bool active) +{ + m_active = active; +} + +void SelectableItem::setPreselected(SelectionMode mode) +{ + m_preSelected = mode; + selectionCallback(); +} + +void SelectableItem::applyPreselection() +{ + m_selected = selected(); + m_preSelected = SelectionMode::Undefined; +} + +void SelectableItem::selectionCallback() {} + +void SelectableItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + m_active = true; + QGraphicsObject::mousePressEvent(event); +} + +void SelectableItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (type() == KeyframeItem::Type && !selected()) + return; + + QGraphicsObject::mouseMoveEvent(event); +} + +void SelectableItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + m_active = false; + QGraphicsObject::mouseReleaseEvent(event); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/selectableitem.h b/src/plugins/qmldesigner/components/curveeditor/detail/selectableitem.h new file mode 100644 index 0000000000..0a2a4898a1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/selectableitem.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QGraphicsObject> + +namespace DesignTools { + +enum ItemType +{ + ItemTypeKeyframe = QGraphicsItem::UserType + 1, + ItemTypeHandle = QGraphicsItem::UserType + 2, + ItemTypeCurve = QGraphicsItem::UserType + 3 +}; + +enum class SelectionMode : unsigned int +{ + Undefined, + Clear, + New, + Add, + Remove, + Toggle +}; + +class SelectableItem : public QGraphicsObject +{ + Q_OBJECT + +public: + SelectableItem(QGraphicsItem *parent = nullptr); + + ~SelectableItem() override; + + bool activated() const; + + bool selected() const; + + void setActivated(bool active); + + void setPreselected(SelectionMode mode); + + void applyPreselection(); + +protected: + virtual void selectionCallback(); + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + +private: + bool m_active; + + bool m_selected; + + SelectionMode m_preSelected; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/selector.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/selector.cpp new file mode 100644 index 0000000000..633c1ddd39 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/selector.cpp @@ -0,0 +1,221 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "selector.h" +#include "graphicsview.h" +#include "keyframeitem.h" +#include "playhead.h" + +#include <QApplication> + +#include <cmath> + +namespace DesignTools { + +Selector::Selector() {} + +void Selector::paint(QPainter *painter) +{ + QPen pen(Qt::white); + + painter->save(); + painter->setPen(pen); + + if (!m_lasso.isEmpty()) + painter->drawPath(m_lasso); + + if (!m_rect.isNull()) + painter->drawRect(m_rect); + + painter->restore(); +} + +void Selector::mousePress(QMouseEvent *event, GraphicsView *view) +{ + m_shortcut = Shortcut(event); + + if (view->hasActiveHandle()) + return; + + if (select(SelectionTool::Undefined, view->globalToScene(event->globalPos()), view)) + applyPreSelection(view); + + m_mouseInit = event->globalPos(); + m_mouseCurr = event->globalPos(); + + QPointF click = view->globalToScene(m_mouseInit); + + m_lasso = QPainterPath(click); + m_lasso.closeSubpath(); + + m_rect = QRectF(click, click); +} + +void Selector::mouseMove(QMouseEvent *event, GraphicsView *view, Playhead &playhead) +{ + if (m_mouseInit.isNull()) + return; + + QPointF delta = event->globalPos() - m_mouseInit; + if (delta.manhattanLength() < QApplication::startDragDistance()) + return; + + if (m_shortcut == m_shortcuts.newSelection || m_shortcut == m_shortcuts.addToSelection + || m_shortcut == m_shortcuts.removeFromSelection + || m_shortcut == m_shortcuts.toggleSelection) { + if (view->hasActiveItem()) + return; + + select(m_tool, view->globalToScene(event->globalPos()), view); + + event->accept(); + view->viewport()->update(); + + } else if (m_shortcut == m_shortcuts.zoom) { + double bigger = std::abs(delta.x()) > std::abs(delta.y()) ? delta.x() : delta.y(); + double factor = bigger / view->width(); + view->setZoomX(view->zoomX() + factor, m_mouseInit); + m_mouseCurr = event->globalPos(); + event->accept(); + + } else if (m_shortcut == m_shortcuts.pan) { + view->scrollContent(-delta.x(), -delta.y()); + playhead.resize(view); + m_mouseCurr = event->globalPos(); + } +} + +void Selector::mouseRelease(QMouseEvent *event, GraphicsView *view) +{ + Q_UNUSED(event); + + applyPreSelection(view); + + m_shortcut = Shortcut(); + m_mouseInit = QPoint(); + m_mouseCurr = QPoint(); + m_lasso = QPainterPath(); + m_rect = QRectF(); +} + +bool Selector::select(const SelectionTool &tool, const QPointF &pos, GraphicsView *view) +{ + auto selectWidthTool = [this, tool](SelectionMode mode, const QPointF &pos, GraphicsView *view) { + switch (tool) { + case SelectionTool::Lasso: + return lassoSelection(mode, pos, view); + case SelectionTool::Rectangle: + return rectangleSelection(mode, pos, view); + default: + return pressSelection(mode, pos, view); + } + }; + + if (m_shortcut == m_shortcuts.newSelection) { + clearSelection(view); + return selectWidthTool(SelectionMode::New, pos, view); + } else if (m_shortcut == m_shortcuts.addToSelection) { + return selectWidthTool(SelectionMode::Add, pos, view); + } else if (m_shortcut == m_shortcuts.removeFromSelection) { + return selectWidthTool(SelectionMode::Remove, pos, view); + } else if (m_shortcut == m_shortcuts.toggleSelection) { + return selectWidthTool(SelectionMode::Toggle, pos, view); + } + + return false; +} + +bool Selector::pressSelection(SelectionMode mode, const QPointF &pos, GraphicsView *view) +{ + bool out = false; + const auto itemList = view->items(); + for (auto *item : itemList) { + if (auto *frame = qgraphicsitem_cast<KeyframeItem *>(item)) { + QRectF itemRect = frame->mapRectToScene(frame->boundingRect()); + if (itemRect.contains(pos)) { + frame->setPreselected(mode); + out = true; + } + } + } + return out; +} + +bool Selector::rectangleSelection(SelectionMode mode, const QPointF &pos, GraphicsView *view) +{ + bool out = false; + m_rect.setBottomRight(pos); + const auto itemList = view->items(); + for (auto *item : itemList) { + if (auto *keyframeItem = qgraphicsitem_cast<KeyframeItem *>(item)) { + if (m_rect.contains(keyframeItem->pos())) { + keyframeItem->setPreselected(mode); + out = true; + } else { + keyframeItem->setPreselected(SelectionMode::Undefined); + } + } + } + return out; +} + +bool Selector::lassoSelection(SelectionMode mode, const QPointF &pos, GraphicsView *view) +{ + bool out = false; + m_lasso.lineTo(pos); + const auto itemList = view->items(); + for (auto *item : itemList) { + if (auto *keyframeItem = qgraphicsitem_cast<KeyframeItem *>(item)) { + if (m_lasso.contains(keyframeItem->pos())) { + keyframeItem->setPreselected(mode); + out = true; + } else { + keyframeItem->setPreselected(SelectionMode::Undefined); + } + } + } + return out; +} + +void Selector::clearSelection(GraphicsView *view) +{ + const auto itemList = view->items(); + for (auto *item : itemList) { + if (auto *frameItem = qgraphicsitem_cast<KeyframeItem *>(item)) { + frameItem->setPreselected(SelectionMode::Clear); + frameItem->applyPreselection(); + } + } +} + +void Selector::applyPreSelection(GraphicsView *view) +{ + const auto itemList = view->items(); + for (auto *item : itemList) { + if (auto *keyframeItem = qgraphicsitem_cast<KeyframeItem *>(item)) + keyframeItem->applyPreselection(); + } +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/selector.h b/src/plugins/qmldesigner/components/curveeditor/detail/selector.h new file mode 100644 index 0000000000..e8f53d9d59 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/selector.h @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyle.h" +#include "selectableitem.h" + +#include <QMouseEvent> +#include <QPainterPath> +#include <QPoint> +#include <QRectF> + +namespace DesignTools { + +class GraphicsView; +class Playhead; + +enum class SelectionTool { Undefined, Lasso, Rectangle }; + +class Selector +{ +public: + Selector(); + + void paint(QPainter *painter); + + void mousePress(QMouseEvent *event, GraphicsView *view); + + void mouseMove(QMouseEvent *event, GraphicsView *view, Playhead &playhead); + + void mouseRelease(QMouseEvent *event, GraphicsView *view); + +private: + bool select(const SelectionTool &tool, const QPointF &pos, GraphicsView *view); + + bool pressSelection(SelectionMode mode, const QPointF &pos, GraphicsView *view); + + bool rectangleSelection(SelectionMode mode, const QPointF &pos, GraphicsView *view); + + bool lassoSelection(SelectionMode mode, const QPointF &pos, GraphicsView *view); + + void clearSelection(GraphicsView *view); + + void applyPreSelection(GraphicsView *view); + + Shortcuts m_shortcuts = Shortcuts(); + + Shortcut m_shortcut; + + SelectionMode m_mode = SelectionMode::Undefined; + + SelectionTool m_tool = SelectionTool::Rectangle; + + QPoint m_mouseInit = QPoint(); + + QPoint m_mouseCurr = QPoint(); + + QPainterPath m_lasso = QPainterPath(); + + QRectF m_rect = QRectF(); +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/shortcut.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/shortcut.cpp new file mode 100644 index 0000000000..d6d04dbbae --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/shortcut.cpp @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "shortcut.h" + +namespace DesignTools { + +Shortcut::Shortcut() + : m_key() + , m_buttons() + , m_modifiers() +{} + +Shortcut::Shortcut(QMouseEvent *event) + : m_key() + , m_buttons(event->buttons()) + , m_modifiers(event->modifiers()) +{} + +Shortcut::Shortcut(const Qt::KeyboardModifiers &mods, const Qt::Key &key) + : m_key(key) + , m_buttons() + , m_modifiers(mods) +{} + +Shortcut::Shortcut(const Qt::MouseButtons &buttons, const Qt::KeyboardModifiers &mods) + : m_key() + , m_buttons(buttons) + , m_modifiers(mods) +{} + +Shortcut::Shortcut(const Qt::MouseButtons &buttons, + const Qt::KeyboardModifiers &mods, + const Qt::Key &key) + : m_key(key) + , m_buttons(buttons) + , m_modifiers(mods) +{} + +bool Shortcut::exactMatch(const Qt::Key &key) const +{ + return m_key == key; +} + +bool Shortcut::exactMatch(const Qt::MouseButton &button) const +{ + return static_cast<int>(m_buttons) == static_cast<int>(button); +} + +bool Shortcut::exactMatch(const Qt::KeyboardModifier &modifier) const +{ + return static_cast<int>(m_modifiers) == static_cast<int>(modifier); +} + +bool Shortcut::operator==(const Shortcut &other) const +{ + return m_key == other.m_key && m_buttons == other.m_buttons && m_modifiers == other.m_modifiers; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/shortcut.h b/src/plugins/qmldesigner/components/curveeditor/detail/shortcut.h new file mode 100644 index 0000000000..a9e075bd8b --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/shortcut.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QMouseEvent> + +namespace DesignTools { + +class Shortcut +{ +public: + Shortcut(); + + Shortcut(QMouseEvent *event); + + Shortcut(const Qt::KeyboardModifiers &mods, const Qt::Key &key); + + Shortcut(const Qt::MouseButtons &buttons, const Qt::KeyboardModifiers &mods = Qt::NoModifier); + + Shortcut(const Qt::MouseButtons &buttons, const Qt::KeyboardModifiers &mods, const Qt::Key &key); + + bool exactMatch(const Qt::Key &key) const; + + bool exactMatch(const Qt::MouseButton &button) const; + + bool exactMatch(const Qt::KeyboardModifier &modifier) const; + + bool operator==(const Shortcut &other) const; + +private: + Qt::Key m_key; + + Qt::MouseButtons m_buttons; + + Qt::KeyboardModifiers m_modifiers; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/treeitemdelegate.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/treeitemdelegate.cpp new file mode 100644 index 0000000000..21633ddce1 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/treeitemdelegate.cpp @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "treeitemdelegate.h" +#include "treeitem.h" + +#include <QEvent> +#include <QMouseEvent> +#include <QPainter> + +namespace DesignTools { + +TreeItemDelegate::TreeItemDelegate(const CurveEditorStyle &style, QObject *parent) + : QStyledItemDelegate(parent) + , m_style(style) +{} + +QSize TreeItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return QStyledItemDelegate::sizeHint(option, index); +} + +void TreeItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (index.column() == 1 || index.column() == 2) { + + int height = option.rect.size().height(); + QRect iconRect(QPoint(0, 0), QSize(height, height)); + iconRect.moveCenter(option.rect.center()); + + auto *treeItem = static_cast<TreeItem *>(index.internalPointer()); + if (option.state & QStyle::State_MouseOver && iconRect.contains(m_mousePos)) { + + painter->fillRect(option.rect, option.backgroundBrush); + + if (index.column() == 1) { + + if (treeItem->locked()) { + + QPixmap pixmap = pixmapFromIcon( + m_style.treeItemStyle.unlockedIcon, + iconRect.size(), + m_style.fontColor); + + painter->drawPixmap(iconRect, pixmap); + + } else { + + QPixmap pixmap = pixmapFromIcon( + m_style.treeItemStyle.lockedIcon, + iconRect.size(), + m_style.fontColor); + + painter->drawPixmap(iconRect, pixmap); + } + + } else if (index.column() == 2) { + + if (treeItem->pinned()) { + + QPixmap pixmap = pixmapFromIcon( + m_style.treeItemStyle.unpinnedIcon, + iconRect.size(), + m_style.fontColor); + + painter->drawPixmap(iconRect, pixmap); + + } else { + + QPixmap pixmap = pixmapFromIcon( + m_style.treeItemStyle.pinnedIcon, + iconRect.size(), + m_style.fontColor); + + painter->drawPixmap(iconRect, pixmap); + + } + } + + } else { + + if (treeItem->locked() && index.column() == 1) { + + QPixmap pixmap = pixmapFromIcon( + m_style.treeItemStyle.lockedIcon, + iconRect.size(), + m_style.fontColor); + + painter->drawPixmap(iconRect, pixmap); + + } else if (treeItem->pinned() && index.column() == 2) { + + QPixmap pixmap = pixmapFromIcon( + m_style.treeItemStyle.pinnedIcon, + iconRect.size(), + m_style.fontColor); + + painter->drawPixmap(iconRect, pixmap); + + } + } + } else { + QStyledItemDelegate::paint(painter, option, index); + } +} + +void TreeItemDelegate::setStyle(const CurveEditorStyle &style) +{ + m_style = style; +} + +bool TreeItemDelegate::editorEvent(QEvent *event, + QAbstractItemModel *model, + const QStyleOptionViewItem &option, + const QModelIndex &index) +{ + if (event->type() == QEvent::MouseMove) + m_mousePos = static_cast<QMouseEvent *>(event)->pos(); + + return QStyledItemDelegate::editorEvent(event, model, option, index); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/treeitemdelegate.h b/src/plugins/qmldesigner/components/curveeditor/detail/treeitemdelegate.h new file mode 100644 index 0000000000..6479f48942 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/treeitemdelegate.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "curveeditorstyle.h" + +#include <QStyledItemDelegate> + +namespace DesignTools { + +class TreeItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + TreeItemDelegate(const CurveEditorStyle &style, QObject *parent = nullptr); + + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + + void paint( + QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + void setStyle(const CurveEditorStyle &style); + +protected: + bool editorEvent( + QEvent *event, + QAbstractItemModel *model, + const QStyleOptionViewItem &option, + const QModelIndex &index) override; + +private: + CurveEditorStyle m_style; + + QPoint m_mousePos; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/treemodel.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/treemodel.cpp new file mode 100644 index 0000000000..1ea037091d --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/treemodel.cpp @@ -0,0 +1,156 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "treemodel.h" +#include "treeitem.h" +#include "detail/graphicsview.h" + +#include <QIcon> + +namespace DesignTools { + +TreeModel::TreeModel(QObject *parent) + : QAbstractItemModel(parent) + , m_view(nullptr) + , m_root(new TreeItem("Root")) +{} + +TreeModel::~TreeModel() +{ + if (m_root) { + delete m_root; + m_root = nullptr; + } + + m_view = nullptr; +} + +QVariant TreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + TreeItem *item = static_cast<TreeItem *>(index.internalPointer()); + + if (role == Qt::DecorationRole && index.column() == 0) + return item->icon(); + + if (role != Qt::DisplayRole) + return QVariant(); + + return item->data(index.column()); +} + +QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) + return m_root->headerData(section); + + return QVariant(); +} + +QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + TreeItem *parentItem = m_root; + + if (parent.isValid()) + parentItem = static_cast<TreeItem *>(parent.internalPointer()); + + if (TreeItem *childItem = parentItem->child(row)) + return createIndex(row, column, childItem); + + return QModelIndex(); +} + +QModelIndex TreeModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + TreeItem *childItem = static_cast<TreeItem *>(index.internalPointer()); + + if (TreeItem *parentItem = childItem->parent()) { + if (parentItem == m_root) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); + } + return QModelIndex(); +} + +int TreeModel::rowCount(const QModelIndex &parent) const +{ + if (parent.column() > 0) + return 0; + + TreeItem *parentItem = m_root; + + if (parent.isValid()) + parentItem = static_cast<TreeItem *>(parent.internalPointer()); + + return parentItem->rowCount(); +} + +int TreeModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_root->columnCount(); +} + +void TreeModel::setGraphicsView(GraphicsView *view) +{ + m_view = view; +} + +GraphicsView *TreeModel::graphicsView() const +{ + return m_view; +} + +void TreeModel::initialize() +{ + if (m_root) + delete m_root; + + m_root = new TreeItem("Root"); +} + +TreeItem *TreeModel::root() +{ + return m_root; +} + +TreeItem *TreeModel::find(unsigned int id) +{ + return m_root->find(id); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/treemodel.h b/src/plugins/qmldesigner/components/curveeditor/detail/treemodel.h new file mode 100644 index 0000000000..209b2ee506 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/treemodel.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QAbstractItemModel> + +#include <vector> + +namespace DesignTools { + +class GraphicsView; +class TreeItem; + +class TreeModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + TreeModel(QObject *parent = nullptr); + + ~TreeModel() override; + + QVariant data(const QModelIndex &index, int role) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex parent(const QModelIndex &index) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + void setGraphicsView(GraphicsView *view); + +protected: + GraphicsView *graphicsView() const; + + void initialize(); + + TreeItem *root(); + + TreeItem *find(unsigned int id); + +private: + GraphicsView *m_view; + + TreeItem *m_root; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/treeview.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/treeview.cpp new file mode 100644 index 0000000000..3b9a4ef7c5 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/treeview.cpp @@ -0,0 +1,127 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "treeview.h" +#include "curveeditormodel.h" +#include "curveitem.h" +#include "treeitem.h" +#include "treeitemdelegate.h" + +#include <QHeaderView> +#include <QMouseEvent> + +namespace DesignTools { + +TreeView::TreeView(CurveEditorModel *model, QWidget *parent) + : QTreeView(parent) +{ + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + setUniformRowHeights(true); + setRootIsDecorated(false); + setMouseTracking(true); + setHeaderHidden(true); + + model->setParent(this); + setModel(model); + + auto expandItems = [this]() { expandAll(); }; + connect(model, &QAbstractItemModel::modelReset, expandItems); + + auto *delegate = new TreeItemDelegate(model->style(), this); + setItemDelegate(delegate); + + setSelectionBehavior(QAbstractItemView::SelectRows); + setSelectionMode(QAbstractItemView::ExtendedSelection); + + connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &TreeView::changeSelection); + setStyle(model->style()); + + header()->setSectionResizeMode(0, QHeaderView::Stretch); + header()->setSectionResizeMode(1, QHeaderView::Fixed); + header()->setSectionResizeMode(2, QHeaderView::Fixed); + + header()->setStretchLastSection(false); + header()->resizeSection(1, 20); + header()->resizeSection(2, 20); +} + +void TreeView::setStyle(const CurveEditorStyle &style) +{ + QPalette pal = palette(); + pal.setBrush(QPalette::Base, style.backgroundBrush); + pal.setBrush(QPalette::Button, style.backgroundAlternateBrush); + pal.setBrush(QPalette::Text, style.fontColor); + + // Tmp to see what happens on windows/macOS. + pal.setBrush(backgroundRole(), Qt::white); + pal.setBrush(foregroundRole(), Qt::white); + + setPalette(pal); + + if (auto *delegate = qobject_cast<TreeItemDelegate *>(itemDelegate())) + delegate->setStyle(style); +} + +void TreeView::changeCurve(unsigned int id, const AnimationCurve &curve) +{ + if (auto *curveModel = qobject_cast<CurveEditorModel *>(model())) + curveModel->setCurve(id, curve); +} + +void TreeView::changeSelection(const QItemSelection &selected, const QItemSelection &deselected) +{ + Q_UNUSED(selected); + Q_UNUSED(deselected); + + std::vector<CurveItem *> curves; + for (auto index : selectedIndexes()) { + if (index.isValid() && index.column() == 0) { + auto *treeItem = static_cast<TreeItem *>(index.internalPointer()); + if (auto *propertyItem = treeItem->asPropertyItem()) + curves.push_back(new CurveItem(treeItem->id(), propertyItem->curve())); + } + } + + emit curvesSelected(curves); +} + +QSize TreeView::sizeHint() const +{ + return QSize(170, 300); +} + +void TreeView::mousePressEvent(QMouseEvent *event) +{ + QModelIndex index = indexAt(event->pos()); + if (index.isValid()) { + auto *treeItem = static_cast<TreeItem *>(index.internalPointer()); + if (index.column() == 1) + treeItem->setLocked(!treeItem->locked()); + else if (index.column() == 2) + treeItem->setPinned(!treeItem->pinned()); + } + QTreeView::mousePressEvent(event); +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/treeview.h b/src/plugins/qmldesigner/components/curveeditor/detail/treeview.h new file mode 100644 index 0000000000..9d3af647ad --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/treeview.h @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QTreeView> + +namespace DesignTools { + +class AnimationCurve; +class CurveEditorModel; +class CurveItem; + +struct CurveEditorStyle; + +class TreeView : public QTreeView +{ + Q_OBJECT + +signals: + void curvesSelected(const std::vector<CurveItem *> &curves); + +public: + TreeView(CurveEditorModel *model, QWidget *parent = nullptr); + + void changeCurve(unsigned int id, const AnimationCurve &curve); + + void setStyle(const CurveEditorStyle &style); + +protected: + QSize sizeHint() const override; + + void mousePressEvent(QMouseEvent *event) override; + +private: + void changeSelection(const QItemSelection &selected, const QItemSelection &deselected); +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/utils.cpp b/src/plugins/qmldesigner/components/curveeditor/detail/utils.cpp new file mode 100644 index 0000000000..4933bcbe88 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/utils.cpp @@ -0,0 +1,107 @@ + +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QPalette> +#include <QPointF> +#include <QRectF> +#include <QTransform> + +namespace DesignTools { + +double clamp(double val, double lo, double hi) +{ + return val < lo ? lo : (val > hi ? hi : val); +} + +double lerp(double blend, double a, double b) +{ + return (1.0 - blend) * a + blend * b; +} + +double scaleX(const QTransform &transform) +{ + return transform.m11(); +} + +double scaleY(const QTransform &transform) +{ + return transform.m22(); +} + +void grow(QRectF &rect, const QPointF &point) +{ + if (rect.left() > point.x()) + rect.setLeft(point.x()); + + if (rect.right() < point.x()) + rect.setRight(point.x()); + + if (rect.top() > point.y()) + rect.setTop(point.y()); + + if (rect.bottom() < point.y()) + rect.setBottom(point.y()); +} + +QRectF bbox(const QRectF &rect, const QTransform &transform) +{ + QRectF out = rect; + grow(out, transform.map(rect.topLeft())); + grow(out, transform.map(rect.topRight())); + grow(out, transform.map(rect.bottomLeft())); + grow(out, transform.map(rect.bottomRight())); + return out; +} + +QPalette singleColorPalette(const QColor &color) +{ + QPalette palette; + palette.setColor(QPalette::Window, color); + palette.setColor(QPalette::Background, color); + palette.setColor(QPalette::WindowText, color); + palette.setColor(QPalette::Foreground, color); + palette.setColor(QPalette::Base, color); + palette.setColor(QPalette::AlternateBase, color); + palette.setColor(QPalette::ToolTipBase, color); + palette.setColor(QPalette::ToolTipText, color); + palette.setColor(QPalette::Text, color); + + palette.setColor(QPalette::Button, color); + palette.setColor(QPalette::ButtonText, color); + palette.setColor(QPalette::BrightText, color); + palette.setColor(QPalette::Light, color); + palette.setColor(QPalette::Midlight, color); + palette.setColor(QPalette::Dark, color); + palette.setColor(QPalette::Mid, color); + palette.setColor(QPalette::Shadow, color); + palette.setColor(QPalette::Highlight, color); + palette.setColor(QPalette::HighlightedText, color); + palette.setColor(QPalette::Link, color); + palette.setColor(QPalette::LinkVisited, color); + return palette; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/detail/utils.h b/src/plugins/qmldesigner/components/curveeditor/detail/utils.h new file mode 100644 index 0000000000..77cbd2c7bf --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/detail/utils.h @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 + +QT_BEGIN_NAMESPACE +class QColor; +class QPalette; +class QPointF; +class QRectF; +class QTransform; +QT_END_NAMESPACE + +namespace DesignTools { + +double clamp(double val, double lo, double hi); + +double lerp(double blend, double a, double b); + +double scaleX(const QTransform &transform); + +double scaleY(const QTransform &transform); + +void grow(QRectF &rect, const QPointF &point); + +QRectF bbox(const QRectF &rect, const QTransform &transform); + +QPalette singleColorPalette(const QColor &color); + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/keyframe.cpp b/src/plugins/qmldesigner/components/curveeditor/keyframe.cpp new file mode 100644 index 0000000000..8ff577c0a5 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/keyframe.cpp @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "keyframe.h" + +namespace DesignTools { + +Keyframe::Keyframe() + : m_position() + , m_leftHandle() + , m_rightHandle() +{} + +Keyframe::Keyframe(const QPointF &position) + : m_position(position) + , m_leftHandle() + , m_rightHandle() +{} + +Keyframe::Keyframe(const QPointF &position, const QPointF &leftHandle, const QPointF &rightHandle) + : m_position(position) + , m_leftHandle(leftHandle) + , m_rightHandle(rightHandle) +{} + +bool Keyframe::hasLeftHandle() const +{ + return !m_leftHandle.isNull(); +} + +bool Keyframe::hasRightHandle() const +{ + return !m_rightHandle.isNull(); +} + +QPointF Keyframe::position() const +{ + return m_position; +} + +QPointF Keyframe::leftHandle() const +{ + return m_leftHandle; +} + +QPointF Keyframe::rightHandle() const +{ + return m_rightHandle; +} + +void Keyframe::setPosition(const QPointF &pos) +{ + m_position = pos; +} + +void Keyframe::setLeftHandle(const QPointF &pos) +{ + m_leftHandle = pos; +} + +void Keyframe::setRightHandle(const QPointF &pos) +{ + m_rightHandle = pos; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/keyframe.h b/src/plugins/qmldesigner/components/curveeditor/keyframe.h new file mode 100644 index 0000000000..5e6042531b --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/keyframe.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 <QPointF> + +namespace DesignTools { + +class Keyframe +{ +public: + Keyframe(); + + Keyframe(const QPointF &position); + + Keyframe(const QPointF &position, const QPointF &leftHandle, const QPointF &rightHandle); + + bool hasLeftHandle() const; + + bool hasRightHandle() const; + + QPointF position() const; + + QPointF leftHandle() const; + + QPointF rightHandle() const; + + void setPosition(const QPointF &pos); + + void setLeftHandle(const QPointF &pos); + + void setRightHandle(const QPointF &pos); + +private: + QPointF m_position; + + QPointF m_leftHandle; + + QPointF m_rightHandle; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/treeitem.cpp b/src/plugins/qmldesigner/components/curveeditor/treeitem.cpp new file mode 100644 index 0000000000..beed419e54 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/treeitem.cpp @@ -0,0 +1,227 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "treeitem.h" + +#include <QIcon> +#include <QVariant> + +namespace DesignTools { + +TreeItem::TreeItem(const QString &name) + : m_name(name) + , m_id(0) + , m_locked(false) + , m_pinned(false) + , m_parent(nullptr) + , m_children() +{} + +TreeItem::~TreeItem() +{ + m_parent = nullptr; + + qDeleteAll(m_children); +} + +QIcon TreeItem::icon() const +{ + return QIcon(); +} + +NodeTreeItem *TreeItem::asNodeItem() +{ + return nullptr; +} + +PropertyTreeItem *TreeItem::asPropertyItem() +{ + return nullptr; +} + +unsigned int TreeItem::id() const +{ + return m_id; +} + +bool TreeItem::locked() const +{ + return m_locked; +} + +bool TreeItem::pinned() const +{ + return m_pinned; +} + +int TreeItem::row() const +{ + if (m_parent) { + for (size_t i = 0; i < m_parent->m_children.size(); ++i) { + if (m_parent->m_children[i] == this) + return i; + } + } + + return 0; +} + +int TreeItem::column() const +{ + return 0; +} + +int TreeItem::rowCount() const +{ + return m_children.size(); +} + +int TreeItem::columnCount() const +{ + return 3; +} + +TreeItem *TreeItem::parent() const +{ + return m_parent; +} + +TreeItem *TreeItem::child(int row) const +{ + if (row < 0 || row >= static_cast<int>(m_children.size())) + return nullptr; + + return m_children.at(row); +} + +TreeItem *TreeItem::find(unsigned int id) const +{ + for (auto *child : m_children) { + if (child->id() == id) + return child; + + if (auto *childsChild = child->find(id)) + return childsChild; + } + + return nullptr; +} + +QVariant TreeItem::data(int column) const +{ + switch (column) { + case 0: + return QVariant(m_name); + case 1: + return QVariant(m_locked); + case 2: + return QVariant(m_pinned); + case 3: + return QVariant(m_id); + default: + return QVariant(); + } +} + +QVariant TreeItem::headerData(int column) const +{ + switch (column) { + case 0: + return QString("Name"); + case 1: + return QString("L"); + case 2: + return QString("P"); + case 3: + return QString("Id"); + default: + return QVariant(); + } +} + +void TreeItem::setId(unsigned int &id) +{ + m_id = id; + + for (auto *child : m_children) + child->setId(++id); +} + +void TreeItem::addChild(TreeItem *child) +{ + child->m_parent = this; + m_children.push_back(child); +} + +void TreeItem::setLocked(bool locked) +{ + m_locked = locked; +} + +void TreeItem::setPinned(bool pinned) +{ + m_pinned = pinned; +} + + +NodeTreeItem::NodeTreeItem(const QString &name, const QIcon &icon) + : TreeItem(name) + , m_icon(icon) +{ + Q_UNUSED(icon); +} + +NodeTreeItem *NodeTreeItem::asNodeItem() +{ + return this; +} + +QIcon NodeTreeItem::icon() const +{ + return m_icon; +} + + +PropertyTreeItem::PropertyTreeItem(const QString &name, const AnimationCurve &curve) + : TreeItem(name) + , m_curve(curve) +{} + +PropertyTreeItem *PropertyTreeItem::asPropertyItem() +{ + return this; +} + +AnimationCurve PropertyTreeItem::curve() const +{ + return m_curve; +} + +void PropertyTreeItem::setCurve(const AnimationCurve &curve) +{ + m_curve = curve; +} + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/curveeditor/treeitem.h b/src/plugins/qmldesigner/components/curveeditor/treeitem.h new file mode 100644 index 0000000000..5b31dc2fc8 --- /dev/null +++ b/src/plugins/qmldesigner/components/curveeditor/treeitem.h @@ -0,0 +1,137 @@ + +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Design Tooling +** +** 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 "animationcurve.h" + +#include <QIcon> +#include <QString> + +#include <vector> + +QT_BEGIN_NAMESPACE +class QIcon; +class QVariant; +QT_END_NAMESPACE + +namespace DesignTools { + +class NodeTreeItem; +class PropertyTreeItem; + +class TreeItem +{ +public: + TreeItem(const QString &name); + + virtual ~TreeItem(); + + virtual QIcon icon() const; + + virtual NodeTreeItem *asNodeItem(); + + virtual PropertyTreeItem *asPropertyItem(); + + unsigned int id() const; + + bool locked() const; + + bool pinned() const; + + int row() const; + + int column() const; + + int rowCount() const; + + int columnCount() const; + + TreeItem *parent() const; + + TreeItem *child(int row) const; + + TreeItem *find(unsigned int row) const; + + QVariant data(int column) const; + + QVariant headerData(int column) const; + + void setId(unsigned int &id); + + void addChild(TreeItem *child); + + void setLocked(bool locked); + + void setPinned(bool pinned); + +protected: + QString m_name; + + unsigned int m_id; + + bool m_locked; + + bool m_pinned; + + TreeItem *m_parent; + + std::vector<TreeItem *> m_children; +}; + + +class NodeTreeItem : public TreeItem +{ +public: + NodeTreeItem(const QString &name, const QIcon &icon); + + NodeTreeItem *asNodeItem() override; + + QIcon icon() const override; + +private: + QIcon m_icon; +}; + + +class PropertyTreeItem : public TreeItem +{ +public: + PropertyTreeItem(const QString &name, const AnimationCurve &curve); + + PropertyTreeItem *asPropertyItem() override; + + AnimationCurve curve() const; + + void setCurve(const AnimationCurve &curve); + +private: + using TreeItem::addChild; + + AnimationCurve m_curve; +}; + +} // End namespace DesignTools. diff --git a/src/plugins/qmldesigner/components/formeditor/backgroundaction.cpp b/src/plugins/qmldesigner/components/formeditor/backgroundaction.cpp index 356fbf4f32..6d3cef0fbd 100644 --- a/src/plugins/qmldesigner/components/formeditor/backgroundaction.cpp +++ b/src/plugins/qmldesigner/components/formeditor/backgroundaction.cpp @@ -64,7 +64,7 @@ QWidget *BackgroundAction::createWidget(QWidget *parent) } comboBox->setCurrentIndex(0); - connect(comboBox, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), + connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &BackgroundAction::emitBackgroundChanged); comboBox->setProperty("hideborder", true); diff --git a/src/plugins/qmldesigner/components/formeditor/dragtool.cpp b/src/plugins/qmldesigner/components/formeditor/dragtool.cpp index 0737e2ab45..6446e2422f 100644 --- a/src/plugins/qmldesigner/components/formeditor/dragtool.cpp +++ b/src/plugins/qmldesigner/components/formeditor/dragtool.cpp @@ -42,7 +42,6 @@ static Q_LOGGING_CATEGORY(dragToolInfo, "qtc.qmldesigner.formeditor", QtWarningM namespace QmlDesigner { - DragTool::DragTool(FormEditorView *editorView) : AbstractFormEditorTool(editorView), m_moveManipulator(editorView->scene()->manipulatorLayerItem(), editorView), diff --git a/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp b/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp index 2cc524c799..9f6d3e9cd8 100644 --- a/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp +++ b/src/plugins/qmldesigner/components/formeditor/formeditoritem.cpp @@ -41,7 +41,6 @@ namespace QmlDesigner { - FormEditorScene *FormEditorItem::scene() const { return qobject_cast<FormEditorScene*>(QGraphicsItem::scene()); } @@ -110,7 +109,7 @@ void FormEditorItem::updateGeometry() m_boundingRect = m_paintedBoundingRect.united(m_selectionBoundingRect); setTransform(qmlItemNode().instanceTransformWithContentTransform()); //the property for zValue is called z in QGraphicsObject - if (qmlItemNode().instanceValue("z").isValid()) + if (qmlItemNode().instanceValue("z").isValid() && !qmlItemNode().isRootModelNode()) setZValue(qmlItemNode().instanceValue("z").toDouble()); } @@ -260,7 +259,7 @@ static void paintTextInPlaceHolderForInvisbleItem(QPainter *painter, QFontMetrics fm(font); painter->rotate(90); - if (fm.width(displayText) > (boundingRect.height() - 32) && displayText.length() > 4) { + if (fm.horizontalAdvance(displayText) > (boundingRect.height() - 32) && displayText.length() > 4) { displayText = fm.elidedText(displayText, Qt::ElideRight, boundingRect.height() - 32, Qt::TextShowMnemonic); } diff --git a/src/plugins/qmldesigner/components/formeditor/numberseriesaction.cpp b/src/plugins/qmldesigner/components/formeditor/numberseriesaction.cpp index 0ce0a995ba..27bfbe4f83 100644 --- a/src/plugins/qmldesigner/components/formeditor/numberseriesaction.cpp +++ b/src/plugins/qmldesigner/components/formeditor/numberseriesaction.cpp @@ -59,7 +59,7 @@ QWidget *NumberSeriesAction::createWidget(QWidget *parent) comboBox->setModel(m_comboBoxModel.data()); comboBox->setCurrentIndex(m_comboBoxModelIndex); - connect(comboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), + connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &NumberSeriesAction::emitValueChanged); return comboBox; diff --git a/src/plugins/qmldesigner/components/importmanager/importswidget.cpp b/src/plugins/qmldesigner/components/importmanager/importswidget.cpp index 0d2218a42b..07ada7c858 100644 --- a/src/plugins/qmldesigner/components/importmanager/importswidget.cpp +++ b/src/plugins/qmldesigner/components/importmanager/importswidget.cpp @@ -39,7 +39,7 @@ ImportsWidget::ImportsWidget(QWidget *parent) : { setWindowTitle(tr("Import Manager")); m_addImportComboBox = new ImportManagerComboBox(this); - connect(m_addImportComboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), + connect(m_addImportComboBox, QOverload<int>::of(&QComboBox::activated), this, &ImportsWidget::addSelectedImport); } diff --git a/src/plugins/qmldesigner/components/integration/componentaction.cpp b/src/plugins/qmldesigner/components/integration/componentaction.cpp index daedbe8d7d..c3c6875d63 100644 --- a/src/plugins/qmldesigner/components/integration/componentaction.cpp +++ b/src/plugins/qmldesigner/components/integration/componentaction.cpp @@ -53,7 +53,7 @@ QWidget *ComponentAction::createWidget(QWidget *parent) comboBox->setToolTip(tr("Edit sub components defined in this file.")); comboBox->setModel(m_componentView->standardItemModel()); comboBox->setCurrentIndex(-1); - connect(comboBox, static_cast<void(QComboBox::*)(int)>(&QComboBox::activated), + connect(comboBox, QOverload<int>::of(&QComboBox::activated), this, &ComponentAction::emitCurrentComponentChanged); connect(this, &ComponentAction::currentIndexChanged, comboBox, &QComboBox::setCurrentIndex); diff --git a/src/plugins/qmldesigner/components/integration/designdocument.cpp b/src/plugins/qmldesigner/components/integration/designdocument.cpp index 7a0f1f10b5..4f23154945 100644 --- a/src/plugins/qmldesigner/components/integration/designdocument.cpp +++ b/src/plugins/qmldesigner/components/integration/designdocument.cpp @@ -64,7 +64,6 @@ enum { namespace QmlDesigner { - /** \class QmlDesigner::DesignDocument @@ -197,7 +196,7 @@ QString DesignDocument::simplfiedDisplayName() const return rootModelNode().simplifiedTypeName(); } -void DesignDocument::updateFileName(const Utils::FileName & /*oldFileName*/, const Utils::FileName &newFileName) +void DesignDocument::updateFileName(const Utils::FilePath & /*oldFileName*/, const Utils::FilePath &newFileName) { if (m_documentModel) m_documentModel->setFileUrl(QUrl::fromLocalFile(newFileName.toString())); @@ -210,11 +209,11 @@ void DesignDocument::updateFileName(const Utils::FileName & /*oldFileName*/, con emit displayNameChanged(displayName()); } -Utils::FileName DesignDocument::fileName() const +Utils::FilePath DesignDocument::fileName() const { if (editor()) return editor()->document()->filePath(); - return Utils::FileName(); + return Utils::FilePath(); } Kit *DesignDocument::currentKit() const @@ -251,7 +250,7 @@ void DesignDocument::loadDocument(QPlainTextEdit *edit) m_inFileComponentTextModifier.reset(); - updateFileName(Utils::FileName(), fileName()); + updateFileName(Utils::FilePath(), fileName()); updateQrcFiles(); @@ -288,7 +287,7 @@ void DesignDocument::updateQrcFiles() ProjectExplorer::Project *currentProject = ProjectExplorer::SessionManager::projectForFile(fileName()); if (currentProject) { - for (const Utils::FileName &fileName : currentProject->files(ProjectExplorer::Project::SourceFiles)) { + for (const Utils::FilePath &fileName : currentProject->files(ProjectExplorer::Project::SourceFiles)) { if (fileName.endsWith(".qrc")) QmlJS::ModelManagerInterface::instance()->updateQrcFile(fileName.toString()); } @@ -366,18 +365,13 @@ void DesignDocument::deleteSelected() if (!currentModel()) return; - try { - RewriterTransaction transaction(rewriterView(), QByteArrayLiteral("DesignDocument::deleteSelected")); + rewriterView()->executeInTransaction("DesignDocument::deleteSelected", [this](){ QList<ModelNode> toDelete = view()->selectedModelNodes(); foreach (ModelNode node, toDelete) { if (node.isValid() && !node.isRootNode() && QmlObjectNode::isValidQmlObjectNode(node)) QmlObjectNode(node).destroy(); } - - transaction.commit(); - } catch (const RewritingException &e) { - e.showException(); - } + }); } void DesignDocument::copySelected() @@ -466,10 +460,8 @@ void DesignDocument::paste() } } - QList<ModelNode> pastedNodeList; - - try { - RewriterTransaction transaction(rewriterView(), QByteArrayLiteral("DesignDocument::paste1")); + rewriterView()->executeInTransaction("DesignDocument::paste1", [this, &view, selectedNodes, targetNode](){ + QList<ModelNode> pastedNodeList; int offset = double(qrand()) / RAND_MAX * 20 - 10; @@ -482,14 +474,10 @@ void DesignDocument::paste() } view.setSelectedModelNodes(pastedNodeList); - transaction.commit(); - } catch (const RewritingException &e) { - qWarning() << e.description(); //silent error - } - } else { - try { - RewriterTransaction transaction(rewriterView(), QByteArrayLiteral("DesignDocument::paste2")); + }); + } else { + rewriterView()->executeInTransaction("DesignDocument::paste1", [this, &view, selectedNodes, rootNode](){ currentModel()->attachView(&view); ModelNode pastedNode(view.insertModel(rootNode)); ModelNode targetNode; @@ -501,9 +489,9 @@ void DesignDocument::paste() targetNode = view.rootModelNode(); if (targetNode.hasParentProperty() && - (pastedNode.simplifiedTypeName() == targetNode.simplifiedTypeName()) && - (pastedNode.variantProperty("width").value() == targetNode.variantProperty("width").value()) && - (pastedNode.variantProperty("height").value() == targetNode.variantProperty("height").value())) + (pastedNode.simplifiedTypeName() == targetNode.simplifiedTypeName()) && + (pastedNode.variantProperty("width").value() == targetNode.variantProperty("width").value()) && + (pastedNode.variantProperty("height").value() == targetNode.variantProperty("height").value())) targetNode = targetNode.parentProperty().parentModelNode(); @@ -515,15 +503,9 @@ void DesignDocument::paste() } else { qWarning() << "Cannot reparent to" << targetNode; } - - transaction.commit(); - NodeMetaInfo::clearCache(); - view.setSelectedModelNodes({pastedNode}); - transaction.commit(); - } catch (const RewritingException &e) { - qWarning() << e.description(); //silent error - } + }); + NodeMetaInfo::clearCache(); } } diff --git a/src/plugins/qmldesigner/components/integration/designdocument.h b/src/plugins/qmldesigner/components/integration/designdocument.h index 0bb5377c6d..eba0cf4060 100644 --- a/src/plugins/qmldesigner/components/integration/designdocument.h +++ b/src/plugins/qmldesigner/components/integration/designdocument.h @@ -90,7 +90,7 @@ public: TextEditor::BaseTextEditor *textEditor() const; QPlainTextEdit *plainTextEdit() const; - Utils::FileName fileName() const; + Utils::FilePath fileName() const; ProjectExplorer::Kit *currentKit() const; bool isDocumentLoaded() const; @@ -121,7 +121,7 @@ public: void changeToMaster(); private: // functions - void updateFileName(const Utils::FileName &oldFileName, const Utils::FileName &newFileName); + void updateFileName(const Utils::FilePath &oldFileName, const Utils::FilePath &newFileName); void changeToInFileComponentModel(ComponentTextModifier *textModifer); diff --git a/src/plugins/qmldesigner/components/navigator/nameitemdelegate.cpp b/src/plugins/qmldesigner/components/navigator/nameitemdelegate.cpp index 089a540b8f..8739ec64a8 100644 --- a/src/plugins/qmldesigner/components/navigator/nameitemdelegate.cpp +++ b/src/plugins/qmldesigner/components/navigator/nameitemdelegate.cpp @@ -137,7 +137,7 @@ static QRect drawText(QPainter *painter, displayString = styleOption.fontMetrics.elidedText(displayString, Qt::ElideMiddle, styleOption.rect.width() - extraSpace); displayStringOffset = QPoint(5 + iconOffset, -5); - width = styleOption.fontMetrics.width(displayString); + width = styleOption.fontMetrics.horizontalAdvance(displayString); QPoint textPosition = styleOption.rect.bottomLeft() + displayStringOffset; painter->drawText(textPosition, displayString); diff --git a/src/plugins/qmldesigner/components/navigator/navigatortreemodel.cpp b/src/plugins/qmldesigner/components/navigator/navigatortreemodel.cpp index 126fed8e68..8c89acb7c8 100644 --- a/src/plugins/qmldesigner/components/navigator/navigatortreemodel.cpp +++ b/src/plugins/qmldesigner/components/navigator/navigatortreemodel.cpp @@ -246,14 +246,29 @@ Qt::ItemFlags NavigatorTreeModel::flags(const QModelIndex &index) const | Qt::ItemNeverHasChildren; } +void static appendForcedNodes(const NodeListProperty &property, QList<ModelNode> &list) +{ + const QStringList visibleProperties = NodeHints::fromModelNode(property.parentModelNode()).visibleNonDefaultProperties(); + for (const ModelNode &node : property.parentModelNode().directSubModelNodes()) { + if (!list.contains(node) && visibleProperties.contains(QString::fromUtf8(node.parentProperty().name()))) + list.append(node); + } +} + QList<ModelNode> filteredList(const NodeListProperty &property, bool filter) { if (!filter) return property.toModelNodeList(); - return Utils::filtered(property.toModelNodeList(), [] (const ModelNode &arg) { + QList<ModelNode> list; + + list.append(Utils::filtered(property.toModelNodeList(), [] (const ModelNode &arg) { return QmlItemNode::isValidQmlItemNode(arg) || NodeHints::fromModelNode(arg).visibleInNavigator(); - }); + })); + + appendForcedNodes(property, list); + + return list; } QModelIndex NavigatorTreeModel::index(int row, int column, @@ -431,7 +446,8 @@ bool NavigatorTreeModel::dropMimeData(const QMimeData *mimeData, static bool findTargetProperty(const QModelIndex &rowModelIndex, NavigatorTreeModel *navigatorTreeModel, NodeAbstractProperty *targetProperty, - int *targetRowNumber) + int *targetRowNumber, + const PropertyName &propertyName = {}) { QModelIndex targetItemIndex; PropertyName targetPropertyName; @@ -445,7 +461,10 @@ static bool findTargetProperty(const QModelIndex &rowModelIndex, if (!targetNode.metaInfo().hasDefaultProperty()) return false; - targetPropertyName = targetNode.metaInfo().defaultPropertyName(); + if (propertyName.isEmpty() || !targetNode.metaInfo().hasProperty(propertyName)) + targetPropertyName = targetNode.metaInfo().defaultPropertyName(); + else + targetPropertyName = propertyName; } // Disallow dropping items between properties, which are listed first. @@ -494,26 +513,30 @@ void NavigatorTreeModel::handleItemLibraryItemDrop(const QMimeData *mimeData, in int targetRowNumber = rowNumber; NodeAbstractProperty targetProperty; - bool foundTarget = findTargetProperty(rowModelIndex, this, &targetProperty, &targetRowNumber); + const ItemLibraryEntry itemLibraryEntry = + createItemLibraryEntryFromMimeData(mimeData->data("application/vnd.bauhaus.itemlibraryinfo")); - if (foundTarget) { - const ItemLibraryEntry itemLibraryEntry = - createItemLibraryEntryFromMimeData(mimeData->data("application/vnd.bauhaus.itemlibraryinfo")); + const NodeHints hints = NodeHints::fromItemLibraryEntry(itemLibraryEntry); + + const QString targetPropertyName = hints.forceNonDefaultProperty(); + bool foundTarget = findTargetProperty(rowModelIndex, this, &targetProperty, &targetRowNumber, targetPropertyName.toUtf8()); + + if (foundTarget) { if (!NodeHints::fromItemLibraryEntry(itemLibraryEntry).canBeDroppedInNavigator()) return; - const QmlItemNode newQmlItemNode = QmlItemNode::createQmlItemNode(m_view, itemLibraryEntry, QPointF(), targetProperty); + const QmlObjectNode newQmlObjectNode = QmlItemNode::createQmlObjectNode(m_view, itemLibraryEntry, QPointF(), targetProperty); - if (newQmlItemNode.isValid() && targetProperty.isNodeListProperty()) { + if (newQmlObjectNode.isValid() && targetProperty.isNodeListProperty()) { QList<ModelNode> newModelNodeList; - newModelNodeList.append(newQmlItemNode); + newModelNodeList.append(newQmlObjectNode); moveNodesInteractive(targetProperty, newModelNodeList, targetRowNumber); } - if (newQmlItemNode.isValid()) - m_view->selectModelNode(newQmlItemNode.modelNode()); + if (newQmlObjectNode.isValid()) + m_view->selectModelNode(newQmlObjectNode.modelNode()); } } @@ -545,10 +568,9 @@ void NavigatorTreeModel::handleItemLibraryImageDrop(const QMimeData *mimeData, i void NavigatorTreeModel::moveNodesInteractive(NodeAbstractProperty &parentProperty, const QList<ModelNode> &modelNodes, int targetIndex) { QTC_ASSERT(m_view, return); - try { - const TypeName propertyQmlType = parentProperty.parentModelNode().metaInfo().propertyTypeName(parentProperty.name()); - RewriterTransaction transaction = m_view->beginRewriterTransaction(QByteArrayLiteral("NavigatorTreeModel::moveNodesInteractive")); + m_view->executeInTransaction("NavigatorTreeModel::moveNodesInteractive",[this, &parentProperty, modelNodes, targetIndex](){ + const TypeName propertyQmlType = parentProperty.parentModelNode().metaInfo().propertyTypeName(parentProperty.name()); foreach (const ModelNode &modelNode, modelNodes) { if (modelNode.isValid() && modelNode != parentProperty.parentModelNode() @@ -565,10 +587,7 @@ void NavigatorTreeModel::moveNodesInteractive(NodeAbstractProperty &parentProper } } } - transaction.commit(); - } catch (const RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } + }); } Qt::DropActions NavigatorTreeModel::supportedDropActions() const diff --git a/src/plugins/qmldesigner/components/navigator/navigatortreeview.h b/src/plugins/qmldesigner/components/navigator/navigatortreeview.h index cc5a0e344d..1001977b8f 100644 --- a/src/plugins/qmldesigner/components/navigator/navigatortreeview.h +++ b/src/plugins/qmldesigner/components/navigator/navigatortreeview.h @@ -29,7 +29,6 @@ namespace QmlDesigner { - class NavigatorTreeView : public QTreeView { Q_OBJECT diff --git a/src/plugins/qmldesigner/components/navigator/navigatorview.cpp b/src/plugins/qmldesigner/components/navigator/navigatorview.cpp index 07434102fc..57578d866f 100644 --- a/src/plugins/qmldesigner/components/navigator/navigatorview.cpp +++ b/src/plugins/qmldesigner/components/navigator/navigatorview.cpp @@ -198,16 +198,10 @@ void NavigatorView::handleChangedExport(const ModelNode &modelNode, bool exporte if (rootNode.hasProperty(modelNodeId)) rootNode.removeProperty(modelNodeId); if (exported) { - try { - RewriterTransaction transaction = - beginRewriterTransaction(QByteArrayLiteral("NavigatorTreeModel:exportItem")); - + executeInTransaction("NavigatorTreeModel:exportItem", [this, modelNode](){ QmlObjectNode qmlObjectNode(modelNode); qmlObjectNode.ensureAliasExport(); - transaction.commit(); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } + }); } } @@ -400,7 +394,8 @@ void NavigatorView::upButtonClicked() index--; if (index < 0) index = node.parentProperty().count() - 1; //wrap around - node.parentProperty().toNodeListProperty().slide(oldIndex, index); + if (oldIndex != index) + node.parentProperty().toNodeListProperty().slide(oldIndex, index); } } updateItemSelection(); @@ -417,7 +412,8 @@ void NavigatorView::downButtonClicked() index++; if (index >= node.parentProperty().count()) index = 0; //wrap around - node.parentProperty().toNodeListProperty().slide(oldIndex, index); + if (oldIndex != index) + node.parentProperty().toNodeListProperty().slide(oldIndex, index); } } updateItemSelection(); diff --git a/src/plugins/qmldesigner/components/pathtool/controlpoint.cpp b/src/plugins/qmldesigner/components/pathtool/controlpoint.cpp new file mode 100644 index 0000000000..d2a8bf75c3 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/controlpoint.cpp @@ -0,0 +1,174 @@ +/**************************************************************************** +** +** 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 "controlpoint.h" + +#include <QtDebug> + +#include <variantproperty.h> + +#include <rewritertransaction.h> + +namespace QmlDesigner { + +ControlPoint::ControlPoint() = default; + +ControlPoint::ControlPoint(const ControlPoint &other) = default; + +ControlPoint::ControlPoint(const QPointF &coordinate) + : d(new ControlPointData) +{ + d->coordinate = coordinate; +} + +ControlPoint::ControlPoint(double x, double y) + : d(new ControlPointData) +{ + d->coordinate = QPointF(x, y); +} + +ControlPoint::~ControlPoint() = default; + +ControlPoint &ControlPoint::operator =(const ControlPoint &other) +{ + if (d != other.d) + d = other.d; + + return *this; +} + +void ControlPoint::setX(double x) +{ + d->coordinate.setX(x); +} + +void ControlPoint::setY(double y) +{ + d->coordinate.setY(y); +} + +void ControlPoint::setCoordinate(const QPointF &coordinate) +{ + d->coordinate = coordinate; +} + +void ControlPoint::setPathElementModelNode(const ModelNode &modelNode) +{ + d->pathElementModelNode = modelNode; +} + +ModelNode ControlPoint::pathElementModelNode() const +{ + return d->pathElementModelNode; +} + +void ControlPoint::setPathModelNode(const ModelNode &pathModelNode) +{ + d->pathModelNode = pathModelNode; +} + +ModelNode ControlPoint::pathModelNode() const +{ + return d->pathModelNode; +} + +void ControlPoint::setPointType(PointType pointType) +{ + d->pointType = pointType; +} + +PointType ControlPoint::pointType() const +{ + return d->pointType; +} + +QPointF ControlPoint::coordinate() const +{ + return d->coordinate; +} + +bool ControlPoint::isValid() const +{ + return d.data(); +} + +bool ControlPoint::isEditPoint() const +{ + return isValid() && (pointType() == StartPoint || pointType() == EndPoint); +} + +bool ControlPoint::isControlVertex() const +{ + return isValid() && (pointType() == FirstControlPoint || pointType() == SecondControlPoint); +} + +void ControlPoint::updateModelNode() +{ + switch (pointType()) { + case StartPoint: + d->pathModelNode.variantProperty("startX").setValue(coordinate().x()); + d->pathModelNode.variantProperty("startY").setValue(coordinate().y()); + break; + case FirstControlPoint: + d->pathElementModelNode.variantProperty("control1X").setValue(coordinate().x()); + d->pathElementModelNode.variantProperty("control1Y").setValue(coordinate().y()); + break; + case SecondControlPoint: + d->pathElementModelNode.variantProperty("control2X").setValue(coordinate().x()); + d->pathElementModelNode.variantProperty("control2Y").setValue(coordinate().y()); + break; + case EndPoint: + d->pathElementModelNode.variantProperty("x").setValue(coordinate().x()); + d->pathElementModelNode.variantProperty("y").setValue(coordinate().y()); + break; + case StartAndEndPoint: + d->pathElementModelNode.variantProperty("x").setValue(coordinate().x()); + d->pathElementModelNode.variantProperty("y").setValue(coordinate().y()); + d->pathModelNode.variantProperty("startX").setValue(coordinate().x()); + d->pathModelNode.variantProperty("startY").setValue(coordinate().y()); + break; + } +} + +bool operator ==(const ControlPoint& firstControlPoint, const ControlPoint& secondControlPoint) +{ + return firstControlPoint.d.data() == secondControlPoint.d.data() && firstControlPoint.d.data(); +} + +QDebug operator<<(QDebug debug, const ControlPoint &controlPoint) +{ + if (controlPoint.isValid()) { + debug.nospace() << "ControlPoint(" + << controlPoint.coordinate().x() << ", " + << controlPoint.coordinate().y() << ", " + << controlPoint.pointType() << ')'; + } else { + debug.nospace() << "ControlPoint(invalid)"; + } + + return debug.space(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/controlpoint.h b/src/plugins/qmldesigner/components/pathtool/controlpoint.h new file mode 100644 index 0000000000..39dc184978 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/controlpoint.h @@ -0,0 +1,93 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <modelnode.h> + +#include <QPointF> +#include <QExplicitlySharedDataPointer> + +namespace QmlDesigner { + +enum PointType { + StartPoint, + FirstControlPoint, + SecondControlPoint, + EndPoint, + StartAndEndPoint +}; + +class ControlPointData : public QSharedData +{ +public: + ModelNode pathElementModelNode; + ModelNode pathModelNode; + QPointF coordinate; + PointType pointType; +}; + +class ControlPoint +{ + friend bool operator ==(const ControlPoint& firstControlPoint, const ControlPoint& secondControlPoint); + +public: + ControlPoint(); + ControlPoint(const ControlPoint &other); + ControlPoint(const QPointF &coordinate); + ControlPoint(double x, double y); + + ~ControlPoint(); + + ControlPoint &operator =(const ControlPoint &other); + + void setX(double x); + void setY(double y); + void setCoordinate(const QPointF &coordinate); + QPointF coordinate() const; + + void setPathElementModelNode(const ModelNode &pathElementModelNode); + ModelNode pathElementModelNode() const; + + void setPathModelNode(const ModelNode &pathModelNode); + ModelNode pathModelNode() const; + + void setPointType(PointType pointType); + PointType pointType() const; + + bool isValid() const; + bool isEditPoint() const; + bool isControlVertex() const; + + void updateModelNode(); + +private: + QExplicitlySharedDataPointer<ControlPointData> d; +}; + +bool operator ==(const ControlPoint& firstControlPoint, const ControlPoint& secondControlPoint); +QDebug operator<<(QDebug debug, const ControlPoint &controlPoint); + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/cubicsegment.cpp b/src/plugins/qmldesigner/components/pathtool/cubicsegment.cpp new file mode 100644 index 0000000000..0005514339 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/cubicsegment.cpp @@ -0,0 +1,367 @@ +/**************************************************************************** +** +** 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 "cubicsegment.h" + +#include <qmath.h> +#include <QtDebug> + + +namespace QmlDesigner { + +CubicSegment::CubicSegment() = default; + +CubicSegment CubicSegment::create() +{ + CubicSegment cubicSegment; + cubicSegment.d = new CubicSegmentData; + + return cubicSegment; +} + +void CubicSegment::setModelNode(const ModelNode &modelNode) +{ + d->modelNode = modelNode; +} + +ModelNode CubicSegment::modelNode() const +{ + return d->modelNode; +} + +void CubicSegment::setFirstControlPoint(const ControlPoint &firstControlPoint) +{ + d->firstControllPoint = firstControlPoint; +} + +void CubicSegment::setFirstControlPoint(double x, double y) +{ + d->firstControllPoint.setX(x); + d->firstControllPoint.setY(y); +} + +void CubicSegment::setFirstControlPoint(const QPointF &coordiante) +{ + d->firstControllPoint.setCoordinate(coordiante); +} + +void CubicSegment::setSecondControlPoint(const ControlPoint &secondControlPoint) +{ + d->secondControllPoint = secondControlPoint; + d->secondControllPoint.setPathElementModelNode(d->modelNode); + d->secondControllPoint.setPointType(FirstControlPoint); +} + +void CubicSegment::setSecondControlPoint(double x, double y) +{ + d->secondControllPoint.setX(x); + d->secondControllPoint.setY(y); + d->secondControllPoint.setPathElementModelNode(d->modelNode); + d->secondControllPoint.setPointType(FirstControlPoint); +} + +void CubicSegment::setSecondControlPoint(const QPointF &coordiante) +{ + d->secondControllPoint.setCoordinate(coordiante); + d->secondControllPoint.setPathElementModelNode(d->modelNode); + d->secondControllPoint.setPointType(FirstControlPoint); +} + +void CubicSegment::setThirdControlPoint(const ControlPoint &thirdControlPoint) +{ + d->thirdControllPoint = thirdControlPoint; + d->thirdControllPoint.setPathElementModelNode(d->modelNode); + d->thirdControllPoint.setPointType(SecondControlPoint); +} + +void CubicSegment::setThirdControlPoint(double x, double y) +{ + d->thirdControllPoint.setX(x); + d->thirdControllPoint.setY(y); + d->thirdControllPoint.setPathElementModelNode(d->modelNode); + d->thirdControllPoint.setPointType(SecondControlPoint); +} + +void CubicSegment::setThirdControlPoint(const QPointF &coordiante) +{ + d->thirdControllPoint.setCoordinate(coordiante); + d->thirdControllPoint.setPathElementModelNode(d->modelNode); + d->thirdControllPoint.setPointType(SecondControlPoint); +} + +void CubicSegment::setFourthControlPoint(const ControlPoint &fourthControlPoint) +{ + d->fourthControllPoint = fourthControlPoint; + d->fourthControllPoint.setPathElementModelNode(d->modelNode); + d->fourthControllPoint.setPointType(EndPoint); +} + +void CubicSegment::setFourthControlPoint(double x, double y) +{ + d->fourthControllPoint.setX(x); + d->fourthControllPoint.setY(y); + d->fourthControllPoint.setPathElementModelNode(d->modelNode); + d->fourthControllPoint.setPointType(EndPoint); +} + +void CubicSegment::setFourthControlPoint(const QPointF &coordiante) +{ + d->fourthControllPoint.setCoordinate(coordiante); + d->fourthControllPoint.setPathElementModelNode(d->modelNode); + d->fourthControllPoint.setPointType(EndPoint); +} + +void CubicSegment::setAttributes(const QMap<QString, QVariant> &attributes) +{ + d->attributes = attributes; +} + +void CubicSegment::setPercent(double percent) +{ + d->percent = percent; +} + +ControlPoint CubicSegment::firstControlPoint() const +{ + return d->firstControllPoint; +} + +ControlPoint CubicSegment::secondControlPoint() const +{ + return d->secondControllPoint; +} + +ControlPoint CubicSegment::thirdControlPoint() const +{ + return d->thirdControllPoint; +} + +ControlPoint CubicSegment::fourthControlPoint() const +{ + return d->fourthControllPoint; +} + +const QMap<QString, QVariant> CubicSegment::attributes() const +{ + return d->attributes; +} + +double CubicSegment::percent() const +{ + return d->percent; +} + +QList<ControlPoint> CubicSegment::controlPoints() const +{ + QList<ControlPoint> controlPointList; + + controlPointList.reserve(4); + + controlPointList.append(firstControlPoint()); + controlPointList.append(secondControlPoint()); + controlPointList.append(thirdControlPoint()); + controlPointList.append(fourthControlPoint()); + + return controlPointList; +} + +double CubicSegment::firstControlX() const +{ + return firstControlPoint().coordinate().x(); +} + +double CubicSegment::firstControlY() const +{ + return firstControlPoint().coordinate().y(); +} + +double CubicSegment::secondControlX() const +{ + return secondControlPoint().coordinate().x(); +} + +double CubicSegment::secondControlY() const +{ + return secondControlPoint().coordinate().y(); +} + +double CubicSegment::thirdControlX() const +{ + return thirdControlPoint().coordinate().x(); +} + +double CubicSegment::thirdControlY() const +{ + return thirdControlPoint().coordinate().y(); +} + +double CubicSegment::fourthControlX() const +{ + return fourthControlPoint().coordinate().x(); +} + +double CubicSegment::fourthControlY() const +{ + return fourthControlPoint().coordinate().y(); +} + +double CubicSegment::quadraticControlX() const +{ + return -0.25 * firstControlX() + 0.75 * secondControlX() + 0.75 * thirdControlX() - 0.25 * fourthControlX(); +} + +double CubicSegment::quadraticControlY() const +{ + return -0.25 * firstControlY() + 0.75 * secondControlY() + 0.75 * thirdControlY() - 0.25 * fourthControlY(); +} + +bool CubicSegment::isValid() const +{ + return d.data(); +} + +bool CubicSegment::canBeConvertedToLine() const +{ + return canBeConvertedToQuad() + && qFuzzyIsNull(((3. * d->firstControllPoint.coordinate()) + - (6. * d->secondControllPoint.coordinate()) + + (3. * d->thirdControllPoint.coordinate())).manhattanLength());; +} + +bool CubicSegment::canBeConvertedToQuad() const +{ + return qFuzzyIsNull(((3. * d->secondControllPoint.coordinate()) + - (3 * d->thirdControllPoint.coordinate()) + + d->fourthControllPoint.coordinate() + - d->firstControllPoint.coordinate()).manhattanLength()); +} + +QPointF CubicSegment::sample(double t) const +{ + return qPow(1.-t, 3.) * firstControlPoint().coordinate() + + 3 * qPow(1.-t, 2.) * t * secondControlPoint().coordinate() + + 3 * qPow(t, 2.) * (1. - t) * thirdControlPoint().coordinate() + + qPow(t, 3.) * fourthControlPoint().coordinate(); +} + +double CubicSegment::minimumDistance(const QPointF &pickPoint, double &tReturnValue) const +{ + double actualMinimumDistance = 10000000.; + for (double t = 0.0; t <= 1.0; t += 0.1) { + QPointF samplePoint = sample(t); + QPointF distanceVector = pickPoint - samplePoint; + if (distanceVector.manhattanLength() < actualMinimumDistance) { + actualMinimumDistance = distanceVector.manhattanLength(); + tReturnValue = t; + } + } + + return actualMinimumDistance; +} + +static QPointF interpolatedPoint(double t, const QPointF &firstPoint, const QPointF &secondPoint) +{ + return (secondPoint - firstPoint) * t + firstPoint; +} + +QPair<CubicSegment, CubicSegment> CubicSegment::split(double t) +{ + // first pass + QPointF secondPointFirstSegment = interpolatedPoint(t, firstControlPoint().coordinate(), secondControlPoint().coordinate()); + QPointF firstIntermediatPoint = interpolatedPoint(t, secondControlPoint().coordinate(), thirdControlPoint().coordinate()); + QPointF thirdPointSecondSegment = interpolatedPoint(t, thirdControlPoint().coordinate(), fourthControlPoint().coordinate()); + + // second pass + QPointF thirdPointFirstSegment = interpolatedPoint(t, secondPointFirstSegment, firstIntermediatPoint); + QPointF secondPointSecondSegment = interpolatedPoint(t, firstIntermediatPoint, thirdPointSecondSegment); + + // third pass + QPointF midPoint = interpolatedPoint(t, thirdPointFirstSegment, secondPointSecondSegment); + ControlPoint midControlPoint(midPoint); + + + CubicSegment firstCubicSegment = CubicSegment::create(); + firstCubicSegment.setFirstControlPoint(firstControlPoint().coordinate()); + firstCubicSegment.setSecondControlPoint(secondPointFirstSegment); + firstCubicSegment.setThirdControlPoint(thirdPointFirstSegment); + firstCubicSegment.setFourthControlPoint(midControlPoint); + + CubicSegment secondCubicSegment = CubicSegment::create(); + secondCubicSegment.setFirstControlPoint(midControlPoint); + secondCubicSegment.setSecondControlPoint(secondPointSecondSegment); + secondCubicSegment.setThirdControlPoint(thirdPointSecondSegment); + secondCubicSegment.setFourthControlPoint(fourthControlPoint().coordinate()); + + qDebug() << firstCubicSegment << secondCubicSegment; + + return {firstCubicSegment, secondCubicSegment}; +} + +void CubicSegment::makeStraightLine() +{ + QPointF lineVector = fourthControlPoint().coordinate() - firstControlPoint().coordinate(); + QPointF newSecondControlPoint = firstControlPoint().coordinate() + (lineVector * 0.3); + QPointF newThirdControlPoint = fourthControlPoint().coordinate() - (lineVector * 0.3); + setSecondControlPoint(newSecondControlPoint); + setThirdControlPoint(newThirdControlPoint); +} + +void CubicSegment::updateModelNode() +{ + firstControlPoint().updateModelNode(); + secondControlPoint().updateModelNode(); + thirdControlPoint().updateModelNode(); + fourthControlPoint().updateModelNode(); +} + +CubicSegmentData::CubicSegmentData() + : firstControllPoint(0., 0.), + secondControllPoint(0., 0.), + thirdControllPoint(0., 0.), + fourthControllPoint(0., 0.), + percent(-1.0) +{ +} + +bool operator ==(const CubicSegment& firstCubicSegment, const CubicSegment& secondCubicSegment) +{ + return firstCubicSegment.d.data() == secondCubicSegment.d.data(); +} + +QDebug operator<<(QDebug debug, const CubicSegment &cubicSegment) +{ + if (cubicSegment.isValid()) { + debug.nospace() << "CubicSegment(" + << cubicSegment.firstControlPoint() << ", " + << cubicSegment.secondControlPoint() << ", " + << cubicSegment.thirdControlPoint() << ", " + << cubicSegment.fourthControlPoint() << ')'; + } else { + debug.nospace() << "CubicSegment(invalid)"; + } + + return debug.space(); +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/cubicsegment.h b/src/plugins/qmldesigner/components/pathtool/cubicsegment.h new file mode 100644 index 0000000000..e22b4e1aa3 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/cubicsegment.h @@ -0,0 +1,126 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "controlpoint.h" + +#include <modelnode.h> + +#include <QMap> + +#include <QPointF> +#include <QExplicitlySharedDataPointer> + +namespace QmlDesigner { + +class CubicSegmentData : public QSharedData +{ +public: + CubicSegmentData(); + ModelNode modelNode; + ControlPoint firstControllPoint; + ControlPoint secondControllPoint; + ControlPoint thirdControllPoint; + ControlPoint fourthControllPoint; + QMap<QString, QVariant> attributes; + double percent; +}; + +class CubicSegment +{ + friend bool operator ==(const CubicSegment& firstCubicSegment, const CubicSegment& secondCubicSegment); + +public: + CubicSegment(); + + static CubicSegment create(); + + void setModelNode(const ModelNode &modelNode); + ModelNode modelNode() const; + + void setFirstControlPoint(const ControlPoint &firstControlPoint); + void setFirstControlPoint(double x, double y); + void setFirstControlPoint(const QPointF &coordiante); + + void setSecondControlPoint(const ControlPoint &secondControlPoint); + void setSecondControlPoint(double x, double y); + void setSecondControlPoint(const QPointF &coordiante); + + void setThirdControlPoint(const ControlPoint &thirdControlPoint); + void setThirdControlPoint(double x, double y); + void setThirdControlPoint(const QPointF &coordiante); + + void setFourthControlPoint(const ControlPoint &fourthControlPoint); + void setFourthControlPoint(double x, double y); + void setFourthControlPoint(const QPointF &coordiante); + + void setAttributes(const QMap<QString, QVariant> &attributes); + + void setPercent(double percent); + + ControlPoint firstControlPoint() const; + ControlPoint secondControlPoint() const; + ControlPoint thirdControlPoint() const; + ControlPoint fourthControlPoint() const; + + const QMap<QString, QVariant> attributes() const; + + double percent() const; + + QList<ControlPoint> controlPoints() const; + + double firstControlX() const; + double firstControlY() const; + double secondControlX() const; + double secondControlY() const; + double thirdControlX() const; + double thirdControlY() const; + double fourthControlX() const; + double fourthControlY() const; + double quadraticControlX() const; + double quadraticControlY() const; + + bool isValid() const; + bool canBeConvertedToLine() const; + bool canBeConvertedToQuad() const; + + QPointF sample(double t) const; + double minimumDistance(const QPointF &pickPoint, double &t) const; + + QPair<CubicSegment, CubicSegment> split(double t); + + void makeStraightLine(); + + void updateModelNode(); + +private: + QExplicitlySharedDataPointer<CubicSegmentData> d; +}; + +bool operator ==(const CubicSegment& firstCubicSegment, const CubicSegment& secondCubicSegment); +QDebug operator<<(QDebug debug, const CubicSegment &cubicSegment); + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/pathitem.cpp b/src/plugins/qmldesigner/components/pathtool/pathitem.cpp new file mode 100644 index 0000000000..76fe6f0b90 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathitem.cpp @@ -0,0 +1,971 @@ +/**************************************************************************** +** +** 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 "pathitem.h" + +#include <exception.h> +#include <nodeproperty.h> +#include <variantproperty.h> +#include <nodelistproperty.h> +#include <rewritingexception.h> +#include <rewritertransaction.h> +#include <formeditorscene.h> +#include <formeditorview.h> +#include <theme.h> + +#include <QPainter> +#include <QMenu> +#include <QtDebug> +#include <QGraphicsSceneMouseEvent> + +namespace QmlDesigner { + +PathItem::PathItem(FormEditorScene* scene) + : m_selectionManipulator(this), + m_lastPercent(-1.), + m_formEditorItem(nullptr), + m_dontUpdatePath(false) +{ + scene->addItem(this); + setFlag(QGraphicsItem::ItemIsMovable, false); +} + +PathItem::~PathItem() +{ + m_formEditorItem = nullptr; +} + +static ModelNode pathModelNode(FormEditorItem *formEditorItem) +{ + ModelNode modelNode = formEditorItem->qmlItemNode().modelNode(); + + return modelNode.nodeProperty("path").modelNode(); +} + +using PropertyPair = QPair<PropertyName, QVariant>; + +void PathItem::writeLinePath(const ModelNode &pathNode, const CubicSegment &cubicSegment) +{ + QList<PropertyPair> propertyList; + propertyList.append(PropertyPair("x", cubicSegment.fourthControlX())); + propertyList.append(PropertyPair("y", cubicSegment.fourthControlY())); + + ModelNode lineNode = pathNode.view()->createModelNode("QtQuick.PathLine", pathNode.majorVersion(), pathNode.minorVersion(), propertyList); + pathNode.nodeListProperty("pathElements").reparentHere(lineNode); +} + +void PathItem::writeQuadPath(const ModelNode &pathNode, const CubicSegment &cubicSegment) +{ + QList<QPair<PropertyName, QVariant> > propertyList; + propertyList.append(PropertyPair("controlX", cubicSegment.quadraticControlX())); + propertyList.append(PropertyPair("controlY", cubicSegment.quadraticControlY())); + propertyList.append(PropertyPair("x", cubicSegment.fourthControlX())); + propertyList.append(PropertyPair("y", cubicSegment.fourthControlY())); + + ModelNode lineNode = pathNode.view()->createModelNode("QtQuick.PathQuad", pathNode.majorVersion(), pathNode.minorVersion(), propertyList); + pathNode.nodeListProperty("pathElements").reparentHere(lineNode); +} + +void PathItem::writeCubicPath(const ModelNode &pathNode, const CubicSegment &cubicSegment) +{ + QList<QPair<PropertyName, QVariant> > propertyList; + propertyList.append(PropertyPair("control1X", cubicSegment.secondControlX())); + propertyList.append(PropertyPair("control1Y", cubicSegment.secondControlY())); + propertyList.append(PropertyPair("control2X", cubicSegment.thirdControlX())); + propertyList.append(PropertyPair("control2Y", cubicSegment.thirdControlY())); + propertyList.append(PropertyPair("x", cubicSegment.fourthControlX())); + propertyList.append(PropertyPair("y", cubicSegment.fourthControlY())); + + ModelNode lineNode = pathNode.view()->createModelNode("QtQuick.PathCubic", pathNode.majorVersion(), pathNode.minorVersion(), propertyList); + pathNode.nodeListProperty("pathElements").reparentHere(lineNode); +} + +void PathItem::writePathAttributes(const ModelNode &pathNode, const QMap<QString, QVariant> &attributes) +{ + QMapIterator<QString, QVariant> attributesIterator(attributes); + while (attributesIterator.hasNext()) { + attributesIterator.next(); + QList<QPair<PropertyName, QVariant> > propertyList; + propertyList.append(PropertyPair("name", attributesIterator.key())); + propertyList.append(PropertyPair("value", attributesIterator.value())); + + ModelNode lineNode = pathNode.view()->createModelNode("QtQuick.PathAttribute", pathNode.majorVersion(), pathNode.minorVersion(), propertyList); + pathNode.nodeListProperty("pathElements").reparentHere(lineNode); + } +} + +void PathItem::writePathPercent(const ModelNode& pathNode, double percent) +{ + if (percent >= 0.0) { + QList<QPair<PropertyName, QVariant> > propertyList; + propertyList.append(PropertyPair("value", percent)); + + ModelNode lineNode = pathNode.view()->createModelNode("QtQuick.PathPercent", pathNode.majorVersion(), pathNode.minorVersion(), propertyList); + pathNode.nodeListProperty("pathElements").reparentHere(lineNode); + } +} + +void PathItem::writePathToProperty() +{ + PathUpdateDisabler pathUpdateDisable(this); + + ModelNode pathNode = pathModelNode(formEditorItem()); + + pathNode.view()->executeInTransaction("PathItem::writePathToProperty", [this, &pathNode](){ + QList<ModelNode> pathSegmentNodes = pathNode.nodeListProperty("pathElements").toModelNodeList(); + + foreach (ModelNode pathSegment, pathSegmentNodes) + pathSegment.destroy(); + + if (!m_cubicSegments.isEmpty()) { + pathNode.variantProperty("startX").setValue(m_cubicSegments.constFirst().firstControlPoint().coordinate().x()); + pathNode.variantProperty("startY").setValue(m_cubicSegments.constFirst().firstControlPoint().coordinate().y()); + + foreach (const CubicSegment &cubicSegment, m_cubicSegments) { + writePathAttributes(pathNode, cubicSegment.attributes()); + writePathPercent(pathNode, cubicSegment.percent()); + + if (cubicSegment.canBeConvertedToLine()) + writeLinePath(pathNode, cubicSegment); + else if (cubicSegment.canBeConvertedToQuad()) + writeQuadPath(pathNode, cubicSegment); + else + writeCubicPath(pathNode, cubicSegment); + } + + writePathAttributes(pathNode, m_lastAttributes); + writePathPercent(pathNode, m_lastPercent); + } + }); +} + +void PathItem::writePathAsCubicSegmentsOnly() +{ + PathUpdateDisabler pathUpdateDisabler(this); + + ModelNode pathNode = pathModelNode(formEditorItem()); + pathNode.view()->executeInTransaction("PathItem::writePathAsCubicSegmentsOnly", [this, &pathNode](){ + + QList<ModelNode> pathSegmentNodes = pathNode.nodeListProperty("pathElements").toModelNodeList(); + + foreach (ModelNode pathSegment, pathSegmentNodes) + pathSegment.destroy(); + + if (!m_cubicSegments.isEmpty()) { + pathNode.variantProperty("startX").setValue(m_cubicSegments.constFirst().firstControlPoint().coordinate().x()); + pathNode.variantProperty("startY").setValue(m_cubicSegments.constFirst().firstControlPoint().coordinate().y()); + + + foreach (const CubicSegment &cubicSegment, m_cubicSegments) { + writePathAttributes(pathNode, cubicSegment.attributes()); + writePathPercent(pathNode, cubicSegment.percent()); + writeCubicPath(pathNode, cubicSegment); + } + + writePathAttributes(pathNode, m_lastAttributes); + writePathPercent(pathNode, m_lastPercent); + } + }); +} + +void PathItem::setFormEditorItem(FormEditorItem *formEditorItem) +{ + m_formEditorItem = formEditorItem; + setTransform(formEditorItem->sceneTransform()); + updatePath(); + +// m_textEdit->setPlainText(m_formEditorItem->qmlItemNode().modelValue("path").toString()); +} + +static bool hasPath(FormEditorItem *formEditorItem) +{ + ModelNode modelNode = formEditorItem->qmlItemNode().modelNode(); + + return modelNode.hasProperty("path") && modelNode.property("path").isNodeProperty(); +} + +QPointF startPoint(const ModelNode &modelNode) +{ + QPointF point; + + if (modelNode.hasProperty("startX")) + point.setX(modelNode.variantProperty("startX").value().toDouble()); + + if (modelNode.hasProperty("startY")) + point.setY(modelNode.variantProperty("startY").value().toDouble()); + + return point; +} + +static void addCubicSegmentToPainterPath(const CubicSegment &cubicSegment, QPainterPath &painterPath) +{ + painterPath.cubicTo(cubicSegment.secondControlPoint().coordinate(), + cubicSegment.thirdControlPoint().coordinate(), + cubicSegment.fourthControlPoint().coordinate()); + +} + +static void drawCubicSegments(const QList<CubicSegment> &cubicSegments, QPainter *painter) +{ + painter->save(); + + QPainterPath curvePainterPath(cubicSegments.constFirst().firstControlPoint().coordinate()); + + foreach (const CubicSegment &cubicSegment, cubicSegments) + addCubicSegmentToPainterPath(cubicSegment, curvePainterPath); + + painter->setPen(QPen(Qt::black, 1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); + painter->drawPath(curvePainterPath); + + painter->restore(); +} + +static void drawControlLine(const CubicSegment &cubicSegment, QPainter *painter) +{ + static const QPen solidPen(QColor(104, 183, 214), 1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin); + painter->setPen(solidPen); + painter->drawLine(cubicSegment.firstControlPoint().coordinate(), + cubicSegment.secondControlPoint().coordinate()); + + QVector<double> dashVector; + dashVector.append(4); + dashVector.append(4); + QPen dashedPen(QColor(104, 183, 214), 1, Qt::CustomDashLine, Qt::FlatCap, Qt::MiterJoin); + dashedPen.setDashPattern(dashVector); + painter->setPen(dashedPen); + painter->drawLine(cubicSegment.secondControlPoint().coordinate(), + cubicSegment.thirdControlPoint().coordinate()); + + painter->setPen(QPen(QColor(104, 183, 214), 1, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); + painter->drawLine(cubicSegment.thirdControlPoint().coordinate(), + cubicSegment.fourthControlPoint().coordinate()); +} + +static void drawControlLines(const QList<CubicSegment> &cubicSegments, QPainter *painter) +{ + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, false); + + foreach (const CubicSegment &cubicSegment, cubicSegments) + drawControlLine(cubicSegment, painter); + + painter->restore(); +} + +static QRectF controlPointShape(-2, -2, 5, 5); + +static void drawControlPoint(const ControlPoint &controlPoint, const QList<ControlPoint> &selectionPoints, QPainter *painter) +{ + static const QColor editPointColor(0, 110, 255); + static const QColor controlVertexColor(0, 110, 255); + static const QColor selectionPointColor(0, 255, 0); + + double originX = controlPoint.coordinate().x(); + double originY = controlPoint.coordinate().y(); + + if (controlPoint.isEditPoint()) { + if (selectionPoints.contains(controlPoint)) { + painter->setBrush(selectionPointColor); + painter->setPen(selectionPointColor); + } else { + painter->setBrush(editPointColor); + painter->setPen(editPointColor); + } + painter->setRenderHint(QPainter::Antialiasing, false); + painter->drawRect(controlPointShape.adjusted(originX -1, originY - 1, originX - 1, originY - 1)); + painter->setRenderHint(QPainter::Antialiasing, true); + } else { + if (selectionPoints.contains(controlPoint)) { + painter->setBrush(selectionPointColor); + painter->setPen(selectionPointColor); + } else { + painter->setBrush(controlVertexColor); + painter->setPen(controlVertexColor); + } + painter->drawEllipse(controlPointShape.adjusted(originX, originY, originX, originY)); + } +} + +static void drawControlPoints(const QList<ControlPoint> &controlPoints, const QList<ControlPoint> &selectionPoints, QPainter *painter) +{ + painter->save(); + + foreach (const ControlPoint &controlPoint, controlPoints) + drawControlPoint(controlPoint, selectionPoints, painter); + + painter->restore(); +} + +static void drawPositionOverlay(const ControlPoint &controlPoint, QPainter *painter) +{ + QPoint position = controlPoint.coordinate().toPoint(); + position.rx() += 3; + position.ry() -= 3; + + QString postionText(QString(QLatin1String("x: %1 y: %2")).arg(controlPoint.coordinate().x()).arg(controlPoint.coordinate().y())); + painter->drawText(position, postionText); +} + +static void drawPostionOverlays(const QList<SelectionPoint> &selectedPoints, QPainter *painter) +{ + painter->save(); + QFont font = painter->font(); + font.setPixelSize(Theme::instance()->smallFontPixelSize()); + painter->setFont(font); + painter->setPen(QColor(0, 0, 0)); + + foreach (const SelectionPoint &selectedPoint, selectedPoints) + drawPositionOverlay(selectedPoint.controlPoint, painter); + + painter->restore(); +} + +static void drawMultiSelectionRectangle(const QRectF &selectionRectangle, QPainter *painter) +{ + painter->save(); + static QColor selectionBrush(painter->pen().color()); + selectionBrush.setAlpha(50); + painter->setRenderHint(QPainter::Antialiasing, false); + painter->setBrush(selectionBrush); + painter->drawRect(selectionRectangle); + painter->restore(); +} + +void PathItem::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) +{ + painter->save(); + + painter->setRenderHint(QPainter::Antialiasing, true); + + if (!m_cubicSegments.isEmpty()) { + drawCubicSegments(m_cubicSegments, painter); + drawControlLines(m_cubicSegments, painter); + drawControlPoints(controlPoints(), m_selectionManipulator.allControlPoints(), painter); + drawPostionOverlays(m_selectionManipulator.singleSelectedPoints(), painter); + if (m_selectionManipulator.isMultiSelecting()) + drawMultiSelectionRectangle(m_selectionManipulator.multiSelectionRectangle(), painter); + } + + painter->restore(); +} + +FormEditorItem *PathItem::formEditorItem() const +{ + return m_formEditorItem; +} + +static CubicSegment createCubicSegmentForLine(const ModelNode &lineNode, const ControlPoint &startControlPoint) +{ + CubicSegment cubicSegment = CubicSegment::create(); + cubicSegment.setModelNode(lineNode); + + if (lineNode.hasProperty("x") + && lineNode.hasProperty("y")) { + + QPointF controlPoint0Line = startControlPoint.coordinate(); + QPointF controlPoint1Line(lineNode.variantProperty("x").value().toDouble(), + lineNode.variantProperty("y").value().toDouble()); + + QPointF controlPoint1Cubic = controlPoint0Line + (1./3.) * (controlPoint1Line - controlPoint0Line); + QPointF controlPoint2Cubic = controlPoint0Line + (2./3.) * (controlPoint1Line - controlPoint0Line); + + cubicSegment.setFirstControlPoint(startControlPoint); + cubicSegment.setSecondControlPoint(controlPoint1Cubic); + cubicSegment.setThirdControlPoint(controlPoint2Cubic); + cubicSegment.setFourthControlPoint(controlPoint1Line); + } else { + qWarning() << "PathLine has not all entries!"; + } + + return cubicSegment; +} + +static CubicSegment createCubicSegmentForQuad(const ModelNode &quadNode, const ControlPoint &startControlPoint) +{ + CubicSegment cubicSegment = CubicSegment::create(); + cubicSegment.setModelNode(quadNode); + + if (quadNode.hasProperty("controlX") + && quadNode.hasProperty("controlY") + && quadNode.hasProperty("x") + && quadNode.hasProperty("y")) { + QPointF controlPoint0Quad = startControlPoint.coordinate(); + QPointF controlPoint1Quad(quadNode.variantProperty("controlX").value().toDouble(), + quadNode.variantProperty("controlY").value().toDouble()); + QPointF controlPoint2Quad(quadNode.variantProperty("x").value().toDouble(), + quadNode.variantProperty("y").value().toDouble()); + + QPointF controlPoint1Cubic = controlPoint0Quad + (2./3.) * (controlPoint1Quad - controlPoint0Quad); + QPointF controlPoint2Cubic = controlPoint2Quad + (2./3.) * (controlPoint1Quad - controlPoint2Quad); + + cubicSegment.setFirstControlPoint(startControlPoint); + cubicSegment.setSecondControlPoint(controlPoint1Cubic); + cubicSegment.setThirdControlPoint(controlPoint2Cubic); + cubicSegment.setFourthControlPoint(controlPoint2Quad); + } else { + qWarning() << "PathQuad has not all entries!"; + } + + return cubicSegment; +} + +static CubicSegment createCubicSegmentForCubic(const ModelNode &cubicNode, const ControlPoint &startControlPoint) +{ + CubicSegment cubicSegment = CubicSegment::create(); + cubicSegment.setModelNode(cubicNode); + + if (cubicNode.hasProperty("control1X") + && cubicNode.hasProperty("control1Y") + && cubicNode.hasProperty("control2X") + && cubicNode.hasProperty("control2Y") + && cubicNode.hasProperty("x") + && cubicNode.hasProperty("y")) { + + cubicSegment.setFirstControlPoint(startControlPoint); + cubicSegment.setSecondControlPoint(cubicNode.variantProperty("control1X").value().toDouble(), + cubicNode.variantProperty("control1Y").value().toDouble()); + cubicSegment.setThirdControlPoint(cubicNode.variantProperty("control2X").value().toDouble(), + cubicNode.variantProperty("control2Y").value().toDouble()); + cubicSegment.setFourthControlPoint(cubicNode.variantProperty("x").value().toDouble(), + cubicNode.variantProperty("y").value().toDouble()); + } else { + qWarning() << "PathCubic has not all entries!"; + } + + return cubicSegment; +} + +static QRectF boundingRectForPath(const QList<ControlPoint> &controlPoints) +{ + double xMinimum = 0.; + double xMaximum = 0.; + double yMinimum = 0.; + double yMaximum = 0.; + + foreach (const ControlPoint & controlPoint, controlPoints) { + xMinimum = qMin(xMinimum, controlPoint.coordinate().x()); + xMaximum = qMax(xMaximum, controlPoint.coordinate().x()); + yMinimum = qMin(yMinimum, controlPoint.coordinate().y()); + yMaximum = qMax(yMaximum, controlPoint.coordinate().y()); + } + + return QRect(xMinimum, yMinimum, xMaximum - xMinimum, yMaximum - yMinimum); +} + +void PathItem::updateBoundingRect() +{ + QRectF controlBoundingRect = boundingRectForPath(controlPoints()).adjusted(-100, -100, 200, 100); + + if (m_selectionManipulator.isMultiSelecting()) + controlBoundingRect = controlBoundingRect.united(m_selectionManipulator.multiSelectionRectangle()); + + setBoundingRect(instanceBoundingRect().united(controlBoundingRect)); +} + +QRectF PathItem::instanceBoundingRect() const +{ + if (formEditorItem()) + return formEditorItem()->qmlItemNode().instanceBoundingRect(); + + return {}; +} + +void PathItem::readControlPoints() +{ + ModelNode pathNode = pathModelNode(formEditorItem()); + + m_cubicSegments.clear(); + + if (pathNode.hasNodeListProperty("pathElements")) { + ControlPoint firstControlPoint(startPoint(pathNode)); + firstControlPoint.setPathModelNode(pathNode); + firstControlPoint.setPointType(StartPoint); + + QMap<QString, QVariant> actualAttributes; + double percent = -1.0; + + foreach (const ModelNode &childNode, pathNode.nodeListProperty("pathElements").toModelNodeList()) { + + if (childNode.type() == "QtQuick.PathAttribute") { + actualAttributes.insert(childNode.variantProperty("name").value().toString(), childNode.variantProperty("value").value()); + } else if (childNode.type() == "QtQuick.PathPercent") { + percent = childNode.variantProperty("value").value().toDouble(); + } else { + CubicSegment newCubicSegement; + + if (childNode.type() == "QtQuick.PathLine") + newCubicSegement = createCubicSegmentForLine(childNode, firstControlPoint); + else if (childNode.type() == "QtQuick.PathQuad") + newCubicSegement = createCubicSegmentForQuad(childNode, firstControlPoint); + else if (childNode.type() == "QtQuick.PathCubic") + newCubicSegement = createCubicSegmentForCubic(childNode, firstControlPoint); + else + continue; + + newCubicSegement.setPercent(percent); + newCubicSegement.setAttributes(actualAttributes); + + firstControlPoint = newCubicSegement.fourthControlPoint(); + qDebug() << "Can be converted to quad" << newCubicSegement.canBeConvertedToQuad(); + qDebug() << "Can be converted to line" << newCubicSegement.canBeConvertedToLine(); + m_cubicSegments.append(newCubicSegement); + actualAttributes.clear(); + percent = -1.0; + } + } + + m_lastAttributes = actualAttributes; + m_lastPercent = percent; + + if (m_cubicSegments.constFirst().firstControlPoint().coordinate() == m_cubicSegments.constLast().fourthControlPoint().coordinate()) { + CubicSegment lastCubicSegment = m_cubicSegments.constLast(); + lastCubicSegment.setFourthControlPoint(m_cubicSegments.constFirst().firstControlPoint()); + lastCubicSegment.fourthControlPoint().setPathModelNode(pathNode); + lastCubicSegment.fourthControlPoint().setPointType(StartAndEndPoint); + } + } +} + +static CubicSegment getMinimumDistanceSegment(const QPointF &pickPoint, const QList<CubicSegment> &cubicSegments, double maximumDistance, double *t = nullptr) +{ + CubicSegment minimumDistanceSegment; + double actualMinimumDistance = maximumDistance; + + foreach (const CubicSegment &cubicSegment, cubicSegments) { + double tSegment = 0.; + double cubicSegmentMinimumDistance = cubicSegment.minimumDistance(pickPoint, tSegment); + if (cubicSegmentMinimumDistance < actualMinimumDistance) { + minimumDistanceSegment = cubicSegment; + actualMinimumDistance = cubicSegmentMinimumDistance; + if (t) + *t = tSegment; + } + } + + return minimumDistanceSegment; +} + +void PathItem::splitCubicSegment(CubicSegment &cubicSegment, double t) +{ + QPair<CubicSegment, CubicSegment> newCubicSegmentPair = cubicSegment.split(t); + int indexOfOldCubicSegment = m_cubicSegments.indexOf(cubicSegment); + + m_cubicSegments.removeAt(indexOfOldCubicSegment); + m_cubicSegments.insert(indexOfOldCubicSegment, newCubicSegmentPair.first); + m_cubicSegments.insert(indexOfOldCubicSegment + 1, newCubicSegmentPair.second); +} + +void PathItem::closePath() +{ + if (!m_cubicSegments.isEmpty()) { + const CubicSegment &firstCubicSegment = m_cubicSegments.constFirst(); + CubicSegment lastCubicSegment = m_cubicSegments.constLast(); + lastCubicSegment.setFourthControlPoint(firstCubicSegment.firstControlPoint()); + writePathAsCubicSegmentsOnly(); + } +} + +void PathItem::openPath() +{ + if (!m_cubicSegments.isEmpty()) { + const CubicSegment &firstCubicSegment = m_cubicSegments.constFirst(); + CubicSegment lastCubicSegment = m_cubicSegments.constLast(); + QPointF newEndPoint = firstCubicSegment.firstControlPoint().coordinate(); + newEndPoint.setX(newEndPoint.x() + 10.); + lastCubicSegment.setFourthControlPoint(ControlPoint(newEndPoint)); + writePathAsCubicSegmentsOnly(); + } +} + +QAction *PathItem::createClosedPathAction(QMenu *contextMenu) const +{ + auto closedPathAction = new QAction(contextMenu); + closedPathAction->setCheckable(true); + closedPathAction->setChecked(isClosedPath()); + closedPathAction->setText(tr("Closed Path")); + contextMenu->addAction(closedPathAction); + + if (m_cubicSegments.count() == 1) + closedPathAction->setDisabled(true); + + return closedPathAction; +} + +void PathItem::createGlobalContextMenu(const QPoint &menuPosition) +{ + QMenu contextMenu; + + QAction *closedPathAction = createClosedPathAction(&contextMenu); + + QAction *activatedAction = contextMenu.exec(menuPosition); + + if (activatedAction == closedPathAction) + makePathClosed(closedPathAction->isChecked()); +} + +void PathItem::createCubicSegmentContextMenu(CubicSegment &cubicSegment, const QPoint &menuPosition, double t) +{ + QMenu contextMenu; + + auto splitSegmentAction = new QAction(&contextMenu); + splitSegmentAction->setText(tr("Split Segment")); + contextMenu.addAction(splitSegmentAction); + + auto straightLinePointAction = new QAction(&contextMenu); + straightLinePointAction->setText(tr("Make Curve Segment Straight")); + contextMenu.addAction(straightLinePointAction); + + if (m_cubicSegments.count() == 1 && isClosedPath()) + straightLinePointAction->setDisabled(true); + + QAction *closedPathAction = createClosedPathAction(&contextMenu); + + QAction *activatedAction = contextMenu.exec(menuPosition); + + if (activatedAction == straightLinePointAction) { + cubicSegment.makeStraightLine(); + PathUpdateDisabler pathItemDisabler(this, PathUpdateDisabler::DontUpdatePath); + RewriterTransaction rewriterTransaction = + cubicSegment.modelNode().view()->beginRewriterTransaction(QByteArrayLiteral("PathItem::createCubicSegmentContextMenu")); + cubicSegment.updateModelNode(); + rewriterTransaction.commit(); + } else if (activatedAction == splitSegmentAction) { + splitCubicSegment(cubicSegment, t); + writePathAsCubicSegmentsOnly(); + } else if (activatedAction == closedPathAction) { + makePathClosed(closedPathAction->isChecked()); + } +} + + +void PathItem::createEditPointContextMenu(const ControlPoint &controlPoint, const QPoint &menuPosition) +{ + QMenu contextMenu; + auto removeEditPointAction = new QAction(&contextMenu); + removeEditPointAction->setText(tr("Remove Edit Point")); + contextMenu.addAction(removeEditPointAction); + + QAction *closedPathAction = createClosedPathAction(&contextMenu); + + if (m_cubicSegments.count() <= 1 || (m_cubicSegments.count() == 2 && isClosedPath())) + removeEditPointAction->setDisabled(true); + + QAction *activatedAction = contextMenu.exec(menuPosition); + + if (activatedAction == removeEditPointAction) + removeEditPoint(controlPoint); + else if (activatedAction == closedPathAction) + makePathClosed(closedPathAction->isChecked()); +} + +const QList<ControlPoint> PathItem::controlPoints() const +{ + QList<ControlPoint> controlPointList; + controlPointList.reserve((m_cubicSegments.count() * 4)); + + if (!m_cubicSegments.isEmpty()) + controlPointList.append(m_cubicSegments.constFirst().firstControlPoint()); + + foreach (const CubicSegment &cubicSegment, m_cubicSegments) { + controlPointList.append(cubicSegment.secondControlPoint()); + controlPointList.append(cubicSegment.thirdControlPoint()); + controlPointList.append(cubicSegment.fourthControlPoint()); + } + + if (isClosedPath()) + controlPointList.pop_back(); + + return controlPointList; +} + +bool hasLineOrQuadPathElements(const QList<ModelNode> &modelNodes) +{ + foreach (const ModelNode &modelNode, modelNodes) { + if (modelNode.type() == "QtQuick.PathLine" + || modelNode.type() == "QtQuick.PathQuad") + return true; + } + + return false; +} + +void PathItem::updatePath() +{ + if (m_dontUpdatePath) + return; + + if (hasPath(formEditorItem())) { + readControlPoints(); + + ModelNode pathNode = pathModelNode(formEditorItem()); + + if (hasLineOrQuadPathElements(pathNode.nodeListProperty("pathElements").toModelNodeList())) + writePathAsCubicSegmentsOnly(); + } + + updateBoundingRect(); + update(); +} + +QRectF PathItem::boundingRect() const +{ + return m_boundingRect; +} + +void PathItem::setBoundingRect(const QRectF &boundingRect) +{ + m_boundingRect = boundingRect; +} + +static bool controlPointIsNearMousePosition(const ControlPoint &controlPoint, const QPointF &mousePosition) +{ + QPointF distanceVector = controlPoint.coordinate() - mousePosition; + + if (distanceVector.manhattanLength() < 10) + return true; + + return false; +} + +static bool controlPointsAreNearMousePosition(const QList<ControlPoint> &controlPoints, const QPointF &mousePosition) +{ + foreach (const ControlPoint &controlPoint, controlPoints) { + if (controlPointIsNearMousePosition(controlPoint, mousePosition)) + return true; + } + + return false; +} + +static ControlPoint pickControlPoint(const QList<ControlPoint> &controlPoints, const QPointF &mousePosition) +{ + foreach (const ControlPoint &controlPoint, controlPoints) { + if (controlPointIsNearMousePosition(controlPoint, mousePosition)) + return controlPoint; + } + + return ControlPoint(); +} + +void PathItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + if (m_selectionManipulator.hasMultiSelection()) { + m_selectionManipulator.setStartPoint(event->pos()); + } else { + ControlPoint pickedControlPoint = pickControlPoint(controlPoints(), event->pos()); + + if (pickedControlPoint.isValid()) { + m_selectionManipulator.addSingleControlPointSmartly(pickedControlPoint); + m_selectionManipulator.startMoving(event->pos()); + } else { + m_selectionManipulator.startMultiSelection(event->pos()); + } + } + } +} + +bool hasMoveStartDistance(const QPointF &startPoint, const QPointF &updatePoint) +{ + return (startPoint - updatePoint).manhattanLength() > 10; +} + +void PathItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + if (controlPointsAreNearMousePosition(controlPoints(), event->pos())) + setCursor(Qt::SizeAllCursor); + else + setCursor(Qt::ArrowCursor); + + PathUpdateDisabler pathUpdateDisabler(this, PathUpdateDisabler::DontUpdatePath); + if (event->buttons().testFlag(Qt::LeftButton)) { + if (m_selectionManipulator.isMultiSelecting()) { + m_selectionManipulator.updateMultiSelection(event->pos()); + update(); + } else if (m_selectionManipulator.hasSingleSelection()) { + setCursor(Qt::SizeAllCursor); + m_selectionManipulator.updateMoving(event->pos(), event->modifiers()); + updatePathModelNodes(m_selectionManipulator.allSelectionSinglePoints()); + updateBoundingRect(); + update(); + } else if (m_selectionManipulator.hasMultiSelection()) { + setCursor(Qt::SizeAllCursor); + if (m_selectionManipulator.isMoving()) { + m_selectionManipulator.updateMoving(event->pos(), event->modifiers()); + updatePathModelNodes(m_selectionManipulator.allSelectionSinglePoints()); + updateBoundingRect(); + update(); + } else if (hasMoveStartDistance(m_selectionManipulator.startPoint(), event->pos())) { + m_selectionManipulator.startMoving(m_selectionManipulator.startPoint()); + m_selectionManipulator.updateMoving(event->pos(), event->modifiers()); + updatePathModelNodes(m_selectionManipulator.allSelectionSinglePoints()); + updateBoundingRect(); + update(); + } + } + } +} + +void PathItem::updatePathModelNodes(const QList<SelectionPoint> &changedPoints) +{ + PathUpdateDisabler pathUpdateDisabler(this, PathUpdateDisabler::DontUpdatePath); + + try { + RewriterTransaction rewriterTransaction = + formEditorItem()->qmlItemNode().view()->beginRewriterTransaction(QByteArrayLiteral("PathItem::createCubicSegmentContextMenu")); + + foreach (SelectionPoint changedPoint, changedPoints) + changedPoint.controlPoint.updateModelNode(); + + rewriterTransaction.commit(); + } catch (const Exception &e) { + e.showException(); + } +} + +void PathItem::disablePathUpdates() +{ + m_dontUpdatePath = true; +} + +void PathItem::enablePathUpdates() +{ + m_dontUpdatePath = false; +} + +bool PathItem::pathUpdatesDisabled() const +{ + return m_dontUpdatePath; +} + +void PathItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->button() == Qt::LeftButton) { + if (m_selectionManipulator.isMultiSelecting()) { + m_selectionManipulator.updateMultiSelection(event->pos()); + m_selectionManipulator.endMultiSelection(); + } else if (m_selectionManipulator.hasSingleSelection()) { + m_selectionManipulator.updateMoving(event->pos(), event->modifiers()); + updatePathModelNodes(m_selectionManipulator.allSelectionSinglePoints()); + updateBoundingRect(); + m_selectionManipulator.clearSingleSelection(); + } else if (m_selectionManipulator.hasMultiSelection()) { + if (m_selectionManipulator.isMoving()) { + m_selectionManipulator.updateMoving(event->pos(), event->modifiers()); + m_selectionManipulator.endMoving(); + updatePathModelNodes(m_selectionManipulator.multiSelectedPoints()); + updateBoundingRect(); + } else { + m_selectionManipulator.clearMultiSelection(); + } + } + } else if (event->button() == Qt::RightButton) { + ControlPoint pickedControlPoint = pickControlPoint(controlPoints(), event->pos()); + if (pickedControlPoint.isEditPoint()) { + createEditPointContextMenu(pickedControlPoint, event->screenPos()); + } else { + double t = 0.0; + CubicSegment minimumDistanceSegment = getMinimumDistanceSegment(event->pos(), m_cubicSegments, 20., &t); + if (minimumDistanceSegment.isValid()) + createCubicSegmentContextMenu(minimumDistanceSegment, event->screenPos(), t); + else + createGlobalContextMenu(event->screenPos()); + } + } + + update(); + +} + +bool PathItem::isClosedPath() const +{ + if (m_cubicSegments.isEmpty()) + return false; + + ControlPoint firstControlPoint = m_cubicSegments.constFirst().firstControlPoint(); + ControlPoint lastControlPoint = m_cubicSegments.constLast().fourthControlPoint(); + + return firstControlPoint == lastControlPoint; +} + +void PathItem::makePathClosed(bool pathShoudlBeClosed) +{ + if (pathShoudlBeClosed && !isClosedPath()) + closePath(); + else if (!pathShoudlBeClosed && isClosedPath()) + openPath(); +} + +QList<CubicSegment> cubicSegmentsContainingControlPoint(const ControlPoint &controlPoint, const QList<CubicSegment> &allCubicSegments) +{ + QList<CubicSegment> cubicSegmentsHasControlPoint; + + foreach (const CubicSegment &cubicSegment, allCubicSegments) { + if (cubicSegment.controlPoints().contains(controlPoint)) + cubicSegmentsHasControlPoint.append(cubicSegment); + } + + return cubicSegmentsHasControlPoint; +} + +void PathItem::removeEditPoint(const ControlPoint &controlPoint) +{ + QList<CubicSegment> cubicSegments = cubicSegmentsContainingControlPoint(controlPoint, m_cubicSegments); + + if (cubicSegments.count() == 1) { + m_cubicSegments.removeOne(cubicSegments.constFirst()); + } else if (cubicSegments.count() == 2){ + CubicSegment mergedCubicSegment = CubicSegment::create(); + const CubicSegment &firstCubicSegment = cubicSegments.at(0); + const CubicSegment &secondCubicSegment = cubicSegments.at(1); + mergedCubicSegment.setFirstControlPoint(firstCubicSegment.firstControlPoint()); + mergedCubicSegment.setSecondControlPoint(firstCubicSegment.secondControlPoint()); + mergedCubicSegment.setThirdControlPoint(secondCubicSegment.thirdControlPoint()); + mergedCubicSegment.setFourthControlPoint(secondCubicSegment.fourthControlPoint()); + + int indexOfFirstCubicSegment = m_cubicSegments.indexOf(firstCubicSegment); + m_cubicSegments.removeAt(indexOfFirstCubicSegment); + m_cubicSegments.removeAt(indexOfFirstCubicSegment); + m_cubicSegments.insert(indexOfFirstCubicSegment, mergedCubicSegment); + } + + writePathAsCubicSegmentsOnly(); +} + +PathUpdateDisabler::PathUpdateDisabler(PathItem *pathItem, PathUpdate updatePath) + : m_pathItem(pathItem), + m_updatePath(updatePath) +{ + pathItem->disablePathUpdates(); +} + +PathUpdateDisabler::~PathUpdateDisabler() +{ + m_pathItem->enablePathUpdates(); + if (m_updatePath == UpdatePath) + m_pathItem->updatePath(); +} + +} diff --git a/src/plugins/qmldesigner/components/pathtool/pathitem.h b/src/plugins/qmldesigner/components/pathtool/pathitem.h new file mode 100644 index 0000000000..17981283a6 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathitem.h @@ -0,0 +1,140 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QGraphicsObject> +#include <QWeakPointer> +#include <QMap> +#include <QVariant> +#include <qmldesignercorelib_global.h> + +#include "cubicsegment.h" +#include "pathselectionmanipulator.h" + +QT_BEGIN_NAMESPACE +class QTextEdit; +class QAction; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class FormEditorScene; +class FormEditorItem; +class PathItem; + +class PathUpdateDisabler +{ +public: + enum PathUpdate + { + UpdatePath, + DontUpdatePath + }; + + PathUpdateDisabler(PathItem *pathItem, PathUpdate updatePath = UpdatePath); + ~PathUpdateDisabler(); + +private: + PathItem *m_pathItem; + PathUpdate m_updatePath; +}; + +class PathItem : public QGraphicsObject +{ + Q_OBJECT + friend class PathUpdateDisabler; +public: + enum + { + Type = 0xEAAC + }; + PathItem(FormEditorScene* scene); + ~PathItem() override; + int type() const override; + + void setFormEditorItem(FormEditorItem *formEditorItem); + FormEditorItem *formEditorItem() const; + + QList<QGraphicsItem*> findAllChildItems() const; + + void updatePath(); + void writePathToProperty(); + void writePathAsCubicSegmentsOnly(); + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + QRectF boundingRect() const override; + void setBoundingRect(const QRectF &boundingRect); + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + + bool isClosedPath() const; + const QList<ControlPoint> controlPoints() const; + +protected: + void updateBoundingRect(); + QRectF instanceBoundingRect() const; + void writeLinePath(const ModelNode &pathNode, const CubicSegment &cubicSegment); + void writeQuadPath(const ModelNode &pathNode, const CubicSegment &cubicSegment); + void writeCubicPath(const ModelNode &pathNode, const CubicSegment &cubicSegment); + void writePathAttributes(const ModelNode &pathNode, const QMap<QString, QVariant> &attributes); + void writePathPercent(const ModelNode &pathNode, double percent); + void readControlPoints(); + void splitCubicSegment(CubicSegment &cubicSegment, double t); + void closePath(); + void openPath(); + void createGlobalContextMenu(const QPoint &menuPosition); + void createCubicSegmentContextMenu(CubicSegment &cubicSegment, const QPoint &menuPosition, double t); + void createEditPointContextMenu(const ControlPoint &controlPoint, const QPoint &menuPosition); + void makePathClosed(bool pathShoudlBeClosed); + void removeEditPoint(const ControlPoint &controlPoint); + void updatePathModelNodes(const QList<SelectionPoint> &changedPoints); + void disablePathUpdates(); + void enablePathUpdates(); + bool pathUpdatesDisabled() const; + QAction *createClosedPathAction(QMenu *contextMenu) const; + +signals: + void textChanged(const QString &text); + +private: + PathSelectionManipulator m_selectionManipulator; + QList<CubicSegment> m_cubicSegments; + QPointF m_startPoint; + QRectF m_boundingRect; + QMap<QString, QVariant> m_lastAttributes; + double m_lastPercent; + FormEditorItem *m_formEditorItem; + bool m_dontUpdatePath; +}; + +inline int PathItem::type() const +{ + return Type; +} +} diff --git a/src/plugins/qmldesigner/components/pathtool/pathselectionmanipulator.cpp b/src/plugins/qmldesigner/components/pathtool/pathselectionmanipulator.cpp new file mode 100644 index 0000000000..6df6976233 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathselectionmanipulator.cpp @@ -0,0 +1,299 @@ +/**************************************************************************** +** +** 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 "pathselectionmanipulator.h" + +#include "pathitem.h" + +#include <QtDebug> + +namespace QmlDesigner { + +PathSelectionManipulator::PathSelectionManipulator(PathItem *pathItem) + : m_pathItem(pathItem), + m_isMultiSelecting(false), + m_isMoving(false) +{ +} + +void PathSelectionManipulator::clear() +{ + clearSingleSelection(); + clearMultiSelection(); + m_isMultiSelecting = false; + m_isMoving = false; +} + +void PathSelectionManipulator::clearSingleSelection() +{ + m_singleSelectedPoints.clear(); + m_automaticallyAddedSinglePoints.clear(); +} + +void PathSelectionManipulator::clearMultiSelection() +{ + m_multiSelectedPoints.clear(); +} + +static SelectionPoint createSelectionPoint(const ControlPoint &controlPoint) +{ + SelectionPoint selectionPoint; + selectionPoint.controlPoint = controlPoint; + selectionPoint.startPosition = controlPoint.coordinate(); + + return selectionPoint; +} + +void PathSelectionManipulator::addMultiSelectionControlPoint(const ControlPoint &controlPoint) +{ + m_multiSelectedPoints.append(createSelectionPoint(controlPoint)); +} + +static ControlPoint getControlPoint(const QList<ControlPoint> &selectedPoints, const ControlPoint &controlPoint, int indexOffset, bool isClosedPath) +{ + int controlPointIndex = selectedPoints.indexOf(controlPoint); + if (controlPointIndex >= 0) { + int offsetIndex = controlPointIndex + indexOffset; + if (offsetIndex >= 0 && offsetIndex < selectedPoints.count()) + return selectedPoints.at(offsetIndex); + else if (isClosedPath) { + if (offsetIndex == -1) + return selectedPoints.constLast(); + else if (offsetIndex < selectedPoints.count()) + return selectedPoints.at(1); + } + } + + return ControlPoint(); +} + +QList<SelectionPoint> PathSelectionManipulator::singleSelectedPoints() +{ + return m_singleSelectedPoints; +} + +QList<SelectionPoint> PathSelectionManipulator::automaticallyAddedSinglePoints() +{ + return m_automaticallyAddedSinglePoints; +} + +QList<SelectionPoint> PathSelectionManipulator::allSelectionSinglePoints() +{ + + return m_singleSelectedPoints + m_automaticallyAddedSinglePoints; +} + +QList<SelectionPoint> PathSelectionManipulator::multiSelectedPoints() +{ + return m_multiSelectedPoints; +} + +QList<SelectionPoint> PathSelectionManipulator::allSelectionPoints() +{ + return m_singleSelectedPoints + m_multiSelectedPoints + m_automaticallyAddedSinglePoints; +} + +QList<ControlPoint> PathSelectionManipulator::allControlPoints() +{ + QList<ControlPoint> controlPoints; + + foreach (const SelectionPoint &selectionPoint, m_singleSelectedPoints) + controlPoints.append(selectionPoint.controlPoint); + + foreach (const SelectionPoint &selectionPoint, m_automaticallyAddedSinglePoints) + controlPoints.append(selectionPoint.controlPoint); + + foreach (const SelectionPoint &selectionPoint, m_multiSelectedPoints) + controlPoints.append(selectionPoint.controlPoint); + + return controlPoints; +} + +bool PathSelectionManipulator::hasSingleSelection() const +{ + return !m_singleSelectedPoints.isEmpty(); +} + +bool PathSelectionManipulator::hasMultiSelection() const +{ + return !m_multiSelectedPoints.isEmpty(); +} + +void PathSelectionManipulator::startMultiSelection(const QPointF &startPoint) +{ + m_startPoint = startPoint; + m_isMultiSelecting = true; +} + +void PathSelectionManipulator::updateMultiSelection(const QPointF &updatePoint) +{ + clearMultiSelection(); + + m_updatePoint = updatePoint; + + QRectF selectionRect(m_startPoint, updatePoint); + + foreach (const ControlPoint &controlPoint, m_pathItem->controlPoints()) { + if (selectionRect.contains(controlPoint.coordinate())) + addMultiSelectionControlPoint(controlPoint); + } +} + +void PathSelectionManipulator::endMultiSelection() +{ + m_isMultiSelecting = false; +} + +SelectionPoint::SelectionPoint() = default; + +SelectionPoint::SelectionPoint(const ControlPoint &controlPoint) + : controlPoint(controlPoint) +{ +} + +bool operator ==(const SelectionPoint &firstSelectionPoint, const SelectionPoint &secondSelectionPoint) +{ + return firstSelectionPoint.controlPoint == secondSelectionPoint.controlPoint; +} + +QPointF PathSelectionManipulator::multiSelectionStartPoint() const +{ + return m_startPoint; +} + +bool PathSelectionManipulator::isMultiSelecting() const +{ + return m_isMultiSelecting; +} + +QRectF PathSelectionManipulator::multiSelectionRectangle() const +{ + return QRectF(m_startPoint, m_updatePoint); +} + +void PathSelectionManipulator::setStartPoint(const QPointF &startPoint) +{ + m_startPoint = startPoint; +} + +QPointF PathSelectionManipulator::startPoint() const +{ + return m_startPoint; +} + +double snapFactor(Qt::KeyboardModifiers keyboardModifier) +{ + if (keyboardModifier.testFlag(Qt::ControlModifier)) + return 10.; + + return 1.; +} + +QPointF roundedVector(const QPointF &vector, double factor = 1.) +{ + QPointF roundedPosition; + + roundedPosition.setX(qRound(vector.x() / factor) * factor); + roundedPosition.setY(qRound(vector.y() / factor) * factor); + + return roundedPosition; +} + +QPointF manipulatedVector(const QPointF &vector, Qt::KeyboardModifiers keyboardModifier) +{ + QPointF manipulatedVector = roundedVector(vector, snapFactor(keyboardModifier)); + + if (keyboardModifier.testFlag(Qt::ShiftModifier)) + manipulatedVector.setX(0.); + + if (keyboardModifier.testFlag(Qt::AltModifier)) + manipulatedVector.setY(0.); + + return manipulatedVector; +} + +static void moveControlPoints(const QList<SelectionPoint> &movePoints, const QPointF &offsetVector) +{ + foreach (SelectionPoint movePoint, movePoints) + movePoint.controlPoint.setCoordinate(movePoint.startPosition + offsetVector); +} + +void PathSelectionManipulator::startMoving(const QPointF &startPoint) +{ + m_isMoving = true; + m_startPoint = startPoint; +} + +void PathSelectionManipulator::updateMoving(const QPointF &updatePoint, Qt::KeyboardModifiers keyboardModifier) +{ + m_updatePoint = updatePoint; + QPointF offsetVector = manipulatedVector(updatePoint - m_startPoint, keyboardModifier) ; + moveControlPoints(allSelectionPoints(), offsetVector); +} + +void PathSelectionManipulator::endMoving() +{ + updateMultiSelectedStartPoint(); + m_isMoving = false; +} + +bool PathSelectionManipulator::isMoving() const +{ + return m_isMoving; +} + +void PathSelectionManipulator::updateMultiSelectedStartPoint() +{ + QList<SelectionPoint> oldSelectionPoints = m_multiSelectedPoints; + + m_multiSelectedPoints.clear(); + + foreach (SelectionPoint selectionPoint, oldSelectionPoints) { + selectionPoint.startPosition = selectionPoint.controlPoint.coordinate(); + m_multiSelectedPoints.append(selectionPoint); + } +} + +void PathSelectionManipulator::addSingleControlPoint(const ControlPoint &controlPoint) +{ + m_singleSelectedPoints.append(createSelectionPoint(controlPoint)); +} + +void PathSelectionManipulator::addSingleControlPointSmartly(const ControlPoint &editPoint) +{ + m_singleSelectedPoints.append(createSelectionPoint(editPoint)); + + if (editPoint.isEditPoint()) { + ControlPoint previousControlPoint = getControlPoint(m_pathItem->controlPoints(), editPoint, -1, m_pathItem->isClosedPath()); + if (previousControlPoint.isValid()) + m_automaticallyAddedSinglePoints.append(createSelectionPoint(previousControlPoint)); + + ControlPoint nextControlPoint= getControlPoint(m_pathItem->controlPoints(), editPoint, 1, m_pathItem->isClosedPath()); + if (nextControlPoint.isValid()) + m_automaticallyAddedSinglePoints.append(createSelectionPoint(nextControlPoint)); + } +} + +} // namespace QMlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/pathselectionmanipulator.h b/src/plugins/qmldesigner/components/pathtool/pathselectionmanipulator.h new file mode 100644 index 0000000000..def0148367 --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathselectionmanipulator.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QList> +#include <QPair> +#include <QPoint> + +#include "controlpoint.h" + +namespace QmlDesigner { + +class PathItem; + +struct SelectionPoint +{ + SelectionPoint(); + SelectionPoint(const ControlPoint &controlPoint); + ControlPoint controlPoint; + QPointF startPosition; +}; + +class PathSelectionManipulator +{ +public: + PathSelectionManipulator(PathItem *pathItem); + + void clear(); + void clearSingleSelection(); + void clearMultiSelection(); + + void addMultiSelectionControlPoint(const ControlPoint &controlPoint); + void addSingleControlPoint(const ControlPoint &controlPoint); + void addSingleControlPointSmartly(const ControlPoint &editPoint); + + QList<SelectionPoint> singleSelectedPoints(); + QList<SelectionPoint> automaticallyAddedSinglePoints(); + QList<SelectionPoint> allSelectionSinglePoints(); + QList<SelectionPoint> multiSelectedPoints(); + QList<SelectionPoint> allSelectionPoints(); + + QList<ControlPoint> allControlPoints(); + + bool hasSingleSelection() const; + bool hasMultiSelection() const; + + void startMultiSelection(const QPointF &startPoint); + void updateMultiSelection(const QPointF &updatePoint); + void endMultiSelection(); + QPointF multiSelectionStartPoint() const; + bool isMultiSelecting() const; + + QRectF multiSelectionRectangle() const; + + void setStartPoint(const QPointF &startPoint); + QPointF startPoint() const; + void startMoving(const QPointF &startPoint); + void updateMoving(const QPointF &updatePoint, Qt::KeyboardModifiers keyboardModifier); + void endMoving(); + bool isMoving() const; + + void updateMultiSelectedStartPoint(); + +private: + QList<SelectionPoint> m_singleSelectedPoints; + QList<SelectionPoint> m_automaticallyAddedSinglePoints; + QList<SelectionPoint> m_multiSelectedPoints; + QPointF m_startPoint; + QPointF m_updatePoint; + PathItem *m_pathItem; + bool m_isMultiSelecting; + bool m_isMoving; +}; + +bool operator ==(const SelectionPoint& firstSelectionPoint, const SelectionPoint& secondSelectionPoint); + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/pathtool.cpp b/src/plugins/qmldesigner/components/pathtool/pathtool.cpp new file mode 100644 index 0000000000..a01ae050ac --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathtool.cpp @@ -0,0 +1,315 @@ +/**************************************************************************** +** +** 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 "pathtool.h" + +#include <formeditorscene.h> +#include <formeditorview.h> +#include <formeditorwidget.h> +#include <itemutilfunctions.h> +#include <formeditoritem.h> + +#include "pathitem.h" + +#include <nodemetainfo.h> +#include <qmlitemnode.h> +#include <nodeproperty.h> +#include <nodelistproperty.h> +#include <qmldesignerplugin.h> + +#include <abstractaction.h> +#include <designeractionmanager.h> + +#include <QApplication> +#include <QGraphicsSceneMouseEvent> +#include <QAction> +#include <QDebug> +#include <QPair> + +namespace QmlDesigner { + +static bool isNonSupportedPathElement(const ModelNode &pathElement) +{ + if (pathElement.type() == "QtQuick.PathCubic") + return false; + + if (pathElement.type() == "QtQuick.PathAttribute") + return false; + + if (pathElement.type() == "QtQuick.PathPercent") + return false; + + if (pathElement.type() == "QtQuick.PathAttribute") + return false; + + if (pathElement.type() == "QtQuick.PathQuad") + return false; + + if (pathElement.type() == "QtQuick.PathLine") + return false; + + return true; +} + + +static int pathRankForModelNode(const ModelNode &modelNode) { + if (modelNode.metaInfo().hasProperty("path")) { + if (modelNode.hasNodeProperty("path")) { + ModelNode pathNode = modelNode.nodeProperty("path").modelNode(); + if (pathNode.metaInfo().isSubclassOf("QtQuick.Path") && pathNode.hasNodeListProperty("pathElements")) { + QList<ModelNode> pathElements = pathNode.nodeListProperty("pathElements").toModelNodeList(); + if (pathElements.isEmpty()) + return 0; + + foreach (const ModelNode &pathElement, pathElements) { + if (isNonSupportedPathElement(pathElement)) + return 0; + } + } + } + + return 20; + } + + return 0; +} + +class PathToolAction : public AbstractAction +{ +public: + PathToolAction() : AbstractAction(QCoreApplication::translate("PathToolAction","Edit Path")) {} + + QByteArray category() const override + { + return QByteArray(); + } + + QByteArray menuId() const override + { + return "PathTool"; + } + + int priority() const override + { + return CustomActionsPriority; + } + + Type type() const override + { + return ContextMenuAction; + } + +protected: + bool isVisible(const SelectionContext &selectionContext) const override + { + if (selectionContext.scenePosition().isNull()) + return false; + + if (selectionContext.singleNodeIsSelected()) + return pathRankForModelNode(selectionContext.currentSingleSelectedNode()) > 0; + + return false; + } + + bool isEnabled(const SelectionContext &selectionContext) const override + { + return isVisible(selectionContext); + } +}; + +PathTool::PathTool() + : m_pathToolView(this) +{ + auto textToolAction = new PathToolAction; + QmlDesignerPlugin::instance()->designerActionManager().addDesignerAction(textToolAction); + connect(textToolAction->action(), &QAction::triggered, [=]() { + if (m_pathToolView.model()) + m_pathToolView.model()->detachView(&m_pathToolView); + view()->changeCurrentToolTo(this); + }); + + +} + +PathTool::~PathTool() = default; + +void PathTool::clear() +{ + if (m_pathItem) + m_pathItem->deleteLater(); + + AbstractFormEditorTool::clear(); +} + +void PathTool::mousePressEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * event) +{ + event->setPos(m_pathItem->mapFromScene(event->scenePos())); + event->setLastPos(m_pathItem->mapFromScene(event->lastScenePos())); + scene()->sendEvent(m_pathItem.data(), event); +} + +void PathTool::mouseMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent *event) +{ + event->setPos(m_pathItem->mapFromScene(event->scenePos())); + event->setLastPos(m_pathItem->mapFromScene(event->lastScenePos())); + scene()->sendEvent(m_pathItem.data(), event); +} + +void PathTool::hoverMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * event) +{ + event->setPos(m_pathItem->mapFromScene(event->scenePos())); + event->setLastPos(m_pathItem->mapFromScene(event->lastScenePos())); + scene()->sendEvent(m_pathItem.data(), event); +} + +void PathTool::keyPressEvent(QKeyEvent *keyEvent) +{ + if (keyEvent->key() == Qt::Key_Escape) { + m_pathItem->writePathToProperty(); + keyEvent->accept(); + } +} + +void PathTool::keyReleaseEvent(QKeyEvent * keyEvent) +{ + if (keyEvent->key() == Qt::Key_Escape) { + keyEvent->accept(); + if (m_pathToolView.model()) + m_pathToolView.model()->detachView(&m_pathToolView); + view()->changeToSelectionTool(); + } +} + +void PathTool::dragLeaveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ + +} + +void PathTool::dragMoveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ + +} + +void PathTool::mouseReleaseEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent *event) +{ + event->setPos(m_pathItem->mapFromScene(event->scenePos())); + event->setLastPos(m_pathItem->mapFromScene(event->lastScenePos())); + scene()->sendEvent(m_pathItem.data(), event); +} + + +void PathTool::mouseDoubleClickEvent(const QList<QGraphicsItem*> & /*itemList*/, QGraphicsSceneMouseEvent *event) +{ + if (m_pathItem.data() && !m_pathItem->boundingRect().contains(m_pathItem->mapFromScene(event->scenePos()))) { + m_pathItem->writePathToProperty(); + view()->changeToSelectionTool(); + event->accept(); + } +} + +void PathTool::itemsAboutToRemoved(const QList<FormEditorItem*> &removedItemList) +{ + if (m_pathItem == nullptr) + return; + + if (removedItemList.contains(m_pathItem->formEditorItem())) + view()->changeToSelectionTool(); +} + +static bool hasPathProperty(FormEditorItem *formEditorItem) +{ + return formEditorItem->qmlItemNode().modelNode().metaInfo().hasProperty("path"); +} + +void PathTool::selectedItemsChanged(const QList<FormEditorItem*> &itemList) +{ + if (m_pathItem.data() && itemList.contains(m_pathItem->formEditorItem())) + m_pathItem->writePathToProperty(); + + delete m_pathItem.data(); + if (!itemList.isEmpty() && hasPathProperty(itemList.constFirst())) { + FormEditorItem *formEditorItem = itemList.constFirst(); + m_pathItem = new PathItem(scene()); + m_pathItem->setParentItem(scene()->manipulatorLayerItem()); + m_pathItem->setFormEditorItem(formEditorItem); + formEditorItem->qmlItemNode().modelNode().model()->attachView(&m_pathToolView); + } else { + if (m_pathToolView.model()) + m_pathToolView.model()->detachView(&m_pathToolView); + view()->changeToSelectionTool(); + } +} + +void PathTool::instancesCompleted(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +void PathTool::instancesParentChanged(const QList<FormEditorItem *> & /*itemList*/) +{ +} + +void PathTool::instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > &propertyList) +{ + using ModelNodePropertyNamePair = QPair<ModelNode, PropertyName>; + foreach (const ModelNodePropertyNamePair &propertyPair, propertyList) { + if (propertyPair.first == m_pathItem->formEditorItem()->qmlItemNode().modelNode() + && propertyPair.second == "path") + m_pathItem->updatePath(); + } +} + +void PathTool::formEditorItemsChanged(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +int PathTool::wantHandleItem(const ModelNode &modelNode) const +{ + return pathRankForModelNode(modelNode); +} + +QString PathTool::name() const +{ + return QCoreApplication::translate("PathTool", "Path Tool"); +} + +ModelNode PathTool::editingPathViewModelNode() const +{ + if (m_pathItem) + return m_pathItem->formEditorItem()->qmlItemNode().modelNode(); + + return ModelNode(); +} + +void PathTool::pathChanged() +{ + if (m_pathItem) + m_pathItem->updatePath(); +} + +} diff --git a/src/plugins/qmldesigner/components/pathtool/pathtool.h b/src/plugins/qmldesigner/components/pathtool/pathtool.h new file mode 100644 index 0000000000..a2a65db27d --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathtool.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "abstractcustomtool.h" +#include "selectionindicator.h" + +#include <QHash> +#include <QPointer> +#include <QColorDialog> + +#include "pathtoolview.h" + +namespace QmlDesigner { + +class PathItem; + +class PathTool : public QObject, public AbstractCustomTool +{ + Q_OBJECT +public: + PathTool(); + ~PathTool() override; + + void mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void hoverMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; + + void dragLeaveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + void dragMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + + void itemsAboutToRemoved(const QList<FormEditorItem*> &itemList) override; + + void selectedItemsChanged(const QList<FormEditorItem*> &itemList) override; + + void instancesCompleted(const QList<FormEditorItem*> &itemList) override; + void instancesParentChanged(const QList<FormEditorItem *> &itemList) override; + void instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > &propertyList) override; + + void clear() override; + + void formEditorItemsChanged(const QList<FormEditorItem*> &itemList) override; + + int wantHandleItem(const ModelNode &modelNode) const override; + + QString name() const override; + + ModelNode editingPathViewModelNode() const; + + void pathChanged(); + +private: + PathToolView m_pathToolView; + QPointer<PathItem> m_pathItem; +}; + +} diff --git a/src/plugins/qmldesigner/components/pathtool/pathtool.pri b/src/plugins/qmldesigner/components/pathtool/pathtool.pri new file mode 100644 index 0000000000..eba35315fe --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathtool.pri @@ -0,0 +1,13 @@ +HEADERS += $$PWD/pathtool.h +HEADERS += $$PWD/pathselectionmanipulator.h +HEADERS += $$PWD/pathtoolview.h +HEADERS += $$PWD/controlpoint.h +HEADERS += $$PWD/cubicsegment.h +HEADERS += $$PWD/pathitem.h + +SOURCES += $$PWD/pathtool.cpp +SOURCES += $$PWD/pathselectionmanipulator.cpp +SOURCES += $$PWD/pathtoolview.cpp +SOURCES += $$PWD/controlpoint.cpp +SOURCES += $$PWD/cubicsegment.cpp +SOURCES += $$PWD/pathitem.cpp diff --git a/src/plugins/qmldesigner/components/pathtool/pathtoolview.cpp b/src/plugins/qmldesigner/components/pathtool/pathtoolview.cpp new file mode 100644 index 0000000000..ea1e28832a --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathtoolview.cpp @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** 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 "pathtoolview.h" + +#include <nodeproperty.h> +#include <variantproperty.h> +#include <modelnode.h> +#include <metainfo.h> + +#include "pathtool.h" + +#include <QtDebug> + +namespace QmlDesigner { + +PathToolView::PathToolView(PathTool *pathTool) + : m_pathTool(pathTool) +{ +} + +static bool isInEditedPath(const NodeAbstractProperty &propertyParent, const ModelNode &editingPathViewModelNode) +{ + if (editingPathViewModelNode.isValid()) { + if (editingPathViewModelNode.hasNodeProperty("path")) { + ModelNode pathModelNode = editingPathViewModelNode.nodeProperty("path").modelNode(); + if (pathModelNode.metaInfo().isSubclassOf("QtQuick.Path")) { + if (propertyParent.name() == "pathElements" && propertyParent.parentModelNode() == pathModelNode) + return true; + } + } + } + + return false; +} + +void PathToolView::nodeReparented(const ModelNode & /*node*/, + const NodeAbstractProperty & newPropertyParent, + const NodeAbstractProperty & /*oldPropertyParent*/, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (isInEditedPath(newPropertyParent, m_pathTool->editingPathViewModelNode())) + m_pathTool->pathChanged(); +} + +bool variantPropertyInEditedPath(const VariantProperty &variantProperty, const ModelNode &editingPathViewModelNode) +{ + ModelNode pathElementModelNode = variantProperty.parentModelNode(); + if (pathElementModelNode.hasParentProperty()) { + if (isInEditedPath(pathElementModelNode.parentProperty(), editingPathViewModelNode)) + return true; + } + + return false; +} + +bool changesEditedPath(const QList<VariantProperty> &propertyList, const ModelNode &editingPathViewModelNode) +{ + foreach (const VariantProperty variantProperty, propertyList) { + if (variantPropertyInEditedPath(variantProperty, editingPathViewModelNode)) + return true; + } + + return false; +} + +void PathToolView::variantPropertiesChanged(const QList<VariantProperty> &propertyList, AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (changesEditedPath(propertyList, m_pathTool->editingPathViewModelNode())) + m_pathTool->pathChanged(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/pathtool/pathtoolview.h b/src/plugins/qmldesigner/components/pathtool/pathtoolview.h new file mode 100644 index 0000000000..46b688923e --- /dev/null +++ b/src/plugins/qmldesigner/components/pathtool/pathtoolview.h @@ -0,0 +1,47 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <abstractview.h> + +namespace QmlDesigner { + +class PathTool; + +class PathToolView : public AbstractView +{ + Q_OBJECT +public: + PathToolView(PathTool *pathTool); + + void nodeReparented(const ModelNode &node, const NodeAbstractProperty &newPropertyParent, const NodeAbstractProperty &oldPropertyParent, AbstractView::PropertyChangeFlags propertyChange) override; + void variantPropertiesChanged(const QList<VariantProperty>& propertyList, PropertyChangeFlags propertyChange) override; + +private: + PathTool *m_pathTool; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.cpp index 5d7b1aeaaf..245d604707 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.cpp @@ -85,6 +85,11 @@ QUrl FileResourcesModel::path() const return m_path; } +QUrl FileResourcesModel::dirPath() const +{ + return QUrl::fromLocalFile(m_dirPath.path()); +} + void FileResourcesModel::setFilter(const QString &filter) { if (m_filter != filter) { @@ -162,16 +167,14 @@ void FileResourcesModel::setupModel() m_lock = true; m_model.clear(); - QDir dir; - - dir = QFileInfo(m_path.toLocalFile()).dir(); + m_dirPath = QFileInfo(m_path.toLocalFile()).dir(); QStringList filterList = m_filter.split(QLatin1Char(' ')); - QDirIterator it(dir.absolutePath(), filterList, QDir::Files, QDirIterator::Subdirectories); + QDirIterator it(m_dirPath.absolutePath(), filterList, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { QString absolutePath = it.next(); - m_model.append(dir.relativeFilePath(absolutePath)); + m_model.append(m_dirPath.relativeFilePath(absolutePath)); } m_lock = false; diff --git a/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.h b/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.h index e0d6fc9725..f686631079 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.h +++ b/src/plugins/qmldesigner/components/propertyeditor/fileresourcesmodel.h @@ -27,6 +27,7 @@ #include <qmlitemnode.h> +#include <QDir> #include <QObject> #include <QStringList> #include <QUrl> @@ -40,6 +41,7 @@ class FileResourcesModel : public QObject Q_PROPERTY(QString filter READ filter WRITE setFilter) Q_PROPERTY(QVariant modelNodeBackendProperty READ modelNodeBackend WRITE setModelNodeBackend NOTIFY modelNodeBackendChanged) Q_PROPERTY(QUrl path READ path WRITE setPath) + Q_PROPERTY(QUrl dirPath READ dirPath) Q_PROPERTY(QStringList fileModel READ fileModel NOTIFY fileModelChanged) public: @@ -51,6 +53,7 @@ public: void setFileNameStr(const QString &fileName); void setPath(const QUrl &url); QUrl path() const; + QUrl dirPath() const; void setFilter(const QString &filter); QString filter() const; QStringList fileModel() const; @@ -71,6 +74,7 @@ private: private: QUrl m_fileName; QUrl m_path; + QDir m_dirPath; QString m_filter; bool m_lock; QString m_currentPath; diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.cpp index 6bb3153bea..d4b1f47184 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.cpp @@ -27,6 +27,8 @@ #include "qmlanchorbindingproxy.h" #include "propertyeditorview.h" +#include "gradientpresetitem.h" +#include "gradientpresetcustomlistmodel.h" #include <exception.h> #include <nodeproperty.h> @@ -34,14 +36,14 @@ #include <variantproperty.h> #include <abstractview.h> #include <nodemetainfo.h> -#include <rewritertransaction.h> +#include <exception.h> #include <utils/qtcassert.h> #include <QTimer> GradientModel::GradientModel(QObject *parent) : - QAbstractListModel(parent), m_gradientTypeName("Gradient"), m_locked(false) + QAbstractListModel(parent) { } @@ -145,18 +147,16 @@ void GradientModel::addGradient() return; if (!m_itemNode.modelNode().hasNodeProperty(gradientPropertyName().toUtf8())) { - try { + if (m_gradientTypeName != "Gradient") + ensureShapesImport(); + + view()->executeInTransaction("GradientModel::addGradient", [this](){ QColor color = m_itemNode.instanceValue("color").value<QColor>(); if (!color.isValid()) color = QColor(Qt::white); - if (m_gradientTypeName != "Gradient") - ensureShapesImport(); - - QmlDesigner::RewriterTransaction transaction = view()->beginRewriterTransaction(QByteArrayLiteral("GradientModel::addGradient")); - QmlDesigner::ModelNode gradientNode = createGradientNode(); m_itemNode.modelNode().nodeProperty(gradientPropertyName().toUtf8()).reparentHere(gradientNode); @@ -170,16 +170,12 @@ void GradientModel::addGradient() gradientStopNode.variantProperty("position").setValue(1.0); gradientStopNode.variantProperty("color").setValue(QColor(Qt::black)); gradientNode.nodeListProperty("stops").reparentHere(gradientStopNode); - - } catch (const QmlDesigner::Exception &e) { - e.showException(); - } - + }); } setupModel(); if (m_gradientTypeName != "Gradient") - QTimer::singleShot(100, [this](){ view()->resetPuppet(); }); /*Unfortunately required */ + resetPuppet(); /*Unfortunately required */ emit hasGradientChanged(); emit gradientTypeChanged(); } @@ -242,18 +238,18 @@ qreal GradientModel::getPosition(int index) const void GradientModel::removeStop(int index) { if (index < rowCount() - 1 && index != 0) { - QmlDesigner::RewriterTransaction transaction = view()->beginRewriterTransaction(QByteArrayLiteral("GradientModel::removeStop")); - QmlDesigner::ModelNode gradientNode = m_itemNode.modelNode().nodeProperty(gradientPropertyName().toUtf8()).modelNode(); - QmlDesigner::QmlObjectNode stop = gradientNode.nodeListProperty("stops").at(index); - if (stop.isValid()) { - stop.destroy(); - setupModel(); - } + view()->executeInTransaction("GradientModel::removeStop", [this, index](){ + QmlDesigner::ModelNode gradientNode = m_itemNode.modelNode().nodeProperty(gradientPropertyName().toUtf8()).modelNode(); + QmlDesigner::QmlObjectNode stop = gradientNode.nodeListProperty("stops").at(index); + if (stop.isValid()) { + stop.destroy(); + setupModel(); + } + }); } qWarning() << Q_FUNC_INFO << "invalid index"; } - void GradientModel::deleteGradient() { if (!m_itemNode.isValid()) @@ -262,16 +258,7 @@ void GradientModel::deleteGradient() if (!m_itemNode.modelNode().metaInfo().hasProperty(gradientPropertyName().toUtf8())) return; - QmlDesigner::ModelNode modelNode = m_itemNode.modelNode(); - - if (m_itemNode.isInBaseState()) { - if (modelNode.hasProperty(gradientPropertyName().toUtf8())) { - QmlDesigner::RewriterTransaction transaction = view()->beginRewriterTransaction(QByteArrayLiteral("GradientModel::deleteGradient")); - QmlDesigner::ModelNode gradientNode = modelNode.nodeProperty(gradientPropertyName().toUtf8()).modelNode(); - if (QmlDesigner::QmlObjectNode(gradientNode).isValid()) - QmlDesigner::QmlObjectNode(gradientNode).destroy(); - } - } + deleteGradientNode(true); emit hasGradientChanged(); emit gradientTypeChanged(); @@ -392,7 +379,11 @@ void GradientModel::ensureShapesImport() { if (!hasShapesImport()) { QmlDesigner::Import timelineImport = QmlDesigner::Import::createLibraryImport("QtQuick.Shapes", "1.0"); - model()->changeImports({timelineImport}, {}); + try { + model()->changeImports({timelineImport}, {}); + } catch (const QmlDesigner::Exception &) { + QTC_ASSERT(false, return); + } } } @@ -444,6 +435,11 @@ QmlDesigner::AbstractView *GradientModel::view() const return m_itemNode.view(); } +void GradientModel::resetPuppet() +{ + QTimer::singleShot(1000, [this]() { view()->resetPuppet(); }); +} + QmlDesigner::ModelNode GradientModel::createGradientNode() { QByteArray fullTypeName = m_gradientTypeName.toUtf8(); @@ -477,6 +473,23 @@ QmlDesigner::ModelNode GradientModel::createGradientStopNode() return view()->createModelNode(fullTypeName, majorVersion, minorVersion); } +void GradientModel::deleteGradientNode(bool saveTransaction) +{ + QmlDesigner::ModelNode modelNode = m_itemNode.modelNode(); + + if (m_itemNode.isInBaseState()) { + if (modelNode.hasProperty(gradientPropertyName().toUtf8())) { + if (saveTransaction) + QmlDesigner::RewriterTransaction transaction = view()->beginRewriterTransaction( + QByteArrayLiteral("GradientModel::deleteGradient")); + QmlDesigner::ModelNode gradientNode + = modelNode.nodeProperty(gradientPropertyName().toUtf8()).modelNode(); + if (QmlDesigner::QmlObjectNode(gradientNode).isValid()) + QmlDesigner::QmlObjectNode(gradientNode).destroy(); + } + } +} + void GradientModel::setGradientProperty(const QString &propertyName, qreal value) { QTC_ASSERT(m_itemNode.isValid(), return); @@ -494,3 +507,102 @@ void GradientModel::setGradientProperty(const QString &propertyName, qreal value e.showException(); } } + +void GradientModel::setPresetByID(int presetID) +{ + const QGradient gradient(GradientPresetItem::createGradientFromPreset( + static_cast<GradientPresetItem::Preset>(presetID))); + const QList<QGradientStop> gradientStops = gradient.stops().toList(); + + QList<qreal> stopsPositions; + QList<QString> stopsColors; + for (const QGradientStop &stop : gradientStops) { + stopsPositions.append(stop.first); + stopsColors.append(stop.second.name()); + } + + setPresetByStops(stopsPositions, stopsColors, gradientStops.size()); +} + +void GradientModel::setPresetByStops(const QList<qreal> &stopsPositions, + const QList<QString> &stopsColors, + int stopsCount) +{ + if (m_locked) + return; + + if (!m_itemNode.isValid() || gradientPropertyName().isEmpty()) + return; + + QmlDesigner::RewriterTransaction transaction = view()->beginRewriterTransaction( + QByteArrayLiteral("GradientModel::setCustomPreset")); + + deleteGradientNode(false); + + if (!m_itemNode.modelNode().hasNodeProperty(gradientPropertyName().toUtf8())) { + try { + + if (m_gradientTypeName != "Gradient") + ensureShapesImport(); + + QmlDesigner::ModelNode gradientNode = createGradientNode(); + + m_itemNode.modelNode() + .nodeProperty(gradientPropertyName().toUtf8()) + .reparentHere(gradientNode); + + for (int i = 0; i < stopsCount; i++) { + QmlDesigner::ModelNode gradientStopNode = createGradientStopNode(); + gradientStopNode.variantProperty("position").setValue(stopsPositions.at(i)); + gradientStopNode.variantProperty("color").setValue(stopsColors.at(i)); + gradientNode.nodeListProperty("stops").reparentHere(gradientStopNode); + } + + } catch (const QmlDesigner::Exception &e) { + e.showException(); + } + } + setupModel(); + + if (m_gradientTypeName != "Gradient") + resetPuppet(); /*Unfortunately required */ + + emit hasGradientChanged(); + emit gradientTypeChanged(); +} + +void GradientModel::savePreset() +{ + //preparing qgradient: + QGradient currentGradient; + QGradientStops currentStops; + QGradientStop stop; //double, color + + for (int i = 0; i < rowCount(); i++) { + stop.first = getPosition(i); + stop.second = getColor(i); + currentStops.append(stop); + } + currentGradient.setStops(currentStops); + const GradientPresetItem item(currentGradient, "Custom Gradient"); + + //reading the custom gradient file + //filling the file with old data + new data + const QString filename(GradientPresetCustomListModel::getFilename()); + QList<GradientPresetItem> items = GradientPresetCustomListModel::storedPresets(filename); + items.append(item); + GradientPresetCustomListModel::storePresets(filename, items); +} + +void GradientModel::updateGradient() +{ + QList<qreal> stops; + QList<QString> colors; + int stopsCount = rowCount(); + for (int i = 0; i < stopsCount; i++) { + stops.append(getPosition(i)); + colors.append(getColor(i).name(QColor::HexArgb)); + } + + setPresetByStops(stops, colors, stopsCount); +} diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.h b/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.h index 48514ae688..c54526838e 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.h +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientmodel.h @@ -70,6 +70,15 @@ public: Q_INVOKABLE void setGradientProperty(const QString &propertyName, qreal value); + Q_INVOKABLE void setPresetByID(int presetID); + Q_INVOKABLE void setPresetByStops(const QList<qreal> &stopsPositions, + const QList<QString> &stopsColors, + int stopsCount); + + Q_INVOKABLE void savePreset(); + + Q_INVOKABLE void updateGradient(); + signals: void anchorBackendChanged(); void hasGradientChanged(); @@ -87,17 +96,19 @@ private: bool locked() const; QmlDesigner::ModelNode createGradientNode(); QmlDesigner::ModelNode createGradientStopNode(); + void deleteGradientNode(bool saveTransaction); private: QmlDesigner::QmlItemNode m_itemNode; QString m_gradientPropertyName; - QString m_gradientTypeName; - bool m_locked; + QString m_gradientTypeName = {"Gradient"}; + bool m_locked = false; bool hasShapesImport() const; void ensureShapesImport(); void setupGradientProperties(const QmlDesigner::ModelNode &gradient); QmlDesigner::Model *model() const; QmlDesigner::AbstractView *view() const; + void resetPuppet(); }; QML_DECLARE_TYPE(GradientModel) diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetcustomlistmodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetcustomlistmodel.cpp new file mode 100644 index 0000000000..a1599a7099 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetcustomlistmodel.cpp @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** 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 "gradientpresetcustomlistmodel.h" +#include "gradientpresetitem.h" + +#include <coreplugin/icore.h> +#include <utils/qtcassert.h> +#include <utils/algorithm.h> + +#include <QHash> +#include <QByteArray> +#include <QDebug> +#include <QSettings> +#include <QFile> + +namespace Internal { + +static const char settingsKey[] = "GradientPresetCustomList"; +static const char settingsFileName[] = "/GradientPresets.ini"; + +QString settingsFullFilePath(const QSettings::Scope &scope) +{ + if (scope == QSettings::SystemScope) + return Core::ICore::installerResourcePath() + settingsFileName; + + return Core::ICore::userResourcePath() + settingsFileName; +} + +} // namespace Internal + +GradientPresetCustomListModel::GradientPresetCustomListModel(QObject *parent) + : GradientPresetListModel(parent) + , m_filename(getFilename()) +{ + qRegisterMetaTypeStreamOperators<GradientPresetItem>("GradientPresetItem"); + readPresets(); +} + +GradientPresetCustomListModel::~GradientPresetCustomListModel() {} + +void GradientPresetCustomListModel::registerDeclarativeType() +{ + qmlRegisterType<GradientPresetCustomListModel>("HelperWidgets", + 2, + 0, + "GradientPresetCustomListModel"); +} + +QString GradientPresetCustomListModel::getFilename() +{ + return Internal::settingsFullFilePath(QSettings::UserScope); +} + +void GradientPresetCustomListModel::storePresets(const QString &filename, + const QList<GradientPresetItem> &items) +{ + const QList<QVariant> presets + = Utils::transform<QList<QVariant>>(items, [](const GradientPresetItem &item) { + return QVariant::fromValue(item); + }); + + QSettings settings(filename, QSettings::IniFormat); + settings.clear(); + settings.setValue(Internal::settingsKey, QVariant::fromValue(presets)); +} + +QList<GradientPresetItem> GradientPresetCustomListModel::storedPresets(const QString &filename) +{ + const QSettings settings(filename, QSettings::IniFormat); + const QVariant presetSettings = settings.value(Internal::settingsKey); + + if (!presetSettings.isValid()) + return {}; + + const QList<QVariant> presets = presetSettings.toList(); + + QList<GradientPresetItem> out; + for (const QVariant &preset : presets) { + if (preset.isValid()) { + out.append(preset.value<GradientPresetItem>()); + } + } + + return out; +} + +void GradientPresetCustomListModel::addGradient(const QList<qreal> &stopsPositions, + const QList<QString> &stopsColors, + int stopsCount) +{ + QGradient tempGradient; + QGradientStops gradientStops; + QGradientStop gradientStop; + for (int i = 0; i < stopsCount; i++) { + gradientStop.first = stopsPositions.at(i); + gradientStop.second = stopsColors.at(i); + gradientStops.push_back(gradientStop); + } + + tempGradient.setStops(gradientStops); + + addItem(GradientPresetItem(tempGradient)); +} + +void GradientPresetCustomListModel::changePresetName(int id, const QString &newName) +{ + QTC_ASSERT(id >= 0, return); + QTC_ASSERT(id < m_items.size(), return); + m_items[id].setPresetName(newName); + writePresets(); +} + +void GradientPresetCustomListModel::deletePreset(int id) +{ + QTC_ASSERT(id >= 0, return); + QTC_ASSERT(id < m_items.size(), return); + beginResetModel(); + m_items.removeAt(id); + writePresets(); + endResetModel(); +} + +void GradientPresetCustomListModel::writePresets() +{ + storePresets(m_filename, m_items); +} + +void GradientPresetCustomListModel::readPresets() +{ + const QList<GradientPresetItem> presets = storedPresets(m_filename); + beginResetModel(); + m_items.clear(); + + for (const GradientPresetItem &preset : presets) { + addItem(preset); + } + endResetModel(); +} diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetcustomlistmodel.h b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetcustomlistmodel.h new file mode 100644 index 0000000000..382b651e3b --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetcustomlistmodel.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "gradientpresetlistmodel.h" + +#include <QObject> +#include <QAbstractListModel> +#include <QtQml/qqml.h> + +class GradientPresetCustomListModel : public GradientPresetListModel +{ + Q_OBJECT + +public: + explicit GradientPresetCustomListModel(QObject *parent = nullptr); + ~GradientPresetCustomListModel() override; + + static void registerDeclarativeType(); + + static QString getFilename(); + static void storePresets(const QString &filename, const QList<GradientPresetItem> &items); + static QList<GradientPresetItem> storedPresets(const QString &filename); + + Q_INVOKABLE void addGradient(const QList<qreal> &stopsPositions, + const QList<QString> &stopsColors, + int stopsCount); + + Q_INVOKABLE void changePresetName(int id, const QString &newName); + Q_INVOKABLE void deletePreset(int id); + + Q_INVOKABLE void writePresets(); + Q_INVOKABLE void readPresets(); + +private: + QString m_filename; +}; + +QML_DECLARE_TYPE(GradientPresetCustomListModel) diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetdefaultlistmodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetdefaultlistmodel.cpp new file mode 100644 index 0000000000..8237390de9 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetdefaultlistmodel.cpp @@ -0,0 +1,61 @@ +/**************************************************************************** +** +** 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 "gradientpresetdefaultlistmodel.h" +#include "gradientpresetitem.h" + +#include <QHash> +#include <QByteArray> +#include <QDebug> +#include <QFile> + +GradientPresetDefaultListModel::GradientPresetDefaultListModel(QObject *parent) + : GradientPresetListModel(parent) +{ + addAllPresets(); +} + +GradientPresetDefaultListModel::~GradientPresetDefaultListModel() {} + +void GradientPresetDefaultListModel::registerDeclarativeType() +{ + qmlRegisterType<GradientPresetDefaultListModel>("HelperWidgets", + 2, + 0, + "GradientPresetDefaultListModel"); +} + +void GradientPresetDefaultListModel::addAllPresets() +{ + const QMetaObject &metaObj = QGradient::staticMetaObject; + const QMetaEnum metaEnum = metaObj.enumerator(metaObj.indexOfEnumerator("Preset")); + + if (!metaEnum.isValid()) + return; + + for (int i = 0; i < metaEnum.keyCount(); i++) { + addItem(GradientPresetItem(GradientPresetItem::Preset(metaEnum.value(i)))); + } +} diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetdefaultlistmodel.h b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetdefaultlistmodel.h new file mode 100644 index 0000000000..831135e052 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetdefaultlistmodel.h @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "gradientpresetlistmodel.h" + +#include <QObject> +#include <QAbstractListModel> +#include <QtQml/qqml.h> + +class GradientPresetDefaultListModel : public GradientPresetListModel +{ + Q_OBJECT + +public: + explicit GradientPresetDefaultListModel(QObject *parent = nullptr); + ~GradientPresetDefaultListModel() override; + + static void registerDeclarativeType(); + +private: + void addAllPresets(); +}; + +QML_DECLARE_TYPE(GradientPresetDefaultListModel) diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetitem.cpp b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetitem.cpp new file mode 100644 index 0000000000..9d2454c4e9 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetitem.cpp @@ -0,0 +1,206 @@ +/**************************************************************************** +** +** 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 "gradientpresetitem.h" + +#include <utils/qtcassert.h> +#include <utils/algorithm.h> + +#include <QVariant> +#include <QMetaObject> +#include <QMetaEnum> +#include <algorithm> +#include <QDebug> + + +GradientPresetItem::GradientPresetItem() + : m_gradientVal(QGradient()) + , m_gradientID(Preset(0)) + , m_presetName(QString()) +{} + +GradientPresetItem::GradientPresetItem(const QGradient &value, const QString &name) + : m_gradientVal(value) + , m_gradientID(Preset(0)) + , m_presetName(name) +{} + +GradientPresetItem::GradientPresetItem(const Preset value) + : m_gradientVal(createGradientFromPreset(value)) + , m_gradientID(value) + , m_presetName(getNameByPreset(value)) +{} + +GradientPresetItem::~GradientPresetItem() = default; + +QVariant GradientPresetItem::getProperty(GradientPresetItem::Property id) const +{ + QVariant out; + + switch (id) { + case objectNameRole: + out.setValue(QString()); + break; + case stopsPosListRole: + out.setValue(stopsPosList()); + break; + case stopsColorListRole: + out.setValue(stopsColorList()); + break; + case stopListSizeRole: + out.setValue(stopListSize()); + break; + case presetNameRole: + out.setValue(presetName()); + break; + case presetIDRole: + out.setValue(presetID()); + break; + default: + qWarning() << "GradientPresetItem Property switch default case"; + break; //replace with assert before switch? + } + + return out; +} + +QGradient GradientPresetItem::gradientVal() const +{ + return m_gradientVal; +} + +void GradientPresetItem::setGradient(const QGradient &value) +{ + m_gradientVal = value; + m_gradientID = Preset(0); + m_presetName = QString(); +} + +void GradientPresetItem::setGradient(const Preset value) +{ + m_gradientID = value; + m_gradientVal = createGradientFromPreset(value); + m_presetName = getNameByPreset(value); +} + +QList<qreal> GradientPresetItem::stopsPosList() const +{ + const QList<QPair<qreal, QColor>> subres = m_gradientVal.stops().toList(); + const QList<qreal> result = Utils::transform<QList<qreal>>(subres, + [](const QPair<qreal, QColor> &item) { + return item.first; + }); + return result; +} + +QList<QString> GradientPresetItem::stopsColorList() const +{ + const QList<QPair<qreal, QColor>> subres = m_gradientVal.stops().toList(); + const QList<QString> result + = Utils::transform<QList<QString>>(subres, [](const QPair<qreal, QColor> &item) { + return item.second.name(); + }); + return result; +} + +int GradientPresetItem::stopListSize() const +{ + return m_gradientVal.stops().size(); +} + +void GradientPresetItem::setPresetName(const QString &value) +{ + m_presetName = value; +} + +QString GradientPresetItem::presetName() const +{ + return m_presetName; +} + +int GradientPresetItem::presetID() const +{ + return static_cast<int>(m_gradientID); +} + +QString GradientPresetItem::getNameByPreset(Preset value) +{ + const QMetaObject &metaObj = QGradient::staticMetaObject; + const QMetaEnum metaEnum = metaObj.enumerator(metaObj.indexOfEnumerator("Preset")); + + if (!metaEnum.isValid()) + return QString("Custom"); + + QString enumName = QString::fromUtf8(metaEnum.valueToKey(static_cast<int>(value))); + + const QStringList sl = enumName.split(QRegExp("(?=[A-Z])"), QString::SkipEmptyParts); + + enumName.clear(); + std::for_each(sl.begin(), sl.end(), [&enumName](const QString &s) { enumName += (s + " "); }); + enumName.chop(1); //let's remove the last empty space + + return (enumName.isEmpty() ? QString("Custom") : enumName); +} + +QGradient GradientPresetItem::createGradientFromPreset(Preset value) +{ +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + return QGradient(value); +#else + Q_UNUSED(value); + return {}; +#endif +} + +QDebug &operator<<(QDebug &stream, const GradientPresetItem &gradient) +{ + stream << "\"stops:" << gradient.m_gradientVal.stops() << "\""; + stream << "\"preset:" << gradient.m_gradientID << "\""; + stream << "\"name:" << gradient.m_presetName << "\""; + return stream; +} + +QDataStream &operator<<(QDataStream &stream, const GradientPresetItem &gradient) +{ + stream << gradient.m_gradientVal.stops(); + + stream << static_cast<int>(gradient.m_gradientID); + stream << gradient.m_presetName; + return stream; +} + +QDataStream &operator>>(QDataStream &stream, GradientPresetItem &gradient) +{ + QGradientStops stops; + stream >> stops; + gradient.m_gradientVal.setStops(stops); + + int gradientID; + stream >> gradientID; + gradient.m_gradientID = static_cast<GradientPresetItem::Preset>(gradientID); + + stream >> gradient.m_presetName; + return stream; +} diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetitem.h b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetitem.h new file mode 100644 index 0000000000..cd0f0017e0 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetitem.h @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QObject> +#include <QGradient> + +class GradientPresetItem +{ + Q_GADGET + + Q_PROPERTY(QList<qreal> stopsPosList READ stopsPosList FINAL) + Q_PROPERTY(QList<QString> stopsColorList READ stopsColorList FINAL) + Q_PROPERTY(int stopListSize READ stopListSize FINAL) + Q_PROPERTY(QString presetName READ presetName FINAL) + Q_PROPERTY(int presetID READ presetID FINAL) + +public: +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + using Preset = QGradient::Preset; +#else + enum Preset {}; +#endif + + explicit GradientPresetItem(); + explicit GradientPresetItem(const QGradient &value, const QString &name = QString()); + explicit GradientPresetItem(const Preset number); + ~GradientPresetItem(); + + enum Property { + objectNameRole = 0, + stopsPosListRole = 1, + stopsColorListRole = 2, + stopListSizeRole = 3, + presetNameRole = 4, + presetIDRole = 5 + }; + + QVariant getProperty(Property id) const; + + QGradient gradientVal() const; + + void setGradient(const QGradient &value); + void setGradient(const Preset value); + + QList<qreal> stopsPosList() const; + QList<QString> stopsColorList() const; + int stopListSize() const; + + void setPresetName(const QString &value); + QString presetName() const; + int presetID() const; + + static QString getNameByPreset(Preset value); + + friend QDebug &operator<<(QDebug &stream, const GradientPresetItem &gradient); + + friend QDataStream &operator<<(QDataStream &stream, const GradientPresetItem &gradient); + friend QDataStream &operator>>(QDataStream &stream, GradientPresetItem &gradient); + + static QGradient createGradientFromPreset(Preset value); + +private: + QGradient m_gradientVal; + Preset m_gradientID; + QString m_presetName; +}; + +Q_DECLARE_METATYPE(GradientPresetItem) diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetlistmodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetlistmodel.cpp new file mode 100644 index 0000000000..1ed95f8719 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetlistmodel.cpp @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** 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 "gradientpresetlistmodel.h" +#include "gradientpresetitem.h" + +#include <QHash> +#include <QByteArray> +#include <QDebug> +#include <QSettings> + +GradientPresetListModel::GradientPresetListModel(QObject *parent) + : QAbstractListModel(parent) +{ + m_roleNames + = {{static_cast<int>(GradientPresetItem::Property::objectNameRole), "objectName"}, + {static_cast<int>(GradientPresetItem::Property::stopsPosListRole), "stopsPosList"}, + {static_cast<int>(GradientPresetItem::Property::stopsColorListRole), "stopsColorList"}, + {static_cast<int>(GradientPresetItem::Property::stopListSizeRole), "stopListSize"}, + {static_cast<int>(GradientPresetItem::Property::presetNameRole), "presetName"}, + {static_cast<int>(GradientPresetItem::Property::presetIDRole), "presetID"}}; +} + +GradientPresetListModel::~GradientPresetListModel() +{ + clearItems(); +} + +int GradientPresetListModel::rowCount(const QModelIndex & /*parent*/) const +{ + return m_items.count(); +} + +QVariant GradientPresetListModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid() && (index.row() >= 0) && (index.row() < m_items.count())) { + if (m_roleNames.contains(role)) { + QVariant value = m_items.at(index.row()) + .getProperty(static_cast<GradientPresetItem::Property>(role)); + + if (auto model = qobject_cast<GradientPresetListModel *>(value.value<QObject *>())) + return QVariant::fromValue(model); + + return value; + } + + qWarning() << Q_FUNC_INFO << "invalid role requested"; + return QVariant(); + } + + qWarning() << Q_FUNC_INFO << "invalid index requested"; + return QVariant(); +} + +QHash<int, QByteArray> GradientPresetListModel::roleNames() const +{ + return m_roleNames; +} + +void GradientPresetListModel::clearItems() +{ + beginResetModel(); + m_items.clear(); + endResetModel(); +} + +void GradientPresetListModel::addItem(const GradientPresetItem &element) +{ + beginResetModel(); + m_items.append(element); + endResetModel(); +} + +const QList<GradientPresetItem> &GradientPresetListModel::items() const +{ + return m_items; +} + +void GradientPresetListModel::sortItems() +{ + auto itemSort = [](const GradientPresetItem &first, const GradientPresetItem &second) { + return (static_cast<int>(first.presetID()) < static_cast<int>(second.presetID())); + }; + + std::sort(m_items.begin(), m_items.end(), itemSort); +} + +void GradientPresetListModel::registerDeclarativeType() +{ + qmlRegisterType<GradientPresetListModel>("HelperWidgets", 2, 0, "GradientPresetListModel"); +} diff --git a/src/plugins/qmldesigner/components/propertyeditor/gradientpresetlistmodel.h b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetlistmodel.h new file mode 100644 index 0000000000..7fce2243dd --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/gradientpresetlistmodel.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <QObject> +#include <QAbstractListModel> +#include <QtQml/qqml.h> + +class GradientPresetItem; + +class GradientPresetListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit GradientPresetListModel(QObject *parent = nullptr); + ~GradientPresetListModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + void clearItems(); + void addItem(const GradientPresetItem &element); + + const QList<GradientPresetItem> &items() const; + + void sortItems(); + + static void registerDeclarativeType(); + +protected: + QList<GradientPresetItem> m_items; + QHash<int, QByteArray> m_roleNames; +}; + +//QML_DECLARE_TYPE(GradientPresetListModel) diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditor.pri b/src/plugins/qmldesigner/components/propertyeditor/propertyeditor.pri index d822fbb70d..b32a744016 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditor.pri +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditor.pri @@ -11,7 +11,14 @@ SOURCES += propertyeditorview.cpp \ propertyeditorwidget.cpp \ fileresourcesmodel.cpp \ gradientmodel.cpp \ - qmlmodelnodeproxy.cpp + qmlmodelnodeproxy.cpp \ + gradientpresetitem.cpp \ + gradientpresetlistmodel.cpp \ + gradientpresetdefaultlistmodel.cpp \ + gradientpresetcustomlistmodel.cpp \ + simplecolorpalette.cpp \ + simplecolorpalettemodel.cpp \ + simplecolorpalettesingleton.cpp HEADERS += propertyeditorview.h \ qmlanchorbindingproxy.h \ @@ -24,6 +31,13 @@ HEADERS += propertyeditorview.h \ propertyeditorwidget.h \ fileresourcesmodel.h \ gradientmodel.h \ - qmlmodelnodeproxy.h + qmlmodelnodeproxy.h \ + gradientpresetitem.h \ + gradientpresetlistmodel.h \ + gradientpresetdefaultlistmodel.h \ + gradientpresetcustomlistmodel.h \ + simplecolorpalette.h \ + simplecolorpalettemodel.h \ + simplecolorpalettesingleton.h QT += qml quick diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp index dc8243e1c8..8fdab5a821 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorcontextobject.cpp @@ -152,22 +152,14 @@ void PropertyEditorContextObject::toogleExportAlias() PropertyName modelNodeId = selectedNode.id().toUtf8(); ModelNode rootModelNode = rewriterView->rootModelNode(); - try { - RewriterTransaction transaction = - rewriterView->beginRewriterTransaction(QByteArrayLiteral("PropertyEditorContextObject:toogleExportAlias")); - + rewriterView->executeInTransaction("PropertyEditorContextObject:toogleExportAlias", [&objectNode, &rootModelNode, modelNodeId](){ if (!objectNode.isAliasExported()) objectNode.ensureAliasExport(); else if (rootModelNode.hasProperty(modelNodeId)) rootModelNode.removeProperty(modelNodeId); - - transaction.commit(); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } + }); } - } void PropertyEditorContextObject::changeTypeName(const QString &typeName) @@ -181,11 +173,8 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName) QTC_ASSERT(!rewriterView->selectedModelNodes().isEmpty(), return); - ModelNode selectedNode = rewriterView->selectedModelNodes().constFirst(); - - try { - RewriterTransaction transaction = - rewriterView->beginRewriterTransaction(QByteArrayLiteral("PropertyEditorContextObject:changeTypeName")); + rewriterView->executeInTransaction("PropertyEditorContextObject:changeTypeName", [this, rewriterView, typeName](){ + ModelNode selectedNode = rewriterView->selectedModelNodes().constFirst(); NodeMetaInfo metaInfo = m_model->metaInfo(typeName.toLatin1()); if (!metaInfo.isValid()) { @@ -193,16 +182,10 @@ void PropertyEditorContextObject::changeTypeName(const QString &typeName) return; } if (selectedNode.isRootNode()) - rewriterView->changeRootNodeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); + rewriterView->changeRootNodeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); else selectedNode.changeType(metaInfo.typeName(), metaInfo.majorVersion(), metaInfo.minorVersion()); - - transaction.commit(); - } catch (RewritingException &exception) { //better safe than sorry! There always might be cases where we fail - exception.showException(); - } - - + }); } void PropertyEditorContextObject::insertKeyframe(const QString &propertyName) diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp index 284b6948ee..12f12ed2bc 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.cpp @@ -40,6 +40,7 @@ #include <coreplugin/icore.h> #include <qmljs/qmljssimplereader.h> +#include <utils/qtcassert.h> #include <utils/algorithm.h> #include <utils/fileutils.h> @@ -281,13 +282,17 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q setupLayoutAttachedProperties(qmlObjectNode, propertyEditor); + // model node + m_backendModelNode.setup(qmlObjectNode.modelNode()); + context()->setContextProperty(QLatin1String("modelNodeBackend"), &m_backendModelNode); + // className auto valueObject = qobject_cast<PropertyEditorValue*>(variantToQObject(m_backendValuesPropertyMap.value(QLatin1String("className")))); if (!valueObject) valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap); valueObject->setName("className"); valueObject->setModelNode(qmlObjectNode.modelNode()); - valueObject->setValue(qmlObjectNode.modelNode().simplifiedTypeName()); + valueObject->setValue(m_backendModelNode.simplifiedTypeName()); QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged); m_backendValuesPropertyMap.insert(QLatin1String("className"), QVariant::fromValue(valueObject)); @@ -296,7 +301,7 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q if (!valueObject) valueObject = new PropertyEditorValue(&m_backendValuesPropertyMap); valueObject->setName("id"); - valueObject->setValue(qmlObjectNode.id()); + valueObject->setValue(m_backendModelNode.nodeId()); QObject::connect(valueObject, &PropertyEditorValue::valueChanged, &backendValuesPropertyMap(), &DesignerPropertyMap::valueChanged); m_backendValuesPropertyMap.insert(QLatin1String("id"), QVariant::fromValue(valueObject)); @@ -310,10 +315,6 @@ void PropertyEditorQmlBackend::setup(const QmlObjectNode &qmlObjectNode, const Q qCInfo(propertyEditorBenchmark) << "anchors:" << time.elapsed(); - // model node - m_backendModelNode.setup(qmlObjectNode.modelNode()); - context()->setContextProperty(QLatin1String("modelNodeBackend"), &m_backendModelNode); - qCInfo(propertyEditorBenchmark) << "context:" << time.elapsed(); contextObject()->setSpecificsUrl(qmlSpecificsFile); @@ -402,23 +403,49 @@ QString PropertyEditorQmlBackend::propertyEditorResourcesPath() { QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType, - const QmlObjectNode &objectNode) + const QmlObjectNode &node) { if (!templateConfiguration() || !templateConfiguration()->isValid()) return QString(); + const auto nodes = templateConfiguration()->children(); + + QStringList sectorTypes; + + for (const QmlJS::SimpleReaderNode::Ptr &node : nodes) { + if (node->propertyNames().contains("separateSection")) + sectorTypes.append(variantToStringList(node->property("typeNames"))); + } + QStringList imports = variantToStringList(templateConfiguration()->property(QStringLiteral("imports"))); QString qmlTemplate = imports.join(QLatin1Char('\n')) + QLatin1Char('\n'); - qmlTemplate += QStringLiteral("Section {\n"); - qmlTemplate += QStringLiteral("caption: \"%1\"\n").arg(objectNode.modelNode().simplifiedTypeName()); - qmlTemplate += QStringLiteral("SectionLayout {\n"); + + qmlTemplate += "Column {\n"; + qmlTemplate += "anchors.left: parent.left\n"; + qmlTemplate += "anchors.right: parent.right\n"; QList<PropertyName> orderedList = type.propertyNames(); - Utils::sort(orderedList); + Utils::sort(orderedList, [type, §orTypes](const PropertyName &left, const PropertyName &right){ + const QString typeNameLeft = QString::fromLatin1(type.propertyTypeName(left)); + const QString typeNameRight = QString::fromLatin1(type.propertyTypeName(right)); + if (typeNameLeft == typeNameRight) + return left > right; + + if (sectorTypes.contains(typeNameLeft)) { + if (sectorTypes.contains(typeNameRight)) + return left > right; + return true; + } else if (sectorTypes.contains(typeNameRight)) { + return false; + } + return left > right; + }); bool emptyTemplate = true; + bool sectionStarted = false; + foreach (const PropertyName &name, orderedList) { if (name.startsWith("__")) @@ -429,18 +456,38 @@ QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type, TypeName typeName = type.propertyTypeName(name); //alias resolution only possible with instance - if (typeName == "alias" && objectNode.isValid()) - typeName = objectNode.instanceType(name); + if (typeName == "alias" && node.isValid()) + typeName = node.instanceType(name); + + auto nodes = templateConfiguration()->children(); if (!superType.hasProperty(name) && type.propertyIsWritable(name) && !name.contains(".")) { - foreach (const QmlJS::SimpleReaderNode::Ptr &node, templateConfiguration()->children()) + + foreach (const QmlJS::SimpleReaderNode::Ptr &node, nodes) if (variantToStringList(node->property(QStringLiteral("typeNames"))).contains(QString::fromLatin1(typeName))) { const QString fileName = propertyTemplatesPath() + node->property(QStringLiteral("sourceFile")).toString(); QFile file(fileName); if (file.open(QIODevice::ReadOnly)) { QString source = QString::fromUtf8(file.readAll()); file.close(); + const bool section = node->propertyNames().contains("separateSection"); + if (section) { + qmlTemplate += "Section {\n"; + qmlTemplate += "anchors.left: parent.left\n"; + qmlTemplate += "anchors.right: parent.right\n"; + qmlTemplate += QString("caption: \"%1\"\n").arg(QString::fromUtf8(properName)); + } else if (!sectionStarted) { + qmlTemplate += QStringLiteral("Section {\n"); + qmlTemplate += QStringLiteral("caption: \"%1\"\n").arg(QString::fromUtf8(type.simplifiedTypeName())); + qmlTemplate += "anchors.left: parent.left\n"; + qmlTemplate += "anchors.right: parent.right\n"; + qmlTemplate += QStringLiteral("SectionLayout {\n"); + sectionStarted = true; + } + qmlTemplate += source.arg(QString::fromUtf8(name)).arg(QString::fromUtf8(properName)); + if (section) + qmlTemplate += "}\n"; emptyTemplate = false; } else { qWarning().nospace() << "template definition source file not found:" << fileName; @@ -448,8 +495,12 @@ QString PropertyEditorQmlBackend::templateGeneration(const NodeMetaInfo &type, } } } - qmlTemplate += QStringLiteral("}\n"); //Section - qmlTemplate += QStringLiteral("}\n"); //SectionLayout + if (sectionStarted) { + qmlTemplate += QStringLiteral("}\n"); //Section + qmlTemplate += QStringLiteral("}\n"); //SectionLayout + } + + qmlTemplate += "}\n"; if (emptyTemplate) return QString(); @@ -469,6 +520,36 @@ TypeName PropertyEditorQmlBackend::fixTypeNameForPanes(const TypeName &typeName) return fixedTypeName; } +static NodeMetaInfo findCommonSuperClass(const NodeMetaInfo &first, const NodeMetaInfo &second) +{ + for (const NodeMetaInfo &info : first.superClasses()) { + if (second.isSubclassOf(info.typeName())) + return info; + } + return first; +} + +NodeMetaInfo PropertyEditorQmlBackend::findCommonAncestor(const ModelNode &node) +{ + if (!node.isValid()) + return {}; + + QTC_ASSERT(node.metaInfo().isValid(), return {}); + + AbstractView *view = node.view(); + + if (view->selectedModelNodes().count() > 1) { + NodeMetaInfo commonClass = node.metaInfo(); + for (const ModelNode ¤tNode : view->selectedModelNodes()) { + if (currentNode.metaInfo().isValid() && !currentNode.isSubclassOf(commonClass.typeName(), -1, -1)) + commonClass = findCommonSuperClass(currentNode.metaInfo(), commonClass); + } + return commonClass; + } + + return node.metaInfo(); +} + TypeName PropertyEditorQmlBackend::qmlFileName(const NodeMetaInfo &nodeInfo) { const TypeName fixedTypeName = fixTypeNameForPanes(nodeInfo.typeName()); @@ -526,10 +607,10 @@ void PropertyEditorQmlBackend::setValueforLayoutAttachedProperties(const QmlObje setValue(qmlObjectNode, name, properDefaultLayoutAttachedProperties(qmlObjectNode, propertyName)); } -QUrl PropertyEditorQmlBackend::getQmlUrlForModelNode(const ModelNode &modelNode, TypeName &className) +QUrl PropertyEditorQmlBackend::getQmlUrlForMetaInfo(const NodeMetaInfo &metaInfo, TypeName &className) { - if (modelNode.isValid()) { - foreach (const NodeMetaInfo &info, modelNode.metaInfo().classHierarchy()) { + if (metaInfo.isValid()) { + foreach (const NodeMetaInfo &info, metaInfo.classHierarchy()) { QUrl fileUrl = fileToUrl(locateQmlFile(info, QString::fromUtf8(qmlFileName(info)))); if (fileUrl.isValid()) { className = info.typeName(); diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.h b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.h index a0012a1cc1..51279a1fc6 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.h +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorqmlbackend.h @@ -68,11 +68,10 @@ public: PropertyEditorValue *propertyValueForName(const QString &propertyName); static QString propertyEditorResourcesPath(); - static QString templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType, - const QmlObjectNode &objectNode); + static QString templateGeneration(const NodeMetaInfo &type, const NodeMetaInfo &superType, const QmlObjectNode &node); static QUrl getQmlFileUrl(const TypeName &relativeTypeName, const NodeMetaInfo &info = NodeMetaInfo()); - static QUrl getQmlUrlForModelNode(const ModelNode &modelNode, TypeName &className); + static QUrl getQmlUrlForMetaInfo(const NodeMetaInfo &modelNode, TypeName &className); static bool checkIfUrlExists(const QUrl &url); @@ -83,6 +82,8 @@ public: void setupLayoutAttachedProperties(const QmlObjectNode &qmlObjectNode, PropertyEditorView *propertyEditor); + static NodeMetaInfo findCommonAncestor(const ModelNode &node); + private: void createPropertyEditorValue(const QmlObjectNode &qmlObjectNode, const PropertyName &name, const QVariant &value, diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp index 0467355bf4..9dd0a2da24 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.cpp @@ -213,20 +213,13 @@ void PropertyEditorView::changeValue(const QString &name) castedValue = QVariant(newColor); } - try { - if (!value->value().isValid()) { //reset - qmlObjectNode.removeProperty(propertyName); - } else { - if (castedValue.isValid() && !castedValue.isNull()) { - m_locked = true; - qmlObjectNode.setVariantProperty(propertyName, castedValue); - m_locked = false; - } + if (!value->value().isValid()) { //reset + removePropertyFromModel(propertyName); + } else { + if (castedValue.isValid() && !castedValue.isNull()) { + commitVariantValueToModel(propertyName, castedValue); } } - catch (const RewritingException &e) { - e.showException(); - } } void PropertyEditorView::changeExpression(const QString &propertyName) @@ -242,9 +235,7 @@ void PropertyEditorView::changeExpression(const QString &propertyName) if (!m_selectedNode.isValid()) return; - RewriterTransaction transaction = beginRewriterTransaction(QByteArrayLiteral("PropertyEditorView::changeExpression")); - - try { + executeInTransaction("PropertyEditorView::changeExpression", [this, name](){ PropertyName underscoreName(name); underscoreName.replace('.', '_'); @@ -260,7 +251,6 @@ void PropertyEditorView::changeExpression(const QString &propertyName) if (qmlObjectNode.modelNode().metaInfo().propertyTypeName(name) == "QColor") { if (QColor(value->expression().remove('"')).isValid()) { qmlObjectNode.setVariantProperty(name, QColor(value->expression().remove('"'))); - transaction.commit(); //committing in the try block return; } } else if (qmlObjectNode.modelNode().metaInfo().propertyTypeName(name) == "bool") { @@ -270,7 +260,6 @@ void PropertyEditorView::changeExpression(const QString &propertyName) qmlObjectNode.setVariantProperty(name, true); else qmlObjectNode.setVariantProperty(name, false); - transaction.commit(); //committing in the try block return; } } else if (qmlObjectNode.modelNode().metaInfo().propertyTypeName(name) == "int") { @@ -278,7 +267,6 @@ void PropertyEditorView::changeExpression(const QString &propertyName) int intValue = value->expression().toInt(&ok); if (ok) { qmlObjectNode.setVariantProperty(name, intValue); - transaction.commit(); //committing in the try block return; } } else if (qmlObjectNode.modelNode().metaInfo().propertyTypeName(name) == "qreal") { @@ -286,7 +274,6 @@ void PropertyEditorView::changeExpression(const QString &propertyName) qreal realValue = value->expression().toDouble(&ok); if (ok) { qmlObjectNode.setVariantProperty(name, realValue); - transaction.commit(); //committing in the try block return; } } @@ -298,12 +285,7 @@ void PropertyEditorView::changeExpression(const QString &propertyName) if (qmlObjectNode.expression(name) != value->expression() || !qmlObjectNode.propertyAffectedByCurrentState(name)) qmlObjectNode.setBindingProperty(name, value->expression()); - transaction.commit(); //committing in the try block - } - - catch (const RewritingException &e) { - e.showException(); - } + }); /* end of transaction */ } void PropertyEditorView::exportPopertyAsAlias(const QString &name) @@ -317,9 +299,7 @@ void PropertyEditorView::exportPopertyAsAlias(const QString &name) if (!m_selectedNode.isValid()) return; - RewriterTransaction transaction = beginRewriterTransaction(QByteArrayLiteral("PropertyEditorView::exportPopertyAsAlias")); - - try { + executeInTransaction("PropertyEditorView::exportPopertyAsAlias", [this, name](){ const QString id = m_selectedNode.validId(); QString upperCasePropertyName = name; upperCasePropertyName.replace(0, 1, upperCasePropertyName.at(0).toUpper()); @@ -333,11 +313,7 @@ void PropertyEditorView::exportPopertyAsAlias(const QString &name) return; } rootModelNode().bindingProperty(propertyName).setDynamicTypeNameAndExpression("alias", id + "." + name); - - transaction.commit(); //committing in the try block - } catch (const RewritingException &e) { - e.showException(); - } + }); } void PropertyEditorView::removeAliasExport(const QString &name) @@ -351,9 +327,7 @@ void PropertyEditorView::removeAliasExport(const QString &name) if (!m_selectedNode.isValid()) return; - RewriterTransaction transaction = beginRewriterTransaction(QByteArrayLiteral("PropertyEditorView::exportPopertyAsAlias")); - - try { + executeInTransaction("PropertyEditorView::exportPopertyAsAlias", [this, name](){ const QString id = m_selectedNode.validId(); for (const BindingProperty &property : rootModelNode().bindingProperties()) @@ -361,10 +335,7 @@ void PropertyEditorView::removeAliasExport(const QString &name) rootModelNode().removeProperty(property.name()); break; } - transaction.commit(); //committing in the try block - } catch (const RewritingException &e) { - e.showException(); - } + }); } bool PropertyEditorView::locked() const @@ -446,13 +417,16 @@ void PropertyEditorView::resetView() void PropertyEditorView::setupQmlBackend() { TypeName specificsClassName; - QUrl qmlFile(PropertyEditorQmlBackend::getQmlUrlForModelNode(m_selectedNode, specificsClassName)); + + const NodeMetaInfo commonAncestor = PropertyEditorQmlBackend::findCommonAncestor(m_selectedNode); + + const QUrl qmlFile(PropertyEditorQmlBackend::getQmlUrlForMetaInfo(commonAncestor, specificsClassName)); QUrl qmlSpecificsFile; TypeName diffClassName; - if (m_selectedNode.isValid()) { - diffClassName = m_selectedNode.metaInfo().typeName(); - foreach (const NodeMetaInfo &metaInfo, m_selectedNode.metaInfo().classHierarchy()) { + if (commonAncestor.isValid()) { + diffClassName = commonAncestor.typeName(); + foreach (const NodeMetaInfo &metaInfo, commonAncestor.classHierarchy()) { if (PropertyEditorQmlBackend::checkIfUrlExists(qmlSpecificsFile)) break; qmlSpecificsFile = PropertyEditorQmlBackend::getQmlFileUrl(metaInfo.typeName() + "Specifics", metaInfo); @@ -465,8 +439,8 @@ void PropertyEditorView::setupQmlBackend() QString specificQmlData; - if (m_selectedNode.isValid() && m_selectedNode.metaInfo().isValid() && diffClassName != m_selectedNode.type()) - specificQmlData = PropertyEditorQmlBackend::templateGeneration(m_selectedNode.metaInfo(), model()->metaInfo(diffClassName), m_selectedNode); + if (commonAncestor.isValid() && m_selectedNode.metaInfo().isValid() && diffClassName != m_selectedNode.type()) + specificQmlData = PropertyEditorQmlBackend::templateGeneration(commonAncestor, model()->metaInfo(diffClassName), m_selectedNode); PropertyEditorQmlBackend *currentQmlBackend = m_qmlBackendHash.value(qmlFile.toString()); @@ -515,14 +489,51 @@ void PropertyEditorView::setupQmlBackend() } +void PropertyEditorView::commitVariantValueToModel(const PropertyName &propertyName, const QVariant &value) +{ + m_locked = true; + try { + RewriterTransaction transaction = beginRewriterTransaction("PropertyEditorView::commitVariantValueToMode"); + + for (const ModelNode &node : m_selectedNode.view()->selectedModelNodes()) { + if (QmlObjectNode::isValidQmlObjectNode(node)) + QmlObjectNode(node).setVariantProperty(propertyName, value); + } + transaction.commit(); + } + catch (const RewritingException &e) { + e.showException(); + } + m_locked = false; +} + +void PropertyEditorView::removePropertyFromModel(const PropertyName &propertyName) +{ + m_locked = true; + try { + RewriterTransaction transaction = beginRewriterTransaction("PropertyEditorView::removePropertyFromModel"); + + for (const ModelNode &node : m_selectedNode.view()->selectedModelNodes()) { + if (QmlObjectNode::isValidQmlObjectNode(node)) + QmlObjectNode(node).removeProperty(propertyName); + } + + transaction.commit(); + } + catch (const RewritingException &e) { + e.showException(); + } + m_locked = false; +} + void PropertyEditorView::selectedNodesChanged(const QList<ModelNode> &selectedNodeList, const QList<ModelNode> &lastSelectedNodeList) { Q_UNUSED(lastSelectedNodeList); - if (selectedNodeList.isEmpty() || selectedNodeList.count() > 1) + if (selectedNodeList.isEmpty()) select(ModelNode()); - else if (m_selectedNode != selectedNodeList.constFirst()) + else select(selectedNodeList.constFirst()); } @@ -542,10 +553,11 @@ void PropertyEditorView::modelAttached(Model *model) m_locked = true; if (!m_setupCompleted) { - m_singleShotTimer->setSingleShot(true); - m_singleShotTimer->setInterval(100); - connect(m_singleShotTimer, &QTimer::timeout, this, &PropertyEditorView::setupPanes); - m_singleShotTimer->start(); + QTimer::singleShot(50, this, [this]{ + PropertyEditorView::setupPanes(); + /* workaround for QTBUG-75847 */ + reloadQml(); + }); } m_locked = false; @@ -648,6 +660,9 @@ void PropertyEditorView::instanceInformationsChanged(const QMultiHash<ModelNode, if (!m_selectedNode.isValid()) return; + if (!m_qmlBackEndForCurrentType) + return; + m_locked = true; QList<InformationName> informationNameList = informationChangedHash.values(m_selectedNode); if (informationNameList.contains(Anchor) diff --git a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.h b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.h index 85bd8286f5..e7f57cf186 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.h +++ b/src/plugins/qmldesigner/components/propertyeditor/propertyeditorview.h @@ -110,6 +110,9 @@ private: //functions void delayedResetView(); void setupQmlBackend(); + void commitVariantValueToModel(const PropertyName &propertyName, const QVariant &value); + void removePropertyFromModel(const PropertyName &propertyName); + private: //variables ModelNode m_selectedNode; QWidget *m_parent; diff --git a/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.cpp b/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.cpp index bc6b4376b6..7a38f74f34 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.cpp @@ -292,9 +292,9 @@ void QmlAnchorBindingProxy::setDefaultRelativeRightTarget() } } -RewriterTransaction QmlAnchorBindingProxy::beginRewriterTransaction(const QByteArray &identifier) +bool QmlAnchorBindingProxy::executeInTransaction(const QByteArray &identifier, const AbstractView::OperationBlock &lambda) { - return m_qmlItemNode.modelNode().view()->beginRewriterTransaction(identifier); + return m_qmlItemNode.modelNode().view()->executeInTransaction(identifier, lambda); } bool QmlAnchorBindingProxy::hasParent() const @@ -361,20 +361,11 @@ void QmlAnchorBindingProxy::setTopTarget(const QString &target) if (!newTarget.isValid()) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setTopTarget")); - + executeInTransaction("QmlAnchorBindingProxy::setTopTarget", [this, newTarget](){ m_topTarget = newTarget; - setDefaultRelativeTopTarget(); - anchorTop(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit topTargetChanged(); } @@ -393,18 +384,12 @@ void QmlAnchorBindingProxy::setBottomTarget(const QString &target) if (!newTarget.isValid()) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setBottomTarget")); - + executeInTransaction("QmlAnchorBindingProxy::setBottomTarget", [this, newTarget](){ m_bottomTarget = newTarget; setDefaultRelativeBottomTarget(); anchorBottom(); - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit bottomTargetChanged(); } @@ -422,18 +407,11 @@ void QmlAnchorBindingProxy::setLeftTarget(const QString &target) if (!newTarget.isValid()) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setLeftTarget")); - + executeInTransaction("QmlAnchorBindingProxy::setLeftTarget", [this, newTarget](){ m_leftTarget = newTarget; setDefaultRelativeLeftTarget(); anchorLeft(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit leftTargetChanged(); } @@ -451,18 +429,11 @@ void QmlAnchorBindingProxy::setRightTarget(const QString &target) if (!newTarget.isValid()) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRightTarget")); - + executeInTransaction("QmlAnchorBindingProxy::setRightTarget", [this, newTarget](){ m_rightTarget = newTarget; setDefaultRelativeRightTarget(); anchorRight(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit rightTargetChanged(); } @@ -480,17 +451,10 @@ void QmlAnchorBindingProxy::setVerticalTarget(const QString &target) if (!newTarget.isValid()) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setVerticalTarget")); - + executeInTransaction("QmlAnchorBindingProxy::setVerticalTarget", [this, newTarget](){ m_verticalTarget = newTarget; anchorVertical(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit verticalTargetChanged(); } @@ -508,17 +472,10 @@ void QmlAnchorBindingProxy::setHorizontalTarget(const QString &target) if (!newTarget.isValid()) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setHorizontalTarget")); - + executeInTransaction("QmlAnchorBindingProxy::setHorizontalTarget", [this, newTarget](){ m_horizontalTarget = newTarget; - anchorHorizontal();\ - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + anchorHorizontal(); + }); emit horizontalTargetChanged(); } @@ -531,18 +488,10 @@ void QmlAnchorBindingProxy::setRelativeAnchorTargetTop(QmlAnchorBindingProxy::Re if (target == m_relativeTopTarget) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRelativeAnchorTargetTop")); - + executeInTransaction("QmlAnchorBindingProxy::setRelativeAnchorTargetTop", [this, target](){ m_relativeTopTarget = target; - anchorTop(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetTopChanged(); } @@ -555,19 +504,10 @@ void QmlAnchorBindingProxy::setRelativeAnchorTargetBottom(QmlAnchorBindingProxy: if (target == m_relativeBottomTarget) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRelativeAnchorTargetBottom")); - + executeInTransaction("QmlAnchorBindingProxy::setRelativeAnchorTargetBottom", [this, target](){ m_relativeBottomTarget = target; - - anchorBottom(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetBottomChanged(); } @@ -580,18 +520,11 @@ void QmlAnchorBindingProxy::setRelativeAnchorTargetLeft(QmlAnchorBindingProxy::R if (target == m_relativeLeftTarget) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRelativeAnchorTargetLeft")); - + executeInTransaction("QmlAnchorBindingProxy::setRelativeAnchorTargetLeft", [this, target](){ m_relativeLeftTarget = target; - anchorLeft(); - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetLeftChanged(); } @@ -604,18 +537,10 @@ void QmlAnchorBindingProxy::setRelativeAnchorTargetRight(QmlAnchorBindingProxy:: if (target == m_relativeRightTarget) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRelativeAnchorTargetRight")); - + executeInTransaction("QmlAnchorBindingProxy::setRelativeAnchorTargetRight", [this, target](){ m_relativeRightTarget = target; - anchorRight(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetRightChanged(); @@ -629,18 +554,11 @@ void QmlAnchorBindingProxy::setRelativeAnchorTargetVertical(QmlAnchorBindingProx if (target == m_relativeVerticalTarget) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRelativeAnchorTargetVertical")); + executeInTransaction("QmlAnchorBindingProxy::setRelativeAnchorTargetVertical", [this, target](){ m_relativeVerticalTarget = target; - anchorVertical(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetVerticalChanged(); } @@ -653,18 +571,10 @@ void QmlAnchorBindingProxy::setRelativeAnchorTargetHorizontal(QmlAnchorBindingPr if (target == m_relativeHorizontalTarget) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRelativeAnchorTargetHorizontal")); - + executeInTransaction("QmlAnchorBindingProxy::setRelativeAnchorTargetHorizontal", [this, target](){ m_relativeHorizontalTarget = target; - anchorHorizontal(); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetHorizontalChanged(); } @@ -709,12 +619,10 @@ int QmlAnchorBindingProxy::indexOfPossibleTargetItem(const QString &targetName) return possibleTargetItems().indexOf(targetName); } -void QmlAnchorBindingProxy::resetLayout() { - - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::resetLayout")); +void QmlAnchorBindingProxy::resetLayout() +{ + executeInTransaction("QmlAnchorBindingProxy::resetLayout", [this](){ m_qmlItemNode.anchors().removeAnchors(); m_qmlItemNode.anchors().removeMargins(); @@ -722,11 +630,7 @@ void QmlAnchorBindingProxy::resetLayout() { restoreProperty(modelNode(), "y"); restoreProperty(modelNode(), "width"); restoreProperty(modelNode(), "height"); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit topAnchorChanged(); emit bottomAnchorChanged(); @@ -743,10 +647,7 @@ void QmlAnchorBindingProxy::setBottomAnchor(bool anchor) if (bottomAnchored() == anchor) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setBottomAnchor")); - + executeInTransaction("QmlAnchorBindingProxy::setBottomAnchor", [this, anchor](){ if (!anchor) { removeBottomAnchor(); } else { @@ -756,10 +657,7 @@ void QmlAnchorBindingProxy::setBottomAnchor(bool anchor) backupPropertyAndRemove(modelNode(), "height"); } - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetBottomChanged(); emit bottomAnchorChanged(); @@ -776,10 +674,8 @@ void QmlAnchorBindingProxy::setLeftAnchor(bool anchor) if (leftAnchored() == anchor) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setLeftAnchor")); + executeInTransaction("QmlAnchorBindingProxy::setLeftAnchor", [this, anchor](){ if (!anchor) { removeLeftAnchor(); } else { @@ -791,10 +687,7 @@ void QmlAnchorBindingProxy::setLeftAnchor(bool anchor) backupPropertyAndRemove(modelNode(), "width"); } - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetLeftChanged(); emit leftAnchorChanged(); @@ -810,10 +703,7 @@ void QmlAnchorBindingProxy::setRightAnchor(bool anchor) if (rightAnchored() == anchor) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setRightAnchor")); - + executeInTransaction("QmlAnchorBindingProxy::setRightAnchor", [this, anchor](){ if (!anchor) { removeRightAnchor(); } else { @@ -824,10 +714,7 @@ void QmlAnchorBindingProxy::setRightAnchor(bool anchor) backupPropertyAndRemove(modelNode(), "width"); } - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetRightChanged(); emit rightAnchorChanged(); @@ -1026,10 +913,7 @@ void QmlAnchorBindingProxy::setTopAnchor(bool anchor) if (topAnchored() == anchor) return; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setTopAnchor")); - + executeInTransaction("QmlAnchorBindingProxy::setTopAnchor", [this, anchor](){ if (!anchor) { removeTopAnchor(); } else { @@ -1040,10 +924,7 @@ void QmlAnchorBindingProxy::setTopAnchor(bool anchor) if (bottomAnchored()) backupPropertyAndRemove(modelNode(), "height"); } - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit relativeAnchorTargetTopChanged(); emit topAnchorChanged(); @@ -1052,70 +933,44 @@ void QmlAnchorBindingProxy::setTopAnchor(bool anchor) } void QmlAnchorBindingProxy::removeTopAnchor() { - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::removeTopAnchor")); - + executeInTransaction("QmlAnchorBindingProxy::removeTopAnchor", [this](){ m_qmlItemNode.anchors().removeAnchor(AnchorLineTop); m_qmlItemNode.anchors().removeMargin(AnchorLineTop); restoreProperty(modelNode(), "y"); restoreProperty(modelNode(), "height"); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); } -void QmlAnchorBindingProxy::removeBottomAnchor() { - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::removeBottomAnchor")); - +void QmlAnchorBindingProxy::removeBottomAnchor() +{ + executeInTransaction("QmlAnchorBindingProxy::removeBottomAnchor", [this](){ m_qmlItemNode.anchors().removeAnchor(AnchorLineBottom); m_qmlItemNode.anchors().removeMargin(AnchorLineBottom); - restoreProperty(modelNode(), "height"); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); } -void QmlAnchorBindingProxy::removeLeftAnchor() { - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::removeLeftAnchor")); - +void QmlAnchorBindingProxy::removeLeftAnchor() +{ + executeInTransaction("QmlAnchorBindingProxy::removeLeftAnchor", [this](){ m_qmlItemNode.anchors().removeAnchor(AnchorLineLeft); m_qmlItemNode.anchors().removeMargin(AnchorLineLeft); restoreProperty(modelNode(), "x"); restoreProperty(modelNode(), "width"); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); } -void QmlAnchorBindingProxy::removeRightAnchor() { - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::removeRightAnchor")); - +void QmlAnchorBindingProxy::removeRightAnchor() +{ + executeInTransaction("QmlAnchorBindingProxy::removeRightAnchor", [this](){ m_qmlItemNode.anchors().removeAnchor(AnchorLineRight); m_qmlItemNode.anchors().removeMargin(AnchorLineRight); restoreProperty(modelNode(), "width"); - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); } void QmlAnchorBindingProxy::setVerticalCentered(bool centered) @@ -1128,10 +983,7 @@ void QmlAnchorBindingProxy::setVerticalCentered(bool centered) m_locked = true; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setVerticalCentered")); - + executeInTransaction("QmlAnchorBindingProxy::setVerticalCentered", [this, centered](){ if (!centered) { m_qmlItemNode.anchors().removeAnchor(AnchorLineVerticalCenter); m_qmlItemNode.anchors().removeMargin(AnchorLineVerticalCenter); @@ -1141,10 +993,7 @@ void QmlAnchorBindingProxy::setVerticalCentered(bool centered) anchorVertical(); } - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); m_locked = false; emit relativeAnchorTargetVerticalChanged(); @@ -1161,10 +1010,7 @@ void QmlAnchorBindingProxy::setHorizontalCentered(bool centered) m_locked = true; - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::setHorizontalCentered")); - + executeInTransaction("QmlAnchorBindingProxy::setHorizontalCentered", [this, centered](){ if (!centered) { m_qmlItemNode.anchors().removeAnchor(AnchorLineHorizontalCenter); m_qmlItemNode.anchors().removeMargin(AnchorLineHorizontalCenter); @@ -1173,11 +1019,7 @@ void QmlAnchorBindingProxy::setHorizontalCentered(bool centered) anchorHorizontal(); } - - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); m_locked = false; emit relativeAnchorTargetHorizontalChanged(); @@ -1256,12 +1098,7 @@ bool QmlAnchorBindingProxy::horizontalCentered() void QmlAnchorBindingProxy::fill() { - - try { - RewriterTransaction transaction = beginRewriterTransaction( - QByteArrayLiteral("QmlAnchorBindingProxy::fill")); - - + executeInTransaction("QmlAnchorBindingProxy::fill", [this](){ backupPropertyAndRemove(modelNode(), "x"); backupPropertyAndRemove(modelNode(), "y"); backupPropertyAndRemove(modelNode(), "width"); @@ -1277,10 +1114,7 @@ void QmlAnchorBindingProxy::fill() m_qmlItemNode.anchors().removeMargin(AnchorLineTop); m_qmlItemNode.anchors().removeMargin(AnchorLineBottom); - transaction.commit(); - } catch (const Exception &e) { - e.showException(); - } + }); emit topAnchorChanged(); emit bottomAnchorChanged(); diff --git a/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.h b/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.h index cf42ea055a..0bd562add2 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.h +++ b/src/plugins/qmldesigner/components/propertyeditor/qmlanchorbindingproxy.h @@ -210,7 +210,7 @@ private: void setDefaultRelativeLeftTarget(); void setDefaultRelativeRightTarget(); - RewriterTransaction beginRewriterTransaction(const QByteArray &identifier); + bool executeInTransaction(const QByteArray &identifier, const AbstractView::OperationBlock &lambda); QmlItemNode targetIdToNode(const QString &id) const; QString idForNode(const QmlItemNode &qmlItemNode) const; diff --git a/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.cpp b/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.cpp index 6f56b055c8..934c284691 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.cpp @@ -23,6 +23,7 @@ ** ****************************************************************************/ +#include "abstractview.h" #include "qmlmodelnodeproxy.h" #include <QtQml> @@ -66,4 +67,34 @@ ModelNode QmlModelNodeProxy::modelNode() const return m_qmlItemNode.modelNode(); } +bool QmlModelNodeProxy::multiSelection() const +{ + if (!m_qmlItemNode.isValid()) + return false; + + return m_qmlItemNode.view()->selectedModelNodes().count() > 1; +} + +QString QmlModelNodeProxy::nodeId() const +{ + if (!m_qmlItemNode.isValid()) + return {}; + + if (multiSelection()) + return tr("multiselection"); + + return m_qmlItemNode.id(); +} + +QString QmlModelNodeProxy::simplifiedTypeName() const +{ + if (!m_qmlItemNode.isValid()) + return {}; + + if (multiSelection()) + return tr("multiselection"); + + return m_qmlItemNode.simplifiedTypeName(); +} + } diff --git a/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.h b/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.h index 0a73583355..6037f32752 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.h +++ b/src/plugins/qmldesigner/components/propertyeditor/qmlmodelnodeproxy.h @@ -35,7 +35,8 @@ class QmlModelNodeProxy : public QObject { Q_OBJECT - Q_PROPERTY(QmlDesigner::ModelNode modelNode READ modelNode NOTIFY modelNodeChanged) + Q_PROPERTY(QmlDesigner::ModelNode modelNode READ modelNode NOTIFY modelNodeChanged) + Q_PROPERTY(bool multiSelection READ multiSelection NOTIFY modelNodeChanged) public: explicit QmlModelNodeProxy(QObject *parent = nullptr); @@ -51,6 +52,12 @@ public: ModelNode modelNode() const; + bool multiSelection() const; + + QString nodeId() const; + + QString simplifiedTypeName() const; + signals: void modelNodeChanged(); void selectionToBeChanged(); diff --git a/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp b/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp index 4e58374759..862d16ba22 100644 --- a/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp +++ b/src/plugins/qmldesigner/components/propertyeditor/quick2propertyeditorview.cpp @@ -28,6 +28,9 @@ #include "propertyeditorvalue.h" #include "fileresourcesmodel.h" #include "gradientmodel.h" +#include "gradientpresetdefaultlistmodel.h" +#include "gradientpresetcustomlistmodel.h" +#include "simplecolorpalettemodel.h" #include "qmlanchorbindingproxy.h" #include "theme.h" @@ -48,6 +51,9 @@ void Quick2PropertyEditorView::registerQmlTypes() PropertyEditorValue::registerDeclarativeTypes(); FileResourcesModel::registerDeclarativeType(); GradientModel::registerDeclarativeType(); + GradientPresetDefaultListModel::registerDeclarativeType(); + GradientPresetCustomListModel::registerDeclarativeType(); + SimpleColorPaletteModel::registerDeclarativeType(); Internal::QmlAnchorBindingProxy::registerDeclarativeType(); } } diff --git a/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalette.cpp b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalette.cpp new file mode 100644 index 0000000000..c88d83aa0a --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalette.cpp @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#include "simplecolorpalette.h" + +#include "designersettings.h" + +#include <QDebug> + +namespace QmlDesigner { + +PaletteColor::PaletteColor() + : m_color(QColor()) + , m_colorCode(QColor().name()) + , m_isFavorite(false) +{} + +PaletteColor::PaletteColor(const QString &colorCode) + : m_color(colorCode) + , m_colorCode(colorCode) + , m_isFavorite(false) +{} + +PaletteColor::PaletteColor(const QColor &color) + : m_color(color) + , m_colorCode(color.name(QColor::HexArgb)) + , m_isFavorite(false) +{} + +QVariant PaletteColor::getProperty(Property id) const +{ + QVariant out; + + switch (id) { + case objectNameRole: + out.setValue(QString()); + break; + case colorRole: + out.setValue(color()); + break; + case colorCodeRole: + out.setValue(colorCode()); + break; + case isFavoriteRole: + out.setValue(isFavorite()); + break; + default: + qWarning() << "PaletteColor Property switch default case"; + break; //replace with assert before switch? + } + + return out; +} + +QColor PaletteColor::color() const +{ + return m_color; +} + +void PaletteColor::setColor(const QColor &value) +{ + m_color = value; + m_colorCode = m_color.name(QColor::HexArgb); +} + +QString PaletteColor::colorCode() const +{ + return m_colorCode; +} + +bool PaletteColor::isFavorite() const +{ + return m_isFavorite; +} + +void PaletteColor::setFavorite(bool favorite) +{ + m_isFavorite = favorite; +} + +bool PaletteColor::toggleFavorite() +{ + return m_isFavorite = !m_isFavorite; +} + +bool PaletteColor::operator==(const PaletteColor &other) const +{ + return (m_color == other.m_color); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalette.h b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalette.h new file mode 100644 index 0000000000..342c9832e7 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalette.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** 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 <QObject> +#include <QtQml/qqml.h> +#include <QColor> + +namespace QmlDesigner { + +class PaletteColor +{ + Q_GADGET + + Q_PROPERTY(QColor color READ color FINAL) + Q_PROPERTY(QString colorCode READ colorCode FINAL) + Q_PROPERTY(bool isFavorite READ isFavorite FINAL) +public: + PaletteColor(); + PaletteColor(const QString &colorCode); + PaletteColor(const QColor &value); + ~PaletteColor() = default; + + enum Property { + objectNameRole = 0, + colorRole = 1, + colorCodeRole = 2, + isFavoriteRole = 3 + }; + + QVariant getProperty(Property id) const; + + QColor color() const; + void setColor(const QColor &value); + + QString colorCode() const; + + bool isFavorite() const; + void setFavorite(bool favorite); + bool toggleFavorite(); + + bool operator==(const PaletteColor &other) const; + +private: + QColor m_color; + QString m_colorCode; + bool m_isFavorite; +}; + +} // namespace QmlDesigner + +Q_DECLARE_METATYPE(QmlDesigner::PaletteColor) diff --git a/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettemodel.cpp b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettemodel.cpp new file mode 100644 index 0000000000..b3207f0006 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettemodel.cpp @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#include "simplecolorpalettemodel.h" +#include "simplecolorpalette.h" +#include "simplecolorpalettesingleton.h" + +#include "designersettings.h" + +#include <QHash> +#include <QByteArray> +#include <QDebug> +#include <QSettings> + +namespace QmlDesigner { + +SimpleColorPaletteModel::SimpleColorPaletteModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(&SimpleColorPaletteSingleton::getInstance(), + &SimpleColorPaletteSingleton::paletteChanged, + this, + &SimpleColorPaletteModel::setPalette); + m_roleNames = {{static_cast<int>(PaletteColor::Property::objectNameRole), "objectName"}, + {static_cast<int>(PaletteColor::Property::colorRole), "color"}, + {static_cast<int>(PaletteColor::Property::colorCodeRole), "colorCode"}, + {static_cast<int>(PaletteColor::Property::isFavoriteRole), "isFavorite"}}; + + setPalette(); +} + +SimpleColorPaletteModel::~SimpleColorPaletteModel() +{ + clearItems(); +} + +int SimpleColorPaletteModel::rowCount(const QModelIndex & /*parent*/) const +{ + return m_items.count(); +} + +QVariant SimpleColorPaletteModel::data(const QModelIndex &index, int role) const +{ + if (index.isValid() && (index.row() >= 0) && (index.row() < m_items.count())) { + if (m_roleNames.contains(role)) { + QVariant value = m_items.at(index.row()) + .getProperty(static_cast<PaletteColor::Property>(role)); + if (auto model = qobject_cast<SimpleColorPaletteModel *>(value.value<QObject *>())) + return QVariant::fromValue(model); + + return value; + } + + qWarning() << Q_FUNC_INFO << "invalid role requested"; + return QVariant(); + } + + qWarning() << Q_FUNC_INFO << "invalid index requested"; + return QVariant(); +} + +QHash<int, QByteArray> SimpleColorPaletteModel::roleNames() const +{ + return m_roleNames; +} + +void SimpleColorPaletteModel::clearItems() +{ + beginResetModel(); + m_items.clear(); + endResetModel(); +} + +void SimpleColorPaletteModel::addItem(const QString &item) +{ + PaletteColor palette(item); + addItem(palette); +} + +void SimpleColorPaletteModel::addItem(const PaletteColor &item) +{ + SimpleColorPaletteSingleton::getInstance().addItem(item); +} + +const QList<PaletteColor> &SimpleColorPaletteModel::items() const +{ + return m_items; +} + +void SimpleColorPaletteModel::sortItems() +{ + SimpleColorPaletteSingleton::getInstance().sortItems(); +} + +void SimpleColorPaletteModel::registerDeclarativeType() +{ + qmlRegisterType<SimpleColorPaletteModel>("HelperWidgets", 2, 0, "SimpleColorPaletteModel"); +} + +void SimpleColorPaletteModel::toggleFavorite(int id) +{ + SimpleColorPaletteSingleton::getInstance().toggleFavorite(id); +} + +void SimpleColorPaletteModel::setPalette() +{ + beginResetModel(); + m_items = SimpleColorPaletteSingleton::getInstance().getItems(); + m_favoriteOffset = SimpleColorPaletteSingleton::getInstance().getFavoriteOffset(); + m_paletteSize = SimpleColorPaletteSingleton::getInstance().getPaletteSize(); + endResetModel(); +} + +bool SimpleColorPaletteModel::read() +{ + return SimpleColorPaletteSingleton::getInstance().readPalette(); +} + +void SimpleColorPaletteModel::write() +{ + SimpleColorPaletteSingleton::getInstance().writePalette(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettemodel.h b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettemodel.h new file mode 100644 index 0000000000..3ed2cc8a31 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettemodel.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 <QAbstractListModel> +#include <QtQml/qqml.h> +#include <QList> + +namespace QmlDesigner { + +class PaletteColor; + +class SimpleColorPaletteModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit SimpleColorPaletteModel(QObject *parent = nullptr); + ~SimpleColorPaletteModel() override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + void clearItems(); + Q_INVOKABLE void addItem(const QString &item); + void addItem(const PaletteColor &item); + + const QList<PaletteColor> &items() const; + + void sortItems(); + + static void registerDeclarativeType(); + + Q_INVOKABLE void toggleFavorite(int id); + + bool read(); + void write(); + +private slots: + void setPalette(); + +private: + void enqueue(const PaletteColor &item); + +private: + int m_paletteSize; + int m_favoriteOffset; + QList<PaletteColor> m_items; + QHash<int, QByteArray> m_roleNames; +}; + +} // namespace QmlDesigner + +QML_DECLARE_TYPE(QmlDesigner::SimpleColorPaletteModel) diff --git a/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettesingleton.cpp b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettesingleton.cpp new file mode 100644 index 0000000000..ccea50bbf2 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettesingleton.cpp @@ -0,0 +1,185 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#include "simplecolorpalettesingleton.h" +#include "simplecolorpalette.h" + +#include "designersettings.h" + +#include <QDebug> +#include <QSettings> + +namespace QmlDesigner { + +SimpleColorPaletteSingleton::SimpleColorPaletteSingleton() + : m_items() + , m_favoriteOffset(0) +{ + if (!readPalette()) { + for (int i = 0; i < m_paletteSize; i++) + m_items.append(PaletteColor()); + } +} + +SimpleColorPaletteSingleton &SimpleColorPaletteSingleton::getInstance() +{ + static SimpleColorPaletteSingleton singleton; + + return singleton; +} + +void SimpleColorPaletteSingleton::addItem(const PaletteColor &item) +{ + if (m_favoriteOffset >= m_paletteSize) + return; + + if (item.isFavorite()) { + int contains = m_items.indexOf(item); + if (contains != -1) { + if (m_items.at(contains).isFavorite()) + return; + else + m_items.removeAt(contains); + } + m_items.insert(0, item); + m_favoriteOffset++; + } else if (m_items.contains(item)) + return; + else + m_items.insert(m_favoriteOffset, item); + + while (m_items.size() > m_paletteSize) { + m_items.removeLast(); + } + + writePalette(); + + emit paletteChanged(); +} + +QList<PaletteColor> SimpleColorPaletteSingleton::getItems() const +{ + return m_items; +} + +int SimpleColorPaletteSingleton::getPaletteSize() const +{ + return m_paletteSize; +} + +int SimpleColorPaletteSingleton::getFavoriteOffset() const +{ + return m_favoriteOffset; +} + +void SimpleColorPaletteSingleton::sortItems() +{ + auto itemSort = [](const PaletteColor &first, const PaletteColor &second) { + return (static_cast<int>(first.isFavorite()) < static_cast<int>(second.isFavorite())); + }; + + std::sort(m_items.begin(), m_items.end(), itemSort); + + emit paletteChanged(); +} + +void SimpleColorPaletteSingleton::toggleFavorite(int id) +{ + bool toggleResult = m_items[id].toggleFavorite(); + + if (toggleResult) { + m_favoriteOffset++; + m_items.move(id, 0); + } else { + m_favoriteOffset--; + m_items.move(id, m_favoriteOffset); + } + + if (m_favoriteOffset < 0) + m_favoriteOffset = 0; + else if (m_favoriteOffset > m_paletteSize) + m_favoriteOffset = m_paletteSize; + + emit paletteChanged(); +} + +bool SimpleColorPaletteSingleton::readPalette() +{ + QList<PaletteColor> proxy; + const QStringList stringData = QmlDesigner::DesignerSettings::getValue( + QmlDesigner::DesignerSettingsKey::SIMPLE_COLOR_PALETTE_CONTENT) + .toStringList(); + + int favCounter = 0; + + for (int i = 0; i < stringData.size(); i++) { + const QStringList strsep = stringData.at(i).split(";"); + if (strsep.size() != 2) { + continue; + } + PaletteColor colorItem(strsep.at(0)); + bool isFav = static_cast<bool>(strsep.at(1).toInt()); + colorItem.setFavorite(isFav); + if (isFav) + favCounter++; + proxy.append(colorItem); + } + + if (proxy.size() == 0) { + return false; + } + + while (proxy.size() > m_paletteSize) { + proxy.removeLast(); + } + while (proxy.size() < m_paletteSize) { + proxy.append(PaletteColor()); + } + + m_items.clear(); + m_items = proxy; + m_favoriteOffset = favCounter; + + return true; +} + +void SimpleColorPaletteSingleton::writePalette() +{ + QStringList output; + QString subres; + + for (int i = 0; i < m_items.size(); i++) { + subres = m_items.at(i).color().name(QColor::HexArgb); + subres += ";"; + subres += QString::number(static_cast<int>(m_items.at(i).isFavorite())); + output.push_back(subres); + subres.clear(); + } + + QmlDesigner::DesignerSettings::setValue( + QmlDesigner::DesignerSettingsKey::SIMPLE_COLOR_PALETTE_CONTENT, output); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettesingleton.h b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettesingleton.h new file mode 100644 index 0000000000..77ad2b0732 --- /dev/null +++ b/src/plugins/qmldesigner/components/propertyeditor/simplecolorpalettesingleton.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** 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 <QObject> +#include <QAbstractListModel> +#include <QtQml/qqml.h> +#include <QList> +#include <QColor> +#include <simplecolorpalette.h> + +namespace QmlDesigner { + +class SimpleColorPaletteSingleton : public QObject +{ + Q_OBJECT +public: + static SimpleColorPaletteSingleton &getInstance(); + + bool readPalette(); + void writePalette(); + + void addItem(const PaletteColor &item); + QList<PaletteColor> getItems() const; + + int getPaletteSize() const; + int getFavoriteOffset() const; + + void sortItems(); + + void toggleFavorite(int id); + + SimpleColorPaletteSingleton(const SimpleColorPaletteSingleton &) = delete; + void operator=(const SimpleColorPaletteSingleton &) = delete; + +signals: + void paletteChanged(); + +private: + SimpleColorPaletteSingleton(); + +private: + QList<PaletteColor> m_items; + const int m_paletteSize = 6; + int m_favoriteOffset; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/sourcetool/sourcetool.cpp b/src/plugins/qmldesigner/components/sourcetool/sourcetool.cpp new file mode 100644 index 0000000000..d121e60c73 --- /dev/null +++ b/src/plugins/qmldesigner/components/sourcetool/sourcetool.cpp @@ -0,0 +1,252 @@ +/**************************************************************************** +** +** 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 "sourcetool.h" + +#include "formeditorscene.h" +#include "formeditorview.h" +#include "formeditorwidget.h" +#include "itemutilfunctions.h" +#include "formeditoritem.h" + +#include "resizehandleitem.h" + +#include "nodemetainfo.h" +#include "qmlitemnode.h" +#include <qmldesignerplugin.h> + +#include <abstractaction.h> + +#include <utils/icon.h> + +#include <QApplication> +#include <QGraphicsSceneMouseEvent> +#include <QAction> +#include <QDebug> +#include <QPair> +#include <QUrl> + +namespace { + +bool modelNodeHasUrlSource(const QmlDesigner::ModelNode &modelNode) +{ + QmlDesigner::NodeMetaInfo metaInfo = modelNode.metaInfo(); + if (metaInfo.isValid()) { + if (metaInfo.hasProperty("source")) { + if (metaInfo.propertyTypeName("source") == "QUrl") + return true; + if (metaInfo.propertyTypeName("source") == "url") + return true; + } + } + return false; +} + +} //namespace + +namespace QmlDesigner { + +class SourceToolAction : public AbstractAction +{ +public: + SourceToolAction() : AbstractAction(QCoreApplication::translate("SourceToolAction","Change Source URL...")) + { + const Utils::Icon prevIcon({ + {QLatin1String(":/utils/images/fileopen.png"), Utils::Theme::OutputPanes_NormalMessageTextColor}}, Utils::Icon::MenuTintedStyle); + + action()->setIcon(prevIcon.icon()); + } + + QByteArray category() const override + { + return QByteArray(); + } + + QByteArray menuId() const override + { + return "SourceTool"; + } + + int priority() const override + { + return CustomActionsPriority; + } + + Type type() const override + { + return FormEditorAction; + } + +protected: + bool isVisible(const SelectionContext &selectionContext) const override + { + if (selectionContext.singleNodeIsSelected()) + return modelNodeHasUrlSource(selectionContext.currentSingleSelectedNode()); + return false; + } + + bool isEnabled(const SelectionContext &selectionContext) const override + { + return isVisible(selectionContext); + } +}; + + +SourceTool::SourceTool() +{ + auto sourceToolAction = new SourceToolAction; + QmlDesignerPlugin::instance()->designerActionManager().addDesignerAction(sourceToolAction); + connect(sourceToolAction->action(), &QAction::triggered, [=]() { + view()->changeCurrentToolTo(this); + }); +} + +SourceTool::~SourceTool() = default; + +void SourceTool::clear() +{ + AbstractFormEditorTool::clear(); +} + +void SourceTool::mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) +{ + AbstractFormEditorTool::mousePressEvent(itemList, event); +} + +void SourceTool::mouseMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * /*event*/) +{ +} + +void SourceTool::hoverMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * /*event*/) +{ +} + +void SourceTool::keyPressEvent(QKeyEvent * /*keyEvent*/) +{ +} + +void SourceTool::keyReleaseEvent(QKeyEvent * /*keyEvent*/) +{ +} + +void SourceTool::dragLeaveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ +} + +void SourceTool::dragMoveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ +} + +void SourceTool::mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) +{ + AbstractFormEditorTool::mouseReleaseEvent(itemList, event); +} + +void SourceTool::mouseDoubleClickEvent(const QList<QGraphicsItem*> &itemList, QGraphicsSceneMouseEvent *event) +{ + AbstractFormEditorTool::mouseDoubleClickEvent(itemList, event); +} + +void SourceTool::itemsAboutToRemoved(const QList<FormEditorItem*> &removedItemList) +{ + if (removedItemList.contains(m_formEditorItem)) + view()->changeToSelectionTool(); +} + +static QString baseDirectory(const QUrl &url) +{ + QString filePath = url.toLocalFile(); + return QFileInfo(filePath).absoluteDir().path(); +} + +void SourceTool::selectedItemsChanged(const QList<FormEditorItem*> &itemList) +{ + if (!itemList.isEmpty()) { + m_formEditorItem = itemList.constFirst(); + m_oldFileName = m_formEditorItem->qmlItemNode().modelValue("source").toString(); + + QString openDirectory = baseDirectory(view()->model()->fileUrl()); + if (openDirectory.isEmpty()) + openDirectory = baseDirectory(view()->model()->fileUrl()); + + QString fileName = QFileDialog::getOpenFileName(nullptr, + tr("Open File"), + openDirectory); + fileSelected(fileName); + + } else { + view()->changeToSelectionTool(); + } +} + +void SourceTool::instancesCompleted(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +void SourceTool::instancesParentChanged(const QList<FormEditorItem *> & /*itemList*/) +{ +} + +void SourceTool::instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > & /*propertyList*/) +{ +} + +void SourceTool::formEditorItemsChanged(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +int SourceTool::wantHandleItem(const ModelNode &modelNode) const +{ + if (modelNodeHasUrlSource(modelNode)) + return 15; + + return 0; +} + +QString SourceTool::name() const +{ + return tr("Source Tool"); +} + +void SourceTool::fileSelected(const QString &fileName) +{ + if (m_formEditorItem + && QFileInfo(fileName).isFile()) { + QString modelFilePath = view()->model()->fileUrl().toLocalFile(); + QDir modelFileDirectory = QFileInfo(modelFilePath).absoluteDir(); + QString relativeFilePath = modelFileDirectory.relativeFilePath(fileName); + if (m_oldFileName != relativeFilePath) { + m_formEditorItem->qmlItemNode().setVariantProperty("source", relativeFilePath); + } + } + + view()->changeToSelectionTool(); +} + +} diff --git a/src/plugins/qmldesigner/components/sourcetool/sourcetool.h b/src/plugins/qmldesigner/components/sourcetool/sourcetool.h new file mode 100644 index 0000000000..16797b3912 --- /dev/null +++ b/src/plugins/qmldesigner/components/sourcetool/sourcetool.h @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include <abstractcustomtool.h> +#include "selectionindicator.h" + +#include <QHash> +#include <QPointer> +#include <QFileDialog> + +namespace QmlDesigner { + +class SelectionContext; + +class SourceTool : public QObject, public AbstractCustomTool +{ + Q_OBJECT +public: + SourceTool(); + ~SourceTool() override; + + void mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void hoverMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; + + void dragLeaveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + void dragMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + + void itemsAboutToRemoved(const QList<FormEditorItem*> &itemList) override; + + void selectedItemsChanged(const QList<FormEditorItem*> &itemList) override; + + void instancesCompleted(const QList<FormEditorItem*> &itemList) override; + void instancesParentChanged(const QList<FormEditorItem *> &itemList) override; + void instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > &propertyList) override; + + void clear() override; + + void formEditorItemsChanged(const QList<FormEditorItem*> &itemList) override; + + int wantHandleItem(const ModelNode &modelNode) const override; + + QString name() const override; + +private: + /* private methods */ + void fileSelected(const QString &fileName); + + + /* members */ + FormEditorItem *m_formEditorItem = nullptr; + QString m_oldFileName; +}; + +} diff --git a/src/plugins/qmldesigner/components/sourcetool/sourcetool.pri b/src/plugins/qmldesigner/components/sourcetool/sourcetool.pri new file mode 100644 index 0000000000..8117fd73e9 --- /dev/null +++ b/src/plugins/qmldesigner/components/sourcetool/sourcetool.pri @@ -0,0 +1,3 @@ +HEADERS += $$PWD/sourcetool.h + +SOURCES += $$PWD/sourcetool.cpp diff --git a/src/plugins/qmldesigner/components/stateseditor/stateseditormodel.cpp b/src/plugins/qmldesigner/components/stateseditor/stateseditormodel.cpp index 07e758b35a..ae7400e382 100644 --- a/src/plugins/qmldesigner/components/stateseditor/stateseditormodel.cpp +++ b/src/plugins/qmldesigner/components/stateseditor/stateseditormodel.cpp @@ -185,6 +185,7 @@ void StatesEditorModel::renameState(int internalNodeId, const QString &newName) newName.isEmpty() ? tr("The empty string as a name is reserved for the base state.") : tr("Name already used in another state")); + reset(); } else { m_statesEditorView->renameState(internalNodeId, newName); } diff --git a/src/plugins/qmldesigner/components/stateseditor/stateseditorview.h b/src/plugins/qmldesigner/components/stateseditor/stateseditorview.h index 2f8bc56650..572b454892 100644 --- a/src/plugins/qmldesigner/components/stateseditor/stateseditorview.h +++ b/src/plugins/qmldesigner/components/stateseditor/stateseditorview.h @@ -31,7 +31,6 @@ namespace QmlDesigner { - class StatesEditorModel; class StatesEditorWidget; diff --git a/src/plugins/qmldesigner/components/texttool/textedititem.cpp b/src/plugins/qmldesigner/components/texttool/textedititem.cpp new file mode 100644 index 0000000000..1ce3244cbc --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/textedititem.cpp @@ -0,0 +1,98 @@ +/**************************************************************************** +** +** 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 "textedititem.h" + +#include <formeditorscene.h> +#include <nodemetainfo.h> +#include <rewritingexception.h> + +#include <QLineEdit> +#include <QTextEdit> + +namespace QmlDesigner { + +TextEditItem::TextEditItem(FormEditorScene* scene) + : TextEditItemWidget(scene) + , m_formEditorItem(nullptr) +{ + connect(lineEdit(), &QLineEdit::returnPressed, this, &TextEditItem::returnPressed); +} + +TextEditItem::~TextEditItem() +{ + m_formEditorItem = nullptr; +} + +void TextEditItem::writeTextToProperty() +{ + if (m_formEditorItem) { + try { + if (text().isEmpty()) + m_formEditorItem->qmlItemNode().removeProperty("text"); + else if (m_formEditorItem->qmlItemNode().isTranslatableText("text")) + m_formEditorItem->qmlItemNode().setBindingProperty("text", QmlObjectNode::generateTranslatableText(text())); + else + m_formEditorItem->qmlItemNode().setVariantProperty("text", text()); + } + catch (const RewritingException &e) { + e.showException(); + } + } +} + +void TextEditItem::setFormEditorItem(FormEditorItem *formEditorItem) +{ + m_formEditorItem = formEditorItem; + QRectF rect = formEditorItem->qmlItemNode().instancePaintedBoundingRect().united(formEditorItem->qmlItemNode().instanceBoundingRect()).adjusted(-12, -4, 12 ,4); + setGeometry(rect); + + NodeMetaInfo metaInfo = m_formEditorItem->qmlItemNode().modelNode().metaInfo(); + if (metaInfo.isValid() && + (metaInfo.isSubclassOf("QtQuick.TextEdit") + || metaInfo.isSubclassOf("QtQuick.Controls.TextArea"))) { + QSize maximumSize = rect.size().toSize(); + activateTextEdit(maximumSize); + } else { + activateLineEdit(); + } + + setTransform(formEditorItem->sceneTransform()); + updateText(); +} + +FormEditorItem *TextEditItem::formEditorItem() const +{ + return m_formEditorItem; +} + +void TextEditItem::updateText() +{ + if (formEditorItem()) { + TextEditItemWidget::updateText(formEditorItem()->qmlItemNode(). + stripedTranslatableText("text")); + } +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/texttool/textedititem.h b/src/plugins/qmldesigner/components/texttool/textedititem.h new file mode 100644 index 0000000000..5497dadbe1 --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/textedititem.h @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ +#pragma once + +#include "textedititemwidget.h" + +namespace QmlDesigner { + +class FormEditorScene; +class FormEditorItem; + +class TextEditItem : public TextEditItemWidget +{ + Q_OBJECT +public: + TextEditItem(FormEditorScene* scene); + ~TextEditItem() override; + int type() const override; + + void setFormEditorItem(FormEditorItem *formEditorItem); + FormEditorItem *formEditorItem() const; + + void updateText(); + void writeTextToProperty(); + +signals: + void returnPressed(); +private: + FormEditorItem *m_formEditorItem; +}; + +inline int TextEditItem::type() const +{ + return 0xEAAB; +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/texttool/textedititemwidget.cpp b/src/plugins/qmldesigner/components/texttool/textedititemwidget.cpp new file mode 100644 index 0000000000..40efb67e77 --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/textedititemwidget.cpp @@ -0,0 +1,125 @@ +/**************************************************************************** +** +** 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 "textedititemwidget.h" + +#include <utils/theme/theme.h> + +#include <QLineEdit> +#include <QGraphicsScene> +#include <QPainter> +#include <QTextEdit> + +namespace QmlDesigner { + +TextEditItemWidget::TextEditItemWidget(QGraphicsScene* scene) +{ + scene->addItem(this); + setFlag(QGraphicsItem::ItemIsMovable, false); + activateLineEdit(); +} + +TextEditItemWidget::~TextEditItemWidget() +{ + setWidget(nullptr); +} + +void TextEditItemWidget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) +{ + painter->fillRect(boundingRect(), Qt::white); + + /* Cursor painting is broken. + * QGraphicsProxyWidget::paint(painter, option, widget); + * We draw manually instead. + */ + + QPixmap pixmap = widget()->grab(); + painter->drawPixmap(0, 0, pixmap); +} + +QLineEdit* TextEditItemWidget::lineEdit() const +{ + if (m_lineEdit.isNull()) { + m_lineEdit.reset(new QLineEdit); + m_lineEdit->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + QPalette palette = m_lineEdit->palette(); + static QColor selectionColor = Utils::creatorTheme()->color(Utils::Theme::QmlDesigner_FormEditorSelectionColor); + palette.setColor(QPalette::Highlight, selectionColor); + palette.setColor(QPalette::HighlightedText, Qt::white); + palette.setColor(QPalette::Base, Qt::white); + palette.setColor(QPalette::Text, Qt::black); + m_lineEdit->setPalette(palette); + } + return m_lineEdit.data(); +} + +QTextEdit* TextEditItemWidget::textEdit() const +{ + if (m_textEdit.isNull()) { + m_textEdit.reset(new QTextEdit); + QPalette palette = m_textEdit->palette(); + static QColor selectionColor = Utils::creatorTheme()->color(Utils::Theme::QmlDesigner_FormEditorSelectionColor); + palette.setColor(QPalette::Highlight, selectionColor); + palette.setColor(QPalette::HighlightedText, Qt::white); + palette.setColor(QPalette::Base, Qt::white); + palette.setColor(QPalette::Text, Qt::black); + m_textEdit->setPalette(palette); + } + + return m_textEdit.data(); +} + +void TextEditItemWidget::activateTextEdit(const QSize &maximumSize) +{ + textEdit()->setMaximumSize(maximumSize); + textEdit()->setFocus(); + setWidget(textEdit()); +} + +void TextEditItemWidget::activateLineEdit() +{ + lineEdit()->setFocus(); + setWidget(lineEdit()); +} + +QString TextEditItemWidget::text() const +{ + if (widget() == m_lineEdit.data()) + return m_lineEdit->text(); + else if (widget() == m_textEdit.data()) + return m_textEdit->toPlainText(); + return QString(); +} + +void TextEditItemWidget::updateText(const QString &text) +{ + if (widget() == m_lineEdit.data()) { + m_lineEdit->setText(text); + m_lineEdit->selectAll(); + } else if (widget() == m_textEdit.data()) { + m_textEdit->setText(text); + m_textEdit->selectAll(); + } +} +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/texttool/textedititemwidget.h b/src/plugins/qmldesigner/components/texttool/textedititemwidget.h new file mode 100644 index 0000000000..7ff2909919 --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/textedititemwidget.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ +#pragma once + +#include <QGraphicsProxyWidget> +#include <QScopedPointer> + +QT_BEGIN_NAMESPACE +class QTextEdit; +class QLineEdit; +class QGraphicsScene; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class TextEditItemWidget : public QGraphicsProxyWidget +{ + Q_OBJECT +public: + TextEditItemWidget(QGraphicsScene *scene); + ~TextEditItemWidget() override; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + void activateTextEdit(const QSize &maximumSize); + void activateLineEdit(); + void updateText(const QString &text); + +protected: + QLineEdit* lineEdit() const; + QTextEdit* textEdit() const; + + QString text() const; +private: + mutable QScopedPointer<QLineEdit> m_lineEdit; + mutable QScopedPointer<QTextEdit> m_textEdit; +}; +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/texttool/texttool.cpp b/src/plugins/qmldesigner/components/texttool/texttool.cpp new file mode 100644 index 0000000000..76dc414c03 --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/texttool.cpp @@ -0,0 +1,270 @@ +/**************************************************************************** +** +** 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 "texttool.h" + +#include "formeditorscene.h" +#include "formeditorview.h" +#include "formeditorwidget.h" +#include "itemutilfunctions.h" +#include "formeditoritem.h" + +#include "resizehandleitem.h" +#include "textedititem.h" + +#include "nodemetainfo.h" +#include "qmlitemnode.h" +#include <qmldesignerplugin.h> + +#include <abstractaction.h> +#include <designeractionmanager.h> + +#include <QApplication> +#include <QGraphicsSceneMouseEvent> +#include <QAction> +#include <QDebug> +#include <QPair> + +namespace QmlDesigner { + +class TextToolAction : public AbstractAction +{ +public: + TextToolAction() : AbstractAction(QCoreApplication::translate("TextToolAction","Edit Text")) {} + + QByteArray category() const override + { + return QByteArray(); + } + + QByteArray menuId() const override + { + return "TextTool"; + } + + int priority() const override + { + return CustomActionsPriority; + } + + Type type() const override + { + return ContextMenuAction; + } + +protected: + bool isVisible(const SelectionContext &selectionContext) const override + { + if (selectionContext.scenePosition().isNull()) + return false; + + if (selectionContext.singleNodeIsSelected()) + return selectionContext.currentSingleSelectedNode().metaInfo().hasProperty("text"); + + return false; + } + + bool isEnabled(const SelectionContext &selectionContext) const override + { + return isVisible(selectionContext); + } +}; + +TextTool::TextTool() +{ + auto textToolAction = new TextToolAction; + QmlDesignerPlugin::instance()->designerActionManager().addDesignerAction(textToolAction); + connect(textToolAction->action(), &QAction::triggered, [=]() { + view()->changeCurrentToolTo(this); + }); +} + +TextTool::~TextTool() = default; + +void TextTool::clear() +{ + if (textItem()) { + textItem()->clearFocus(); + textItem()->deleteLater(); + } + + AbstractFormEditorTool::clear(); +} + +void TextTool::mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) +{ + event->setPos(textItem()->mapFromScene(event->scenePos())); + event->setLastPos(textItem()->mapFromScene(event->lastScenePos())); + scene()->sendEvent(textItem(), event); + AbstractFormEditorTool::mousePressEvent(itemList, event); +} + +void TextTool::mouseMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent *event) +{ event->setPos(textItem()->mapFromScene(event->scenePos())); + event->setLastPos(textItem()->mapFromScene(event->lastScenePos())); + scene()->sendEvent(textItem(), event); +} + +void TextTool::hoverMoveEvent(const QList<QGraphicsItem*> & /*itemList*/, + QGraphicsSceneMouseEvent * event) +{ + event->setPos(textItem()->mapFromScene(event->scenePos())); + event->setLastPos(textItem()->mapFromScene(event->lastScenePos())); + scene()->sendEvent(textItem(), event); +} + +void TextTool::keyPressEvent(QKeyEvent *keyEvent) +{ + if (keyEvent->key() == Qt::Key_Escape) { + textItem()->writeTextToProperty(); + keyEvent->accept(); + } else { + scene()->sendEvent(textItem(), keyEvent); + } +} + +void TextTool::keyReleaseEvent(QKeyEvent *keyEvent) +{ + if (keyEvent->key() == Qt::Key_Escape) { + keyEvent->accept(); + view()->changeToSelectionTool(); + } else { + scene()->sendEvent(textItem(), keyEvent); + } +} + +void TextTool::dragLeaveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ + +} + +void TextTool::dragMoveEvent(const QList<QGraphicsItem*> &/*itemList*/, QGraphicsSceneDragDropEvent * /*event*/) +{ + +} + +void TextTool::mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) +{ + if (!itemList.contains(textItem())) { + textItem()->writeTextToProperty(); + view()->changeToSelectionTool(); + } + AbstractFormEditorTool::mouseReleaseEvent(itemList, event); +} + + +void TextTool::mouseDoubleClickEvent(const QList<QGraphicsItem*> & /*itemList*/, QGraphicsSceneMouseEvent *event) +{ + if (textItem() && !textItem()->boundingRect().contains(textItem()->mapFromScene(event->scenePos()))) { + textItem()->writeTextToProperty(); + view()->changeToSelectionTool(); + } else { + event->setPos(textItem()->mapFromScene(event->scenePos())); + event->setLastPos(textItem()->mapFromScene(event->lastScenePos())); + scene()->sendEvent(textItem(), event); + } +} + +void TextTool::itemsAboutToRemoved(const QList<FormEditorItem*> &removedItemList) +{ + if (textItem() == nullptr) + return; + + if (removedItemList.contains(textItem()->formEditorItem())) + view()->changeToSelectionTool(); +} + +void TextTool::selectedItemsChanged(const QList<FormEditorItem*> &itemList) +{ + if (textItem()) { + textItem()->writeTextToProperty(); + view()->changeToSelectionTool(); + } + if (!itemList.isEmpty()) { + FormEditorItem *formEditorItem = itemList.constFirst(); + m_textItem = new TextEditItem(scene()); + textItem()->setParentItem(scene()->manipulatorLayerItem()); + textItem()->setFormEditorItem(formEditorItem); + connect(textItem(), &TextEditItem::returnPressed, [this] { + textItem()->writeTextToProperty(); + view()->changeToSelectionTool(); + }); + } else { + view()->changeToSelectionTool(); + } +} + +void TextTool::instancesCompleted(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +void TextTool::instancesParentChanged(const QList<FormEditorItem *> & /*itemList*/) +{ +} + +void TextTool::instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > &propertyList) +{ + using ModelNodePropertyNamePair = QPair<ModelNode, PropertyName>; + foreach (const ModelNodePropertyNamePair &propertyPair, propertyList) { + if (propertyPair.first == textItem()->formEditorItem()->qmlItemNode().modelNode() + && propertyPair.second == "text") + textItem()->updateText(); + } +} + +void TextTool::formEditorItemsChanged(const QList<FormEditorItem*> & /*itemList*/) +{ +} + +int TextTool::wantHandleItem(const ModelNode &modelNode) const +{ + if (modelNode.metaInfo().hasProperty("text")) + return 20; + + return 0; +} + +QString TextTool::name() const +{ + return QCoreApplication::translate("TextTool", "Text Tool"); +} + +void TextTool::focusLost() +{ + if (textItem()) { + textItem()->writeTextToProperty(); + view()->changeToSelectionTool(); + } +} + +TextEditItem *TextTool::textItem() const +{ + return m_textItem.data(); +} + +} diff --git a/src/plugins/qmldesigner/components/texttool/texttool.h b/src/plugins/qmldesigner/components/texttool/texttool.h new file mode 100644 index 0000000000..b7c352c917 --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/texttool.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "abstractcustomtool.h" +#include "selectionindicator.h" + +#include <QHash> +#include <QPointer> +#include <QColorDialog> + +namespace QmlDesigner { + +class TextEditItem; + +class TextTool : public QObject, public AbstractCustomTool +{ + Q_OBJECT +public: + TextTool(); + ~TextTool() override; + + void mousePressEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void hoverMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneMouseEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; + + void dragLeaveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + void dragMoveEvent(const QList<QGraphicsItem*> &itemList, + QGraphicsSceneDragDropEvent * event) override; + + void itemsAboutToRemoved(const QList<FormEditorItem*> &itemList) override; + + void selectedItemsChanged(const QList<FormEditorItem*> &itemList) override; + + void instancesCompleted(const QList<FormEditorItem*> &itemList) override; + void instancesParentChanged(const QList<FormEditorItem *> &itemList) override; + void instancePropertyChange(const QList<QPair<ModelNode, PropertyName> > &propertyList) override; + + void clear() override; + + void formEditorItemsChanged(const QList<FormEditorItem*> &itemList) override; + + int wantHandleItem(const ModelNode &modelNode) const override; + + QString name() const override; + + void focusLost() override; + +protected: + TextEditItem *textItem() const; + +private: + QPointer<TextEditItem> m_textItem; +}; + +} diff --git a/src/plugins/qmldesigner/components/texttool/texttool.pri b/src/plugins/qmldesigner/components/texttool/texttool.pri new file mode 100644 index 0000000000..0cfdfb7629 --- /dev/null +++ b/src/plugins/qmldesigner/components/texttool/texttool.pri @@ -0,0 +1,7 @@ +HEADERS += $$PWD/texttool.h +HEADERS += $$PWD/textedititem.h +HEADERS += $$PWD/textedititemwidget.h + +SOURCES += $$PWD/texttool.cpp +SOURCES += $$PWD/textedititem.cpp +SOURCES += $$PWD/textedititemwidget.cpp diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvas.cpp b/src/plugins/qmldesigner/components/timelineeditor/canvas.cpp new file mode 100644 index 0000000000..eab3d0bf56 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvas.cpp @@ -0,0 +1,359 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "canvas.h" +#include "easingcurve.h" + +#include <QPainter> +#include <QPointF> +#include <QSize> + +namespace QmlDesigner { + +Canvas::Canvas(int width, + int height, + int marginX, + int marginY, + int cellCountX, + int cellCountY, + int offsetX, + int offsetY) + : m_width(width) + , m_height(height) + , m_marginX(marginX) + , m_marginY(marginY) + , m_cellCountX(cellCountX) + , m_cellCountY(cellCountY) + , m_offsetX(offsetX) + , m_offsetY(offsetY) + , m_scale(1.0) +{} + +QRectF Canvas::gridRect() const +{ + double w = static_cast<double>(m_width); + double h = static_cast<double>(m_height); + double mx = static_cast<double>(m_marginX); + double my = static_cast<double>(m_marginY); + double gw = w - 2.0 * mx; + double gh = h - 2.0 * my; + + if (m_style.aspect != 0) { + if (m_style.aspect < (w / h)) + gw = gh * m_style.aspect; + else + gh = gw / m_style.aspect; + } + + auto rect = QRectF(mx, my, gw * m_scale, gh * m_scale); + rect.moveCenter(QPointF(w / 2.0, h / 2.0)); + return rect; +} + +void Canvas::setCanvasStyle(const CanvasStyle &style) +{ + m_style = style; +} + +void Canvas::setScale(double scale) +{ + if (scale > 0.05) + m_scale = scale; +} + +void Canvas::resize(const QSize &size) +{ + m_width = size.width(); + m_height = size.height(); +} + +void Canvas::paintGrid(QPainter *painter, const QBrush &background) +{ + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + QPen pen = painter->pen(); + + pen.setWidthF(m_style.thinLineWidth); + pen.setColor(m_style.thinLineColor); + painter->setPen(pen); + + painter->fillRect(0, 0, m_width, m_height, background); + + QRectF rect = gridRect(); + + // Thin lines. + const int lineCountX = m_cellCountX + 1; + const double cellWidth = rect.width() / static_cast<double>(m_cellCountX); + + // Vertical + double x = rect.left(); + for (int i = 0; i < lineCountX; ++i) { + paintLine(painter, QPoint(x, rect.top()), QPoint(x, rect.bottom())); + x += cellWidth; + } + + const int lineCountY = m_cellCountY + 1; + const double cellHeight = rect.height() / static_cast<double>(m_cellCountY); + + // Horizontal + double y = rect.top(); + for (int i = 0; i < lineCountY; ++i) { + paintLine(painter, QPoint(rect.left(), y), QPoint(rect.right(), y)); + y += cellHeight; + } + + // Thick lines. + pen.setWidthF(m_style.thickLineWidth); + pen.setColor(m_style.thickLineColor); + painter->setPen(pen); + + if (m_offsetX != 0) { + const int minX = rect.left() + (cellWidth * m_offsetX); + const int maxX = rect.right() - (cellWidth * m_offsetX); + paintLine(painter, QPoint(minX, rect.top()), QPoint(minX, rect.bottom())); + paintLine(painter, QPoint(maxX, rect.top()), QPoint(maxX, rect.bottom())); + } + + if (m_offsetY != 0) { + const int minY = rect.top() + (cellHeight * m_offsetY); + const int maxY = rect.bottom() - (cellHeight * m_offsetY); + paintLine(painter, QPoint(rect.left(), minY), QPoint(rect.right(), minY)); + paintLine(painter, QPoint(rect.left(), maxY), QPoint(rect.right(), maxY)); + } + + painter->restore(); +} + +void Canvas::paintCurve(QPainter *painter, const EasingCurve &curve, const QColor &color) +{ + EasingCurve mapped = mapTo(curve); + painter->strokePath(mapped.path(), QPen(QBrush(color), m_style.curveWidth)); +} + +void Canvas::paintControlPoints(QPainter *painter, const EasingCurve &curve) +{ + QVector<QPointF> points = curve.toCubicSpline(); + int count = points.count(); + + if (count <= 1) + return; + + painter->save(); + + QPen pen = painter->pen(); + pen.setWidthF(m_style.handleLineWidth); + pen.setColor(m_style.endPointColor); + + painter->setPen(pen); + painter->setBrush(m_style.endPointColor); + + // First and last point including handle. + paintLine(painter, mapTo(QPointF(0.0, 0.0)).toPoint(), mapTo(points.at(0)).toPoint()); + paintPoint(painter, QPointF(0.0, 0.0), false); + paintPoint(painter, points.at(0), false, curve.active() == 0); + + paintLine(painter, mapTo(QPointF(1.0, 1.0)).toPoint(), mapTo(points.at(count - 2)).toPoint()); + paintPoint(painter, QPointF(1.0, 1.0), false); + paintPoint(painter, points.at(count - 2), false, curve.active() == (count - 2)); + + pen.setColor(m_style.interPointColor); + painter->setPen(pen); + painter->setBrush(m_style.interPointColor); + + for (int i = 0; i < count - 1; ++i) { + if (curve.isHandle(i)) + continue; + + paintLine(painter, mapTo(points.at(i)).toPoint(), mapTo(points.at(i + 1)).toPoint()); + + if (i > 0) + paintLine(painter, mapTo(points.at(i - 1)).toPoint(), mapTo(points.at(i)).toPoint()); + } + + // Paint Points. + int active = curve.active(); + for (int i = 1; i < count - 2; ++i) + paintPoint(painter, points.at(i), curve.isSmooth(i), active == i); + + painter->restore(); +} + +void Canvas::paintProgress(QPainter *painter, const EasingCurve &curve, double progress) +{ + painter->save(); + + painter->setPen(Qt::green); + painter->setBrush(QBrush(Qt::green)); + + QPointF pos1(progress, curve.valueForProgress(progress)); + pos1 = mapTo(pos1); + + QRectF rect = gridRect(); + + painter->drawLine(rect.left(), pos1.y(), rect.right(), pos1.y()); + painter->drawLine(pos1.x(), rect.top(), pos1.x(), rect.bottom()); + + painter->restore(); +} + +QPointF Canvas::mapTo(const QPointF &point) const +{ + QRectF rect = gridRect(); + + const double cellWidth = rect.width() / static_cast<double>(m_cellCountX); + const double cellHeight = rect.height() / static_cast<double>(m_cellCountY); + + const double offsetX = cellWidth * m_offsetX; + const double offsetY = cellHeight * m_offsetY; + + const int width = rect.width() - 2 * offsetX; + const int height = rect.height() - 2 * offsetY; + + auto tmp = QPointF(point.x() * width + rect.left() + offsetX, + height - point.y() * height + rect.top() + offsetY); + + return tmp; +} + +CanvasStyle Canvas::canvasStyle() const +{ + return m_style; +} + +double Canvas::scale() const +{ + return m_scale; +} + +QPointF Canvas::normalize(const QPointF &point) const +{ + QRectF rect = gridRect(); + return QPointF(point.x() / rect.width(), point.y() / rect.height()); +} + +EasingCurve Canvas::mapTo(const EasingCurve &curve) const +{ + QVector<QPointF> controlPoints = curve.toCubicSpline(); + + for (auto &point : controlPoints) + point = mapTo(point); + + return EasingCurve(mapTo(curve.start()), controlPoints); +} + +QPointF Canvas::mapFrom(const QPointF &point) const +{ + QRectF rect = gridRect(); + + const double cellWidth = rect.width() / static_cast<double>(m_cellCountX); + const double cellHeight = rect.height() / static_cast<double>(m_cellCountY); + + const double offsetX = cellWidth * m_offsetX; + const double offsetY = cellHeight * m_offsetY; + + const int width = rect.width() - 2 * offsetX; + const int height = rect.height() - 2 * offsetY; + + return QPointF((point.x() - rect.left() - offsetX) / width, + 1 - (point.y() - rect.top() - offsetY) / height); +} + +EasingCurve Canvas::mapFrom(const EasingCurve &curve) const +{ + QVector<QPointF> controlPoints = curve.toCubicSpline(); + for (auto &point : controlPoints) + point = mapFrom(point); + + EasingCurve result; + result.fromCubicSpline(controlPoints); + return result; +} + +QPointF Canvas::clamp(const QPointF &point) const +{ + QRectF r = gridRect(); + QPointF p = point; + + if (p.x() > r.right()) + p.rx() = r.right(); + + if (p.x() < r.left()) + p.rx() = r.left(); + + if (p.y() < r.top()) + p.ry() = r.top(); + + if (p.y() > r.bottom()) + p.ry() = r.bottom(); + + return p; +} + +void Canvas::paintLine(QPainter *painter, const QPoint &p1, const QPoint &p2) +{ + painter->drawLine(p1 + QPointF(0.5, 0.5), p2 + QPointF(0.5, 0.5)); +} + +void Canvas::paintPoint(QPainter *painter, const QPointF &point, bool smooth, bool active) +{ + const double pointSize = m_style.handleSize; + const double activePointSize = pointSize + 2; + if (smooth) { + if (active) { + painter->save(); + painter->setPen(Qt::white); + painter->setBrush(QBrush()); + painter->drawEllipse(QRectF(mapTo(point).x() - activePointSize + 0.5, + mapTo(point).y() - activePointSize + 0.5, + activePointSize * 2, + activePointSize * 2)); + painter->restore(); + } + + painter->drawEllipse(QRectF(mapTo(point).x() - pointSize + 0.5, + mapTo(point).y() - pointSize + 0.5, + pointSize * 2, + pointSize * 2)); + + } else { + if (active) { + painter->save(); + painter->setPen(Qt::white); + painter->setBrush(QBrush()); + painter->drawRect(QRectF(mapTo(point).x() - activePointSize + 0.5, + mapTo(point).y() - activePointSize + 0.5, + activePointSize * 2, + activePointSize * 2)); + painter->restore(); + } + painter->drawRect(QRectF(mapTo(point).x() - pointSize + 0.5, + mapTo(point).y() - pointSize + 0.5, + pointSize * 2, + pointSize * 2)); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvas.h b/src/plugins/qmldesigner/components/timelineeditor/canvas.h new file mode 100644 index 0000000000..c4491e4b79 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvas.h @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "canvasstyledialog.h" +#include <qglobal.h> + +QT_FORWARD_DECLARE_CLASS(QBrush); +QT_FORWARD_DECLARE_CLASS(QColor); +QT_FORWARD_DECLARE_CLASS(QPainter); +QT_FORWARD_DECLARE_CLASS(QPointF); +QT_FORWARD_DECLARE_CLASS(QPoint); +QT_FORWARD_DECLARE_CLASS(QSize); + +namespace QmlDesigner { + +class EasingCurve; + +class Canvas +{ +public: + Canvas(int width, + int height, + int marginX, + int marginY, + int cellCountX, + int cellCountY, + int offsetX, + int offsetY); + +public: + CanvasStyle canvasStyle() const; + + double scale() const; + + QRectF gridRect() const; + + QPointF normalize(const QPointF &point) const; + + QPointF mapTo(const QPointF &point) const; + + EasingCurve mapTo(const EasingCurve &curve) const; + + QPointF mapFrom(const QPointF &point) const; + + EasingCurve mapFrom(const EasingCurve &curve) const; + + QPointF clamp(const QPointF &point) const; + + void setCanvasStyle(const CanvasStyle &style); + + void setScale(double scale); + + void resize(const QSize &size); + + void paintGrid(QPainter *painter, const QBrush &background); + + void paintCurve(QPainter *painter, const EasingCurve &curve, const QColor &color); + + void paintControlPoints(QPainter *painter, const EasingCurve &curve); + + void paintProgress(QPainter *painter, const EasingCurve &curve, double progress); + +private: + void paintLine(QPainter *painter, const QPoint &p1, const QPoint &p2); + + void paintPoint(QPainter *painter, const QPointF &point, bool smooth, bool active = false); + +private: + int m_width; + int m_height; + + int m_marginX; + int m_marginY; + + int m_cellCountX; + int m_cellCountY; + + int m_offsetX; + int m_offsetY; + + double m_scale; + + CanvasStyle m_style; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp new file mode 100644 index 0000000000..88d78615ca --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.cpp @@ -0,0 +1,124 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "canvasstyledialog.h" + +#include <QColorDialog> +#include <QDoubleSpinBox> +#include <QLabel> +#include <QPaintEvent> +#include <QPainter> +#include <QVBoxLayout> + +namespace QmlDesigner { + +CanvasStyleDialog::CanvasStyleDialog(const CanvasStyle &style, QWidget *parent) + : QDialog(parent) + , m_aspect(new QDoubleSpinBox(this)) + , m_thinLineWidth(new QDoubleSpinBox(this)) + , m_thickLineWidth(new QDoubleSpinBox(this)) + , m_thinLineColor(new ColorControl(style.thinLineColor, this)) + , m_thickLineColor(new ColorControl(style.thickLineColor, this)) + , m_handleSize(new QDoubleSpinBox(this)) + , m_handleLineWidth(new QDoubleSpinBox(this)) + , m_endPointColor(new ColorControl(style.endPointColor, this)) + , m_interPointColor(new ColorControl(style.interPointColor, this)) + , m_curveWidth(new QDoubleSpinBox(this)) +{ + m_aspect->setValue(style.aspect); + m_thinLineWidth->setValue(style.thinLineWidth); + m_thickLineWidth->setValue(style.thickLineWidth); + m_handleSize->setValue(style.handleSize); + m_handleLineWidth->setValue(style.handleLineWidth); + m_curveWidth->setValue(style.curveWidth); + + int labelWidth = QFontMetrics(this->font()).horizontalAdvance("Inter Handle ColorXX"); + auto addControl = [labelWidth](QVBoxLayout *layout, const QString &name, QWidget *control) { + auto *hbox = new QHBoxLayout; + + QLabel *label = new QLabel(name); + label->setAlignment(Qt::AlignLeft); + label->setFixedWidth(labelWidth); + + hbox->addWidget(label); + hbox->addWidget(control); + layout->addLayout(hbox); + }; + + auto layout = new QVBoxLayout; + addControl(layout, "Aspect Ratio", m_aspect); + + addControl(layout, "Thin Line Width", m_thinLineWidth); + addControl(layout, "Thin Line Color", m_thinLineColor); + + addControl(layout, "Thick Line Width", m_thickLineWidth); + addControl(layout, "Thick Line Color", m_thickLineColor); + + addControl(layout, "Handle Size", m_handleSize); + addControl(layout, "Handle Line Width", m_handleLineWidth); + addControl(layout, "End Handle Color", m_endPointColor); + addControl(layout, "Inter Handle Color", m_interPointColor); + + addControl(layout, "Curve Width", m_curveWidth); + + setLayout(layout); + + auto emitValueChanged = [this]() { + CanvasStyle out; + out.aspect = m_aspect->value(); + out.thinLineWidth = m_thinLineWidth->value(); + out.thickLineWidth = m_thickLineWidth->value(); + out.thinLineColor = m_thinLineColor->value(); + out.thickLineColor = m_thickLineColor->value(); + out.handleSize = m_handleSize->value(); + out.handleLineWidth = m_handleLineWidth->value(); + out.endPointColor = m_endPointColor->value(); + out.interPointColor = m_interPointColor->value(); + out.curveWidth = m_curveWidth->value(); + emit styleChanged(out); + }; + + auto doubleValueChanged = QOverload<double>::of( + &QDoubleSpinBox::valueChanged); + auto colorValueChanged = &ColorControl::valueChanged; + + connect(m_aspect, doubleValueChanged, this, emitValueChanged); + + connect(m_thinLineWidth, doubleValueChanged, this, emitValueChanged); + connect(m_thickLineWidth, doubleValueChanged, this, emitValueChanged); + + connect(m_thinLineColor, colorValueChanged, this, emitValueChanged); + connect(m_thickLineColor, colorValueChanged, this, emitValueChanged); + + connect(m_handleSize, doubleValueChanged, this, emitValueChanged); + connect(m_handleLineWidth, doubleValueChanged, this, emitValueChanged); + + connect(m_endPointColor, colorValueChanged, this, emitValueChanged); + connect(m_interPointColor, colorValueChanged, this, emitValueChanged); + + connect(m_curveWidth, doubleValueChanged, this, emitValueChanged); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h new file mode 100644 index 0000000000..b3b1a86c95 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/canvasstyledialog.h @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinecontrols.h" + +#include <QColor> +#include <QDialog> + +QT_FORWARD_DECLARE_CLASS(QDoubleSpinBox); + +namespace QmlDesigner { + +struct CanvasStyle +{ + qreal aspect = 1.5; + + qreal thinLineWidth = 0.3; + qreal thickLineWidth = 2.5; + + QColor thinLineColor = qRgb(0x99, 0x99, 0x99); + QColor thickLineColor = qRgb(0x5f, 0x5f, 0x5f); + + qreal handleSize = 7.0; + qreal handleLineWidth = 2.0; + + QColor endPointColor = qRgb(0xd6, 0xd3, 0x51); + QColor interPointColor = qRgb(0xce, 0x17, 0x17); + + qreal curveWidth = 3.0; +}; + +class CanvasStyleDialog : public QDialog +{ + Q_OBJECT + +public: + CanvasStyleDialog(const CanvasStyle &style, QWidget *parent = nullptr); + +signals: + void styleChanged(const CanvasStyle &style); + +private: + QDoubleSpinBox *m_aspect; + + QDoubleSpinBox *m_thinLineWidth; + QDoubleSpinBox *m_thickLineWidth; + + ColorControl *m_thinLineColor; + ColorControl *m_thickLineColor; + + QDoubleSpinBox *m_handleSize; + QDoubleSpinBox *m_handleLineWidth; + + ColorControl *m_endPointColor; + ColorControl *m_interPointColor; + + QDoubleSpinBox *m_curveWidth; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp new file mode 100644 index 0000000000..cae0bfab09 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.cpp @@ -0,0 +1,502 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "easingcurve.h" +#include "timelineutils.h" + +#include <QDataStream> +#include <QDebug> +#include <QLineF> +#include <QPainterPath> +#include <QPointF> + +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +EasingCurve::EasingCurve() + : QEasingCurve(QEasingCurve::BezierSpline) + , m_active(-1) + , m_start(0.0, 0.0) +{} + +EasingCurve::EasingCurve(const QEasingCurve &curve) + : QEasingCurve(curve) + , m_active(-1) + , m_start(0.0, 0.0) +{} + +EasingCurve::EasingCurve(const EasingCurve &curve) = default; + +EasingCurve::EasingCurve(const QPointF &start, const QVector<QPointF> &points) + : QEasingCurve(QEasingCurve::BezierSpline) + , m_active(-1) + , m_start(start) +{ + fromCubicSpline(points); +} + +EasingCurve &EasingCurve::operator=(const EasingCurve &curve) = default; + +EasingCurve::~EasingCurve() = default; + +bool EasingCurve::IsValidIndex(int idx) +{ + return idx >= 0; +} + +void EasingCurve::registerStreamOperators() +{ + qRegisterMetaType<QmlDesigner::EasingCurve>("QmlDesigner::EasingCurve"); + qRegisterMetaType<QmlDesigner::NamedEasingCurve>("QmlDesigner::NamedEasingCurve"); + qRegisterMetaTypeStreamOperators<QmlDesigner::EasingCurve>("QmlDesigner::EasingCurve"); + qRegisterMetaTypeStreamOperators<QmlDesigner::NamedEasingCurve>("QmlDesigner::NamedEasingCurve"); +} + +int EasingCurve::count() const +{ + return toCubicSpline().count(); +} + +int EasingCurve::active() const +{ + return m_active; +} + +int EasingCurve::segmentCount() const +{ + return toCubicSpline().count() / 3; +} + +bool EasingCurve::isLegal() const +{ + QPainterPath painterPath(path()); + + double increment = 1.0 / 30.0; + QPointF max = painterPath.pointAtPercent(0.0); + for (double i = increment; i <= 1.0; i += increment) { + QPointF current = painterPath.pointAtPercent(i); + if (current.x() < max.x()) + return false; + else + max = current; + } + return true; +} + +bool EasingCurve::hasActive() const +{ + QTC_ASSERT(m_active < toCubicSpline().size(), return false); + return m_active >= 0; +} + +bool EasingCurve::isValidIndex(int idx) const +{ + return idx >= 0 && idx < toCubicSpline().size(); +} + +bool EasingCurve::isSmooth(int idx) const +{ + auto iter = std::find(m_smoothIds.begin(), m_smoothIds.end(), idx); + return iter != m_smoothIds.end(); +} + +bool EasingCurve::isHandle(int idx) const +{ + return (idx + 1) % 3; +} + +bool EasingCurve::isLeftHandle(int idx) const +{ + return ((idx + 2) % 3) == 0; +} + +QString EasingCurve::toString() const +{ + QLatin1Char c(','); + QString s = QLatin1String("["); + for (const QPointF &point : toCubicSpline()) { + auto x = QString::number(point.x(), 'g', 3); + auto y = QString::number(point.y(), 'g', 3); + s += x + c + y + c; + } + + // Replace last "," with "]" + s.chop(1); + s.append(QLatin1Char(']')); + + return s; +} + +bool EasingCurve::fromString(const QString &code) +{ + if (code.startsWith(QLatin1Char('[')) && code.endsWith(QLatin1Char(']'))) { + const QStringRef cleanCode(&code, 1, code.size() - 2); + const auto stringList = cleanCode.split(QLatin1Char(','), QString::SkipEmptyParts); + + if (stringList.count() >= 6 && (stringList.count() % 6 == 0)) { + bool checkX, checkY; + QVector<QPointF> points; + for (int i = 0; i < stringList.count(); ++i) { + QPointF point; + point.rx() = stringList[i].toDouble(&checkX); + point.ry() = stringList[++i].toDouble(&checkY); + + if (!checkX || !checkY) + return false; + + points.push_back(point); + } + + if (points.constLast() != QPointF(1.0, 1.0)) + return false; + + QEasingCurve easingCurve(QEasingCurve::BezierSpline); + + for (int i = 0; i < points.count() / 3; ++i) { + easingCurve.addCubicBezierSegment(points.at(i * 3), + points.at(i * 3 + 1), + points.at(i * 3 + 2)); + } + + fromCubicSpline(easingCurve.toCubicSpline()); + return true; + } + } + return false; +} + +QPointF EasingCurve::start() const +{ + return m_start; +} + +QPointF EasingCurve::end() const +{ + return toCubicSpline().last(); +} + +QPainterPath EasingCurve::path() const +{ + QPainterPath path; + path.moveTo(m_start); + + QVector<QPointF> controlPoints = toCubicSpline(); + + int numSegments = controlPoints.count() / 3; + for (int i = 0; i < numSegments; i++) { + QPointF p1 = controlPoints.at(i * 3); + QPointF p2 = controlPoints.at(i * 3 + 1); + QPointF p3 = controlPoints.at(i * 3 + 2); + path.cubicTo(p1, p2, p3); + } + + return path; +} + +int EasingCurve::curvePoint(int idx) const +{ + if (isHandle(idx)) { + if (isLeftHandle(idx)) + return idx + 1; + else + return idx - 1; + } + return idx; +} + +QPointF EasingCurve::point(int idx) const +{ + QVector<QPointF> controlPoints = toCubicSpline(); + + QTC_ASSERT(controlPoints.count() > idx || idx < 0, return QPointF()); + + return controlPoints.at(idx); +} + +int EasingCurve::hit(const QPointF &point, double threshold) const +{ + int id = -1; + qreal distance = std::numeric_limits<qreal>::max(); + + QVector<QPointF> controlPoints = toCubicSpline(); + for (int i = 0; i < controlPoints.size() - 1; ++i) { + qreal d = QLineF(point, controlPoints.at(i)).length(); + if (d < threshold && d < distance) { + distance = d; + id = i; + } + } + return id; +} + +void EasingCurve::makeDefault() +{ + QVector<QPointF> controlPoints; + controlPoints.append(QPointF(0.0, 0.2)); + controlPoints.append(QPointF(0.3, 0.5)); + controlPoints.append(QPointF(0.5, 0.5)); + + controlPoints.append(QPointF(0.7, 0.5)); + controlPoints.append(QPointF(1.0, 0.8)); + controlPoints.append(QPointF(1.0, 1.0)); + + fromCubicSpline(controlPoints); + + m_smoothIds.push_back(2); +} + +void EasingCurve::clearActive() +{ + m_active = -1; +} + +void EasingCurve::setActive(int idx) +{ + m_active = idx; +} + +void EasingCurve::makeSmooth(int idx) +{ + if (!isSmooth(idx) && !isHandle(idx)) { + QVector<QPointF> controlPoints = toCubicSpline(); + + QPointF before = m_start; + if (idx > 3) + before = controlPoints.at(idx - 3); + + QPointF after = end(); + if ((idx + 3) < controlPoints.count()) + after = controlPoints.at(idx + 3); + + QPointF tangent = (after - before) / 6; + + QPointF thisPoint = controlPoints.at(idx); + + if (idx > 0) + controlPoints[idx - 1] = thisPoint - tangent; + + if (idx + 1 < controlPoints.count()) + controlPoints[idx + 1] = thisPoint + tangent; + + fromCubicSpline(controlPoints); + + m_smoothIds.push_back(idx); + } +} + +void EasingCurve::breakTangent(int idx) +{ + if (isSmooth(idx) && !isHandle(idx)) { + QVector<QPointF> controlPoints = toCubicSpline(); + + QPointF before = m_start; + if (idx > 3) + before = controlPoints.at(idx - 3); + + QPointF after = end(); + if ((idx + 3) < controlPoints.count()) + after = controlPoints.at(idx + 3); + + QPointF thisPoint = controlPoints.at(idx); + + if (idx > 0) + controlPoints[idx - 1] = (before - thisPoint) / 3 + thisPoint; + + if (idx + 1 < controlPoints.count()) + controlPoints[idx + 1] = (after - thisPoint) / 3 + thisPoint; + + fromCubicSpline(controlPoints); + + auto iter = std::find(m_smoothIds.begin(), m_smoothIds.end(), idx); + m_smoothIds.erase(iter); + } +} + +void EasingCurve::addPoint(const QPointF &point) +{ + QVector<QPointF> controlPoints = toCubicSpline(); + + int splitIndex = 0; + for (int i = 0; i < controlPoints.size() - 1; ++i) { + if (!isHandle(i)) { + if (controlPoints.at(i).x() > point.x()) + break; + + splitIndex = i; + } + } + + QPointF before = m_start; + if (splitIndex > 0) { + before = controlPoints.at(splitIndex); + } + + QPointF after = end(); + if ((splitIndex + 3) < controlPoints.count()) { + after = controlPoints.at(splitIndex + 3); + } + + int newIdx; + + if (splitIndex > 0) { + newIdx = splitIndex + 3; + controlPoints.insert(splitIndex + 2, (point + after) / 2); + controlPoints.insert(splitIndex + 2, point); + controlPoints.insert(splitIndex + 2, (point + before) / 2); + } else { + newIdx = splitIndex + 2; + controlPoints.insert(splitIndex + 1, (point + after) / 2); + controlPoints.insert(splitIndex + 1, point); + controlPoints.insert(splitIndex + 1, (point + before) / 2); + } + + fromCubicSpline(controlPoints); + + QTC_ASSERT(!isHandle(newIdx), return ); + + m_active = newIdx; + + breakTangent(newIdx); + makeSmooth(newIdx); +} + +void EasingCurve::setPoint(int idx, const QPointF &point) +{ + if (!isValidIndex(idx)) + return; + + QVector<QPointF> controlPoints = toCubicSpline(); + + controlPoints[idx] = point; + + fromCubicSpline(controlPoints); +} + +void EasingCurve::movePoint(int idx, const QPointF &vector) +{ + if (!isValidIndex(idx)) + return; + + QVector<QPointF> controlPoints = toCubicSpline(); + + controlPoints[idx] += vector; + + fromCubicSpline(controlPoints); +} + +void EasingCurve::deletePoint(int idx) +{ + if (!isValidIndex(idx)) + return; + + QVector<QPointF> controlPoints = toCubicSpline(); + + controlPoints.remove(idx - 1, 3); + + fromCubicSpline(controlPoints); +} + +void EasingCurve::fromCubicSpline(const QVector<QPointF> &points) +{ + QEasingCurve tmp(QEasingCurve::BezierSpline); + + int numSegments = points.count() / 3; + for (int i = 0; i < numSegments; ++i) { + tmp.addCubicBezierSegment(points.at(i * 3), points.at(i * 3 + 1), points.at(i * 3 + 2)); + } + swap(tmp); +} + +QDebug &operator<<(QDebug &stream, const EasingCurve &curve) +{ + stream << static_cast<QEasingCurve>(curve); + stream << "\"active:" << curve.m_active << "\""; + stream << "\"smooth ids:" << curve.m_smoothIds << "\""; + return stream; +} + +QDataStream &operator<<(QDataStream &stream, const EasingCurve &curve) +{ + // Ignore the active flag. + stream << static_cast<QEasingCurve>(curve); + stream << curve.toCubicSpline(); + stream << curve.m_smoothIds; + return stream; +} + +QDataStream &operator>>(QDataStream &stream, EasingCurve &curve) +{ + // This is to circumvent a bug in QEasingCurve serialization. + QVector<QPointF> points; + + // Ignore the active flag. + stream >> static_cast<QEasingCurve &>(curve); + stream >> points; + curve.fromCubicSpline(points); + stream >> curve.m_smoothIds; + + return stream; +} + +NamedEasingCurve::NamedEasingCurve() + : m_name() + , m_curve() +{} + +NamedEasingCurve::NamedEasingCurve(const QString &name, const EasingCurve &curve) + : m_name(name) + , m_curve(curve) +{} + +NamedEasingCurve::NamedEasingCurve(const NamedEasingCurve &other) = default; + +NamedEasingCurve::~NamedEasingCurve() = default; + +QString NamedEasingCurve::name() const +{ + return m_name; +} + +EasingCurve NamedEasingCurve::curve() const +{ + return m_curve; +} + +QDataStream &operator<<(QDataStream &stream, const NamedEasingCurve &curve) +{ + stream << curve.m_name; + stream << curve.m_curve; + return stream; +} + +QDataStream &operator>>(QDataStream &stream, NamedEasingCurve &curve) +{ + stream >> curve.m_name; + stream >> curve.m_curve; + return stream; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurve.h b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.h new file mode 100644 index 0000000000..d46f5600aa --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurve.h @@ -0,0 +1,157 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QEasingCurve> +#include <QMetaType> +#include <QPointF> + +QT_FORWARD_DECLARE_CLASS(QPainterPath); + +namespace QmlDesigner { + +class EasingCurve : public QEasingCurve +{ +public: + EasingCurve(); + + EasingCurve(const QEasingCurve &curve); + + EasingCurve(const EasingCurve &curve); + + EasingCurve(const QPointF &start, const QVector<QPointF> &points); + + virtual ~EasingCurve(); + + EasingCurve &operator=(const EasingCurve &curve); + + static bool IsValidIndex(int idx); + + static void registerStreamOperators(); + +public: + int count() const; + + int active() const; + + int segmentCount() const; + + bool hasActive() const; + + bool isLegal() const; + + bool isValidIndex(int idx) const; + + bool isSmooth(int idx) const; + + bool isHandle(int idx) const; + + bool isLeftHandle(int idx) const; + + QString toString() const; + + QPointF start() const; + + QPointF end() const; + + QPainterPath path() const; + + int curvePoint(int idx) const; + + QPointF point(int idx) const; + + int hit(const QPointF &point, double threshold) const; + +public: + void makeDefault(); + + void clearActive(); + + void setActive(int idx); + + void makeSmooth(int idx); + + void breakTangent(int idx); + + void addPoint(const QPointF &point); + + void setPoint(int idx, const QPointF &point); + + void movePoint(int idx, const QPointF &vector); + + void deletePoint(int idx); + + bool fromString(const QString &string); + + void fromCubicSpline(const QVector<QPointF> &points); + + friend QDebug &operator<<(QDebug &stream, const EasingCurve &curve); + + friend QDataStream &operator<<(QDataStream &stream, const EasingCurve &curve); + + friend QDataStream &operator>>(QDataStream &stream, EasingCurve &curve); + + friend std::ostream &operator<<(std::ostream &stream, const EasingCurve &curve); + + friend std::istream &operator>>(std::istream &stream, EasingCurve &curve); + +private: + int m_active; + + QPointF m_start; + + std::vector<int> m_smoothIds; +}; + +class NamedEasingCurve +{ +public: + NamedEasingCurve(); + + NamedEasingCurve(const QString &name, const EasingCurve &curve); + + NamedEasingCurve(const NamedEasingCurve &other); + + virtual ~NamedEasingCurve(); + + QString name() const; + + EasingCurve curve() const; + + friend QDataStream &operator<<(QDataStream &stream, const NamedEasingCurve &curve); + + friend QDataStream &operator>>(QDataStream &stream, NamedEasingCurve &curve); + +private: + QString m_name; + + EasingCurve m_curve; +}; + +} // namespace QmlDesigner + +Q_DECLARE_METATYPE(QmlDesigner::EasingCurve); +Q_DECLARE_METATYPE(QmlDesigner::NamedEasingCurve); diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp new file mode 100644 index 0000000000..a069dc187b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.cpp @@ -0,0 +1,298 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "easingcurvedialog.h" + +#include "preseteditor.h" +#include "splineeditor.h" + +#include <QApplication> +#include <QGridLayout> +#include <QGroupBox> +#include <QHBoxLayout> +#include <QLabel> +#include <QMessageBox> +#include <QPlainTextEdit> +#include <QPushButton> +#include <QSizePolicy> +#include <QSpinBox> +#include <QTabBar> +#include <QTabWidget> +#include <QVBoxLayout> + +#include <abstractview.h> +#include <bindingproperty.h> +#include <rewritingexception.h> +#include <theme.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +EasingCurveDialog::EasingCurveDialog(const QList<ModelNode> &frames, QWidget *parent) + : QDialog(parent) + , m_splineEditor(new SplineEditor(this)) + , m_text(new QPlainTextEdit(this)) + , m_presets(new PresetEditor(this)) + , m_durationLayout(new QHBoxLayout) + , m_buttons(new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel + | QDialogButtonBox::Ok)) + , m_label(new QLabel) + , m_frames(frames) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + auto tw = new QTabWidget; + tw->setTabPosition(QTabWidget::East); + tw->addTab(m_splineEditor, "Curve"); + tw->addTab(m_text, "Text"); + + connect(tw, &QTabWidget::currentChanged, this, &EasingCurveDialog::tabClicked); + connect(m_text, &QPlainTextEdit::textChanged, this, &EasingCurveDialog::textChanged); + + auto labelFont = m_label->font(); + labelFont.setPointSize(labelFont.pointSize() + 2); + m_label->setFont(labelFont); + + auto hSpacing = qApp->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); + auto vSpacing = qApp->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing); + auto *vbox = new QVBoxLayout; + vbox->setContentsMargins(2, 0, 0, vSpacing); + vbox->addWidget(m_label); + + auto *presetBar = new QTabBar; + + auto smallFont = presetBar->font(); + smallFont.setPixelSize(Theme::instance()->smallFontPixelSize()); + + presetBar->setFont(smallFont); + presetBar->setExpanding(false); + presetBar->setDrawBase(false); + presetBar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + auto *durationLabel = new QLabel("Duration (ms)"); + auto *durationEdit = new QSpinBox; + durationEdit->setMaximum(std::numeric_limits<int>::max()); + durationEdit->setValue(1000); + auto *animateButton = new QPushButton("Preview"); + + m_durationLayout->setContentsMargins(0, vSpacing, 0, 0); + m_durationLayout->addWidget(durationLabel); + m_durationLayout->addWidget(durationEdit); + m_durationLayout->addWidget(animateButton); + + m_durationLayout->insertSpacing(1, hSpacing); + m_durationLayout->insertSpacing(2, hSpacing); + m_durationLayout->insertSpacing(4, hSpacing); + m_durationLayout->addStretch(hSpacing); + + m_buttons->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + auto callButtonsClicked = [this](QAbstractButton *button) { + buttonsClicked(m_buttons->standardButton(button)); + }; + + connect(m_buttons, &QDialogButtonBox::clicked, this, callButtonsClicked); + + auto *buttonLayout = new QVBoxLayout; + buttonLayout->setContentsMargins(0, vSpacing, 0, 0); + buttonLayout->addWidget(m_buttons); + + auto *grid = new QGridLayout; + grid->setVerticalSpacing(0); + grid->addLayout(vbox, 0, 0); + grid->addWidget(presetBar, 0, 1, Qt::AlignBottom); + + grid->addWidget(tw); + grid->addWidget(m_presets, 1, 1); + grid->addLayout(m_durationLayout, 2, 0); + grid->addLayout(buttonLayout, 2, 1); + + auto *groupBox = new QGroupBox; + groupBox->setLayout(grid); + + auto *tabWidget = new QTabWidget(this); + tabWidget->addTab(groupBox, "Easing Curve Editor"); + + auto *mainBox = new QVBoxLayout; + mainBox->addWidget(tabWidget); + setLayout(mainBox); + + connect(m_splineEditor, + &SplineEditor::easingCurveChanged, + this, + &EasingCurveDialog::updateEasingCurve); + + connect(m_presets, &PresetEditor::presetChanged, m_splineEditor, &SplineEditor::setEasingCurve); + + connect(durationEdit, + QOverload<int>::of(&QSpinBox::valueChanged), + m_splineEditor, + &SplineEditor::setDuration); + + connect(animateButton, &QPushButton::clicked, m_splineEditor, &SplineEditor::animate); + + m_presets->initialize(presetBar); + + m_splineEditor->setDuration(durationEdit->value()); + + resize(QSize(1421, 918)); +} + +void EasingCurveDialog::initialize(const QString &curveString) +{ + EasingCurve curve; + if (curveString.isEmpty()) { + QEasingCurve qcurve; + qcurve.addCubicBezierSegment(QPointF(0.2, 0.2), QPointF(0.8, 0.8), QPointF(1.0, 1.0)); + curve = EasingCurve(qcurve); + } else + curve.fromString(curveString); + + m_splineEditor->setEasingCurve(curve); +} + +void EasingCurveDialog::runDialog(const QList<ModelNode> &frames, QWidget *parent) +{ + if (frames.empty()) + return; + + EasingCurveDialog dialog(frames, parent); + + ModelNode current = frames.last(); + + if (current.hasBindingProperty("easing.bezierCurve")) + dialog.initialize(current.bindingProperty("easing.bezierCurve").expression()); + else + dialog.initialize(""); + + dialog.exec(); +} + +bool EasingCurveDialog::apply() +{ + QTC_ASSERT(!m_frames.empty(), return false); + + EasingCurve curve = m_splineEditor->easingCurve(); + if (!curve.isLegal()) { + QMessageBox msgBox; + msgBox.setText("Attempting to apply invalid curve to keyframe"); + msgBox.setInformativeText("Please solve the issue before proceeding."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + return false; + } + AbstractView *view = m_frames.first().view(); + + return view->executeInTransaction("EasingCurveDialog::apply", [this, view](){ + auto expression = m_splineEditor->easingCurve().toString(); + for (const auto &frame : m_frames) + frame.bindingProperty("easing.bezierCurve").setExpression(expression); + }); +} + +void EasingCurveDialog::textChanged() +{ + auto curve = m_splineEditor->easingCurve(); + curve.fromString(m_text->toPlainText()); + m_splineEditor->setEasingCurve(curve); +} + +void EasingCurveDialog::tabClicked(int id) +{ + if (auto tw = qobject_cast<const QTabWidget *>(sender())) { + int seid = tw->indexOf(m_splineEditor); + if (seid == id) { + for (int i = 0; i < m_durationLayout->count(); ++i) { + auto *item = m_durationLayout->itemAt(i); + if (auto *widget = item->widget()) + widget->show(); + } + + auto curve = m_splineEditor->easingCurve(); + curve.fromString(m_text->toPlainText()); + m_splineEditor->setEasingCurve(curve); + + } else { + for (int i = 0; i < m_durationLayout->count(); ++i) { + auto *item = m_durationLayout->itemAt(i); + if (auto *widget = item->widget()) + widget->hide(); + } + + auto curve = m_splineEditor->easingCurve(); + m_text->setPlainText(curve.toString()); + } + } +} + +void EasingCurveDialog::presetTabClicked(int id) +{ + m_presets->activate(id); +} + +void EasingCurveDialog::updateEasingCurve(const EasingCurve &curve) +{ + if (!curve.isLegal()) { + auto *save = m_buttons->button(QDialogButtonBox::Save); + save->setEnabled(false); + + auto *ok = m_buttons->button(QDialogButtonBox::Ok); + ok->setEnabled(false); + + m_label->setText("Invalid Curve!"); + } else { + auto *save = m_buttons->button(QDialogButtonBox::Save); + save->setEnabled(true); + + auto *ok = m_buttons->button(QDialogButtonBox::Ok); + ok->setEnabled(true); + + m_label->setText(""); + } + + m_presets->update(curve); +} + +void EasingCurveDialog::buttonsClicked(QDialogButtonBox::StandardButton button) +{ + switch (button) { + case QDialogButtonBox::Ok: + if (apply()) + close(); + break; + + case QDialogButtonBox::Cancel: + close(); + break; + + case QDialogButtonBox::Save: + m_presets->writePresets(m_splineEditor->easingCurve()); + break; + + default: + break; + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h new file mode 100644 index 0000000000..a8c026989c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/easingcurvedialog.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QDialog> +#include <QDialogButtonBox> + +#include <modelnode.h> + +QT_BEGIN_NAMESPACE +class QLabel; +class QPlainTextEdit; +class QHBoxLayout; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class SplineEditor; +class PresetEditor; +class EasingCurve; + +class EasingCurveDialog : public QDialog +{ + Q_OBJECT + +public: + EasingCurveDialog(const QList<ModelNode> &frames, QWidget *parent = nullptr); + + void initialize(const QString &curveString); + + static void runDialog(const QList<ModelNode> &frames, QWidget *parent = nullptr); + +private: + bool apply(); + + void textChanged(); + + void tabClicked(int id); + + void presetTabClicked(int id); + + void buttonsClicked(QDialogButtonBox::StandardButton button); + + void updateEasingCurve(const EasingCurve &curve); + +private: + SplineEditor *m_splineEditor = nullptr; + + QPlainTextEdit *m_text = nullptr; + + PresetEditor *m_presets = nullptr; + + QHBoxLayout *m_durationLayout = nullptr; + + QDialogButtonBox *m_buttons = nullptr; + + QLabel *m_label = nullptr; + + QList<ModelNode> m_frames; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png Binary files differnew file mode 100644 index 0000000000..af651276ed --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png Binary files differnew file mode 100644 index 0000000000..58a7174288 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/add_timeline@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/animation.png b/src/plugins/qmldesigner/components/timelineeditor/images/animation.png Binary files differnew file mode 100644 index 0000000000..20ad0273b5 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/animation.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png Binary files differnew file mode 100644 index 0000000000..1ecf1857c7 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/animation@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png Binary files differnew file mode 100644 index 0000000000..69c93ebe3e --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png Binary files differnew file mode 100644 index 0000000000..9bd8a52e59 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/back_one_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png Binary files differnew file mode 100644 index 0000000000..bda4dc0095 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png Binary files differnew file mode 100644 index 0000000000..3d5c3abe05 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_editor@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png Binary files differnew file mode 100644 index 0000000000..4842ac0738 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png Binary files differnew file mode 100644 index 0000000000..0d99fc180c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/curve_picker@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png Binary files differnew file mode 100644 index 0000000000..0846f194e0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png Binary files differnew file mode 100644 index 0000000000..8e5ddc3930 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/forward_one_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png Binary files differnew file mode 100644 index 0000000000..64a28ca075 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png Binary files differnew file mode 100644 index 0000000000..534737f385 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/global_record_keyframes@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png Binary files differnew file mode 100644 index 0000000000..5655e0b278 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..2f522c22b6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/is_keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png Binary files differnew file mode 100644 index 0000000000..6e1c9f912a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe-16px.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png Binary files differnew file mode 100644 index 0000000000..6bf7d1ad53 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..5102e279a1 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png Binary files differnew file mode 100644 index 0000000000..8a3eaa7888 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png Binary files differnew file mode 100644 index 0000000000..e0168a097a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png Binary files differnew file mode 100644 index 0000000000..2c12d98e01 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..4bbbe6cd3f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png Binary files differnew file mode 100644 index 0000000000..58ccb7c765 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png Binary files differnew file mode 100644 index 0000000000..829dd99391 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_autobezier_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png Binary files differnew file mode 100644 index 0000000000..a195ac5fca --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png Binary files differnew file mode 100644 index 0000000000..fd879e5837 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png Binary files differnew file mode 100644 index 0000000000..b84a800097 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..0ad868dcd6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png Binary files differnew file mode 100644 index 0000000000..e840819f2d --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png Binary files differnew file mode 100644 index 0000000000..e5f63f1fc9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_linear_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png Binary files differnew file mode 100644 index 0000000000..f85d3f78fd --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png Binary files differnew file mode 100644 index 0000000000..2f65f7970c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png Binary files differnew file mode 100644 index 0000000000..9798c76115 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..b4ee45a566 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png Binary files differnew file mode 100644 index 0000000000..1e39f84502 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png Binary files differnew file mode 100644 index 0000000000..b99474718c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_lineartobezier_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png Binary files differnew file mode 100644 index 0000000000..4b6a7c8978 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png Binary files differnew file mode 100644 index 0000000000..fd85a10758 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_active@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png Binary files differnew file mode 100644 index 0000000000..9c0a1fd550 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png Binary files differnew file mode 100644 index 0000000000..1299a370fc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_inactive@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png Binary files differnew file mode 100644 index 0000000000..7b5faebae0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png Binary files differnew file mode 100644 index 0000000000..726ead7a43 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/keyframe_manualbezier_selected@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png Binary files differnew file mode 100644 index 0000000000..d68aa73214 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png Binary files differnew file mode 100644 index 0000000000..f5265a2218 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/local_record_keyframes@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png Binary files differnew file mode 100644 index 0000000000..f38fbef1d4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png Binary files differnew file mode 100644 index 0000000000..b760a04133 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/loop_playback@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png Binary files differnew file mode 100644 index 0000000000..415ec0127f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..3f1e24e04a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/next_keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png Binary files differnew file mode 100644 index 0000000000..001ca37b1c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png Binary files differnew file mode 100644 index 0000000000..95e8567ccb --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/pause_playback@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png b/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png Binary files differnew file mode 100644 index 0000000000..518a77f404 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/playhead.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png Binary files differnew file mode 100644 index 0000000000..7f6778556b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/playhead@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png Binary files differnew file mode 100644 index 0000000000..52ba668973 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png Binary files differnew file mode 100644 index 0000000000..df151051fc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/previous_keyframe@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png Binary files differnew file mode 100644 index 0000000000..0589f982a7 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png Binary files differnew file mode 100644 index 0000000000..9eed9ce3c3 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/remove_timeline@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png Binary files differnew file mode 100644 index 0000000000..0cf0865c48 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png Binary files differnew file mode 100644 index 0000000000..f05dfcd9ed --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/start_playback@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png b/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png Binary files differnew file mode 100644 index 0000000000..d4ecf00031 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/timeline-16px.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png Binary files differnew file mode 100644 index 0000000000..910b856638 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png Binary files differnew file mode 100644 index 0000000000..abefa72fb3 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_first_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png Binary files differnew file mode 100644 index 0000000000..d6bc429196 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png Binary files differnew file mode 100644 index 0000000000..affc3c9848 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/to_last_frame@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png Binary files differnew file mode 100644 index 0000000000..83d441d64f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png Binary files differnew file mode 100644 index 0000000000..0fec4b269e --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_left@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png Binary files differnew file mode 100644 index 0000000000..611684a7f6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png Binary files differnew file mode 100644 index 0000000000..c1dbdbc56c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/work_area_handle_right@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png Binary files differnew file mode 100644 index 0000000000..eec61eb86c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png Binary files differnew file mode 100644 index 0000000000..1706de0bb4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_big@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png Binary files differnew file mode 100644 index 0000000000..20433d99c4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png Binary files differnew file mode 100644 index 0000000000..326ea32c25 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/images/zoom_small@2x.png diff --git a/src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp new file mode 100644 index 0000000000..8ea7692e3d --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.cpp @@ -0,0 +1,560 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "preseteditor.h" + +#include "canvas.h" +#include "easingcurve.h" +#include "timelineicons.h" + +#include <QAbstractButton> +#include <QApplication> +#include <QContextMenuEvent> +#include <QMenu> +#include <QMessageBox> +#include <QPainter> +#include <QPixmap> +#include <QSettings> +#include <QStandardItemModel> +#include <QString> + +#include <coreplugin/icore.h> +#include <theme.h> + +namespace QmlDesigner { + +constexpr int iconWidth = 86; +constexpr int iconHeight = 86; +constexpr int itemFrame = 3; +constexpr int itemWidth = iconWidth + 2 * itemFrame; +constexpr int unsavedMarkSize = 18; + +constexpr int spacingg = 5; + +const QColor background = Qt::white; + +const QColor labelBackground = qRgb(0x70, 0x70, 0x70); +const QColor canvasBackground = qRgb(0x46, 0x46, 0x46); +const QColor curveLine = qRgb(0xe6, 0xe7, 0xe8); + +PresetItemDelegate::PresetItemDelegate() = default; + +void PresetItemDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &opt, + const QModelIndex &index) const +{ + QStyleOptionViewItem option = opt; + initStyleOption(&option, index); + + auto *w = option.widget; + auto *style = w == nullptr ? qApp->style() : w->style(); + + QSize textSize = QSize(option.rect.width(), + style->subElementRect(QStyle::SE_ItemViewItemText, &option, w).height()); + + auto textRect = QRect(option.rect.topLeft(), textSize); + textRect.moveBottom(option.rect.bottom()); + + option.font.setPixelSize(Theme::instance()->smallFontPixelSize()); + + painter->save(); + painter->fillRect(option.rect, canvasBackground); + + if (option.text.isEmpty()) + painter->fillRect(textRect, canvasBackground); + else + painter->fillRect(textRect, Theme::instance()->qmlDesignerButtonColor()); + + style->drawControl(QStyle::CE_ItemViewItem, &option, painter, option.widget); + + QVariant dirty = option.index.data(PresetList::ItemRole_Dirty); + if (dirty.isValid()) { + if (dirty.toBool()) { + QRect asteriskRect(option.rect.right() - unsavedMarkSize, + itemFrame, + unsavedMarkSize, + unsavedMarkSize); + + QFont font = painter->font(); + font.setPixelSize(unsavedMarkSize); + painter->setFont(font); + + auto pen = painter->pen(); + pen.setColor(Qt::white); + painter->setPen(pen); + + painter->drawText(asteriskRect, Qt::AlignTop | Qt::AlignRight, "*"); + } + } + painter->restore(); +} + +QSize PresetItemDelegate::sizeHint(const QStyleOptionViewItem &opt, const QModelIndex &index) const +{ + QSize size = QStyledItemDelegate::sizeHint(opt, index); + size.rwidth() = itemWidth; + return size; +} + +QIcon paintPreview() +{ + QPixmap pm(iconWidth, iconHeight); + pm.fill(canvasBackground); + return QIcon(pm); +} + +QIcon paintPreview(const EasingCurve &curve) +{ + QPixmap pm(iconWidth, iconHeight); + pm.fill(canvasBackground); + + QPainter painter(&pm); + painter.setRenderHint(QPainter::Antialiasing, true); + + Canvas canvas(iconWidth, iconHeight, 2, 2, 9, 6, 0, 1); + canvas.paintCurve(&painter, curve, curveLine); + + return QIcon(pm); +} + +namespace Internal { + +static const char settingsKey[] = "EasingCurveList"; +static const char settingsFileName[] = "/EasingCurves.ini"; + +QString settingsFullFilePath(const QSettings::Scope &scope) +{ + if (scope == QSettings::SystemScope) + return Core::ICore::installerResourcePath() + settingsFileName; + + return Core::ICore::userResourcePath() + settingsFileName; +} + +} // namespace Internal + +PresetList::PresetList(QSettings::Scope scope, QWidget *parent) + : QListView(parent) + , m_scope(scope) + , m_index(-1) + , m_filename(Internal::settingsFullFilePath(scope)) +{ + int magic = 4; + int scrollBarWidth = this->style()->pixelMetric(QStyle::PM_ScrollBarExtent); + const int width = 3 * itemWidth + 4 * spacingg + scrollBarWidth + magic; + + setFixedWidth(width); + + setModel(new QStandardItemModel); + + setItemDelegate(new PresetItemDelegate); + + setSpacing(spacingg); + + setUniformItemSizes(true); + + setIconSize(QSize(iconWidth, iconHeight)); + + setSelectionMode(QAbstractItemView::SingleSelection); + + setViewMode(QListView::IconMode); + + setFlow(QListView::LeftToRight); + + setMovement(QListView::Static); + + setWrapping(true); + + setTextElideMode(Qt::ElideMiddle); + + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); +} + +void PresetList::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) +{ + for (const QModelIndex &index : deselected.indexes()) { + if (dirty(index)) { + QMessageBox msgBox; + msgBox.setText("The preset has been modified."); + msgBox.setInformativeText("Do you want to save your changes?"); + msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard + | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Save); + + if (QAbstractButton *button = msgBox.button(QMessageBox::Discard)) + button->setText("Discard Changes"); + + if (QAbstractButton *button = msgBox.button(QMessageBox::Cancel)) + button->setText("Cancel Selection"); + + int ret = msgBox.exec(); + + switch (ret) { + case QMessageBox::Save: + // Save the preset and continue selection. + writePresets(); + break; + case QMessageBox::Discard: + // Discard changes to the curve and continue selection. + revert(index); + break; + + case QMessageBox::Cancel: + // Cancel selection operation and leave the curve untouched. + selectionModel()->select(index, QItemSelectionModel::ClearAndSelect); + return; + + default: + // should never be reachedDiscard + break; + } + } + } + + for (const auto &index : selected.indexes()) { + QVariant curveData = model()->data(index, ItemRole_Data); + if (curveData.isValid()) + emit presetChanged(curveData.value<EasingCurve>()); + } +} + +bool PresetList::hasSelection() const +{ + return selectionModel()->hasSelection(); +} + +bool PresetList::dirty(const QModelIndex &index) const +{ + return model()->data(index, ItemRole_Dirty).toBool(); +} + +int PresetList::index() const +{ + return m_index; +} + +bool PresetList::isEditable(const QModelIndex &index) const +{ + QFlags<Qt::ItemFlag> flags(model()->flags(index)); + return flags.testFlag(Qt::ItemIsEditable); +} + +void PresetList::initialize(int index) +{ + m_index = index; + + readPresets(); +} + +void PresetList::readPresets() +{ + auto *simodel = qobject_cast<QStandardItemModel *>(model()); + + simodel->clear(); + + QList<NamedEasingCurve> curves = storedCurves(); + + for (int i = 0; i < curves.size(); ++i) { + QVariant curveData = QVariant::fromValue(curves[i].curve()); + + auto *item = new QStandardItem(paintPreview(curves[i].curve()), curves[i].name()); + item->setData(curveData, ItemRole_Data); + item->setEditable(m_scope == QSettings::UserScope); + item->setToolTip(curves[i].name()); + + simodel->setItem(i, item); + } +} + +void PresetList::writePresets() +{ + QList<QVariant> presets; + for (int i = 0; i < model()->rowCount(); ++i) { + QModelIndex index = model()->index(i, 0); + + QVariant nameData = model()->data(index, Qt::DisplayRole); + QVariant curveData = model()->data(index, ItemRole_Data); + + if (nameData.isValid() && curveData.isValid()) { + NamedEasingCurve curve(nameData.toString(), curveData.value<QmlDesigner::EasingCurve>()); + + presets << QVariant::fromValue(curve); + } + + model()->setData(index, false, ItemRole_Dirty); + } + + QSettings settings(m_filename, QSettings::IniFormat); + settings.clear(); + settings.setValue(Internal::settingsKey, QVariant::fromValue(presets)); +} + +void PresetList::revert(const QModelIndex &index) +{ + auto *simodel = qobject_cast<QStandardItemModel *>(model()); + if (auto *item = simodel->itemFromIndex(index)) { + QString name = item->data(Qt::DisplayRole).toString(); + QList<NamedEasingCurve> curves = storedCurves(); + + for (const auto &curve : curves) { + if (curve.name() == name) { + item->setData(false, ItemRole_Dirty); + item->setData(paintPreview(curve.curve()), Qt::DecorationRole); + item->setData(QVariant::fromValue(curve.curve()), ItemRole_Data); + item->setToolTip(name); + return; + } + } + } +} + +void PresetList::updateCurve(const EasingCurve &curve) +{ + if (!selectionModel()->hasSelection()) + return; + + QVariant icon = QVariant::fromValue(paintPreview(curve)); + QVariant curveData = QVariant::fromValue(curve); + + for (const auto &index : selectionModel()->selectedIndexes()) + setItemData(index, curveData, icon); +} + +void PresetList::contextMenuEvent(QContextMenuEvent *event) +{ + event->accept(); + + if (m_scope == QSettings::SystemScope) + return; + + QMenu menu; + + QAction *addAction = menu.addAction(tr("Add Preset")); + + connect(addAction, &QAction::triggered, [&]() { createItem(); }); + + if (selectionModel()->hasSelection()) { + QAction *removeAction = menu.addAction(tr("Delete Selected Preset")); + connect(removeAction, &QAction::triggered, [&]() { removeSelectedItem(); }); + } + + menu.exec(event->globalPos()); +} + +void PresetList::dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector<int> &roles) +{ + if (topLeft == bottomRight && roles.contains(0)) { + QVariant name = model()->data(topLeft, 0); + model()->setData(topLeft, name, Qt::ToolTipRole); + } +} + +void PresetList::createItem() +{ + EasingCurve curve; + curve.makeDefault(); + createItem(createUniqueName(), curve); +} + +void PresetList::createItem(const QString &name, const EasingCurve &curve) +{ + auto *item = new QStandardItem(paintPreview(curve), name); + item->setData(QVariant::fromValue(curve), ItemRole_Data); + item->setToolTip(name); + + int row = model()->rowCount(); + qobject_cast<QStandardItemModel *>(model())->setItem(row, item); + + QModelIndex index = model()->index(row, 0); + + // Why is that needed? SingleSelection is specified. + selectionModel()->clear(); + selectionModel()->select(index, QItemSelectionModel::Select); +} + +void PresetList::removeSelectedItem() +{ + for (const auto &index : selectionModel()->selectedIndexes()) + model()->removeRow(index.row()); + + writePresets(); +} + +void PresetList::setItemData(const QModelIndex &index, const QVariant &curve, const QVariant &icon) +{ + if (isEditable(index)) { + model()->setData(index, curve, PresetList::ItemRole_Data); + model()->setData(index, true, PresetList::ItemRole_Dirty); + model()->setData(index, icon, Qt::DecorationRole); + } +} + +QString PresetList::createUniqueName() const +{ + QStringList names = allNames(); + auto nameIsUnique = [&](const QString &name) { + auto iter = std::find(names.begin(), names.end(), name); + if (iter == names.end()) + return true; + else + return false; + }; + + int counter = 0; + QString tmp("Default"); + QString name = tmp; + + while (!nameIsUnique(name)) + name = tmp + QString(" %1").arg(counter++); + + return name; +} + +QStringList PresetList::allNames() const +{ + QStringList names; + for (int i = 0; i < model()->rowCount(); ++i) { + QModelIndex index = model()->index(i, 0); + QVariant nameData = model()->data(index, Qt::DisplayRole); + if (nameData.isValid()) + names << nameData.toString(); + } + + return names; +} + +QList<NamedEasingCurve> PresetList::storedCurves() const +{ + QSettings settings(m_filename, QSettings::IniFormat); + QVariant presetSettings = settings.value(Internal::settingsKey); + + if (!presetSettings.isValid()) + return QList<NamedEasingCurve>(); + + QList<QVariant> presets = presetSettings.toList(); + + QList<NamedEasingCurve> out; + for (const QVariant &preset : presets) + if (preset.isValid()) + out << preset.value<NamedEasingCurve>(); + + return out; +} + +PresetEditor::PresetEditor(QWidget *parent) + : QStackedWidget(parent) + , m_presets(new PresetList(QSettings::SystemScope)) + , m_customs(new PresetList(QSettings::UserScope)) +{ + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + + addWidget(m_presets); + addWidget(m_customs); + + connect(m_presets, &PresetList::presetChanged, this, &PresetEditor::presetChanged); + connect(m_customs, &PresetList::presetChanged, this, &PresetEditor::presetChanged); +} + +void PresetEditor::initialize(QTabBar *bar) +{ + m_presets->initialize(bar->addTab("Presets")); + m_customs->initialize(bar->addTab("Custom")); + + connect(bar, &QTabBar::currentChanged, this, &PresetEditor::activate); + connect(this, &PresetEditor::currentChanged, bar, &QTabBar::setCurrentIndex); + + m_presets->selectionModel()->clear(); + m_customs->selectionModel()->clear(); + + activate(m_presets->index()); +} + +void PresetEditor::activate(int id) +{ + if (id == m_presets->index()) + setCurrentWidget(m_presets); + else + setCurrentWidget(m_customs); +} + +void PresetEditor::update(const EasingCurve &curve) +{ + if (isCurrent(m_presets)) + m_presets->selectionModel()->clear(); + else { + if (m_customs->selectionModel()->hasSelection()) { + QVariant icon = QVariant::fromValue(paintPreview(curve)); + QVariant curveData = QVariant::fromValue(curve); + for (const QModelIndex &index : m_customs->selectionModel()->selectedIndexes()) + m_customs->setItemData(index, curveData, icon); + } + } +} + +bool PresetEditor::writePresets(const EasingCurve &curve) +{ + if (!curve.isLegal()) { + QMessageBox msgBox; + msgBox.setText("Attempting to save invalid curve"); + msgBox.setInformativeText("Please solve the issue before proceeding."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + return false; + } + + if (auto current = qobject_cast<const PresetList *>(currentWidget())) { + if (current->index() == m_presets->index() + || (current->index() == m_customs->index() && !m_customs->hasSelection())) { + bool ok; + QString name = QInputDialog::getText(this, + tr("Save Preset"), + tr("Name"), + QLineEdit::Normal, + QString(), + &ok); + + if (ok && !name.isEmpty()) { + activate(m_customs->index()); + m_customs->createItem(name, curve); + } + } + + m_customs->writePresets(); + return true; + } + + return false; +} + +bool PresetEditor::isCurrent(PresetList *list) +{ + if (auto current = qobject_cast<const PresetList *>(currentWidget())) + return list->index() == current->index(); + + return false; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/preseteditor.h b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.h new file mode 100644 index 0000000000..6fab3e7adb --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/preseteditor.h @@ -0,0 +1,152 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QInputDialog> +#include <QListView> +#include <QSettings> +#include <QStackedWidget> +#include <QStyledItemDelegate> + +QT_FORWARD_DECLARE_CLASS(QString) + +namespace QmlDesigner { + +class EasingCurve; +class NamedEasingCurve; + +class PresetItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + PresetItemDelegate(); + + void paint(QPainter *painter, + const QStyleOptionViewItem &opt, + const QModelIndex &index) const override; + + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +class PresetList : public QListView +{ + Q_OBJECT + +public: + enum ItemRoles { + ItemRole_Undefined = Qt::UserRole, + ItemRole_Data, + ItemRole_Dirty, + ItemRole_Modifiable + }; + +signals: + void presetChanged(const EasingCurve &curve); + +public: + PresetList(QSettings::Scope scope, QWidget *parent = nullptr); + + void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected) override; + + bool hasSelection() const; + + bool dirty(const QModelIndex &index) const; + + int index() const; + + bool isEditable(const QModelIndex &index) const; + + void initialize(int index); + + void readPresets(); + + void writePresets(); + + void revert(const QModelIndex &index); + + void updateCurve(const EasingCurve &curve); + + void createItem(); + + void createItem(const QString &name, const EasingCurve &curve); + + void setItemData(const QModelIndex &index, const QVariant &curve, const QVariant &icon); + +protected: + void contextMenuEvent(QContextMenuEvent *event) override; + + void dataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector<int> &roles = QVector<int>()) override; + +private: + QStringList allNames() const; + + QList<NamedEasingCurve> storedCurves() const; + + QString createUniqueName() const; + + void removeSelectedItem(); + +private: + QSettings::Scope m_scope; + + int m_index; + + QString m_filename; +}; + +class PresetEditor : public QStackedWidget +{ + Q_OBJECT + +signals: + void presetChanged(const EasingCurve &curve); + +public: + explicit PresetEditor(QWidget *parent = nullptr); + + void initialize(QTabBar *bar); + + void activate(int id); + + void update(const EasingCurve &curve); + + void readPresets(); + + bool writePresets(const EasingCurve &curve); + +private: + bool isCurrent(PresetList *list); + +private: + PresetList *m_presets; + + PresetList *m_customs; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp new file mode 100644 index 0000000000..4120aaee21 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.cpp @@ -0,0 +1,55 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "setframevaluedialog.h" +#include "ui_setframevaluedialog.h" + +namespace QmlDesigner { + +SetFrameValueDialog::SetFrameValueDialog(QWidget *parent) + : QDialog(parent) + , ui(new Ui::SetFrameValueDialog) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); +} + +SetFrameValueDialog::~SetFrameValueDialog() +{ + delete ui; +} + +QLineEdit *SetFrameValueDialog::lineEdit() const +{ + return ui->lineEdit; +} + +void SetFrameValueDialog::setPropertName(const QString &name) +{ + setWindowTitle(tr("Change %1").arg(name)); + ui->label->setText(name); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h new file mode 100644 index 0000000000..e7ed226b67 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QDialog> + +QT_FORWARD_DECLARE_CLASS(QLineEdit) + +namespace QmlDesigner { + +namespace Ui { +class SetFrameValueDialog; +} + +class SetFrameValueDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SetFrameValueDialog(QWidget *parent = nullptr); + ~SetFrameValueDialog() override; + + QLineEdit *lineEdit() const; + + void setPropertName(const QString &name); + +private: + Ui::SetFrameValueDialog *ui; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui new file mode 100644 index 0000000000..2fa1241e4a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/setframevaluedialog.ui @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::SetFrameValueDialog</class> + <widget class="QDialog" name="QmlDesigner::SetFrameValueDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>184</width> + <height>79</height> + </rect> + </property> + <property name="windowTitle"> + <string>Dialog</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Value</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="lineEdit"/> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QmlDesigner::SetFrameValueDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QmlDesigner::SetFrameValueDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp new file mode 100644 index 0000000000..44ef1f194c --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.cpp @@ -0,0 +1,274 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "splineeditor.h" + +#include <theme.h> + +#include <QAction> +#include <QApplication> +#include <QContextMenuEvent> +#include <QMenu> +#include <QMouseEvent> +#include <QPainter> +#include <QPropertyAnimation> +#include <QResizeEvent> + +namespace QmlDesigner { + +SplineEditor::SplineEditor(QWidget *parent) + : QWidget(parent) + , m_canvas(0, 0, 25, 25, 9, 6, 0, 1) + , m_animation(new QPropertyAnimation(this, "progress")) +{ + m_animation->setStartValue(0.0); + m_animation->setEndValue(1.0); + m_animation->setLoopCount(1); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); +} + +double SplineEditor::progress() const +{ + return m_progress; +} + +EasingCurve SplineEditor::easingCurve() const +{ + return m_curve; +} + +void SplineEditor::animate() const +{ + m_animation->start(); +} + +void SplineEditor::setDuration(int duration) +{ + m_animation->setDuration(duration); +} + +void SplineEditor::setProgress(double progress) +{ + m_progress = progress; + update(); +} + +void SplineEditor::setEasingCurve(const EasingCurve &curve) +{ + m_curve = curve; + update(); +} + +void SplineEditor::resizeEvent(QResizeEvent *event) +{ + m_canvas.resize(event->size()); + QWidget::resizeEvent(event); +} + +void SplineEditor::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + QPen pen(Qt::black); + pen.setWidth(1); + painter.drawRect(0, 0, width() - 1, height() - 1); + + painter.setRenderHint(QPainter::Antialiasing); + + pen = QPen(Qt::darkGray); + pen.setWidth(1); + painter.setPen(pen); + + QColor curveColor = Qt::white; + if (!m_curve.isLegal()) + curveColor = Qt::red; + + QBrush background(Theme::instance()->qmlDesignerBackgroundColorDarker()); + m_canvas.paintGrid(&painter, background); + m_canvas.paintCurve(&painter, m_curve, curveColor); + m_canvas.paintControlPoints(&painter, m_curve); + + if (m_animation->state() == QAbstractAnimation::Running) + m_canvas.paintProgress(&painter, m_curve, m_progress); +} + +void SplineEditor::mousePressEvent(QMouseEvent *e) +{ + bool clearActive = true; + if (e->button() == Qt::LeftButton) { + EasingCurve mappedCurve = m_canvas.mapTo(m_curve); + int active = mappedCurve.hit(e->pos(), 10); + + if (EasingCurve::IsValidIndex(active)) { + clearActive = false; + m_curve.setActive(active); + mouseMoveEvent(e); + } + + m_mousePress = e->pos(); + e->accept(); + } + + if (clearActive) { + m_curve.clearActive(); + update(); + } +} + +void SplineEditor::mouseReleaseEvent(QMouseEvent *e) +{ + if (e->button() == Qt::LeftButton) { + m_mouseDrag = false; + e->accept(); + } +} + +void dragHandle(EasingCurve &curve, int id, const QPointF &pos) +{ + QPointF distance = pos - curve.point(id); + + curve.setPoint(id, pos); + + if (curve.isLeftHandle(id)) + curve.movePoint(id + 2, -distance); + else + curve.movePoint(id - 2, -distance); +} + +void SplineEditor::mouseMoveEvent(QMouseEvent *e) +{ + // If we've moved more then 25 pixels, assume user is dragging + if (!m_mouseDrag + && QPoint(m_mousePress - e->pos()).manhattanLength() > qApp->startDragDistance()) + m_mouseDrag = true; + + if (m_mouseDrag && m_curve.hasActive()) { + QPointF p = m_canvas.mapFrom(e->pos()); + int active = m_curve.active(); + + if ((active == 0 || active == m_curve.count() - 2) + && e->modifiers().testFlag(Qt::ShiftModifier)) { + if (active == 0) { + QPointF opposite = QPointF(1.0, 1.0) - p; + dragHandle(m_curve, active, p); + dragHandle(m_curve, m_curve.count() - 2, opposite); + + } else { + QPointF opposite = QPointF(1.0, 1.0) - p; + dragHandle(m_curve, active, p); + dragHandle(m_curve, 0, opposite); + } + + } else if (m_curve.isHandle(active)) { + int poc = m_curve.curvePoint(active); + + if (!m_curve.isSmooth(poc)) + m_curve.setPoint(active, p); + else + dragHandle(m_curve, active, p); + + } else { + QPointF targetPoint = p; + QPointF distance = targetPoint - m_curve.point(m_curve.active()); + + m_curve.setPoint(active, targetPoint); + m_curve.movePoint(active + 1, distance); + m_curve.movePoint(active - 1, distance); + } + + update(); + emit easingCurveChanged(m_curve); + } +} + +void SplineEditor::contextMenuEvent(QContextMenuEvent *e) +{ + m_curve.clearActive(); + + QMenu menu; + + EasingCurve mappedCurve = m_canvas.mapTo(m_curve); + int index = mappedCurve.hit(e->pos(), 10); + + if (index > 0 && !m_curve.isHandle(index)) { + QAction *deleteAction = menu.addAction(tr("Delete Point")); + connect(deleteAction, &QAction::triggered, [this, index]() { + m_curve.deletePoint(index); + update(); + emit easingCurveChanged(m_curve); + }); + + QAction *smoothAction = menu.addAction(tr("Smooth Point")); + smoothAction->setCheckable(true); + smoothAction->setChecked(m_curve.isSmooth(index)); + connect(smoothAction, &QAction::triggered, [this, index]() { + m_curve.makeSmooth(index); + update(); + emit easingCurveChanged(m_curve); + }); + + QAction *cornerAction = menu.addAction(tr("Corner Point")); + connect(cornerAction, &QAction::triggered, [this, index]() { + m_curve.breakTangent(index); + update(); + emit easingCurveChanged(m_curve); + }); + + } else { + QAction *addAction = menu.addAction(tr("Add Point")); + connect(addAction, &QAction::triggered, [&]() { + m_curve.addPoint(m_canvas.mapFrom(e->pos())); + m_curve.makeSmooth(m_curve.active()); + update(); + emit easingCurveChanged(m_curve); + }); + } + + QAction *zoomAction = menu.addAction(tr("Reset Zoom")); + connect(zoomAction, &QAction::triggered, [&]() { + m_canvas.setScale(1.0); + update(); + }); + + menu.exec(e->globalPos()); + e->accept(); +} + +void SplineEditor::mouseDoubleClickEvent(QMouseEvent *event) +{ + m_animation->start(); + QWidget::mouseDoubleClickEvent(event); +} + +void SplineEditor::wheelEvent(QWheelEvent *event) +{ + double tmp = event->angleDelta().y() > 0 ? 0.05 : -0.05; + + m_canvas.setScale(m_canvas.scale() + tmp); + event->accept(); + update(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/splineeditor.h b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.h new file mode 100644 index 0000000000..8b454943ad --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/splineeditor.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QWidget> + +#include "canvas.h" +#include "easingcurve.h" + +QT_FORWARD_DECLARE_CLASS(QPropertyAnimation) + +namespace QmlDesigner { + +class SegmentProperties; + +class SplineEditor : public QWidget +{ + Q_OBJECT + + Q_PROPERTY(double progress READ progress WRITE setProgress) + +signals: + void easingCurveChanged(const EasingCurve &curve); + +public: + explicit SplineEditor(QWidget *parent = nullptr); + + double progress() const; + + EasingCurve easingCurve() const; + + void animate() const; + + void setDuration(int duration); + + void setProgress(double progress); + + void setEasingCurve(const EasingCurve &curve); + +protected: + void resizeEvent(QResizeEvent *) override; + + void paintEvent(QPaintEvent *) override; + + void mousePressEvent(QMouseEvent *) override; + + void mouseMoveEvent(QMouseEvent *) override; + + void mouseReleaseEvent(QMouseEvent *) override; + + void contextMenuEvent(QContextMenuEvent *) override; + + void mouseDoubleClickEvent(QMouseEvent *event) override; + + void wheelEvent(QWheelEvent *event) override; + +private: + Canvas m_canvas; + + EasingCurve m_curve; + + QPoint m_mousePress; + + bool m_mouseDrag = false; + + bool m_block = false; + + double m_progress = 0; + + QPropertyAnimation *m_animation; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo b/src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo new file mode 100644 index 0000000000..c1d7e92bf5 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timeline.metainfo @@ -0,0 +1,35 @@ +MetaInfo { + Type { + name: "QtQuick.Timeline.Timeline" + icon: ":/timelineplugin/images/timeline-16px.png" + + ItemLibraryEntry { + name: "Timeline" + category: "none" + version: "1.0" + requiredImport: "none" + } + } + Type { + name: "QtQuick.Timeline.Keyframe" + icon: ":/timelineplugin/images/keyframe-16px.png" + + ItemLibraryEntry { + name: "Keyframe" + category: "none" + version: "1.0" + requiredImport: "none" + } + } + Type { + name: "QtQuick.Timeline.KeyframeGroup" + icon: ":/timelineplugin/images/keyframe-16px.png" + + ItemLibraryEntry { + name: "KeyframeGroup" + category: "none" + version: "1.0" + requiredImport: "none" + } + } +} diff --git a/src/plugins/qmldesigner/components/timelineeditor/timeline.qrc b/src/plugins/qmldesigner/components/timelineeditor/timeline.qrc new file mode 100644 index 0000000000..b793c1f8da --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timeline.qrc @@ -0,0 +1,77 @@ +<RCC> + <qresource prefix="/timelineplugin"> + <file>timeline.metainfo</file> + <file>images/add_timeline.png</file> + <file>images/add_timeline@2x.png</file> + <file>images/animation.png</file> + <file>images/animation@2x.png</file> + <file>images/back_one_frame.png</file> + <file>images/back_one_frame@2x.png</file> + <file>images/curve_editor.png</file> + <file>images/curve_editor@2x.png</file> + <file>images/curve_picker.png</file> + <file>images/curve_picker@2x.png</file> + <file>images/forward_one_frame.png</file> + <file>images/forward_one_frame@2x.png</file> + <file>images/global_record_keyframes.png</file> + <file>images/global_record_keyframes@2x.png</file> + <file>images/is_keyframe.png</file> + <file>images/is_keyframe@2x.png</file> + <file>images/keyframe.png</file> + <file>images/keyframe@2x.png</file> + <file>images/keyframe_autobezier_active.png</file> + <file>images/keyframe_autobezier_active@2x.png</file> + <file>images/keyframe_autobezier_inactive.png</file> + <file>images/keyframe_autobezier_inactive@2x.png</file> + <file>images/keyframe_autobezier_selected.png</file> + <file>images/keyframe_autobezier_selected@2x.png</file> + <file>images/keyframe_linear_active.png</file> + <file>images/keyframe_linear_active@2x.png</file> + <file>images/keyframe_linear_inactive.png</file> + <file>images/keyframe_linear_inactive@2x.png</file> + <file>images/keyframe_linear_selected.png</file> + <file>images/keyframe_linear_selected@2x.png</file> + <file>images/keyframe_lineartobezier_active.png</file> + <file>images/keyframe_lineartobezier_active@2x.png</file> + <file>images/keyframe_lineartobezier_inactive.png</file> + <file>images/keyframe_lineartobezier_inactive@2x.png</file> + <file>images/keyframe_lineartobezier_selected.png</file> + <file>images/keyframe_lineartobezier_selected@2x.png</file> + <file>images/keyframe_manualbezier_active.png</file> + <file>images/keyframe_manualbezier_active@2x.png</file> + <file>images/keyframe_manualbezier_inactive.png</file> + <file>images/keyframe_manualbezier_inactive@2x.png</file> + <file>images/keyframe_manualbezier_selected.png</file> + <file>images/keyframe_manualbezier_selected@2x.png</file> + <file>images/local_record_keyframes.png</file> + <file>images/local_record_keyframes@2x.png</file> + <file>images/loop_playback.png</file> + <file>images/loop_playback@2x.png</file> + <file>images/next_keyframe.png</file> + <file>images/next_keyframe@2x.png</file> + <file>images/previous_keyframe.png</file> + <file>images/previous_keyframe@2x.png</file> + <file>images/start_playback.png</file> + <file>images/start_playback@2x.png</file> + <file>images/to_first_frame.png</file> + <file>images/to_first_frame@2x.png</file> + <file>images/to_last_frame.png</file> + <file>images/to_last_frame@2x.png</file> + <file>images/pause_playback.png</file> + <file>images/pause_playback@2x.png</file> + <file>images/playhead.png</file> + <file>images/playhead@2x.png</file> + <file>images/work_area_handle_left.png</file> + <file>images/work_area_handle_left@2x.png</file> + <file>images/work_area_handle_right.png</file> + <file>images/work_area_handle_right@2x.png</file> + <file>images/zoom_big.png</file> + <file>images/zoom_big@2x.png</file> + <file>images/zoom_small.png</file> + <file>images/zoom_small@2x.png</file> + <file>images/keyframe-16px.png</file> + <file>images/timeline-16px.png</file> + <file>images/remove_timeline.png</file> + <file>images/remove_timeline@2x.png</file> + </qresource> +</RCC> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp new file mode 100644 index 0000000000..485bb8dbb0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.cpp @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineabstracttool.h" + +#include "timelinetooldelegate.h" + +#include <QPointF> + +namespace QmlDesigner { + +TimelineAbstractTool::TimelineAbstractTool(TimelineGraphicsScene *scene) + : m_scene(scene) + , m_delegate(nullptr) +{} + +TimelineAbstractTool::TimelineAbstractTool(TimelineGraphicsScene *scene, + TimelineToolDelegate *delegate) + : m_scene(scene) + , m_delegate(delegate) +{} + +TimelineAbstractTool::~TimelineAbstractTool() = default; + +TimelineGraphicsScene *TimelineAbstractTool::scene() const +{ + return m_scene; +} + +TimelineToolDelegate *TimelineAbstractTool::delegate() const +{ + return m_delegate; +} + +QPointF TimelineAbstractTool::startPosition() const +{ + return m_delegate->startPoint(); +} + +TimelineMovableAbstractItem *TimelineAbstractTool::currentItem() const +{ + return m_delegate->item(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h new file mode 100644 index 0000000000..0411a8d166 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineabstracttool.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QtGlobal> + +QT_FORWARD_DECLARE_CLASS(QGraphicsSceneMouseEvent) +QT_FORWARD_DECLARE_CLASS(QKeyEvent) +QT_FORWARD_DECLARE_CLASS(QPointF) + +namespace QmlDesigner { + +enum class ToolType { Move, Select }; + +class TimelineMovableAbstractItem; +class TimelineGraphicsScene; +class TimelineToolDelegate; + +class TimelineAbstractTool +{ +public: + explicit TimelineAbstractTool(TimelineGraphicsScene *scene); + explicit TimelineAbstractTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate); + virtual ~TimelineAbstractTool(); + + virtual void mousePressEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) = 0; + virtual void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) = 0; + virtual void mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) = 0; + virtual void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) = 0; + + virtual void keyPressEvent(QKeyEvent *keyEvent) = 0; + virtual void keyReleaseEvent(QKeyEvent *keyEvent) = 0; + + TimelineGraphicsScene *scene() const; + + TimelineToolDelegate *delegate() const; + + QPointF startPosition() const; + + TimelineMovableAbstractItem *currentItem() const; + +private: + TimelineGraphicsScene *m_scene; + + TimelineToolDelegate *m_delegate; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp new file mode 100644 index 0000000000..4751765874 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.cpp @@ -0,0 +1,322 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineactions.h" + +#include "timelineutils.h" +#include "timelineview.h" + +#include <bindingproperty.h> +#include <designdocument.h> +#include <designdocumentview.h> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <utils/algorithm.h> +#include <utils/qtcassert.h> +#include <variantproperty.h> +#include <qmldesignerplugin.h> +#include <qmlobjectnode.h> +#include <qmltimelinekeyframegroup.h> + +namespace QmlDesigner { + +TimelineActions::TimelineActions() = default; + +void TimelineActions::deleteAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + targetNode.view()->executeInTransaction("TimelineActions::deleteAllKeyframesForTarget", [=](){ + if (timeline.isValid()) { + for (auto frames : timeline.keyframeGroupsForTarget(targetNode)) + frames.destroy(); + } + }); +} + +void TimelineActions::insertAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + targetNode.view()->executeInTransaction("TimelineActions::insertAllKeyframesForTarget", [=](){ + auto object = QmlObjectNode(targetNode); + if (timeline.isValid() && object.isValid()) { + for (auto frames : timeline.keyframeGroupsForTarget(targetNode)) { + QVariant value = object.instanceValue(frames.propertyName()); + frames.setValue(value, timeline.currentKeyframe()); + } + } + + }); +} + +void TimelineActions::copyAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + DesignDocumentView::copyModelNodes(Utils::transform(timeline.keyframeGroupsForTarget(targetNode), + &QmlTimelineKeyframeGroup::modelNode)); +} + +void TimelineActions::pasteKeyframesToTarget(const ModelNode &targetNode, + const QmlTimeline &timeline) +{ + if (timeline.isValid()) { + QScopedPointer<Model> pasteModel(DesignDocumentView::pasteToModel()); + + if (!pasteModel) + return; + + DesignDocumentView view; + pasteModel->attachView(&view); + + if (!view.rootModelNode().isValid()) + return; + + ModelNode rootNode = view.rootModelNode(); + + //Sanity check + if (!QmlTimelineKeyframeGroup::checkKeyframesType(rootNode)) { + for (const ModelNode &node : rootNode.directSubModelNodes()) + if (!QmlTimelineKeyframeGroup::checkKeyframesType(node)) + return; + } + + pasteModel->detachView(&view); + + view.executeInTransaction("TimelineActions::pasteKeyframesToTarget", [=, &view](){ + + + targetNode.view()->model()->attachView(&view); + + ModelNode nonConstTargetNode = targetNode; + nonConstTargetNode.validId(); + + if (QmlTimelineKeyframeGroup::checkKeyframesType(rootNode)) { + /* Single selection */ + + ModelNode newNode = view.insertModel(rootNode); + QmlTimelineKeyframeGroup frames(newNode); + frames.setTarget(targetNode); + + timeline.modelNode().defaultNodeListProperty().reparentHere(newNode); + + } else { + /* Multi selection */ + for (const ModelNode &node : rootNode.directSubModelNodes()) { + ModelNode newNode = view.insertModel(node); + QmlTimelineKeyframeGroup frames(newNode); + frames.setTarget(targetNode); + timeline.modelNode().defaultNodeListProperty().reparentHere(newNode); + } + } + }); + } +} + +void TimelineActions::copyKeyframes(const QList<ModelNode> &keyframes) +{ + QList<ModelNode> nodes; + for (const auto &node : keyframes) { + NodeAbstractProperty pp = node.parentProperty(); + QTC_ASSERT(pp.isValid(), return ); + + ModelNode parentModelNode = pp.parentModelNode(); + for (const auto &property : parentModelNode.properties()) { + auto name = property.name(); + if (property.isBindingProperty()) { + BindingProperty bp = property.toBindingProperty(); + ModelNode bpNode = bp.resolveToModelNode(); + if (bpNode.isValid()) + node.setAuxiliaryData(name, bpNode.id()); + } else if (property.isVariantProperty()) { + VariantProperty vp = property.toVariantProperty(); + node.setAuxiliaryData(name, vp.value()); + } + } + + nodes << node; + } + + DesignDocumentView::copyModelNodes(nodes); +} + +bool isKeyframe(const ModelNode &node) +{ + return node.isValid() && node.metaInfo().isValid() + && node.metaInfo().isSubclassOf("QtQuick.Timeline.Keyframe"); +} + +QVariant getValue(const ModelNode &node) +{ + if (node.isValid()) + return node.variantProperty("value").value(); + + return QVariant(); +} + +qreal getTime(const ModelNode &node) +{ + Q_ASSERT(node.isValid()); + Q_ASSERT(node.hasProperty("frame")); + + return node.variantProperty("frame").value().toReal(); +} + +QmlTimelineKeyframeGroup getFrameGroup(const ModelNode &node, + AbstractView *timelineView, + const QmlTimeline &timeline) +{ + QVariant targetId = node.auxiliaryData("target"); + QVariant property = node.auxiliaryData("property"); + + if (targetId.isValid() && property.isValid()) { + ModelNode targetNode = timelineView->modelNodeForId(targetId.toString()); + if (targetNode.isValid()) { + for (QmlTimelineKeyframeGroup frameGrp : timeline.keyframeGroupsForTarget(targetNode)) { + if (frameGrp.propertyName() == property.toByteArray()) + return frameGrp; + } + } + } + return QmlTimelineKeyframeGroup(); +} + +void pasteKeyframe(const qreal expectedTime, + const ModelNode &keyframe, + AbstractView *timelineView, + const QmlTimeline &timeline) +{ + QmlTimelineKeyframeGroup group = getFrameGroup(keyframe, timelineView, timeline); + if (group.isValid()) { + const qreal clampedTime = TimelineUtils::clamp(expectedTime, + timeline.startKeyframe(), + timeline.endKeyframe()); + + // Create a new frame ... + group.setValue(getValue(keyframe), clampedTime); + + // ... look it up by time ... + for (const ModelNode &key : group.keyframePositions()) { + qreal time = key.variantProperty("frame").value().toReal(); + if (qFuzzyCompare(clampedTime, time)) { + // ... and transfer the properties. + for (const auto &property : keyframe.properties()) { + if (property.name() == "frame" || property.name() == "value") + continue; + + if (property.isVariantProperty()) { + auto vp = property.toVariantProperty(); + key.variantProperty(vp.name()).setValue(vp.value()); + } else if (property.isBindingProperty()) { + auto bp = property.toBindingProperty(); + key.bindingProperty(bp.name()).setExpression(bp.expression()); + } + } + } + } + } +} + +std::vector<std::tuple<ModelNode, qreal>> getFramesRelative(const ModelNode &parent) +{ + auto byTime = [](const ModelNode &lhs, const ModelNode &rhs) { + return getTime(lhs) < getTime(rhs); + }; + + std::vector<std::tuple<ModelNode, qreal>> result; + + QList<ModelNode> sortedByTime; + QList<ModelNode> subs(parent.directSubModelNodes()); + + std::copy_if(subs.begin(), subs.end(), std::back_inserter(sortedByTime), &isKeyframe); + std::sort(sortedByTime.begin(), sortedByTime.end(), byTime); + + if (!sortedByTime.empty()) { + qreal firstTime = getTime(sortedByTime.first()); + for (ModelNode keyframe : sortedByTime) + result.emplace_back(keyframe, getTime(keyframe) - firstTime); + } + + return result; +} + +void TimelineActions::pasteKeyframes(AbstractView *timelineView, const QmlTimeline &timeline) +{ + QScopedPointer<Model> pasteModel(DesignDocumentView::pasteToModel()); + + if (!pasteModel) + return; + + DesignDocumentView view; + pasteModel->attachView(&view); + + if (!view.rootModelNode().isValid()) + return; + + const qreal currentTime = timeline.currentKeyframe(); + + ModelNode rootNode = view.rootModelNode(); + + timelineView->executeInTransaction("TimelineActions::pasteKeyframes", [=](){ + if (isKeyframe(rootNode)) + pasteKeyframe(currentTime, rootNode, timelineView, timeline); + else + for (auto frame : getFramesRelative(rootNode)) + pasteKeyframe(currentTime + std::get<1>(frame), + std::get<0>(frame), + timelineView, + timeline); + + }); +} + +bool TimelineActions::clipboardContainsKeyframes() +{ + QScopedPointer<Model> pasteModel(DesignDocumentView::pasteToModel()); + + if (!pasteModel) + return false; + + DesignDocumentView view; + pasteModel->attachView(&view); + + if (!view.rootModelNode().isValid()) + return false; + + ModelNode rootNode = view.rootModelNode(); + + if (!rootNode.hasAnySubModelNodes()) + return false; + + //Sanity check + if (!QmlTimelineKeyframeGroup::checkKeyframesType(rootNode)) { + for (const ModelNode &node : rootNode.directSubModelNodes()) + if (!QmlTimelineKeyframeGroup::checkKeyframesType(node)) + return false; + } + + return true; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineactions.h b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.h new file mode 100644 index 0000000000..bae09b110f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineactions.h @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <modelnode.h> +#include <qmltimeline.h> + +namespace QmlDesigner { + +class TimelineActions +{ +public: + static void deleteAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline); + static void insertAllKeyframesForTarget(const ModelNode &targetNode, + const QmlTimeline &timeline); + static void copyAllKeyframesForTarget(const ModelNode &targetNode, const QmlTimeline &timeline); + static void pasteKeyframesToTarget(const ModelNode &targetNode, const QmlTimeline &timeline); + + static void copyKeyframes(const QList<ModelNode> &keyframes); + static void pasteKeyframes(AbstractView *timelineView, const QmlTimeline &TimelineActions); + + static bool clipboardContainsKeyframes(); + +private: + TimelineActions(); +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp new file mode 100644 index 0000000000..032a133f89 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.cpp @@ -0,0 +1,262 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineanimationform.h" +#include "ui_timelineanimationform.h" + +#include <abstractview.h> +#include <bindingproperty.h> +#include <exception> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <signalhandlerproperty.h> +#include <variantproperty.h> +#include <qmlitemnode.h> +#include <qmlobjectnode.h> + +#include <coreplugin/messagebox.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +TimelineAnimationForm::TimelineAnimationForm(QWidget *parent) + : QWidget(parent) + , ui(new Ui::TimelineAnimationForm) +{ + ui->setupUi(this); + + connectSpinBox(ui->duration, "duration"); + connectSpinBox(ui->loops, "loops"); + + connectSpinBox(ui->startFrame, "from"); + connectSpinBox(ui->endFrame, "to"); + + connect(ui->loops, QOverload<int>::of(&QSpinBox::valueChanged), [this]() { + ui->continuous->setChecked(ui->loops->value() == -1); + }); + + connect(ui->continuous, &QCheckBox::toggled, [this](bool checked) { + if (checked) { + setProperty("loops", -1); + ui->loops->setValue(-1); + } else { + setProperty("loops", 1); + ui->loops->setValue(1); + } + }); + + connect(ui->idLineEdit, &QLineEdit::editingFinished, [this]() { + QTC_ASSERT(m_timeline.isValid(), return ); + + static QString lastString; + + const QString newId = ui->idLineEdit->text(); + + if (lastString == newId) + return; + + lastString = newId; + + if (newId == animation().id()) + return; + + bool error = false; + + if (!ModelNode::isValidId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 is an invalid id.").arg(newId)); + error = true; + } else if (animation().view()->hasId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 already exists.").arg(newId)); + } else { + animation().setIdWithRefactoring(newId); + error = true; + } + + if (error) { + lastString.clear(); + ui->idLineEdit->setText(animation().id()); + } + }); + + connect(ui->running, &QCheckBox::clicked, [this](bool checked) { + if (checked) { + setProperty("running", true); + } else { + setProperty("running", false); + } + }); + + connect(ui->pingPong, &QCheckBox::clicked, [this](bool checked) { + if (checked) { + setProperty("pingPong", true); + } else { + setProperty("pingPong", false); + } + }); + + connect(ui->transitionToState, + QOverload<int>::of(&QComboBox::activated), + [this](int index) { + if (!m_animation.isValid()) + return; + if (!m_animation.view()->rootModelNode().hasId()) + return; + + ModelNode rootNode = m_animation.view()->rootModelNode(); + + if (index == 0) { + if (m_animation.signalHandlerProperty("onFinished").isValid()) + m_animation.removeProperty("onFinished"); + } else if (index == 1) { + m_animation.signalHandlerProperty("onFinished") + .setSource(rootNode.id() + ".state = \"" + "\""); + } else { + m_animation.signalHandlerProperty("onFinished") + .setSource(rootNode.id() + ".state = \"" + + ui->transitionToState->currentText() + "\""); + } + }); +} + +TimelineAnimationForm::~TimelineAnimationForm() +{ + delete ui; +} + +void TimelineAnimationForm::setup(const ModelNode &animation) +{ + m_timeline = QmlTimeline(animation.parentProperty().parentModelNode()); + setAnimation(animation); + setupAnimation(); +} + +ModelNode TimelineAnimationForm::animation() const +{ + return m_animation; +} + +void TimelineAnimationForm::setAnimation(const ModelNode &animation) +{ + m_animation = animation; +} + +void TimelineAnimationForm::setupAnimation() +{ + if (!m_animation.isValid()) + setEnabled(false); + + if (m_animation.isValid()) { + setEnabled(true); + + ui->idLineEdit->setText(m_animation.id()); + + if (m_animation.hasVariantProperty("duration")) + ui->duration->setValue(m_animation.variantProperty("duration").value().toInt()); + else + ui->duration->setValue(0); + + ui->startFrame->setValue(m_animation.variantProperty("from").value().toInt()); + ui->endFrame->setValue(m_animation.variantProperty("to").value().toInt()); + + if (m_animation.hasVariantProperty("loops")) + ui->loops->setValue(m_animation.variantProperty("loops").value().toInt()); + else + ui->loops->setValue(0); + + if (m_animation.hasVariantProperty("running")) + ui->running->setChecked(m_animation.variantProperty("running").value().toBool()); + else + ui->running->setChecked(false); + + if (m_animation.hasVariantProperty("pingPong")) + ui->pingPong->setChecked(m_animation.variantProperty("pingPong").value().toBool()); + else + ui->pingPong->setChecked(false); + + ui->continuous->setChecked(ui->loops->value() == -1); + } + + populateStateComboBox(); + + ui->duration->setEnabled(m_animation.isValid()); + ui->running->setEnabled(m_animation.isValid()); + ui->continuous->setEnabled(m_animation.isValid()); + ui->loops->setEnabled(m_animation.isValid()); +} + +void TimelineAnimationForm::setProperty(const PropertyName &propertyName, const QVariant &value) +{ + QTC_ASSERT(m_animation.isValid(), return ); + + try { + m_animation.variantProperty(propertyName).setValue(value); + } catch (const Exception &e) { + e.showException(); + } +} + +void TimelineAnimationForm::connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName) +{ + connect(spinBox, &QSpinBox::editingFinished, [this, propertyName, spinBox]() { + setProperty(propertyName, spinBox->value()); + }); +} + +void TimelineAnimationForm::populateStateComboBox() +{ + ui->transitionToState->clear(); + ui->transitionToState->addItem(tr("none")); + ui->transitionToState->addItem(tr("Base State")); + if (!m_animation.isValid()) + return; + QmlObjectNode rootNode = QmlObjectNode(m_animation.view()->rootModelNode()); + if (rootNode.isValid() && rootNode.modelNode().hasId()) { + for (const QmlModelState &state : QmlItemNode(rootNode).states().allStates()) { + ui->transitionToState + ->addItem(state.modelNode().variantProperty("name").value().toString(), + QVariant::fromValue<ModelNode>(state.modelNode())); + } + if (m_animation.signalHandlerProperty("onFinished").isValid()) { + const QString source = m_animation.signalHandlerProperty("onFinished").source(); + const QStringList list = source.split("="); + if (list.count() == 2) { + QString name = list.last().trimmed(); + name.chop(1); + name.remove(0, 1); + if (name.isEmpty()) + ui->transitionToState->setCurrentIndex(1); + else + ui->transitionToState->setCurrentText(name); + } + } + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h new file mode 100644 index 0000000000..1b5eaf6cab --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.h @@ -0,0 +1,64 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QWidget> + +QT_FORWARD_DECLARE_CLASS(QSpinBox) + +namespace QmlDesigner { + +namespace Ui { +class TimelineAnimationForm; +} + +class TimelineAnimationForm : public QWidget +{ + Q_OBJECT + +public: + explicit TimelineAnimationForm(QWidget *parent); + ~TimelineAnimationForm() override; + void setup(const ModelNode &animation); + ModelNode animation() const; + +private: + void setupAnimation(); + + void setAnimation(const ModelNode &animation); + void setProperty(const PropertyName &propertyName, const QVariant &value); + void connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName); + void populateStateComboBox(); + + Ui::TimelineAnimationForm *ui; + + QmlTimeline m_timeline; + ModelNode m_animation; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui new file mode 100644 index 0000000000..5d13cfa726 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineanimationform.ui @@ -0,0 +1,327 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::TimelineAnimationForm</class> + <widget class="QWidget" name="QmlDesigner::TimelineAnimationForm"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>641</width> + <height>176</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="4" column="3"> + <widget class="QLabel" name="label_16"> + <property name="text"> + <string>Loops:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="4" column="4" colspan="2"> + <widget class="QSpinBox" name="loops"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-1</number> + </property> + <property name="maximum"> + <number>1000</number> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="label_15"> + <property name="text"> + <string>Continuous</string> + </property> + </widget> + </item> + <item row="4" column="2"> + <widget class="QCheckBox" name="continuous"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="5" column="2"> + <widget class="QComboBox" name="transitionToState"> + <item> + <property name="text"> + <string>none</string> + </property> + </item> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="minimumSize"> + <size> + <width>160</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Animation Settings</string> + </property> + </widget> + </item> + <item row="1" column="9"> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_11"> + <property name="minimumSize"> + <size> + <width>140</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>Animation ID:</string> + </property> + </widget> + </item> + <item row="3" column="9"> + <spacer name="horizontalSpacer_7"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="8"> + <widget class="QSpinBox" name="duration"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLabel" name="label_23"> + <property name="text"> + <string>Finished:</string> + </property> + </widget> + </item> + <item row="4" column="7"> + <widget class="QLabel" name="label_17"> + <property name="text"> + <string>Ping pong</string> + </property> + </widget> + </item> + <item row="4" column="8"> + <widget class="QCheckBox" name="pingPong"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="4" column="9"> + <spacer name="horizontalSpacer_8"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_20"> + <property name="minimumSize"> + <size> + <width>140</width> + <height>0</height> + </size> + </property> + <property name="text"> + <string>Transition to state:</string> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="QLineEdit" name="idLineEdit"> + <property name="text"> + <string>animation02</string> + </property> + </widget> + </item> + <item row="2" column="3" colspan="2"> + <widget class="QLabel" name="label_18"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Running in base state</string> + </property> + </widget> + </item> + <item row="3" column="2"> + <widget class="QSpinBox" name="startFrame"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="label_12"> + <property name="text"> + <string>Start frame:</string> + </property> + </widget> + </item> + <item row="2" column="5"> + <widget class="QCheckBox" name="running"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="2" column="9"> + <spacer name="horizontalSpacer_6"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>213</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="7"> + <widget class="QLabel" name="label_14"> + <property name="text"> + <string>Duration:</string> + </property> + </widget> + </item> + <item row="3" column="3"> + <widget class="QLabel" name="label_13"> + <property name="text"> + <string>End frame:</string> + </property> + </widget> + </item> + <item row="3" column="4" colspan="3"> + <widget class="QSpinBox" name="endFrame"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h b/src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h new file mode 100644 index 0000000000..6c012beec1 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineconstants.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QGraphicsItem> + +namespace QmlDesigner { +namespace TimelineConstants { +const int sectionHeight = 18; +const int rulerHeight = sectionHeight + 4; +const int sectionWidth = 200; +const int moveableAbstractItemUserType = QGraphicsItem::UserType + 1; +const int timelineSectionItemUserType = QGraphicsItem::UserType + 2; +const int timelinePropertyItemUserType = QGraphicsItem::UserType + 3; +const int textIndentationProperties = 54; +const int textIndentationSections = 24; +const int toolButtonSize = 11; +const int timelineBounds = 8; +const int timelineLeftOffset = 10; + +const char timelineCategory[] = "Timeline"; +const int priorityTimelineCategory = 110; +const char timelineCategoryDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", "Timeline"); + +const char timelineCopyKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Copy All Keyframes"); +const char timelinePasteKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Paste Keyframes"); +const char timelineInsertKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Add Keyframes at Current Frame"); +const char timelineDeleteKeyframesDisplayName[] = QT_TRANSLATE_NOOP("QmlDesignerContextMenu", + "Delete All Keyframes"); + +const char timelineStatusBarFrameNumber[] = QT_TRANSLATE_NOOP("QmlDesignerTimeline", "Frame %1"); + +const char C_QMLTIMELINE[] = "QmlDesigner::Timeline"; +const char C_SETTINGS[] = "QmlDesigner.Settings"; +const char C_ADD_TIMELINE[] = "QmlDesigner.AddTimeline"; +const char C_TO_START[] = "QmlDesigner.ToStart"; +const char C_TO_END[] = "QmlDesigner.ToEnd"; +const char C_PREVIOUS[] = "QmlDesigner.Previous"; +const char C_PLAY[] = "QmlDesigner.Play"; +const char C_NEXT[] = "QmlDesigner.Next"; +const char C_AUTO_KEYFRAME[] = "QmlDesigner.AutoKeyframe"; +const char C_CURVE_PICKER[] = "QmlDesigner.CurvePicker"; +const char C_ZOOM_IN[] = "QmlDesigner.ZoomIn"; +const char C_ZOOM_OUT[] = "QmlDesigner.ZoomOut"; + +const char C_BAR_ITEM_OVERRIDE[] = "Timeline.OverrideColor"; + +const int keyFrameSize = 17; +const int keyFrameMargin = 2; +} // namespace TimelineConstants +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp new file mode 100644 index 0000000000..b3e2e12a3f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.cpp @@ -0,0 +1,45 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinecontext.h" +#include "timelineconstants.h" +#include "timelinewidget.h" + +namespace QmlDesigner { + +TimelineContext::TimelineContext(QWidget *widget) + : IContext(widget) +{ + setWidget(widget); + setContext(Core::Context(TimelineConstants::C_QMLTIMELINE)); +} + +void TimelineContext::contextHelp(const Core::IContext::HelpCallback &callback) const +{ + if (auto *widget = qobject_cast<TimelineWidget *>(m_widget)) + widget->contextHelp(callback); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h new file mode 100644 index 0000000000..a8a6ca3b8f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontext.h @@ -0,0 +1,43 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <coreplugin/icontext.h> + +#include <QWidget> + +namespace QmlDesigner { + +class TimelineContext : public Core::IContext +{ + Q_OBJECT + +public: + explicit TimelineContext(QWidget *widget); + void contextHelp(const HelpCallback &callback) const override; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp new file mode 100644 index 0000000000..682a14f1c7 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.cpp @@ -0,0 +1,211 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinecontrols.h" +#include "timelinepropertyitem.h" + +#include <coreplugin/icore.h> + +#include <QColorDialog> +#include <QMouseEvent> +#include <QPaintEvent> +#include <QPainter> +#include <QToolTip> + +#include <theme.h> + +#include <limits> + +namespace QmlDesigner { + +TimelineControl *createTimelineControl(const TypeName &name) +{ + if (name == "real" || name == "double" || name == "float") + return new FloatControl; + if (name == "QColor" || name == "color") + return new ColorControl; + + return nullptr; +} + +FloatControl::FloatControl() + : QDoubleSpinBox(nullptr) +{ + setValue(0.0); + setButtonSymbols(QAbstractSpinBox::NoButtons); + setFrame(false); +#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) + setStepType(QAbstractSpinBox::AdaptiveDecimalStepType); +#endif + + setMinimum(std::numeric_limits<float>::lowest()); + setMaximum(std::numeric_limits<float>::max()); + + QColor bg = Theme::instance()->qmlDesignerBackgroundColorDarkAlternate(); + + auto p = palette(); + p.setColor(QPalette::Base, bg.darker(110)); + setPalette(p); + + m_timer.setInterval(100); + m_timer.setSingleShot(true); + + auto startTimer = [this]( ) { m_timer.start(); }; + auto deferredSlot = [this]( ) { emit controlValueChanged(QVariant(this->value())); }; + + QObject::connect(this, &QDoubleSpinBox::editingFinished, &m_timer, startTimer); + QObject::connect(&m_timer, &QTimer::timeout, deferredSlot); +} + +FloatControl::~FloatControl() = default; + +QWidget *FloatControl::widget() +{ + return this; +} + +void FloatControl::connect(TimelinePropertyItem *item) +{ + QObject::connect(this, + &FloatControl::controlValueChanged, + item, + &TimelinePropertyItem::changePropertyValue); +} + +QVariant FloatControl::controlValue() const +{ + return QVariant(value()); +} + +void FloatControl::setControlValue(const QVariant &value) +{ + if (value.userType() != QMetaType::Float && value.userType() != QMetaType::Double) + return; + + QSignalBlocker blocker(this); + setValue(value.toDouble()); +} + +void FloatControl::setSize(int width, int height) +{ + setFixedWidth(width); + setFixedHeight(height); +} + +ColorControl::ColorControl() + : QWidget(nullptr) + , m_color(Qt::black) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(20); +} + +ColorControl::ColorControl(const QColor &color, QWidget *parent) + : QWidget(parent) + , m_color(color) +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + setFixedHeight(20); +} + +ColorControl::~ColorControl() = default; + +QWidget *ColorControl::widget() +{ + return this; +} + +void ColorControl::connect(TimelinePropertyItem *item) +{ + QObject::connect(this, + &ColorControl::controlValueChanged, + item, + &TimelinePropertyItem::changePropertyValue); +} + +QVariant ColorControl::controlValue() const +{ + return QVariant(value()); +} + +void ColorControl::setControlValue(const QVariant &value) +{ + if (value.userType() != QMetaType::QColor) + return; + + m_color = qvariant_cast<QColor>(value); +} + +void ColorControl::setSize(int width, int height) +{ + setFixedWidth(width); + setFixedHeight(height); +} + +QColor ColorControl::value() const +{ + return m_color; +} + +bool ColorControl::event(QEvent *event) +{ + if (event->type() == QEvent::ToolTip) { + if (auto helpEvent = static_cast<const QHelpEvent *>(event)) { + QToolTip::showText(helpEvent->globalPos(), m_color.name()); + return true; + } + } + return QWidget::event(event); +} + +void ColorControl::paintEvent(QPaintEvent *event) +{ + QPainter painter(this); + painter.fillRect(event->rect(), m_color); +} + +void ColorControl::mouseReleaseEvent(QMouseEvent *event) +{ + QColor color = QColorDialog::getColor(m_color, Core::ICore::dialogParent()); + + event->accept(); + + if (color != m_color) { + m_color = color; + update(); + emit valueChanged(); + emit controlValueChanged(QVariant(m_color)); + } +} + +void ColorControl::mousePressEvent(QMouseEvent *event) +{ + // Needed to make the mouseRelease Event work if this + // widget is embedded inside a QGraphicsProxyWidget. + QWidget::mousePressEvent(event); + event->accept(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h new file mode 100644 index 0000000000..4c30357be9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinecontrols.h @@ -0,0 +1,123 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <nodemetainfo.h> + +#include <QDoubleSpinBox> +#include <QTimer> +#include <QWidget> + +namespace QmlDesigner { + +class TimelinePropertyItem; + +class TimelineControl +{ +public: + virtual ~TimelineControl() = default; + + virtual QWidget *widget() = 0; + + virtual void connect(TimelinePropertyItem *scene) = 0; + + virtual QVariant controlValue() const = 0; + + virtual void setControlValue(const QVariant &value) = 0; + + virtual void setSize(int width, int height) = 0; +}; + +TimelineControl *createTimelineControl(const TypeName &name); + +class FloatControl : public QDoubleSpinBox, public TimelineControl +{ + Q_OBJECT + +public: + FloatControl(); + + ~FloatControl() override; + + QWidget *widget() override; + + void connect(TimelinePropertyItem *scene) override; + + QVariant controlValue() const override; + + void setControlValue(const QVariant &value) override; + + void setSize(int width, int height) override; + +signals: + void controlValueChanged(const QVariant &value); + +private: + QTimer m_timer; +}; + +class ColorControl : public QWidget, public TimelineControl +{ + Q_OBJECT + +public: + ColorControl(); + + ColorControl(const QColor &color, QWidget *parent = nullptr); + + ~ColorControl() override; + + QWidget *widget() override; + + void connect(TimelinePropertyItem *item) override; + + QVariant controlValue() const override; + + void setControlValue(const QVariant &value) override; + + void setSize(int width, int height) override; + + QColor value() const; + +protected: + bool event(QEvent *event) override; + + void paintEvent(QPaintEvent *event) override; + + void mouseReleaseEvent(QMouseEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + +signals: + void valueChanged(); + + void controlValueChanged(const QVariant &value); + +private: + QColor m_color; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri b/src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri new file mode 100644 index 0000000000..8001748fb0 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineeditor.pri @@ -0,0 +1,81 @@ +QT *= qml quick core + +VPATH += $$PWD + +INCLUDEPATH += $$PWD + +DEFINES += TIMELINE_QML_PATH=\\\"$$PWD/qml/\\\" + +SOURCES += \ + timelineview.cpp \ + timelinewidget.cpp \ + timelinegraphicsscene.cpp \ + timelinegraphicslayout.cpp \ + timelinepropertyitem.cpp \ + timelinesectionitem.cpp \ + timelineitem.cpp \ + timelinemovableabstractitem.cpp \ + timelineabstracttool.cpp \ + timelinemovetool.cpp \ + timelineselectiontool.cpp \ + timelineplaceholder.cpp \ + setframevaluedialog.cpp \ + timelinetoolbar.cpp \ + easingcurvedialog.cpp \ + timelinetoolbutton.cpp \ + timelinesettingsdialog.cpp \ + timelineactions.cpp \ + timelinecontext.cpp \ + timelineutils.cpp \ + timelineanimationform.cpp \ + timelineform.cpp \ + splineeditor.cpp \ + preseteditor.cpp \ + canvas.cpp \ + canvasstyledialog.cpp \ + easingcurve.cpp \ + timelinesettingsmodel.cpp \ + timelinetooldelegate.cpp \ + timelinecontrols.cpp + +HEADERS += \ + timelineview.h \ + timelinewidget.h \ + timelinegraphicsscene.h \ + timelinegraphicslayout.h \ + timelinepropertyitem.h \ + timelinesectionitem.h \ + timelineitem.h \ + timelineconstants.h \ + timelinemovableabstractitem.h \ + timelineabstracttool.h \ + timelinemovetool.h \ + timelineselectiontool.h \ + timelineplaceholder.h \ + timelineicons.h \ + timelinetoolbar.h \ + setframevaluedialog.h \ + easingcurvedialog.h \ + timelinetoolbutton.h \ + timelinesettingsdialog.h \ + timelineactions.h \ + timelinecontext.h \ + timelineutils.h \ + timelineanimationform.h \ + timelineform.h \ + splineeditor.h \ + preseteditor.h \ + canvas.h \ + canvasstyledialog.h \ + easingcurve.h \ + timelinesettingsmodel.h \ + timelinecontrols.h + +RESOURCES += \ + timeline.qrc + +FORMS += \ + setframevaluedialog.ui \ + timelinesettingsdialog.ui \ + timelineanimationform.ui \ + timelineform.ui diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp new file mode 100644 index 0000000000..eb63ad4883 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineform.cpp @@ -0,0 +1,186 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineform.h" +#include "ui_timelineform.h" + +#include <abstractview.h> +#include <bindingproperty.h> +#include <exception> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <variantproperty.h> + +#include <coreplugin/messagebox.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +namespace QmlDesigner { + +TimelineForm::TimelineForm(QWidget *parent) + : QWidget(parent) + , ui(new Ui::TimelineForm) +{ + ui->setupUi(this); + + ui->duration->setVisible(false); + + connect(ui->expressionBindingLineEdit, &QLineEdit::editingFinished, [this]() { + QTC_ASSERT(m_timeline.isValid(), return ); + + + static QString lastString; + + const QString bindingText = ui->expressionBindingLineEdit->text(); + + if (bindingText == lastString) + return; + + lastString = bindingText; + + if (bindingText.isEmpty()) { + ui->animation->setChecked(true); + try { + m_timeline.modelNode().removeProperty("currentFrame"); + } catch (const Exception &e) { + e.showException(); + } + return; + } + + ui->expressionBinding->setChecked(true); + + try { + m_timeline.modelNode() + .bindingProperty("currentFrame") + .setExpression(bindingText); + } catch (const Exception &e) { + e.showException(); + } + }); + + connect(ui->idLineEdit, &QLineEdit::editingFinished, [this]() { + QTC_ASSERT(m_timeline.isValid(), return ); + + static QString lastString; + + const QString newId = ui->idLineEdit->text(); + + if (newId == lastString) + return; + + lastString = newId; + + if (newId == m_timeline.modelNode().id()) + return; + + bool error = false; + + if (!ModelNode::isValidId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 is an invalid id.").arg(newId)); + error = true; + } else if (m_timeline.view()->hasId(newId)) { + Core::AsynchronousMessageBox::warning(tr("Invalid Id"), + tr("%1 already exists.").arg(newId)); + error = true; + } else { + m_timeline.modelNode().setIdWithRefactoring(newId); + } + + if (error) { + lastString.clear(); + ui->idLineEdit->setText(m_timeline.modelNode().id()); + } + }); + + connectSpinBox(ui->startFrame, "startFrame"); + connectSpinBox(ui->endFrame, "endFrame"); +} + +TimelineForm::~TimelineForm() +{ + delete ui; +} + +void TimelineForm::setTimeline(const QmlTimeline &timeline) +{ + m_timeline = timeline; + + ui->expressionBindingLineEdit->clear(); + + if (m_timeline.isValid()) { + ui->idLineEdit->setText(m_timeline.modelNode().displayName()); + ui->duration->setValue(qRound(m_timeline.duration())); + ui->startFrame->setValue( + m_timeline.modelNode().variantProperty("startFrame").value().toInt()); + ui->endFrame->setValue(m_timeline.modelNode().variantProperty("endFrame").value().toInt()); + + ui->duration->setValue(qRound(m_timeline.duration())); + + if (m_timeline.modelNode().hasBindingProperty("currentFrame")) { + ui->expressionBindingLineEdit->setText( + m_timeline.modelNode().bindingProperty("currentFrame").expression()); + ui->expressionBinding->setChecked(true); + } else { + ui->expressionBinding->setChecked(false); + } + } +} + +QmlTimeline TimelineForm::timeline() const +{ + return m_timeline; +} + +void TimelineForm::setHasAnimation(bool b) +{ + ui->expressionBinding->setChecked(!b); + ui->animation->setChecked(b); + ui->expressionBindingLineEdit->setDisabled(b); +} + +void TimelineForm::setProperty(const PropertyName &propertyName, const QVariant &value) +{ + QTC_ASSERT(m_timeline.isValid(), return ); + + try { + m_timeline.modelNode().variantProperty(propertyName).setValue(value); + } catch (const Exception &e) { + e.showException(); + } + ui->duration->setValue(qRound(m_timeline.duration())); +} + +void TimelineForm::connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName) +{ + connect(spinBox, &QSpinBox::editingFinished, [this, propertyName, spinBox]() { + setProperty(propertyName, spinBox->value()); + }); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineform.h b/src/plugins/qmldesigner/components/timelineeditor/timelineform.h new file mode 100644 index 0000000000..7745de3a0f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineform.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QWidget> + +QT_FORWARD_DECLARE_CLASS(QSpinBox) + +namespace QmlDesigner { + +namespace Ui { +class TimelineForm; +} + +class TimelineForm : public QWidget +{ + Q_OBJECT + +public: + explicit TimelineForm(QWidget *parent); + ~TimelineForm() override; + void setTimeline(const QmlTimeline &timeline); + QmlTimeline timeline() const; + void setHasAnimation(bool b); + +private: + void setProperty(const PropertyName &propertyName, const QVariant &value); + void connectSpinBox(QSpinBox *spinBox, const PropertyName &propertyName); + + Ui::TimelineForm *ui; + QmlTimeline m_timeline; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineform.ui b/src/plugins/qmldesigner/components/timelineeditor/timelineform.ui new file mode 100644 index 0000000000..b8b47e4c70 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineform.ui @@ -0,0 +1,254 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::TimelineForm</class> + <widget class="QWidget" name="QmlDesigner::TimelineForm"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>627</width> + <height>170</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="8" colspan="2"> + <spacer name="horizontalSpacer_11"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="5"> + <widget class="QSpinBox" name="endFrame"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="2" column="2" colspan="2"> + <widget class="QSpinBox" name="startFrame"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="minimum"> + <number>-10000</number> + </property> + <property name="maximum"> + <number>10000</number> + </property> + </widget> + </item> + <item row="2" column="6"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Duration</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>Expression binding:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="2" column="8" colspan="2"> + <spacer name="horizontalSpacer_10"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="4"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>End frame:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="3" column="3" colspan="2"> + <widget class="QRadioButton" name="animation"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Animation</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="3" column="8" colspan="2"> + <spacer name="horizontalSpacer_12"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="4" column="1" colspan="5"> + <widget class="QLineEdit" name="expressionBindingLineEdit"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="minimumSize"> + <size> + <width>240</width> + <height>0</height> + </size> + </property> + </widget> + </item> + <item row="4" column="8" colspan="2"> + <spacer name="horizontalSpacer_9"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>49</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="2" column="7"> + <widget class="QSpinBox" name="duration"> + <property name="minimumSize"> + <size> + <width>80</width> + <height>0</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>80</width> + <height>16777215</height> + </size> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="maximum"> + <number>20000</number> + </property> + </widget> + </item> + <item row="3" column="1" colspan="2"> + <widget class="QRadioButton" name="expressionBinding"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Expression binding</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="5"> + <widget class="QLineEdit" name="idLineEdit"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Timeline ID:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="minimumSize"> + <size> + <width>160</width> + <height>0</height> + </size> + </property> + <property name="font"> + <font> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>Timeline Settings</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>Start frame:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp new file mode 100644 index 0000000000..af0f26d249 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.cpp @@ -0,0 +1,162 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinegraphicslayout.h" + +#include "timelinegraphicsscene.h" +#include "timelineplaceholder.h" +#include "timelinesectionitem.h" +#include "timelineview.h" + +#include <QGraphicsLinearLayout> + +#include <cmath> + +namespace QmlDesigner { + +TimelineGraphicsLayout::TimelineGraphicsLayout(TimelineGraphicsScene *scene, TimelineItem *parent) + : TimelineItem(parent) + , m_layout(new QGraphicsLinearLayout) + , m_rulerItem(TimelineRulerSectionItem::create(scene, this)) + , m_placeholder1(TimelinePlaceholder::create(scene, this)) + , m_placeholder2(TimelinePlaceholder::create(scene, this)) +{ + m_layout->setOrientation(Qt::Vertical); + m_layout->setSpacing(0); + m_layout->setContentsMargins(0, 0, 0, 0); + + m_layout->addItem(m_rulerItem); + m_layout->addItem(m_placeholder1); + m_layout->addItem(m_placeholder2); + + setLayout(m_layout); + + setPos(QPointF(0, 0)); + + connect(m_rulerItem, + &TimelineRulerSectionItem::rulerClicked, + this, + &TimelineGraphicsLayout::rulerClicked); +} + +TimelineGraphicsLayout::~TimelineGraphicsLayout() = default; + +double TimelineGraphicsLayout::rulerWidth() const +{ + return m_rulerItem->preferredWidth(); +} + +double TimelineGraphicsLayout::rulerScaling() const +{ + return m_rulerItem->rulerScaling(); +} + +double TimelineGraphicsLayout::rulerDuration() const +{ + return m_rulerItem->rulerDuration(); +} + +double TimelineGraphicsLayout::startFrame() const +{ + return m_rulerItem->startFrame(); +} + +double TimelineGraphicsLayout::endFrame() const +{ + return m_rulerItem->endFrame(); +} + +void TimelineGraphicsLayout::setWidth(int width) +{ + m_rulerItem->setSizeHints(width); + m_placeholder1->setMinimumWidth(width); + m_placeholder2->setMinimumWidth(width); + setPreferredWidth(width); + setMaximumWidth(width); +} + +void TimelineGraphicsLayout::setTimeline(const QmlTimeline &timeline) +{ + m_layout->removeItem(m_rulerItem); + m_layout->removeItem(m_placeholder1); + m_layout->removeItem(m_placeholder2); + + m_rulerItem->setParentItem(nullptr); + m_placeholder1->setParentItem(nullptr); + m_placeholder2->setParentItem(nullptr); + + qDeleteAll(this->childItems()); + + m_rulerItem->setParentItem(this); + m_rulerItem->invalidateRulerSize(timeline); + m_layout->addItem(m_rulerItem); + + m_placeholder1->setParentItem(this); + m_layout->addItem(m_placeholder1); + + m_layout->invalidate(); + + if (timeline.isValid()) { + for (const ModelNode &target : timeline.allTargets()) { + if (target.isValid()) { + auto item = TimelineSectionItem::create(timeline, target, this); + m_layout->addItem(item); + } + } + } + + m_placeholder2->setParentItem(this); + m_layout->addItem(m_placeholder2); + + if (auto *scene = timelineScene()) + if (auto *view = scene->timelineView()) + if (!timeline.isValid() && view->isAttached()) + emit scaleFactorChanged(0); +} + +void TimelineGraphicsLayout::setRulerScaleFactor(int factor) +{ + m_rulerItem->setRulerScaleFactor(factor); +} + +void TimelineGraphicsLayout::invalidate() +{ + m_layout->invalidate(); +} + +int TimelineGraphicsLayout::maximumScrollValue() const +{ + const qreal w = this->geometry().width() - qreal(TimelineConstants::sectionWidth); + const qreal duration = m_rulerItem->rulerDuration() + m_rulerItem->rulerDuration() * 0.1; + const qreal maxr = m_rulerItem->rulerScaling() * duration - w; + return std::round(qMax(maxr, 0.0)); +} + +void TimelineGraphicsLayout::activate() +{ + m_layout->activate(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h new file mode 100644 index 0000000000..d5b7c4debc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicslayout.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineitem.h" + +QT_FORWARD_DECLARE_CLASS(QGraphicsLinearLayout) + +namespace QmlDesigner { + +class TimelineItem; +class TimelineRulerSectionItem; +class TimelinePlaceholder; + +class QmlTimeline; + +class TimelineGraphicsLayout : public TimelineItem +{ + Q_OBJECT + +signals: + void rulerClicked(const QPointF &pos); + + void scaleFactorChanged(int factor); + +public: + TimelineGraphicsLayout(TimelineGraphicsScene *scene, TimelineItem *parent = nullptr); + + ~TimelineGraphicsLayout() override; + +public: + double rulerWidth() const; + + double rulerScaling() const; + + double rulerDuration() const; + + double startFrame() const; + + double endFrame() const; + + void setWidth(int width); + + void setTimeline(const QmlTimeline &timeline); + + void setRulerScaleFactor(int factor); + + void invalidate(); + + int maximumScrollValue() const; + + void activate(); + +private: + QGraphicsLinearLayout *m_layout = nullptr; + + TimelineRulerSectionItem *m_rulerItem = nullptr; + + TimelinePlaceholder *m_placeholder1 = nullptr; + + TimelinePlaceholder *m_placeholder2 = nullptr; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp new file mode 100644 index 0000000000..99cc486710 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.cpp @@ -0,0 +1,720 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinegraphicsscene.h" + +#include "timelineactions.h" +#include "timelinegraphicslayout.h" +#include "timelineitem.h" +#include "timelinemovableabstractitem.h" +#include "timelinemovetool.h" +#include "timelineplaceholder.h" +#include "timelinepropertyitem.h" +#include "timelinesectionitem.h" +#include "timelinetoolbar.h" +#include "timelineview.h" +#include "timelinewidget.h" + +#include <designdocumentview.h> +#include <exception.h> +#include <rewritertransaction.h> +#include <rewriterview.h> +#include <viewmanager.h> +#include <qmldesignerplugin.h> +#include <qmlobjectnode.h> +#include <qmltimelinekeyframegroup.h> + +#include <bindingproperty.h> + +#include <nodeabstractproperty.h> +#include <nodelistproperty.h> +#include <variantproperty.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +#include <QComboBox> +#include <QGraphicsLinearLayout> +#include <QGraphicsProxyWidget> +#include <QGraphicsSceneMouseEvent> +#include <QGraphicsView> +#include <QKeyEvent> + +#include <cmath> + +namespace QmlDesigner { + +QList<QmlTimelineKeyframeGroup> allTimelineFrames(const QmlTimeline &timeline) +{ + QList<QmlTimelineKeyframeGroup> returnList; + + for (const ModelNode &childNode : + timeline.modelNode().defaultNodeListProperty().toModelNodeList()) { + if (QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup(childNode)) + returnList.append(QmlTimelineKeyframeGroup(childNode)); + } + + return returnList; +} + +TimelineGraphicsScene::TimelineGraphicsScene(TimelineWidget *parent) + : QGraphicsScene(parent) + , m_parent(parent) + , m_layout(new TimelineGraphicsLayout(this)) + , m_currentFrameIndicator(new TimelineFrameHandle) + , m_tools(this) +{ + addItem(m_layout); + addItem(m_currentFrameIndicator); + + setSceneRect(m_layout->geometry()); + + connect(m_layout, &QGraphicsWidget::geometryChanged, this, [this]() { + auto rect = m_layout->geometry(); + + setSceneRect(rect); + + if (auto *gview = graphicsView()) + gview->setSceneRect(rect.adjusted(0, TimelineConstants::rulerHeight, 0, 0)); + + if (auto *rview = rulerView()) + rview->setSceneRect(rect); + + m_currentFrameIndicator->setHeight(m_layout->geometry().height()); + }); + + auto moveFrameIndicator = [this](const QPointF &pos) { + m_currentFrameIndicator->commitPosition(pos); + }; + connect(m_layout, &TimelineGraphicsLayout::rulerClicked, moveFrameIndicator); + + auto changeScale = [this](int factor) { + timelineWidget()->changeScaleFactor(factor); + setRulerScaling(qreal(factor)); + }; + connect(m_layout, &TimelineGraphicsLayout::scaleFactorChanged, changeScale); +} + +TimelineGraphicsScene::~TimelineGraphicsScene() +{ + QSignalBlocker block(this); + clearSelection(); + qDeleteAll(items()); +} + +void TimelineGraphicsScene::onShow() +{ + if (timelineView()->isAttached()) { + auto timeline = currentTimeline(); + if (timeline.isValid()) { + int cf = std::round(timeline.currentKeyframe()); + setCurrentFrame(cf); + } + + emit m_layout->scaleFactorChanged(0); + } +} + +void TimelineGraphicsScene::setTimeline(const QmlTimeline &timeline) +{ + if (qFuzzyCompare(timeline.duration(), 0.0)) + return; + + m_layout->setTimeline(timeline); +} + +void TimelineGraphicsScene::clearTimeline() +{ + m_layout->setTimeline(QmlTimeline()); +} + +void TimelineGraphicsScene::setWidth(int width) +{ + m_layout->setWidth(width); + invalidateScrollbar(); +} + +void TimelineGraphicsScene::invalidateLayout() +{ + m_layout->invalidate(); +} + +void TimelineGraphicsScene::setCurrenFrame(const QmlTimeline &timeline, qreal frame) +{ + if (timeline.isValid()) + m_currentFrameIndicator->setPosition(frame); + else + m_currentFrameIndicator->setPosition(0); + + invalidateCurrentValues(); +} + +void TimelineGraphicsScene::setCurrentFrame(int frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) { + timeline.modelNode().setAuxiliaryData("currentFrame@NodeInstance", frame); + m_currentFrameIndicator->setPosition(frame + timeline.startKeyframe()); + } else { + m_currentFrameIndicator->setPosition(0); + } + + invalidateCurrentValues(); + + emitStatusBarFrameMessageChanged(frame); +} + +void TimelineGraphicsScene::setStartFrame(int frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) + timeline.modelNode().variantProperty("startFrame").setValue(frame); +} + +void TimelineGraphicsScene::setEndFrame(int frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) + timeline.modelNode().variantProperty("endFrame").setValue(frame); +} + +qreal TimelineGraphicsScene::rulerScaling() const +{ + return m_layout->rulerScaling(); +} + +int TimelineGraphicsScene::rulerWidth() const +{ + return m_layout->rulerWidth(); +} + +qreal TimelineGraphicsScene::rulerDuration() const +{ + return m_layout->rulerDuration(); +} + +qreal TimelineGraphicsScene::startFrame() const +{ + return m_layout->startFrame(); +} + +qreal TimelineGraphicsScene::endFrame() const +{ + return m_layout->endFrame(); +} + +qreal TimelineGraphicsScene::mapToScene(qreal x) const +{ + return TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset + + (x - startFrame()) * rulerScaling() - scrollOffset(); +} + +qreal TimelineGraphicsScene::mapFromScene(qreal x) const +{ + auto xPosOffset = (x - TimelineConstants::sectionWidth - TimelineConstants::timelineLeftOffset) + + scrollOffset(); + + return xPosOffset / rulerScaling() + startFrame(); +} + +qreal TimelineGraphicsScene::currentFramePosition() const +{ + return currentTimeline().currentKeyframe(); +} + +QVector<qreal> TimelineGraphicsScene::keyframePositions() const +{ + QVector<qreal> positions; + for (const auto &frames : allTimelineFrames(currentTimeline())) + positions.append(keyframePositions(frames)); + return positions; +} + +QVector<qreal> TimelineGraphicsScene::keyframePositions(const QmlTimelineKeyframeGroup &frames) const +{ + const QList<ModelNode> keyframes = frames.keyframePositions(); + QVector<qreal> positions; + for (const ModelNode &modelNode : keyframes) + positions.append(modelNode.variantProperty("frame").value().toReal()); + return positions; +} + +void TimelineGraphicsScene::setRulerScaling(int scaleFactor) +{ + const qreal oldOffset = scrollOffset(); + const qreal oldScaling = m_layout->rulerScaling(); + const qreal oldPosition = mapToScene(currentFramePosition()); + m_layout->setRulerScaleFactor(scaleFactor); + + const qreal newScaling = m_layout->rulerScaling(); + const qreal newPosition = mapToScene(currentFramePosition()); + + const qreal newOffset = oldOffset + (newPosition - oldPosition); + + if (std::isinf(oldScaling) || std::isinf(newScaling)) + setScrollOffset(0); + else { + setScrollOffset(std::round(newOffset)); + + const qreal start = mapToScene(startFrame()); + const qreal head = TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset; + + if (start - head > 0) + setScrollOffset(0); + } + + invalidateSections(); + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) + setCurrenFrame(timeline, + timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal()); + + invalidateScrollbar(); + update(); +} + +void TimelineGraphicsScene::commitCurrentFrame(qreal frame) +{ + QmlTimeline timeline(timelineModelNode()); + + if (timeline.isValid()) { + timeline.modelNode().setAuxiliaryData("currentFrame@NodeInstance", qRound(frame)); + setCurrenFrame(timeline, qRound(frame)); + invalidateCurrentValues(); + } + emitStatusBarFrameMessageChanged(int(frame)); +} + +QList<TimelineKeyframeItem *> TimelineGraphicsScene::selectedKeyframes() const +{ + return m_selectedKeyframes; +} + +bool TimelineGraphicsScene::hasSelection() const +{ + return !m_selectedKeyframes.empty(); +} + +bool TimelineGraphicsScene::isCurrent(TimelineKeyframeItem *keyframe) const +{ + if (m_selectedKeyframes.empty()) + return false; + + return m_selectedKeyframes.back() == keyframe; +} + +bool TimelineGraphicsScene::isKeyframeSelected(TimelineKeyframeItem *keyframe) const +{ + return m_selectedKeyframes.contains(keyframe); +} + +bool TimelineGraphicsScene::multipleKeyframesSelected() const +{ + return m_selectedKeyframes.count() > 1; +} + +void TimelineGraphicsScene::invalidateSectionForTarget(const ModelNode &target) +{ + if (!target.isValid()) + return; + + bool found = false; + for (auto child : m_layout->childItems()) + TimelineSectionItem::updateDataForTarget(child, target, &found); + + if (!found) + invalidateScene(); + + clearSelection(); + invalidateLayout(); +} + +void TimelineGraphicsScene::invalidateKeyframesForTarget(const ModelNode &target) +{ + for (auto child : m_layout->childItems()) + TimelineSectionItem::updateFramesForTarget(child, target); +} + +void TimelineGraphicsScene::invalidateScene() +{ + ModelNode node = timelineView()->modelNodeForId( + timelineWidget()->toolBar()->currentTimelineId()); + setTimeline(QmlTimeline(node)); + invalidateScrollbar(); +} + +void TimelineGraphicsScene::invalidateScrollbar() +{ + double max = m_layout->maximumScrollValue(); + timelineWidget()->setupScrollbar(0, max, scrollOffset()); + if (scrollOffset() > max) + setScrollOffset(max); +} + +void TimelineGraphicsScene::invalidateCurrentValues() +{ + for (auto item : items()) + TimelinePropertyItem::updateTextEdit(item); +} + +void TimelineGraphicsScene::invalidateRecordButtonsStatus() +{ + for (auto item : items()) + TimelinePropertyItem::updateRecordButtonStatus(item); +} + +int TimelineGraphicsScene::scrollOffset() const +{ + return m_scrollOffset; +} + +void TimelineGraphicsScene::setScrollOffset(int offset) +{ + m_scrollOffset = offset; + emitScrollOffsetChanged(); + update(); +} + +QGraphicsView *TimelineGraphicsScene::graphicsView() const +{ + for (auto *v : views()) + if (v->objectName() == "SceneView") + return v; + + return nullptr; +} + +QGraphicsView *TimelineGraphicsScene::rulerView() const +{ + for (auto *v : views()) + if (v->objectName() == "RulerView") + return v; + + return nullptr; +} + +QmlTimeline TimelineGraphicsScene::currentTimeline() const +{ + return QmlTimeline(timelineModelNode()); +} + +QRectF TimelineGraphicsScene::selectionBounds() const +{ + QRectF bbox; + + for (auto *frame : m_selectedKeyframes) + bbox = bbox.united(frame->rect()); + + return bbox; +} + +void TimelineGraphicsScene::selectKeyframes(const SelectionMode &mode, + const QList<TimelineKeyframeItem *> &items) +{ + if (mode == SelectionMode::Remove || mode == SelectionMode::Toggle) { + for (auto *item : items) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (m_selectedKeyframes.contains(keyframe)) { + keyframe->setHighlighted(false); + m_selectedKeyframes.removeAll(keyframe); + + } else if (mode == SelectionMode::Toggle) { + if (!m_selectedKeyframes.contains(keyframe)) { + keyframe->setHighlighted(true); + m_selectedKeyframes << keyframe; + } + } + } + } + + } else { + if (mode == SelectionMode::New) + clearSelection(); + + for (auto item : items) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (!m_selectedKeyframes.contains(keyframe)) { + keyframe->setHighlighted(true); + m_selectedKeyframes.append(keyframe); + } + } + } + } + emit selectionChanged(); +} + +void TimelineGraphicsScene::clearSelection() +{ + for (auto *keyframe : m_selectedKeyframes) + if (keyframe) + keyframe->setHighlighted(false); + + m_selectedKeyframes.clear(); +} + +QList<QGraphicsItem *> TimelineGraphicsScene::itemsAt(const QPointF &pos) +{ + QTransform transform; + + if (auto *gview = graphicsView()) + transform = gview->transform(); + + return items(pos, Qt::IntersectsItemShape, Qt::DescendingOrder, transform); +} + +void TimelineGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + m_tools.mousePressEvent(topItem, event); + QGraphicsScene::mousePressEvent(event); +} + +void TimelineGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + m_tools.mouseMoveEvent(topItem, event); + QGraphicsScene::mouseMoveEvent(event); +} + +void TimelineGraphicsScene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + /* The tool has handle the event last. */ + QGraphicsScene::mouseReleaseEvent(event); + m_tools.mouseReleaseEvent(topItem, event); +} + +void TimelineGraphicsScene::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + auto topItem = TimelineMovableAbstractItem::topMoveableItem(itemsAt(event->scenePos())); + m_tools.mouseDoubleClickEvent(topItem, event); + QGraphicsScene::mouseDoubleClickEvent(event); +} + +void TimelineGraphicsScene::keyPressEvent(QKeyEvent *keyEvent) +{ + if (qgraphicsitem_cast<QGraphicsProxyWidget *>(focusItem())) { + keyEvent->ignore(); + QGraphicsScene::keyPressEvent(keyEvent); + return; + } + + if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) { + switch (keyEvent->key()) { + case Qt::Key_C: + copySelectedKeyframes(); + break; + + case Qt::Key_V: + pasteSelectedKeyframes(); + break; + + default: + QGraphicsScene::keyPressEvent(keyEvent); + break; + } + } else { + switch (keyEvent->key()) { + case Qt::Key_Left: + emit scroll(TimelineUtils::Side::Left); + keyEvent->accept(); + break; + + case Qt::Key_Right: + emit scroll(TimelineUtils::Side::Right); + keyEvent->accept(); + break; + + default: + QGraphicsScene::keyPressEvent(keyEvent); + break; + } + } +} + +void TimelineGraphicsScene::keyReleaseEvent(QKeyEvent *keyEvent) +{ + if (qgraphicsitem_cast<QGraphicsProxyWidget *>(focusItem())) { + keyEvent->ignore(); + QGraphicsScene::keyReleaseEvent(keyEvent); + return; + } + + switch (keyEvent->key()) { + case Qt::Key_Delete: + handleKeyframeDeletion(); + break; + + default: + break; + } + + QGraphicsScene::keyReleaseEvent(keyEvent); +} + +void TimelineGraphicsScene::invalidateSections() +{ + for (auto child : m_layout->childItems()) + TimelineSectionItem::updateData(child); + + clearSelection(); + invalidateLayout(); +} + +TimelineView *TimelineGraphicsScene::timelineView() const +{ + return m_parent->timelineView(); +} + +TimelineWidget *TimelineGraphicsScene::timelineWidget() const +{ + return m_parent; +} + +TimelineToolBar *TimelineGraphicsScene::toolBar() const +{ + return timelineWidget()->toolBar(); +} + +ModelNode TimelineGraphicsScene::timelineModelNode() const +{ + if (timelineView()->isAttached()) { + const QString timelineId = timelineWidget()->toolBar()->currentTimelineId(); + return timelineView()->modelNodeForId(timelineId); + } + + return ModelNode(); +} + +void TimelineGraphicsScene::handleKeyframeDeletion() +{ + QList<ModelNode> nodesToBeDeleted; + for (auto keyframe : m_selectedKeyframes) { + nodesToBeDeleted.append(keyframe->frameNode()); + } + deleteKeyframes(nodesToBeDeleted); +} + +void TimelineGraphicsScene::deleteAllKeyframesForTarget(const ModelNode &targetNode) +{ + TimelineActions::deleteAllKeyframesForTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::insertAllKeyframesForTarget(const ModelNode &targetNode) +{ + TimelineActions::insertAllKeyframesForTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::copyAllKeyframesForTarget(const ModelNode &targetNode) +{ + TimelineActions::copyAllKeyframesForTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::pasteKeyframesToTarget(const ModelNode &targetNode) +{ + TimelineActions::pasteKeyframesToTarget(targetNode, currentTimeline()); +} + +void TimelineGraphicsScene::copySelectedKeyframes() +{ + TimelineActions::copyKeyframes( + Utils::transform(m_selectedKeyframes, &TimelineKeyframeItem::frameNode)); +} + +void TimelineGraphicsScene::pasteSelectedKeyframes() +{ + TimelineActions::pasteKeyframes(timelineView(), currentTimeline()); +} + +void TimelineGraphicsScene::handleKeyframeInsertion(const ModelNode &target, + const PropertyName &propertyName) +{ + timelineView()->insertKeyframe(target, propertyName); +} + +void TimelineGraphicsScene::deleteKeyframeGroup(const ModelNode &group) +{ + if (!QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup(group)) + return; + + timelineView()->executeInTransaction("TimelineGraphicsScene::handleKeyframeGroupDeletion", [group](){ + ModelNode nonConst = group; + nonConst.destroy(); + }); + +} + +void TimelineGraphicsScene::deleteKeyframes(const QList<ModelNode> &frames) +{ + timelineView()->executeInTransaction("TimelineGraphicsScene::handleKeyframeDeletion", [frames](){ + for (auto keyframe : frames) { + if (keyframe.isValid()) { + ModelNode frame = keyframe; + ModelNode parent = frame.parentProperty().parentModelNode(); + keyframe.destroy(); + if (parent.isValid() && parent.defaultNodeListProperty().isEmpty()) + parent.destroy(); + } + } + }); +} + +void TimelineGraphicsScene::activateLayout() +{ + m_layout->activate(); +} + +void TimelineGraphicsScene::emitScrollOffsetChanged() +{ + for (QGraphicsItem *item : items()) + TimelineMovableAbstractItem::emitScrollOffsetChanged(item); +} + +void TimelineGraphicsScene::emitStatusBarFrameMessageChanged(int frame) +{ + emit statusBarMessageChanged( + QString(TimelineConstants::timelineStatusBarFrameNumber).arg(frame)); +} + +bool TimelineGraphicsScene::event(QEvent *event) +{ + switch (event->type()) { + case QEvent::ShortcutOverride: + if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Delete) { + QGraphicsScene::keyPressEvent(static_cast<QKeyEvent *>(event)); + event->accept(); + return true; + } + Q_FALLTHROUGH(); + default: + return QGraphicsScene::event(event); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h new file mode 100644 index 0000000000..b8f93595c4 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinegraphicsscene.h @@ -0,0 +1,181 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinetooldelegate.h" +#include "timelineutils.h" + +#include <qmltimeline.h> + +#include <QGraphicsScene> + +#include <memory> + +QT_FORWARD_DECLARE_CLASS(QGraphicsLinearLayout) +QT_FORWARD_DECLARE_CLASS(QComboBox) + +namespace QmlDesigner { + +class TimelineView; +class TimelineWidget; +class TimelineItem; +class TimelineRulerSectionItem; +class TimelineFrameHandle; +class TimelineAbstractTool; +class TimelineMoveTool; +class TimelineKeyframeItem; +class TimelinePlaceholder; +class TimelineGraphicsLayout; +class TimelineToolBar; + +class TimelineGraphicsScene : public QGraphicsScene +{ + Q_OBJECT + +signals: + void selectionChanged(); + + void scroll(const TimelineUtils::Side &side); + +public: + explicit TimelineGraphicsScene(TimelineWidget *parent); + + ~TimelineGraphicsScene() override; + + void onShow(); + + void setTimeline(const QmlTimeline &timeline); + void clearTimeline(); + + void setWidth(int width); + + void invalidateLayout(); + void setCurrenFrame(const QmlTimeline &timeline, qreal frame); + void setCurrentFrame(int frame); + void setStartFrame(int frame); + void setEndFrame(int frame); + + TimelineView *timelineView() const; + TimelineWidget *timelineWidget() const; + TimelineToolBar *toolBar() const; + + qreal rulerScaling() const; + int rulerWidth() const; + qreal rulerDuration() const; + qreal startFrame() const; + qreal endFrame() const; + + qreal mapToScene(qreal x) const; + qreal mapFromScene(qreal x) const; + + qreal currentFramePosition() const; + QVector<qreal> keyframePositions() const; + QVector<qreal> keyframePositions(const QmlTimelineKeyframeGroup &frames) const; + + void setRulerScaling(int scaling); + + void commitCurrentFrame(qreal frame); + + QList<TimelineKeyframeItem *> selectedKeyframes() const; + + bool hasSelection() const; + bool isCurrent(TimelineKeyframeItem *keyframe) const; + bool isKeyframeSelected(TimelineKeyframeItem *keyframe) const; + bool multipleKeyframesSelected() const; + + void invalidateSectionForTarget(const ModelNode &modelNode); + void invalidateKeyframesForTarget(const ModelNode &modelNode); + + void invalidateScene(); + void invalidateScrollbar(); + void invalidateCurrentValues(); + void invalidateRecordButtonsStatus(); + + int scrollOffset() const; + void setScrollOffset(int offset); + QGraphicsView *graphicsView() const; + QGraphicsView *rulerView() const; + + QmlTimeline currentTimeline() const; + + QRectF selectionBounds() const; + + void selectKeyframes(const SelectionMode &mode, const QList<TimelineKeyframeItem *> &items); + void clearSelection(); + + void handleKeyframeDeletion(); + void deleteAllKeyframesForTarget(const ModelNode &targetNode); + void insertAllKeyframesForTarget(const ModelNode &targetNode); + void copyAllKeyframesForTarget(const ModelNode &targetNode); + void pasteKeyframesToTarget(const ModelNode &targetNode); + + void handleKeyframeInsertion(const ModelNode &target, const PropertyName &propertyName); + + void deleteKeyframeGroup(const ModelNode &group); + void deleteKeyframes(const QList<ModelNode> &frames); + + void activateLayout(); + +signals: + void statusBarMessageChanged(const QString &message); + +protected: + bool event(QEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; + +private: + void copySelectedKeyframes(); + void pasteSelectedKeyframes(); + + void invalidateSections(); + ModelNode timelineModelNode() const; + + void emitScrollOffsetChanged(); + void emitStatusBarFrameMessageChanged(int frame); + + QList<QGraphicsItem *> itemsAt(const QPointF &pos); + +private: + TimelineWidget *m_parent = nullptr; + + TimelineGraphicsLayout *m_layout = nullptr; + + TimelineFrameHandle *m_currentFrameIndicator = nullptr; + + TimelineToolDelegate m_tools; + + QList<TimelineKeyframeItem *> m_selectedKeyframes; + + int m_scrollOffset = 0; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineicons.h b/src/plugins/qmldesigner/components/timelineeditor/timelineicons.h new file mode 100644 index 0000000000..641d4e77b6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineicons.h @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <utils/icon.h> + +namespace QmlDesigner { +namespace TimelineIcons { + +// Icons on the timeline ruler +const Utils::Icon WORK_AREA_HANDLE_LEFT( + ":/timelineplugin/images/work_area_handle_left.png"); +const Utils::Icon WORK_AREA_HANDLE_RIGHT( + ":/timelineplugin/images/work_area_handle_right.png"); +const Utils::Icon PLAYHEAD( + ":/timelineplugin/images/playhead.png"); + +// Icons on the timeline tracks +const Utils::Icon KEYFRAME_LINEAR_INACTIVE( + ":/timelineplugin/images/keyframe_linear_inactive.png"); +const Utils::Icon KEYFRAME_LINEAR_ACTIVE( + ":/timelineplugin/images/keyframe_linear_active.png"); +const Utils::Icon KEYFRAME_LINEAR_SELECTED( + ":/timelineplugin/images/keyframe_linear_selected.png"); +const Utils::Icon KEYFRAME_MANUALBEZIER_INACTIVE( + ":/timelineplugin/images/keyframe_manualbezier_inactive.png"); +const Utils::Icon KEYFRAME_MANUALBEZIER_ACTIVE( + ":/timelineplugin/images/keyframe_manualbezier_active.png"); +const Utils::Icon KEYFRAME_MANUALBEZIER_SELECTED( + ":/timelineplugin/images/keyframe_manualbezier_selected.png"); +const Utils::Icon KEYFRAME_AUTOBEZIER_INACTIVE( + ":/timelineplugin/images/keyframe_autobezier_inactive.png"); +const Utils::Icon KEYFRAME_AUTOBEZIER_ACTIVE( + ":/timelineplugin/images/keyframe_autobezier_active.png"); +const Utils::Icon KEYFRAME_AUTOBEZIER_SELECTED( + ":/timelineplugin/images/keyframe_autobezier_selected.png"); +const Utils::Icon KEYFRAME_LINEARTOBEZIER_INACTIVE( + ":/timelineplugin/images/keyframe_lineartobezier_inactive.png"); +const Utils::Icon KEYFRAME_LINEARTOBEZIER_ACTIVE( + ":/timelineplugin/images/keyframe_lineartobezier_active.png"); +const Utils::Icon KEYFRAME_LINEARTOBEZIER_SELECTED( + ":/timelineplugin/images/keyframe_lineartobezier_selected.png"); + +// Icons on the "section" +const Utils::Icon KEYFRAME( + ":/timelineplugin/images/keyframe.png"); +const Utils::Icon IS_KEYFRAME( + ":/timelineplugin/images/is_keyframe.png"); +const Utils::Icon NEXT_KEYFRAME({ + {":/timelineplugin/images/next_keyframe.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon PREVIOUS_KEYFRAME({ + {":/timelineplugin/images/previous_keyframe.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon LOCAL_RECORD_KEYFRAMES({ + {":/timelineplugin/images/local_record_keyframes.png", Utils::Theme::IconsStopToolBarColor}}); +const Utils::Icon ADD_TIMELINE({ + {":/timelineplugin/images/add_timeline.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon REMOVE_TIMELINE({ + {":/timelineplugin/images/remove_timeline.png", Utils::Theme::IconsBaseColor}}); + +// Icons on the toolbars +const Utils::Icon ANIMATION({ + {":/timelineplugin/images/animation.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon TO_FIRST_FRAME({ + {":/timelineplugin/images/to_first_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon BACK_ONE_FRAME({ + {":/timelineplugin/images/back_one_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon START_PLAYBACK({ + {":/timelineplugin/images/start_playback.png", Utils::Theme::IconsRunToolBarColor}}); +const Utils::Icon PAUSE_PLAYBACK({ + {":/timelineplugin/images/pause_playback.png", Utils::Theme::IconsInterruptToolBarColor}}); +const Utils::Icon FORWARD_ONE_FRAME({ + {":/timelineplugin/images/forward_one_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon TO_LAST_FRAME({ + {":/timelineplugin/images/to_last_frame.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon LOOP_PLAYBACK({ + {":/timelineplugin/images/loop_playback.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon CURVE_PICKER({ + {":/timelineplugin/images/curve_picker.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon CURVE_EDITOR({ + {":/timelineplugin/images/curve_editor.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon GLOBAL_RECORD_KEYFRAMES({ + {":/timelineplugin/images/global_record_keyframes.png", Utils::Theme::IconsStopToolBarColor}}); +const Utils::Icon GLOBAL_RECORD_KEYFRAMES_OFF({ + {":/timelineplugin/images/global_record_keyframes.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon ZOOM_SMALL({ + {":/timelineplugin/images/zoom_small.png", Utils::Theme::IconsBaseColor}}); +const Utils::Icon ZOOM_BIG({ + {":/timelineplugin/images/zoom_big.png", Utils::Theme::IconsBaseColor}}); + +} // namespace TimelineIcons +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp new file mode 100644 index 0000000000..ebe3644e4e --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.cpp @@ -0,0 +1,293 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineitem.h" + +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinesectionitem.h" +#include "timelineutils.h" +#include "timelinewidget.h" + +#include <theme.h> + +#include <coreplugin/icore.h> + +#include <QApplication> +#include <QCursor> +#include <QGraphicsView> +#include <QPainter> + +#include <cmath> + +namespace QmlDesigner { + +TimelineItem::TimelineItem(TimelineItem *parent) + : QGraphicsWidget(parent) +{} + +TimelineGraphicsScene *TimelineItem::timelineScene() const +{ + return static_cast<TimelineGraphicsScene *>(scene()); + ; +} + +TimelineFrameHandle::TimelineFrameHandle(TimelineItem *parent) + : TimelineMovableAbstractItem(parent) +{ + static const QColor color = Theme::getColor(Theme::IconsWarningToolBarColor); + setBrush(color); + setPen(color); + + setRect(0, 0, TimelineConstants::rulerHeight, TimelineConstants::rulerHeight); + setZValue(40); + setCursor(Qt::ClosedHandCursor); + + m_timer.setSingleShot(true); + m_timer.setInterval(15); + QObject::connect(&m_timer, &QTimer::timeout, [this]() { + if (QApplication::mouseButtons() == Qt::LeftButton) + scrollOutOfBounds(); + }); +} + +void TimelineFrameHandle::setHeight(int height) +{ + setRect(rect().x(), rect().y(), rect().width(), height); +} + +void TimelineFrameHandle::setPosition(qreal position) +{ + const qreal scenePos = mapFromFrameToScene(position); + QRectF newRect(scenePos - rect().width() / 2, rect().y(), rect().width(), rect().height()); + + if (!qFuzzyCompare(newRect.x(), rect().x())) { + setRect(newRect); + } + m_position = position; +} + +void TimelineFrameHandle::setPositionInteractive(const QPointF &position) +{ + const double width = timelineScene()->width(); + + if (position.x() > width) { + callSetClampedXPosition(width - (rect().width() / 2) - 1); + m_timer.start(); + } else if (position.x() < TimelineConstants::sectionWidth) { + callSetClampedXPosition(TimelineConstants::sectionWidth); + m_timer.start(); + } else { + callSetClampedXPosition(position.x() - rect().width() / 2); + const qreal frame = std::round(mapFromSceneToFrame(rect().center().x())); + timelineScene()->commitCurrentFrame(frame); + } +} + +void TimelineFrameHandle::commitPosition(const QPointF &point) +{ + setPositionInteractive(point); +} + +void TimelineItem::drawLine(QPainter *painter, qreal x1, qreal y1, qreal x2, qreal y2) +{ + painter->drawLine(QPointF(x1 + 0.5, y1 + 0.5), QPointF(x2 + 0.5, y2 + 0.5)); +} + +qreal TimelineFrameHandle::position() const +{ + return m_position; +} + +TimelineFrameHandle *TimelineFrameHandle::asTimelineFrameHandle() +{ + return this; +} + +void TimelineFrameHandle::scrollOffsetChanged() +{ + setPosition(position()); +} + +QPainterPath TimelineFrameHandle::shape() const +{ + QPainterPath path; + QRectF rect = boundingRect(); + rect.setHeight(TimelineConstants::sectionHeight); + rect.adjust(-4, 0, 4, 0); + path.addEllipse(rect); + return path; +} + +static int devicePixelWidth(const QPixmap &pixmap) +{ + return pixmap.width() / pixmap.devicePixelRatioF(); +} + +static int devicePixelHeight(const QPixmap &pixmap) +{ + return pixmap.height() / pixmap.devicePixelRatioF(); +} + +void TimelineFrameHandle::paint(QPainter *painter, + const QStyleOptionGraphicsItem * /*option*/, + QWidget * /*widget*/) +{ + static const QPixmap playHead = TimelineIcons::PLAYHEAD.pixmap(); + + static const int pixmapHeight = devicePixelHeight(playHead); + static const int pixmapWidth = devicePixelWidth(playHead); + + if (rect().x() < TimelineConstants::sectionWidth - rect().width() / 2) + return; + + painter->save(); + painter->setOpacity(0.8); + const qreal center = rect().width() / 2 + rect().x(); + + painter->setPen(pen()); + + auto offsetTop = pixmapHeight - 7; + TimelineItem::drawLine(painter, center, offsetTop, center, rect().height() - 1); + + const QPointF pmTopLeft(center - pixmapWidth / 2, -4.); + painter->drawPixmap(pmTopLeft, playHead); + + painter->restore(); +} + +QPointF TimelineFrameHandle::mapFromGlobal(const QPoint &pos) const +{ + for (auto *view : timelineScene()->views()) { + if (view->objectName() == "SceneView") { + auto graphicsViewCoords = view->mapFromGlobal(pos); + auto sceneCoords = view->mapToScene(graphicsViewCoords); + return sceneCoords; + } + } + return {}; +} + +int TimelineFrameHandle::computeScrollSpeed() const +{ + const double mouse = mapFromGlobal(QCursor::pos()).x(); + const double width = timelineScene()->width(); + + const double acc = mouse > width ? mouse - width + : double(TimelineConstants::sectionWidth) - mouse; + const double delta = TimelineUtils::clamp<double>(acc, 0., 200.); + const double blend = TimelineUtils::reverseLerp(delta, 0., 200.); + const double factor = TimelineUtils::lerp<double>(blend, 5, 20); + + if (mouse > width) + return scrollOffset() + std::round(factor); + else + return scrollOffset() - std::round(factor); + + return 0; +} + +void TimelineFrameHandle::callSetClampedXPosition(double x) +{ + const int minimumWidth = TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset + - rect().width() / 2; + const int maximumWidth = minimumWidth + + timelineScene()->rulerDuration() * timelineScene()->rulerScaling() + - scrollOffset(); + + setClampedXPosition(x, minimumWidth, maximumWidth); +} + +// Auto scroll when dragging playhead out of bounds. +void TimelineFrameHandle::scrollOutOfBounds() +{ + const double width = timelineScene()->width(); + const double mouse = mapFromGlobal(QCursor::pos()).x(); + + if (mouse > width) + scrollOutOfBoundsMax(); + else if (mouse < TimelineConstants::sectionWidth) + scrollOutOfBoundsMin(); +} + +void TimelineFrameHandle::scrollOutOfBoundsMax() +{ + const double width = timelineScene()->width(); + if (QApplication::mouseButtons() == Qt::LeftButton) { + const double frameWidth = timelineScene()->rulerScaling(); + const double upperThreshold = width - frameWidth; + + if (rect().center().x() > upperThreshold) { + timelineScene()->setScrollOffset(computeScrollSpeed()); + timelineScene()->invalidateScrollbar(); + } + + callSetClampedXPosition(width - (rect().width() / 2) - 1); + m_timer.start(); + } else { + // Mouse release + callSetClampedXPosition(width - (rect().width() / 2) - 1); + + const int frame = std::floor(mapFromSceneToFrame(rect().center().x())); + const int ef = timelineScene()->endFrame(); + timelineScene()->commitCurrentFrame(frame <= ef ? frame : ef); + } +} + +void TimelineFrameHandle::scrollOutOfBoundsMin() +{ + if (QApplication::mouseButtons() == Qt::LeftButton) { + auto offset = computeScrollSpeed(); + + if (offset >= 0) + timelineScene()->setScrollOffset(offset); + else + timelineScene()->setScrollOffset(0); + + timelineScene()->invalidateScrollbar(); + + callSetClampedXPosition(TimelineConstants::sectionWidth); + m_timer.start(); + } else { + // Mouse release + callSetClampedXPosition(TimelineConstants::sectionWidth); + + int frame = mapFromSceneToFrame(rect().center().x()); + + const int sframe = timelineScene()->startFrame(); + if (frame != sframe) { + const qreal framePos = mapFromFrameToScene(frame); + + if (framePos + <= (TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset)) + frame++; + } + + timelineScene()->commitCurrentFrame(frame >= sframe ? frame : sframe); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.h new file mode 100644 index 0000000000..87fb9e3ec8 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineitem.h @@ -0,0 +1,82 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinemovableabstractitem.h" + +#include <QGraphicsRectItem> +#include <QGraphicsWidget> +#include <QTimer> + +namespace QmlDesigner { + +class TimelineItem : public QGraphicsWidget +{ + Q_OBJECT + +public: + explicit TimelineItem(TimelineItem *parent = nullptr); + + static void drawLine(QPainter *painter, qreal x1, qreal y1, qreal x2, qreal y2); + TimelineGraphicsScene *timelineScene() const; +}; + +class TimelineFrameHandle : public TimelineMovableAbstractItem +{ +public: + explicit TimelineFrameHandle(TimelineItem *parent = nullptr); + + void setHeight(int height); + void setPosition(qreal position); + void setPositionInteractive(const QPointF &postion) override; + void commitPosition(const QPointF &point) override; + qreal position() const; + + TimelineFrameHandle *asTimelineFrameHandle() override; + +protected: + void scrollOffsetChanged() override; + QPainterPath shape() const override; + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget = nullptr) override; + +private: + QPointF mapFromGlobal(const QPoint &pos) const; + int computeScrollSpeed() const; + + void callSetClampedXPosition(double x); + void scrollOutOfBounds(); + void scrollOutOfBoundsMax(); + void scrollOutOfBoundsMin(); + +private: + qreal m_position = 0; + + QTimer m_timer; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp new file mode 100644 index 0000000000..4db4567fd6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.cpp @@ -0,0 +1,154 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinemovableabstractitem.h" + +#include "timelinegraphicsscene.h" + +#include "timelineitem.h" + +#include <QGraphicsSceneMouseEvent> + +namespace QmlDesigner { + +TimelineMovableAbstractItem::TimelineMovableAbstractItem(QGraphicsItem *parent) + : QGraphicsRectItem(parent) +{} + +void TimelineMovableAbstractItem::setPositionInteractive(const QPointF &) {} + +void TimelineMovableAbstractItem::commitPosition(const QPointF &) {} + +void TimelineMovableAbstractItem::itemMoved(const QPointF & /*start*/, const QPointF &end) +{ + setPositionInteractive(end); +} + +int TimelineMovableAbstractItem::scrollOffset() const +{ + return timelineScene()->scrollOffset(); +} + +int TimelineMovableAbstractItem::xPosScrollOffset(int x) const +{ + return x + scrollOffset(); +} + +qreal TimelineMovableAbstractItem::mapFromFrameToScene(qreal x) const +{ + return TimelineConstants::sectionWidth + (x - timelineScene()->startFrame()) * rulerScaling() + - scrollOffset() + TimelineConstants::timelineLeftOffset; +} + +qreal TimelineMovableAbstractItem::mapFromSceneToFrame(qreal x) const +{ + return xPosScrollOffset(x - TimelineConstants::sectionWidth + - TimelineConstants::timelineLeftOffset) + / timelineScene()->rulerScaling() + + timelineScene()->startFrame(); +} + +void TimelineMovableAbstractItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + event->accept(); +} + +void TimelineMovableAbstractItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + event->accept(); +} + +void TimelineMovableAbstractItem::setClampedXPosition(qreal x, + qreal minimumWidth, + qreal maximumWidth) +{ + if (x > minimumWidth) { + if (x < maximumWidth) + setRect(x, rect().y(), rect().width(), rect().height()); + else + setRect(maximumWidth, rect().y(), rect().width(), rect().height()); + } else { + setRect(minimumWidth, rect().y(), rect().width(), rect().height()); + } +} + +TimelineMovableAbstractItem *TimelineMovableAbstractItem::cast(QGraphicsItem *item) +{ + return qgraphicsitem_cast<TimelineMovableAbstractItem *>(item); +} + +TimelineMovableAbstractItem *TimelineMovableAbstractItem::topMoveableItem( + const QList<QGraphicsItem *> &items) +{ + for (auto item : items) + if (auto castedItem = TimelineMovableAbstractItem::cast(item)) + return castedItem; + + return nullptr; +} + +void TimelineMovableAbstractItem::emitScrollOffsetChanged(QGraphicsItem *item) +{ + auto movableItem = TimelineMovableAbstractItem::cast(item); + if (movableItem) + movableItem->scrollOffsetChanged(); +} + +TimelineKeyframeItem *TimelineMovableAbstractItem::asTimelineKeyframeItem(QGraphicsItem *item) +{ + auto movableItem = TimelineMovableAbstractItem::cast(item); + + if (movableItem) + return movableItem->asTimelineKeyframeItem(); + + return nullptr; +} + +qreal TimelineMovableAbstractItem::rulerScaling() const +{ + return static_cast<TimelineGraphicsScene *>(scene())->rulerScaling(); +} + +int TimelineMovableAbstractItem::type() const +{ + return Type; +} + +TimelineGraphicsScene *TimelineMovableAbstractItem::timelineScene() const +{ + return static_cast<TimelineGraphicsScene *>(scene()); +} + +TimelineKeyframeItem *TimelineMovableAbstractItem::asTimelineKeyframeItem() +{ + return nullptr; +} + +TimelineFrameHandle *TimelineMovableAbstractItem::asTimelineFrameHandle() +{ + return nullptr; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h new file mode 100644 index 0000000000..4bc11675c2 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovableabstractitem.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineconstants.h" + +#include <QCoreApplication> +#include <QGraphicsRectItem> + +namespace QmlDesigner { + +class TimelineGraphicsScene; +class TimelineKeyframeItem; +class TimelineFrameHandle; + +class TimelineMovableAbstractItem : public QGraphicsRectItem +{ + Q_DECLARE_TR_FUNCTIONS(TimelineMovableAbstractItem) + +public: + enum { Type = TimelineConstants::moveableAbstractItemUserType }; + + explicit TimelineMovableAbstractItem(QGraphicsItem *item); + + int type() const override; + + static TimelineMovableAbstractItem *cast(QGraphicsItem *item); + static TimelineMovableAbstractItem *topMoveableItem(const QList<QGraphicsItem *> &items); + static void emitScrollOffsetChanged(QGraphicsItem *item); + static TimelineKeyframeItem *asTimelineKeyframeItem(QGraphicsItem *item); + + qreal rulerScaling() const; + + virtual void setPositionInteractive(const QPointF &point); + virtual void commitPosition(const QPointF &point); + virtual void itemMoved(const QPointF &start, const QPointF &end); + + int xPosScrollOffset(int x) const; + + qreal mapFromFrameToScene(qreal x) const; + qreal mapFromSceneToFrame(qreal x) const; + + virtual void scrollOffsetChanged() = 0; + + virtual TimelineKeyframeItem *asTimelineKeyframeItem(); + virtual TimelineFrameHandle *asTimelineFrameHandle(); + +protected: + int scrollOffset() const; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + + void setClampedXPosition(qreal x, qreal min, qreal max); + TimelineGraphicsScene *timelineScene() const; + +private: + bool m_multiSelectedMove = false; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp new file mode 100644 index 0000000000..0384d7c0a3 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.cpp @@ -0,0 +1,173 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinemovetool.h" + +#include "timelinegraphicsscene.h" +#include "timelinemovableabstractitem.h" +#include "timelinepropertyitem.h" +#include "timelineview.h" + +#include <exception.h> + +#include <QGraphicsScene> +#include <QGraphicsSceneMouseEvent> + +#include <cmath> + +namespace QmlDesigner { + +static QPointF mapPointToItem(TimelineMovableAbstractItem *item, const QPointF &pos) +{ + if (auto parent = item->parentItem()) + return parent->mapFromScene(pos); + return pos; +} + +QPointF mapToItem(TimelineMovableAbstractItem *item, const QPointF &pos) +{ + if (auto parent = item->parentItem()) + return parent->mapFromScene(pos); + return pos; +} + +QPointF mapToItem(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) +{ + if (auto parent = item->parentItem()) + return parent->mapFromScene(event->scenePos()); + return event->scenePos(); +} + +TimelineMoveTool::TimelineMoveTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate) + : TimelineAbstractTool(scene, delegate) +{} + +void TimelineMoveTool::mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); +} + +void TimelineMoveTool::mouseMoveEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + + if (!currentItem()) + return; + + if (auto *current = currentItem()->asTimelineKeyframeItem()) { + const qreal sourceFrame = qRound(current->mapFromSceneToFrame(current->rect().center().x())); + const qreal targetFrame = qRound(current->mapFromSceneToFrame(event->scenePos().x())); + qreal deltaFrame = targetFrame - sourceFrame; + + const qreal minFrame = scene()->startFrame(); + const qreal maxFrame = scene()->endFrame(); + + auto bbox = scene()->selectionBounds().united(current->rect()); + + double firstFrame = std::round(current->mapFromSceneToFrame(bbox.center().x())); + double lastFrame = std::round(current->mapFromSceneToFrame(bbox.center().x())); + + if ((lastFrame + deltaFrame) > maxFrame) + deltaFrame = maxFrame - lastFrame; + + if ((firstFrame + deltaFrame) <= minFrame) + deltaFrame = minFrame - firstFrame; + + current->setPosition(sourceFrame + deltaFrame); + + for (auto *keyframe : scene()->selectedKeyframes()) { + if (keyframe != current) { + qreal pos = std::round(current->mapFromSceneToFrame(keyframe->rect().center().x())); + keyframe->setPosition(pos + deltaFrame); + } + } + + } else { + currentItem()->itemMoved(mapPointToItem(currentItem(), startPosition()), + mapToItem(currentItem(), event)); + } +} + +void TimelineMoveTool::mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + if (auto *current = currentItem()) { + if (current->asTimelineFrameHandle()) { + double mousePos = event->pos().x(); + double start = current->mapFromFrameToScene(scene()->startFrame()); + double end = current->mapFromFrameToScene(scene()->endFrame()); + + if (mousePos < start) { + scene()->setCurrentFrame(scene()->startFrame()); + scene()->statusBarMessageChanged(QObject::tr("Frame %1").arg(scene()->startFrame())); + return; + } else if (mousePos > end) { + scene()->setCurrentFrame(scene()->endFrame()); + scene()->statusBarMessageChanged(QObject::tr("Frame %1").arg(scene()->endFrame())); + return; + } + } + + scene()->timelineView()->executeInTransaction("TimelineMoveTool::mouseReleaseEvent", [this, current](){ + current->commitPosition(mapToItem(current, current->rect().center())); + + if (current->asTimelineKeyframeItem()) { + double frame = std::round( + current->mapFromSceneToFrame(current->rect().center().x())); + + scene()->statusBarMessageChanged(QObject::tr("Frame %1").arg(frame)); + + for (auto keyframe : scene()->selectedKeyframes()) + if (keyframe != current) + keyframe->commitPosition(mapToItem(current, keyframe->rect().center())); + } + }); + } +} + +void TimelineMoveTool::mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); +} + +void TimelineMoveTool::keyPressEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +void TimelineMoveTool::keyReleaseEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h new file mode 100644 index 0000000000..55b9a39417 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinemovetool.h @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineabstracttool.h" + +#include <QPointF> + +QT_FORWARD_DECLARE_CLASS(QGraphicsRectItem) + +namespace QmlDesigner { + +class TimelineMovableAbstractItem; + +class TimelineMoveTool : public TimelineAbstractTool +{ +public: + explicit TimelineMoveTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate); + void mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + void keyReleaseEvent(QKeyEvent *keyEvent) override; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp new file mode 100644 index 0000000000..7e7cb69fc9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.cpp @@ -0,0 +1,78 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineplaceholder.h" + +#include <theme.h> + +#include <QPainter> + +namespace QmlDesigner { + +TimelinePlaceholder::TimelinePlaceholder(TimelineItem *parent) + : TimelineItem(parent) +{ + setPreferredHeight(TimelineConstants::sectionHeight); + setMinimumHeight(TimelineConstants::sectionHeight); + setMaximumHeight(TimelineConstants::sectionHeight); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); +} + +TimelinePlaceholder *TimelinePlaceholder::create(QGraphicsScene * /*parentScene*/, + TimelineItem *parent) +{ + auto item = new TimelinePlaceholder(parent); + + return item; +} + +void TimelinePlaceholder::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + painter->save(); + static const QColor penColor = Theme::instance()->qmlDesignerBackgroundColorDarker(); + static const QColor backgroundColor = Theme::instance() + ->qmlDesignerBackgroundColorDarkAlternate(); + static const QColor backgroundColorSection = Theme::getColor(Theme::BackgroundColorDark); + + painter->fillRect(0, 0, size().width(), size().height(), backgroundColor); + painter->fillRect(0, 0, TimelineConstants::sectionWidth, size().height(), backgroundColorSection); + + painter->setPen(penColor); + + drawLine(painter, + TimelineConstants::sectionWidth - 1, + 0, + TimelineConstants::sectionWidth - 1, + size().height() - 1); + + drawLine(painter, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight - 1, + size().width(), + TimelineConstants::sectionHeight - 1); + painter->restore(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h new file mode 100644 index 0000000000..14d6d8a2fc --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineplaceholder.h @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineitem.h" + +namespace QmlDesigner { + +class TimelinePlaceholder : public TimelineItem +{ + Q_OBJECT + +public: + static TimelinePlaceholder *create(QGraphicsScene *parentScene, TimelineItem *parent); + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; + +private: + TimelinePlaceholder(TimelineItem *parent = nullptr); +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp new file mode 100644 index 0000000000..beeca23183 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.cpp @@ -0,0 +1,641 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinepropertyitem.h" + +#include "abstractview.h" +#include "easingcurvedialog.h" +#include "setframevaluedialog.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinetoolbar.h" +#include "timelinetoolbutton.h" + +#include <rewritertransaction.h> +#include <rewritingexception.h> +#include <theme.h> +#include <variantproperty.h> +#include <qmlobjectnode.h> + +#include <coreplugin/icore.h> +#include <utils/qtcassert.h> +#include <utils/utilsicons.h> + +#include <utils/algorithm.h> +#include <utils/fileutils.h> + +#include <coreplugin/icore.h> + +#include <QCursor> +#include <QGraphicsProxyWidget> +#include <QGraphicsSceneMouseEvent> +#include <QGraphicsView> +#include <QLineEdit> +#include <QMenu> +#include <QPainter> + +#include <algorithm> + +namespace QmlDesigner { + +static bool s_blockUpdates = false; + +static qreal findNext(const QVector<qreal> &vector, qreal current) +{ + for (qreal n : vector) + if (n > current) + return n; + return current; +} + +static qreal findPrev(const QVector<qreal> &vector, qreal current) +{ + for (qreal n : vector) + if (n < current) + return n; + return current; +} + +static QVector<qreal> getPositions(const QmlTimelineKeyframeGroup &frames) +{ + const QList<ModelNode> keyframes = frames.keyframePositions(); + QVector<qreal> positions; + for (const ModelNode &modelNode : keyframes) + positions.append(modelNode.variantProperty("frame").value().toReal()); + return positions; +} + +static ModelNode getModelNodeForFrame(const QmlTimelineKeyframeGroup &frames, qreal frame) +{ + if (frames.isValid()) { + const QList<ModelNode> keyframes = frames.keyframePositions(); + for (const ModelNode &modelNode : keyframes) + if (qFuzzyCompare(modelNode.variantProperty("frame").value().toReal(), frame)) + return modelNode; + } + + return {}; +} + +static void setEasingCurve(TimelineGraphicsScene *scene, const QList<ModelNode> &keys) +{ + QTC_ASSERT(scene, return ); + EasingCurveDialog::runDialog(keys); +} + +static void editValue(const ModelNode &frame, const QString &propertyName) +{ + const QVariant value = frame.variantProperty("value").value(); + auto dialog = new SetFrameValueDialog(Core::ICore::dialogParent()); + + dialog->lineEdit()->setText(value.toString()); + dialog->setPropertName(propertyName); + + QObject::connect(dialog, &SetFrameValueDialog::rejected, [dialog]() { dialog->deleteLater(); }); + + QObject::connect(dialog, &SetFrameValueDialog::accepted, [dialog, frame, value]() { + dialog->deleteLater(); + int userType = value.userType(); + const QVariant result = dialog->lineEdit()->text(); + + if (result.canConvert(userType)) { + QVariant newValue = result; + newValue.convert(userType); + // canConvert gives true in case if the result is a double but the usertype was interger + // try to fix that with a workaround to convert it to double if convertion resulted in isNull + if (newValue.isNull()) { + newValue = result; + newValue.convert(QMetaType::Double); + } + frame.variantProperty("value").setValue(result); + } + }); + + dialog->show(); +} + +TimelinePropertyItem *TimelinePropertyItem::create(const QmlTimelineKeyframeGroup &frames, + TimelineSectionItem *parent) +{ + ModelNode modelnode = frames.target(); + + bool isRecording = false; + + if (frames.isValid()) + isRecording = frames.isRecording(); + + auto item = new TimelinePropertyItem(parent); + + auto sectionItem = new QGraphicsWidget(item); + + sectionItem->setGeometry(0, + 0, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight); + + sectionItem->setZValue(10); + sectionItem->setCursor(Qt::ArrowCursor); + + item->m_frames = frames; + item->setToolTip(item->propertyName()); + item->resize(parent->size()); + item->setupKeyframes(); + + TimelineToolButton *buttonPrev + = new TimelineToolButton(new QAction(TimelineIcons::PREVIOUS_KEYFRAME.icon(), + tr("Previous Frame")), + sectionItem); + buttonPrev->setToolTip("Jump to previous frame."); + + TimelineToolButton *buttonNext + = new TimelineToolButton(new QAction(TimelineIcons::NEXT_KEYFRAME.icon(), tr("Next Frame")), + sectionItem); + buttonNext->setToolTip("Jump to next frame."); + + connect(buttonPrev, &TimelineToolButton::clicked, item, [item]() { + if (item->m_frames.isValid()) { + QVector<qreal> positions = getPositions(item->m_frames); + std::sort(positions.begin(), positions.end(), std::greater<qreal>()); + const qreal prev = findPrev(positions, item->currentFrame()); + item->timelineScene()->commitCurrentFrame(prev); + } + }); + + connect(buttonNext, &TimelineToolButton::clicked, item, [item]() { + if (item->m_frames.isValid()) { + QVector<qreal> positions = getPositions(item->m_frames); + std::sort(positions.begin(), positions.end(), std::less<qreal>()); + const qreal next = findNext(positions, item->currentFrame()); + item->timelineScene()->commitCurrentFrame(next); + } + }); + + QIcon autoKeyIcon = TimelineUtils::mergeIcons(TimelineIcons::GLOBAL_RECORD_KEYFRAMES, + TimelineIcons::GLOBAL_RECORD_KEYFRAMES_OFF); + auto recact = new QAction(autoKeyIcon, tr("Auto Record")); + recact->setCheckable(true); + recact->setChecked(isRecording); + + auto toggleRecord = [frames](bool check) { frames.toogleRecording(check); }; + connect(recact, &QAction::toggled, toggleRecord); + item->m_recording = new TimelineToolButton(recact, sectionItem); + item->m_recording->setToolTip("Per property recording"); + + const int buttonsY = (TimelineConstants::sectionHeight - 1 - TimelineConstants::toolButtonSize) + / 2; + buttonPrev->setPos(2, buttonsY); + buttonNext->setPos(buttonPrev->size().width() + TimelineConstants::toolButtonSize + 4, buttonsY); + item->m_recording->setPos(buttonNext->geometry().right() + 2, buttonsY); + + QRectF hideToolTipDummy(buttonPrev->geometry().topRight(), buttonNext->geometry().bottomLeft()); + + auto *dummy = new QGraphicsRectItem(sectionItem); + dummy->setPen(Qt::NoPen); + dummy->setRect(hideToolTipDummy); + dummy->setToolTip("Frame indicator"); + + if (!item->m_frames.isValid()) + return item; + + QmlObjectNode objectNode(modelnode); + if (!objectNode.isValid()) + return item; + + auto nameOfType = objectNode.modelNode().metaInfo().propertyTypeName( + item->m_frames.propertyName()); + item->m_control = createTimelineControl(nameOfType); + if (item->m_control) { + item->m_control->setSize((TimelineConstants::sectionWidth / 2.6) - 10, + item->size().height() - 2 + 1); + item->m_control->connect(item); + QGraphicsProxyWidget *proxy = item->timelineScene()->addWidget(item->m_control->widget()); + proxy->setParentItem(sectionItem); + proxy->setPos(qreal(TimelineConstants::sectionWidth) * 2.0 / 3, 0); + item->updateTextEdit(); + } + + updateRecordButtonStatus(item); + + return item; +} + +int TimelinePropertyItem::type() const +{ + return Type; +} + +void TimelinePropertyItem::updateData() +{ + for (auto child : childItems()) + delete qgraphicsitem_cast<TimelineMovableAbstractItem *>(child); + + setupKeyframes(); + updateTextEdit(); +} + +void TimelinePropertyItem::updateFrames() +{ + for (auto child : (childItems())) { + if (auto frameItem = qgraphicsitem_cast<TimelineMovableAbstractItem *>(child)) + static_cast<TimelineKeyframeItem *>(frameItem)->updateFrame(); + } +} + +bool TimelinePropertyItem::isSelected() const +{ + if (m_frames.isValid() && m_frames.target().isValid()) + return m_frames.target().isSelected(); + + return false; +} + +QString convertVariant(const QVariant &variant) +{ + if (variant.userType() == QMetaType::QColor) + return variant.toString(); + + return QString::number(variant.toFloat(), 'f', 2); +} + +void TimelinePropertyItem::updateTextEdit() +{ + if (!m_frames.isValid()) + return; + + QmlObjectNode objectNode(m_frames.target()); + if (objectNode.isValid() && m_control) + m_control->setControlValue(objectNode.instanceValue(m_frames.propertyName())); +} + +void TimelinePropertyItem::updateTextEdit(QGraphicsItem *item) +{ + if (auto timelinePropertyItem = qgraphicsitem_cast<TimelinePropertyItem *>(item)) + timelinePropertyItem->updateTextEdit(); +} + +void TimelinePropertyItem::updateRecordButtonStatus(QGraphicsItem *item) +{ + if (auto timelinePropertyItem = qgraphicsitem_cast<TimelinePropertyItem *>(item)) { + auto frames = timelinePropertyItem->m_frames; + if (frames.isValid()) { + timelinePropertyItem->m_recording->setChecked(frames.isRecording()); + if (frames.timeline().isValid()) + timelinePropertyItem->m_recording->setDisabled(frames.timeline().isRecording()); + } + } +} + +QmlTimelineKeyframeGroup TimelinePropertyItem::frames() const +{ + return m_frames; +} + +QString TimelinePropertyItem::propertyName() const +{ + if (m_frames.isValid()) + return QString::fromUtf8(m_frames.propertyName()); + return QString(); +} + +void TimelinePropertyItem::changePropertyValue(const QVariant &value) +{ + Q_ASSERT(m_frames.isValid()); + + auto timeline = timelineScene()->currentTimeline(); + + if (timelineScene()->toolBar()->recording() || m_recording->isChecked()) { + QmlTimelineKeyframeGroup frames = m_frames; + auto deferredFunc = [frames, value, timeline]() { + auto constFrames = frames; + qreal frame = timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal(); + try { + constFrames.setValue(value, frame); + } catch (const RewritingException &e) { + e.showException(); + } + }; + + // QmlTimelineKeyframeGroup::setValue might create a new keyframe. + // This might result in a temporal cleanup of the graphicsscene and + // therefore a deletion of this property item. + // Adding a keyframe to this already deleted item results in a crash. + QTimer::singleShot(0, deferredFunc); + + } else { + QmlObjectNode objectNode(m_frames.target()); + objectNode.setVariantProperty(m_frames.propertyName(), value); + } +} + +static int devicePixelHeight(const QPixmap &pixmap) +{ + return pixmap.height() / pixmap.devicePixelRatioF(); +} + +void TimelinePropertyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + painter->save(); + + static const QColor penColor = Theme::instance()->qmlDesignerBackgroundColorDarker(); + static const QColor textColor = Theme::getColor(Theme::PanelTextColorLight); + static const QColor backgroundColor = Theme::instance() + ->qmlDesignerBackgroundColorDarkAlternate(); + + static const QPixmap keyframe = TimelineIcons::KEYFRAME.pixmap(); + static const QPixmap isKeyframe = TimelineIcons::IS_KEYFRAME.pixmap(); + + painter->fillRect(0, 0, TimelineConstants::sectionWidth, size().height(), backgroundColor); + painter->fillRect(TimelineConstants::textIndentationProperties - 4, + 0, + TimelineConstants::sectionWidth - TimelineConstants::textIndentationProperties + + 4, + size().height(), + backgroundColor.darker(110)); + + painter->setPen(penColor); + + drawLine(painter, + TimelineConstants::sectionWidth - 1, + 0, + TimelineConstants::sectionWidth - 1, + size().height()); + + drawLine(painter, + TimelineConstants::textIndentationProperties - 4, + TimelineConstants::sectionHeight - 1, + size().width(), + TimelineConstants::sectionHeight - 1); + + painter->setPen(textColor); + + const QFontMetrics metrics(font()); + + const QString elidedText = metrics.elidedText(propertyName(), + Qt::ElideMiddle, + qreal(TimelineConstants::sectionWidth) * 2.0 / 3 + - TimelineConstants::textIndentationProperties, + 0); + + painter->drawText(TimelineConstants::textIndentationProperties, 12, elidedText); + + const bool onKeyFrame = m_frames.isValid() && getPositions(m_frames).contains(currentFrame()); + painter->drawPixmap(TimelineConstants::toolButtonSize + 3, + (TimelineConstants::sectionHeight - 1 - devicePixelHeight(isKeyframe)) / 2, + onKeyFrame ? isKeyframe : keyframe); + painter->restore(); +} + +void TimelinePropertyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + if (event->pos().x() < TimelineConstants::toolButtonSize * 2 + 3 + && event->pos().x() > TimelineConstants::toolButtonSize) { + QMenu mainMenu; + + const ModelNode currentFrameNode = getModelNodeForFrame(m_frames, currentFrame()); + + QAction *insertAction = mainMenu.addAction(tr("Insert Keyframe")); + QObject::connect(insertAction, &QAction::triggered, [this]() { + timelineScene()->handleKeyframeInsertion(m_frames.target(), propertyName().toUtf8()); + }); + + QAction *removeAction = mainMenu.addAction(tr("Delete Keyframe")); + QObject::connect(removeAction, &QAction::triggered, [this, currentFrameNode]() { + timelineScene()->deleteKeyframes({currentFrameNode}); + }); + + QAction *editEasingAction = mainMenu.addAction(tr("Edit Easing Curve...")); + QObject::connect(editEasingAction, &QAction::triggered, [this, currentFrameNode]() { + setEasingCurve(timelineScene(), {currentFrameNode}); + }); + + QAction *editValueAction = mainMenu.addAction(tr("Edit Value for Keyframe...")); + QObject::connect(editValueAction, &QAction::triggered, [this, currentFrameNode]() { + editValue(currentFrameNode, propertyName()); + }); + + const bool hasKeyframe = currentFrameNode.isValid(); + + insertAction->setEnabled(!hasKeyframe); + removeAction->setEnabled(hasKeyframe); + editEasingAction->setEnabled(hasKeyframe); + editValueAction->setEnabled(hasKeyframe); + + mainMenu.exec(event->screenPos()); + event->accept(); + } else if (event->pos().x() > TimelineConstants::toolButtonSize * 3 + 3 + && event->pos().x() < TimelineConstants::sectionWidth) { + QMenu mainMenu; + QAction *deleteAction = mainMenu.addAction(tr("Remove Property")); + + QObject::connect(deleteAction, &QAction::triggered, [this]() { + auto deleteKeyframeGroup = [this]() { timelineScene()->deleteKeyframeGroup(m_frames); }; + QTimer::singleShot(0, deleteKeyframeGroup); + }); + + mainMenu.exec(event->screenPos()); + event->accept(); + } +} + +TimelinePropertyItem::TimelinePropertyItem(TimelineSectionItem *parent) + : TimelineItem(parent) +{ + setPreferredHeight(TimelineConstants::sectionHeight); + setMinimumHeight(TimelineConstants::sectionHeight); + setMaximumHeight(TimelineConstants::sectionHeight); +} + +void TimelinePropertyItem::setupKeyframes() +{ + for (const ModelNode &frame : m_frames.keyframePositions()) + new TimelineKeyframeItem(this, frame); +} + +qreal TimelinePropertyItem::currentFrame() +{ + QmlTimeline timeline = timelineScene()->currentTimeline(); + if (timeline.isValid()) + return timeline.currentKeyframe(); + return 0; +} + +TimelineKeyframeItem::TimelineKeyframeItem(TimelinePropertyItem *parent, const ModelNode &frame) + : TimelineMovableAbstractItem(parent) + , m_frame(frame) + +{ + setPosition(frame.variantProperty("frame").value().toReal()); + setCursor(Qt::ClosedHandCursor); +} + +TimelineKeyframeItem::~TimelineKeyframeItem() +{ + timelineScene()->selectKeyframes(SelectionMode::Remove, {this}); +} + +void TimelineKeyframeItem::updateFrame() +{ + if (s_blockUpdates) + return; + + QTC_ASSERT(m_frame.isValid(), return ); + setPosition(m_frame.variantProperty("frame").value().toReal()); +} + +void TimelineKeyframeItem::setPosition(qreal position) +{ + int offset = (TimelineConstants::sectionHeight - TimelineConstants::keyFrameSize) / 2; + const qreal scenePostion = mapFromFrameToScene(position); + + setRect(scenePostion - TimelineConstants::keyFrameSize / 2, + offset, + TimelineConstants::keyFrameSize, + TimelineConstants::keyFrameSize); +} + +void TimelineKeyframeItem::setPositionInteractive(const QPointF &postion) +{ + qreal left = postion.x() - qreal(TimelineConstants::keyFrameSize) / qreal(2); + setRect(left, rect().y(), rect().width(), rect().height()); +} + +void TimelineKeyframeItem::commitPosition(const QPointF &point) +{ + setPositionInteractive(point); + + const qreal frame = qRound(mapFromSceneToFrame(rect().center().x())); + + setPosition(frame); + + QTC_ASSERT(m_frame.isValid(), return ); + + blockUpdates(); + + m_frame.view()->executeInTransaction("TimelineKeyframeItem::commitPosition", [this, frame](){ + m_frame.variantProperty("frame").setValue(frame); + }); + + enableUpdates(); +} + +TimelineKeyframeItem *TimelineKeyframeItem::asTimelineKeyframeItem() +{ + return this; +} + +void TimelineKeyframeItem::blockUpdates() +{ + s_blockUpdates = true; +} + +void TimelineKeyframeItem::enableUpdates() +{ + s_blockUpdates = false; +} + +bool TimelineKeyframeItem::hasManualBezier() const +{ + return m_frame.isValid() && m_frame.hasProperty("easing.bezierCurve"); +} + +void TimelineKeyframeItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + if (rect().x() < TimelineConstants::sectionWidth - rect().width() / 2) + return; + + painter->save(); + + Utils::Icon icon([this]() { + const bool itemIsSelected = propertyItem()->isSelected(); + const bool manualBezier = hasManualBezier(); + + if (m_highlight && manualBezier) { + return TimelineIcons::KEYFRAME_MANUALBEZIER_SELECTED; + } else if (m_highlight) { + return TimelineIcons::KEYFRAME_LINEAR_SELECTED; + } else if (itemIsSelected && manualBezier) { + return TimelineIcons::KEYFRAME_MANUALBEZIER_ACTIVE; + } else if (itemIsSelected) { + return TimelineIcons::KEYFRAME_LINEAR_ACTIVE; + } else if (manualBezier) { + return TimelineIcons::KEYFRAME_MANUALBEZIER_INACTIVE; + } + + return TimelineIcons::KEYFRAME_LINEAR_INACTIVE; + }()); + + painter->drawPixmap(rect().topLeft() - QPointF(0, 1), icon.pixmap()); + + painter->restore(); +} + +ModelNode TimelineKeyframeItem::frameNode() const +{ + return m_frame; +} + +void TimelineKeyframeItem::setHighlighted(bool b) +{ + m_highlight = b; + update(); +} + +TimelinePropertyItem *TimelineKeyframeItem::propertyItem() const +{ + /* The parentItem is always a TimelinePropertyItem. See constructor */ + return qgraphicsitem_cast<TimelinePropertyItem *>(parentItem()); +} + +void TimelineKeyframeItem::scrollOffsetChanged() +{ + updateFrame(); +} + +void TimelineKeyframeItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + QMenu mainMenu; + QAction *removeAction = mainMenu.addAction(tr("Delete Keyframe")); + QObject::connect(removeAction, &QAction::triggered, [this]() { + timelineScene()->handleKeyframeDeletion(); + }); + + QAction *editEasingAction = mainMenu.addAction(tr("Edit Easing Curve...")); + QObject::connect(editEasingAction, &QAction::triggered, [this]() { + const QList<ModelNode> keys = Utils::transform(timelineScene()->selectedKeyframes(), + &TimelineKeyframeItem::m_frame); + + setEasingCurve(timelineScene(), keys); + }); + + QAction *editValueAction = mainMenu.addAction(tr("Edit Value for Keyframe...")); + QObject::connect(editValueAction, &QAction::triggered, [this]() { + editValue(m_frame, propertyItem()->propertyName()); + }); + + mainMenu.exec(event->screenPos()); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h new file mode 100644 index 0000000000..2b8c00c59b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinepropertyitem.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinecontrols.h" +#include "timelinesectionitem.h" + +#include <qmltimelinekeyframegroup.h> + +#include <modelnode.h> + +#include <QGraphicsRectItem> + +QT_FORWARD_DECLARE_CLASS(QLineEdit) + +namespace QmlDesigner { + +class TimelinePropertyItem; +class TimelineGraphicsScene; +class TimelineToolButton; + +class TimelineKeyframeItem : public TimelineMovableAbstractItem +{ + Q_DECLARE_TR_FUNCTIONS(TimelineKeyframeItem) + +public: + explicit TimelineKeyframeItem(TimelinePropertyItem *parent, const ModelNode &frame); + ~TimelineKeyframeItem() override; + + static void blockUpdates(); + static void enableUpdates(); + + ModelNode frameNode() const; + + void updateFrame(); + + void setHighlighted(bool b); + + void setPosition(qreal position); + + void commitPosition(const QPointF &point) override; + + TimelineKeyframeItem *asTimelineKeyframeItem() override; + +protected: + bool hasManualBezier() const; + + void scrollOffsetChanged() override; + + void setPositionInteractive(const QPointF &postion) override; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + TimelinePropertyItem *propertyItem() const; + + ModelNode m_frame; + + bool m_highlight = false; +}; + +class TimelinePropertyItem : public TimelineItem +{ + Q_OBJECT + +public: + enum { Type = TimelineConstants::timelinePropertyItemUserType }; + + static TimelinePropertyItem *create(const QmlTimelineKeyframeGroup &frames, + TimelineSectionItem *parent = nullptr); + + int type() const override; + + void updateData(); + void updateFrames(); + bool isSelected() const; + + static void updateTextEdit(QGraphicsItem *item); + static void updateRecordButtonStatus(QGraphicsItem *item); + + QmlTimelineKeyframeGroup frames() const; + + QString propertyName() const; + + void changePropertyValue(const QVariant &value); + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + TimelinePropertyItem(TimelineSectionItem *parent = nullptr); + + void setupKeyframes(); + qreal currentFrame(); + void updateTextEdit(); + + QmlTimelineKeyframeGroup m_frames; + TimelineControl *m_control = nullptr; + TimelineToolButton *m_recording = nullptr; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp new file mode 100644 index 0000000000..7bd784a7dd --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.cpp @@ -0,0 +1,1062 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinesectionitem.h" + +#include "abstractview.h" +#include "timelineactions.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinepropertyitem.h" +#include "timelinetoolbutton.h" +#include "timelineutils.h" + +#include <qmltimeline.h> +#include <qmltimelinekeyframegroup.h> + +#include <rewritingexception.h> + +#include <theme.h> + +#include <utils/qtcassert.h> + +#include <QAction> +#include <QColorDialog> +#include <QComboBox> +#include <QGraphicsProxyWidget> +#include <QGraphicsScene> +#include <QGraphicsSceneMouseEvent> +#include <QGraphicsView> +#include <QHBoxLayout> +#include <QMenu> +#include <QPainter> +#include <QToolBar> + +#include <QGraphicsView> + +#include <QDebug> + +#include <cmath> + +static int textOffset = 8; + +namespace QmlDesigner { + +class ClickDummy : public TimelineItem +{ +public: + explicit ClickDummy(TimelineSectionItem *parent) + : TimelineItem(parent) + { + setGeometry(0, 0, TimelineConstants::sectionWidth, TimelineConstants::sectionHeight); + + setZValue(10); + setCursor(Qt::ArrowCursor); + } + +protected: + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } + void mousePressEvent(QGraphicsSceneMouseEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override + { + scene()->sendEvent(parentItem(), event); + } +}; + +TimelineSectionItem::TimelineSectionItem(TimelineItem *parent) + : TimelineItem(parent) +{} + +TimelineSectionItem *TimelineSectionItem::create(const QmlTimeline &timeline, + const ModelNode &target, + TimelineItem *parent) +{ + auto item = new TimelineSectionItem(parent); + + if (target.isValid()) + item->setToolTip(target.id()); + + item->m_targetNode = target; + item->m_timeline = timeline; + + item->createPropertyItems(); + + item->m_dummyItem = new ClickDummy(item); + item->m_dummyItem->update(); + + item->m_barItem = new TimelineBarItem(item); + item->invalidateBar(); + item->invalidateHeight(); + + return item; +} + +void TimelineSectionItem::invalidateBar() +{ + qreal min = m_timeline.minActualKeyframe(m_targetNode); + qreal max = m_timeline.maxActualKeyframe(m_targetNode); + + const qreal sceneMin = m_barItem->mapFromFrameToScene(min); + + QRectF barRect(sceneMin, + 0, + (max - min) * m_barItem->rulerScaling(), + TimelineConstants::sectionHeight - 1); + + m_barItem->setRect(barRect); +} + +int TimelineSectionItem::type() const +{ + return Type; +} + +void TimelineSectionItem::updateData(QGraphicsItem *item) +{ + if (auto sectionItem = qgraphicsitem_cast<TimelineSectionItem *>(item)) + sectionItem->updateData(); +} + +void TimelineSectionItem::updateDataForTarget(QGraphicsItem *item, const ModelNode &target, bool *b) +{ + if (!target.isValid()) + return; + + if (auto sectionItem = qgraphicsitem_cast<TimelineSectionItem *>(item)) { + if (sectionItem->m_targetNode == target) { + sectionItem->updateData(); + if (b) + *b = true; + } + } +} + +void TimelineSectionItem::updateFramesForTarget(QGraphicsItem *item, const ModelNode &target) +{ + if (auto sectionItem = qgraphicsitem_cast<TimelineSectionItem *>(item)) { + if (sectionItem->m_targetNode == target) + sectionItem->updateFrames(); + } +} + +void TimelineSectionItem::moveAllFrames(qreal offset) +{ + if (m_timeline.isValid()) + m_timeline.moveAllKeyframes(m_targetNode, offset); +} + +void TimelineSectionItem::scaleAllFrames(qreal scale) +{ + if (m_timeline.isValid()) + m_timeline.scaleAllKeyframes(m_targetNode, scale); +} + +qreal TimelineSectionItem::firstFrame() +{ + if (!m_timeline.isValid()) + return 0; + + return m_timeline.minActualKeyframe(m_targetNode); +} + +AbstractView *TimelineSectionItem::view() const +{ + return m_timeline.view(); +} + +bool TimelineSectionItem::isSelected() const +{ + return m_targetNode.isValid() && m_targetNode.isSelected(); +} + +ModelNode TimelineSectionItem::targetNode() const +{ + return m_targetNode; +} + +QVector<qreal> TimelineSectionItem::keyframePositions() const +{ + QVector<qreal> out; + for (auto frame : m_timeline.keyframeGroupsForTarget(m_targetNode)) + out.append(timelineScene()->keyframePositions(frame)); + + return out; +} + +QTransform rotatationTransform(qreal degrees) +{ + QTransform transform; + transform.rotate(degrees); + + return transform; +} + +QPixmap rotateby90(const QPixmap &pixmap) +{ + QImage sourceImage = pixmap.toImage(); + QImage destImage(pixmap.height(), pixmap.width(), sourceImage.format()); + + for (int x = 0; x < pixmap.width(); x++) + for (int y = 0; y < pixmap.height(); y++) + destImage.setPixel(y, x, sourceImage.pixel(x, y)); + + QPixmap result = QPixmap::fromImage(destImage); + + result.setDevicePixelRatio(pixmap.devicePixelRatio()); + + return result; +} + +static int devicePixelHeight(const QPixmap &pixmap) +{ + return pixmap.height() / pixmap.devicePixelRatioF(); +} + +void TimelineSectionItem::paint(QPainter *painter, + const QStyleOptionGraphicsItem * /*option*/, + QWidget *) +{ + if (m_targetNode.isValid()) { + painter->save(); + + const QColor textColor = Theme::getColor(Theme::PanelTextColorLight); + const QColor penColor = Theme::instance()->qmlDesignerBackgroundColorDarker(); + QColor brushColor = Theme::getColor(Theme::BackgroundColorDark); + + int fillOffset = 0; + if (isSelected()) { + brushColor = Theme::getColor(Theme::QmlDesigner_HighlightColor); + fillOffset = 1; + } + + painter->fillRect(0, + 0, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight - fillOffset, + brushColor); + painter->fillRect(TimelineConstants::sectionWidth, + 0, + size().width() - TimelineConstants::sectionWidth, + size().height(), + Theme::instance()->qmlDesignerBackgroundColorDarkAlternate()); + + painter->setPen(penColor); + drawLine(painter, + TimelineConstants::sectionWidth - 1, + 0, + TimelineConstants::sectionWidth - 1, + size().height() - 1); + drawLine(painter, + TimelineConstants::sectionWidth, + TimelineConstants::sectionHeight - 1, + size().width(), + TimelineConstants::sectionHeight - 1); + + static const QPixmap arrow = Theme::getPixmap("down-arrow"); + + static const QPixmap arrow90 = rotateby90(arrow); + + const QPixmap rotatedArrow = collapsed() ? arrow90 : arrow; + + const int textOffset = QFontMetrics(font()).ascent() + + (TimelineConstants::sectionHeight - QFontMetrics(font()).height()) + / 2; + + painter->drawPixmap(collapsed() ? 6 : 4, + (TimelineConstants::sectionHeight - devicePixelHeight(rotatedArrow)) / 2, + rotatedArrow); + + painter->setPen(textColor); + + QFontMetrics fm(painter->font()); + const QString elidedId = fm.elidedText(m_targetNode.id(), + Qt::ElideMiddle, + TimelineConstants::sectionWidth + - TimelineConstants::textIndentationSections); + painter->drawText(TimelineConstants::textIndentationSections, textOffset, elidedId); + + painter->restore(); + } +} + +void TimelineSectionItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->pos().y() > TimelineConstants::sectionHeight + || event->pos().x() < TimelineConstants::textIndentationSections) { + TimelineItem::mouseDoubleClickEvent(event); + return; + } + + if (event->button() == Qt::LeftButton) { + event->accept(); + toggleCollapsed(); + } +} + +void TimelineSectionItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->pos().y() > TimelineConstants::sectionHeight) { + TimelineItem::mousePressEvent(event); + return; + } + + if (event->button() == Qt::LeftButton) + event->accept(); +} + +void TimelineSectionItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + if (event->pos().y() > TimelineConstants::sectionHeight) { + TimelineItem::mouseReleaseEvent(event); + return; + } + + if (event->button() != Qt::LeftButton) + return; + + event->accept(); + + if (event->pos().x() > TimelineConstants::textIndentationSections + && event->button() == Qt::LeftButton) { + if (m_targetNode.isValid()) + m_targetNode.view()->setSelectedModelNode(m_targetNode); + } else { + toggleCollapsed(); + } + update(); +} + +void TimelineSectionItem::resizeEvent(QGraphicsSceneResizeEvent *event) +{ + TimelineItem::resizeEvent(event); + + for (auto child : propertyItems()) { + TimelinePropertyItem *item = static_cast<TimelinePropertyItem *>(child); + item->resize(size().width(), TimelineConstants::sectionHeight); + } +} + +void TimelineSectionItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + if (event->pos().x() < TimelineConstants::sectionWidth + && event->pos().y() < TimelineConstants::sectionHeight) { + QMenu mainMenu; + + auto timeline = timelineScene()->currentTimeline(); + + QAction *removeAction = mainMenu.addAction( + TimelineConstants::timelineDeleteKeyframesDisplayName); + QObject::connect(removeAction, &QAction::triggered, [this]() { + timelineScene()->deleteAllKeyframesForTarget(m_targetNode); + }); + + QAction *addKeyframesAction = mainMenu.addAction( + TimelineConstants::timelineInsertKeyframesDisplayName); + QObject::connect(addKeyframesAction, &QAction::triggered, [this]() { + timelineScene()->insertAllKeyframesForTarget(m_targetNode); + }); + + QAction *copyAction = mainMenu.addAction( + TimelineConstants::timelineCopyKeyframesDisplayName); + QObject::connect(copyAction, &QAction::triggered, [this]() { + timelineScene()->copyAllKeyframesForTarget(m_targetNode); + }); + + QAction *pasteAction = mainMenu.addAction( + TimelineConstants::timelinePasteKeyframesDisplayName); + QObject::connect(pasteAction, &QAction::triggered, [this]() { + timelineScene()->pasteKeyframesToTarget(m_targetNode); + }); + + pasteAction->setEnabled(TimelineActions::clipboardContainsKeyframes()); + + mainMenu.exec(event->screenPos()); + event->accept(); + } +} + +void TimelineSectionItem::updateData() +{ + invalidateBar(); + resize(rulerWidth(), size().height()); + invalidateProperties(); + update(); +} + +void TimelineSectionItem::updateFrames() +{ + invalidateBar(); + invalidateFrames(); + update(); +} + +void TimelineSectionItem::invalidateHeight() +{ + int height = 0; + bool visible = true; + + if (collapsed()) { + height = TimelineConstants::sectionHeight; + visible = false; + } else { + height = TimelineConstants::sectionHeight + + m_timeline.keyframeGroupsForTarget(m_targetNode).count() + * TimelineConstants::sectionHeight; + visible = true; + } + + for (auto child : propertyItems()) + child->setVisible(visible); + + setPreferredHeight(height); + setMinimumHeight(height); + setMaximumHeight(height); + timelineScene()->activateLayout(); +} + +void TimelineSectionItem::invalidateProperties() +{ + for (auto child : propertyItems()) { + delete child; + } + + createPropertyItems(); + + for (auto child : propertyItems()) { + TimelinePropertyItem *item = static_cast<TimelinePropertyItem *>(child); + item->updateData(); + item->resize(size().width(), TimelineConstants::sectionHeight); + } + invalidateHeight(); +} + +void TimelineSectionItem::invalidateFrames() +{ + for (auto child : propertyItems()) { + TimelinePropertyItem *item = static_cast<TimelinePropertyItem *>(child); + item->updateFrames(); + } +} + +bool TimelineSectionItem::collapsed() const +{ + return m_targetNode.isValid() && !m_targetNode.hasAuxiliaryData("timeline_expanded"); +} + +void TimelineSectionItem::createPropertyItems() +{ + auto framesList = m_timeline.keyframeGroupsForTarget(m_targetNode); + + int yPos = TimelineConstants::sectionHeight; + for (const auto &frames : framesList) { + auto item = TimelinePropertyItem::create(frames, this); + item->setY(yPos); + yPos = yPos + TimelineConstants::sectionHeight; + } +} + +qreal TimelineSectionItem::rulerWidth() const +{ + return static_cast<TimelineGraphicsScene *>(scene())->rulerWidth(); +} + +void TimelineSectionItem::toggleCollapsed() +{ + QTC_ASSERT(m_targetNode.isValid(), return ); + + if (collapsed()) + m_targetNode.setAuxiliaryData("timeline_expanded", true); + else + m_targetNode.removeAuxiliaryData("timeline_expanded"); + + invalidateHeight(); +} + +QList<QGraphicsItem *> TimelineSectionItem::propertyItems() const +{ + QList<QGraphicsItem *> list; + + for (auto child : childItems()) { + if (m_barItem != child && m_dummyItem != child) + list.append(child); + } + + return list; +} + +TimelineRulerSectionItem::TimelineRulerSectionItem(TimelineItem *parent) + : TimelineItem(parent) +{ + setPreferredHeight(TimelineConstants::rulerHeight); + setMinimumHeight(TimelineConstants::rulerHeight); + setMaximumHeight(TimelineConstants::rulerHeight); + setZValue(10); +} + +static void drawCenteredText(QPainter *p, int x, int y, const QString &text) +{ + QRect rect(x - 16, y - 4, 32, 8); + p->drawText(rect, Qt::AlignCenter, text); +} + +TimelineRulerSectionItem *TimelineRulerSectionItem::create(QGraphicsScene *parentScene, + TimelineItem *parent) +{ + auto item = new TimelineRulerSectionItem(parent); + item->setMaximumHeight(TimelineConstants::rulerHeight); + + auto widget = new QWidget; + widget->setFixedWidth(TimelineConstants::sectionWidth); + + auto toolBar = new QToolBar; + toolBar->setFixedHeight(TimelineConstants::rulerHeight); + + auto layout = new QHBoxLayout(widget); + layout->addWidget(toolBar); + layout->setMargin(0); + + layout->addWidget(toolBar); + layout->setMargin(0); + + QGraphicsProxyWidget *proxy = parentScene->addWidget(widget); + proxy->setParentItem(item); + + return item; +} + +void TimelineRulerSectionItem::invalidateRulerSize(const QmlTimeline &timeline) +{ + m_duration = timeline.duration(); + m_start = timeline.startKeyframe(); + m_end = timeline.endKeyframe(); +} + +void TimelineRulerSectionItem::setRulerScaleFactor(int scaling) +{ + qreal blend = qreal(scaling) / 100.0; + + qreal width = size().width() - qreal(TimelineConstants::sectionWidth); + qreal duration = rulerDuration(); + + qreal offset = duration * 0.1; + qreal maxCount = duration + offset; + qreal minCount = width + / qreal(TimelineConstants::keyFrameSize + + 2 * TimelineConstants::keyFrameMargin); + + qreal count = maxCount < minCount ? maxCount : TimelineUtils::lerp(blend, minCount, maxCount); + + if (count > std::numeric_limits<qreal>::min() && count <= maxCount) + m_scaling = width / count; + else + m_scaling = 1.0; + + update(); +} + +int TimelineRulerSectionItem::getRulerScaleFactor() const +{ + qreal width = size().width() - qreal(TimelineConstants::sectionWidth); + qreal duration = rulerDuration(); + + qreal offset = duration * 0.1; + qreal maxCount = duration + offset; + qreal minCount = width + / qreal(TimelineConstants::keyFrameSize + + 2 * TimelineConstants::keyFrameMargin); + + if (maxCount < minCount) + return -1; + + qreal rcount = width / m_scaling; + qreal rblend = TimelineUtils::reverseLerp(rcount, minCount, maxCount); + + int rfactor = std::round(rblend * 100); + return TimelineUtils::clamp(rfactor, 0, 100); +} + +qreal TimelineRulerSectionItem::rulerScaling() const +{ + return m_scaling; +} + +qreal TimelineRulerSectionItem::rulerDuration() const +{ + return m_duration; +} + +qreal TimelineRulerSectionItem::durationViewportLength() const +{ + return m_duration * m_scaling; +} + +qreal TimelineRulerSectionItem::startFrame() const +{ + return m_start; +} + +qreal TimelineRulerSectionItem::endFrame() const +{ + return m_end; +} + +void TimelineRulerSectionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + static const QColor backgroundColor = Theme::instance() + ->qmlDesignerBackgroundColorDarkAlternate(); + static const QColor penColor = Theme::getColor(Theme::PanelTextColorLight); + static const QColor highlightColor = Theme::instance()->Theme::qmlDesignerButtonColor(); + static const QColor handleColor = Theme::getColor(Theme::QmlDesigner_HighlightColor); + + painter->save(); + painter->save(); + painter->setRenderHint(QPainter::Antialiasing); + painter->translate(-timelineScene()->scrollOffset(), 0); + painter->fillRect(TimelineConstants::sectionWidth, + 0, + size().width() - TimelineConstants::sectionWidth, + size().height(), + backgroundColor); + + painter->translate(TimelineConstants::timelineLeftOffset, 0); + + const QRectF rangeRect(TimelineConstants::sectionWidth, + 0, + m_duration * m_scaling, + size().height()); + + const qreal radius = 5; + const qreal handleWidth = TimelineConstants::timelineBounds * 2; + QRectF boundsRect(0, rangeRect.y(), handleWidth, rangeRect.height()); + + boundsRect.moveRight(rangeRect.left() + TimelineConstants::timelineBounds); + + QPainterPath leftBoundsPath; + leftBoundsPath.addRoundedRect(boundsRect, radius, radius); + painter->fillPath(leftBoundsPath, handleColor); + + boundsRect.moveLeft(rangeRect.right() - TimelineConstants::timelineBounds); + + QPainterPath rightBoundsPath; + rightBoundsPath.addRoundedRect(boundsRect, radius, radius); + painter->fillPath(rightBoundsPath, handleColor); + + painter->fillRect(rangeRect, highlightColor); + + painter->setPen(penColor); + + const int height = size().height() - 1; + + drawLine(painter, + TimelineConstants::sectionWidth + timelineScene()->scrollOffset() + - TimelineConstants::timelineLeftOffset, + height, + size().width() + timelineScene()->scrollOffset(), + height); + + QFont font = painter->font(); + font.setPixelSize(8); + painter->setFont(font); + + paintTicks(painter); + + painter->restore(); + + painter->fillRect(0, 0, TimelineConstants::sectionWidth, size().height(), backgroundColor); + painter->restore(); +} + +void TimelineRulerSectionItem::paintTicks(QPainter *painter) +{ + const int totalWidth = size().width() / m_scaling + timelineScene()->scrollOffset() / m_scaling; + + QFontMetrics fm(painter->font()); + + int minSpacingText = fm.horizontalAdvance(QString("X%1X").arg(rulerDuration())); + int minSpacingLine = 5; + + int deltaText = 0; + int deltaLine = 0; + + // Marks possibly at [1, 5, 10, 50, 100, ...] + int spacing = 1; + bool toggle = true; + while (deltaText == 0) { + int distance = spacing * m_scaling; + + if (distance > minSpacingLine && deltaLine == 0) + deltaLine = spacing; + + if (distance > minSpacingText) { + deltaText = spacing; + break; + } + + if (toggle) { + spacing *= 5; + toggle = false; + } else { + spacing *= 2; + toggle = true; + } + } + + int height = size().height(); + + for (int i = timelineScene()->scrollOffset() / m_scaling; i < totalWidth; ++i) { + if ((i % deltaText) == 0) { + drawCenteredText(painter, + TimelineConstants::sectionWidth + i * m_scaling, + textOffset, + QString::number(m_start + i)); + + drawLine(painter, + TimelineConstants::sectionWidth + i * m_scaling, + height - 2, + TimelineConstants::sectionWidth + i * m_scaling, + height * 0.6); + + } else if ((i % deltaLine) == 0) { + drawLine(painter, + TimelineConstants::sectionWidth + i * m_scaling, + height - 2, + TimelineConstants::sectionWidth + i * m_scaling, + height * 0.75); + } + } +} + +void TimelineRulerSectionItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + TimelineItem::mousePressEvent(event); + emit rulerClicked(event->pos()); +} + +void TimelineRulerSectionItem::resizeEvent(QGraphicsSceneResizeEvent *event) +{ + QGraphicsWidget::resizeEvent(event); + + auto factor = getRulerScaleFactor(); + + if (factor < 0) { + if (event->oldSize().width() < event->newSize().width()) + factor = 0; + else + factor = 100; + } + + emit scaleFactorChanged(factor); +} + +void TimelineRulerSectionItem::setSizeHints(int width) +{ + const int rulerWidth = width; + setPreferredWidth(rulerWidth); + setMinimumWidth(rulerWidth); + setMaximumWidth(rulerWidth); +} + +TimelineBarItem::TimelineBarItem(TimelineSectionItem *parent) + : TimelineMovableAbstractItem(parent) +{ + setAcceptHoverEvents(true); + setPen(Qt::NoPen); +} + +void TimelineBarItem::itemMoved(const QPointF &start, const QPointF &end) +{ + if (isActiveHandle(Location::Undefined)) + dragInit(rect(), start); + + const qreal min = qreal(TimelineConstants::sectionWidth + TimelineConstants::timelineLeftOffset + - scrollOffset()); + const qreal max = qreal(timelineScene()->rulerWidth() - TimelineConstants::sectionWidth + + rect().width()); + + if (isActiveHandle(Location::Center)) + dragCenter(rect(), end, min, max); + else + dragHandle(rect(), end, min, max); + + timelineScene()->statusBarMessageChanged( + tr("Range from %1 to %2") + .arg(qRound(mapFromSceneToFrame(rect().x()))) + .arg(qRound(mapFromSceneToFrame(rect().width() + rect().x())))); +} + +void TimelineBarItem::commitPosition(const QPointF & /*point*/) +{ + if (sectionItem()->view()) { + if (m_handle != Location::Undefined) { + sectionItem()->view()->executeInTransaction("TimelineBarItem::commitPosition", [this](){ + qreal scaleFactor = rect().width() / m_oldRect.width(); + + qreal moved = (rect().topLeft().x() - m_oldRect.topLeft().x()) / rulerScaling(); + qreal supposedFirstFrame = qRound(sectionItem()->firstFrame() + moved); + + sectionItem()->scaleAllFrames(scaleFactor); + sectionItem()->moveAllFrames(supposedFirstFrame - sectionItem()->firstFrame()); + }); + } + } + + m_handle = Location::Undefined; + m_bounds = Location::Undefined; + m_pivot = 0.0; + m_oldRect = QRectF(); +} + +void TimelineBarItem::scrollOffsetChanged() +{ + sectionItem()->invalidateBar(); +} + +void TimelineBarItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + + QColor brushColorSelected = Theme::getColor(Theme::QmlDesigner_HighlightColor); + QColor brushColor = Theme::getColor(Theme::QmlDesigner_HighlightColor).darker(120); + const QColor indicatorColor = Theme::getColor(Theme::PanelTextColorLight); + + ModelNode target = sectionItem()->targetNode(); + if (target.isValid()) { + QColor overrideColor = target.auxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE).value<QColor>(); + if (overrideColor.isValid()) { + brushColorSelected = overrideColor; + brushColor = brushColorSelected.darker(120); + } + } + + const QRectF itemRect = rect(); + + painter->save(); + painter->setClipRect(TimelineConstants::sectionWidth, + 0, + itemRect.width() + itemRect.x(), + itemRect.height()); + + if (sectionItem()->isSelected()) + painter->fillRect(itemRect, brushColorSelected); + else + painter->fillRect(itemRect, brushColor); + + auto positions = sectionItem()->keyframePositions(); + std::sort(positions.begin(), positions.end()); + + auto fcompare = [](auto v1, auto v2) { return qFuzzyCompare(v1, v2); }; + auto unique = std::unique(positions.begin(), positions.end(), fcompare); + positions.erase(unique, positions.end()); + + painter->setPen(indicatorColor); + auto margin = itemRect.height() * 0.166; + auto p1y = itemRect.top() + margin; + auto p2y = itemRect.bottom() - margin; + for (auto pos : positions) { + auto px = mapFromFrameToScene(pos) + 0.5; + painter->drawLine(QLineF(px, p1y, px, p2y)); + } + painter->restore(); +} + +void TimelineBarItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) +{ + const auto p = event->pos(); + + QRectF left, right; + if (handleRects(rect(), left, right)) { + if (left.contains(p) || right.contains(p)) { + if (cursor().shape() != Qt::SizeHorCursor) + setCursor(QCursor(Qt::SizeHorCursor)); + } else if (rect().contains(p)) { + if (cursor().shape() != Qt::ClosedHandCursor) + setCursor(QCursor(Qt::ClosedHandCursor)); + } + } else { + if (rect().contains(p)) + setCursor(QCursor(Qt::ClosedHandCursor)); + } +} + +void TimelineBarItem::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) +{ + QMenu menu; + QAction* overrideColor = menu.addAction(tr("Override Color")); + + auto setColor = [this] () { + ModelNode target = sectionItem()->targetNode(); + if (target.isValid()) { + QColor current = target.auxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE).value<QColor>(); + QColor color = QColorDialog::getColor(current, nullptr); + if (color.isValid()) + target.setAuxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE, color); + } + }; + + QObject::connect(overrideColor, &QAction::triggered, setColor); + + QAction* resetColor = menu.addAction(tr("Reset Color")); + auto reset = [this]() { + ModelNode target = sectionItem()->targetNode(); + if (target.isValid()) + target.removeAuxiliaryData(TimelineConstants::C_BAR_ITEM_OVERRIDE); + }; + QObject::connect(resetColor, &QAction::triggered, reset); + + menu.exec(event->screenPos()); +} + +TimelineSectionItem *TimelineBarItem::sectionItem() const +{ + /* The parentItem is always a TimelineSectionItem. See constructor */ + return qgraphicsitem_cast<TimelineSectionItem *>(parentItem()); +} + +void TimelineBarItem::dragInit(const QRectF &rect, const QPointF &pos) +{ + QRectF left, right; + m_oldRect = rect; + if (handleRects(rect, left, right)) { + if (left.contains(pos)) { + m_handle = Location::Left; + m_pivot = pos.x() - left.topLeft().x(); + } else if (right.contains(pos)) { + m_handle = Location::Right; + m_pivot = pos.x() - right.topRight().x(); + } else if (rect.contains(pos)) { + m_handle = Location::Center; + m_pivot = pos.x() - rect.topLeft().x(); + } + + } else { + if (rect.contains(pos)) { + m_handle = Location::Center; + m_pivot = pos.x() - rect.topLeft().x(); + } + } +} + +void TimelineBarItem::dragCenter(QRectF rect, const QPointF &pos, qreal min, qreal max) +{ + if (validateBounds(pos.x() - rect.topLeft().x())) { + rect.moveLeft(pos.x() - m_pivot); + if (rect.topLeft().x() < min) { + rect.moveLeft(min); + setOutOfBounds(Location::Left); + } else if (rect.topRight().x() > max) { + rect.moveRight(max); + setOutOfBounds(Location::Right); + } + setRect(rect); + } +} + +void TimelineBarItem::dragHandle(QRectF rect, const QPointF &pos, qreal min, qreal max) +{ + QRectF left, right; + handleRects(rect, left, right); + + if (isActiveHandle(Location::Left)) { + if (validateBounds(pos.x() - left.topLeft().x())) { + rect.setLeft(pos.x() - m_pivot); + if (rect.left() < min) { + rect.setLeft(min); + setOutOfBounds(Location::Left); + } else if (rect.left() >= rect.right() - minimumBarWidth) + rect.setLeft(rect.right() - minimumBarWidth); + + setRect(rect); + } + } else if (isActiveHandle(Location::Right)) { + if (validateBounds(pos.x() - right.topRight().x())) { + rect.setRight(pos.x() - m_pivot); + if (rect.right() > max) { + rect.setRight(max); + setOutOfBounds(Location::Right); + } else if (rect.right() <= rect.left() + minimumBarWidth) + rect.setRight(rect.left() + minimumBarWidth); + + setRect(rect); + } + } +} + +bool TimelineBarItem::handleRects(const QRectF &rect, QRectF &left, QRectF &right) const +{ + if (rect.width() < minimumBarWidth) + return false; + + const qreal handleSize = rect.height(); + + auto handleRect = QRectF(0, 0, handleSize, handleSize); + handleRect.moveCenter(rect.center()); + + handleRect.moveLeft(rect.left()); + left = handleRect; + + handleRect.moveRight(rect.right()); + right = handleRect; + + return true; +} + +bool TimelineBarItem::isActiveHandle(Location location) const +{ + return m_handle == location; +} + +void TimelineBarItem::setOutOfBounds(Location location) +{ + m_bounds = location; +} + +bool TimelineBarItem::validateBounds(qreal distance) +{ + if (m_bounds == Location::Left) { + if (distance > m_pivot) + m_bounds = Location::Center; + return false; + + } else if (m_bounds == Location::Right) { + if (distance < m_pivot) + m_bounds = Location::Center; + return false; + } + return true; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h new file mode 100644 index 0000000000..26db04f757 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesectionitem.h @@ -0,0 +1,192 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineitem.h" +#include "timelinemovableabstractitem.h" + +#include <modelnode.h> +#include <qmltimeline.h> + +QT_FORWARD_DECLARE_CLASS(QComboBox) +QT_FORWARD_DECLARE_CLASS(QPainter) + +namespace QmlDesigner { + +class TimelineSectionItem; + +class TimelineBarItem : public TimelineMovableAbstractItem +{ + Q_DECLARE_TR_FUNCTIONS(TimelineBarItem) + + enum class Location { Undefined, Center, Left, Right }; + +public: + explicit TimelineBarItem(TimelineSectionItem *parent); + + void itemMoved(const QPointF &start, const QPointF &end) override; + void commitPosition(const QPointF &point) override; + +protected: + void scrollOffsetChanged() override; + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget = nullptr) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent *) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent * event) override; +private: + TimelineSectionItem *sectionItem() const; + + void dragInit(const QRectF &rect, const QPointF &pos); + void dragCenter(QRectF rect, const QPointF &pos, qreal min, qreal max); + void dragHandle(QRectF rect, const QPointF &pos, qreal min, qreal max); + bool handleRects(const QRectF &rect, QRectF &left, QRectF &right) const; + bool isActiveHandle(Location location) const; + + void setOutOfBounds(Location location); + bool validateBounds(qreal pivot); + +private: + Location m_handle = Location::Undefined; + + Location m_bounds = Location::Undefined; + + qreal m_pivot = 0.0; + + QRectF m_oldRect; + + static constexpr qreal minimumBarWidth = 2.0 + * static_cast<qreal>(TimelineConstants::sectionHeight); +}; + +class TimelineSectionItem : public TimelineItem +{ + Q_OBJECT + +public: + enum { Type = TimelineConstants::timelineSectionItemUserType }; + + static TimelineSectionItem *create(const QmlTimeline &timelineScene, + const ModelNode &target, + TimelineItem *parent); + + void invalidateBar(); + + int type() const override; + + static void updateData(QGraphicsItem *item); + static void updateDataForTarget(QGraphicsItem *item, const ModelNode &target, bool *b); + static void updateFramesForTarget(QGraphicsItem *item, const ModelNode &target); + + void moveAllFrames(qreal offset); + void scaleAllFrames(qreal scale); + qreal firstFrame(); + AbstractView *view() const; + bool isSelected() const; + + ModelNode targetNode() const; + QVector<qreal> keyframePositions() const; + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + void resizeEvent(QGraphicsSceneResizeEvent *event) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + +private: + void updateData(); + void updateFrames(); + void invalidateHeight(); + void invalidateProperties(); + void invalidateFrames(); + bool collapsed() const; + void createPropertyItems(); + qreal rulerWidth() const; + void toggleCollapsed(); + QList<QGraphicsItem *> propertyItems() const; + + TimelineSectionItem(TimelineItem *parent = nullptr); + ModelNode m_targetNode; + QmlTimeline m_timeline; + + TimelineBarItem *m_barItem; + TimelineItem *m_dummyItem; +}; + +class TimelineRulerSectionItem : public TimelineItem +{ + Q_OBJECT + +signals: + void rulerClicked(const QPointF &pos); + + void scaleFactorChanged(int scale); + +public: + static TimelineRulerSectionItem *create(QGraphicsScene *parentScene, TimelineItem *parent); + + void invalidateRulerSize(const QmlTimeline &timeline); + + void setRulerScaleFactor(int scaling); + + int getRulerScaleFactor() const; + + qreal rulerScaling() const; + qreal rulerDuration() const; + qreal durationViewportLength() const; + qreal startFrame() const; + qreal endFrame() const; + + QComboBox *comboBox() const; + + void setSizeHints(int width); + +signals: + void addTimelineClicked(); + +protected: + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + + void resizeEvent(QGraphicsSceneResizeEvent *event) override; + +private: + TimelineRulerSectionItem(TimelineItem *parent = nullptr); + + void paintTicks(QPainter *painter); + + QComboBox *m_comboBox = nullptr; + + qreal m_duration = 0; + qreal m_start = 0; + qreal m_end = 0; + qreal m_scaling = 1; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp new file mode 100644 index 0000000000..53367c9b67 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.cpp @@ -0,0 +1,179 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineselectiontool.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelinemovableabstractitem.h" +#include "timelinepropertyitem.h" +#include "timelinetooldelegate.h" + +#include <QGraphicsRectItem> +#include <QGraphicsSceneMouseEvent> +#include <QPen> + +namespace QmlDesigner { + +TimelineSelectionTool::TimelineSelectionTool(TimelineGraphicsScene *scene, + TimelineToolDelegate *delegate) + : TimelineAbstractTool(scene, delegate) + , m_selectionRect(new QGraphicsRectItem) +{ + scene->addItem(m_selectionRect); + QPen pen(Qt::black); + pen.setJoinStyle(Qt::MiterJoin); + pen.setCosmetic(true); + m_selectionRect->setPen(pen); + m_selectionRect->setBrush(QColor(128, 128, 128, 50)); + m_selectionRect->setZValue(100); + m_selectionRect->hide(); +} + +TimelineSelectionTool::~TimelineSelectionTool() = default; + +SelectionMode TimelineSelectionTool::selectionMode(QGraphicsSceneMouseEvent *event) +{ + if (event->modifiers().testFlag(Qt::ControlModifier)) { + if (event->modifiers().testFlag(Qt::ShiftModifier)) + return SelectionMode::Add; + else + return SelectionMode::Toggle; + } + + return SelectionMode::New; +} + +void TimelineSelectionTool::mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + if (event->buttons() == Qt::LeftButton && selectionMode(event) == SelectionMode::New) + deselect(); +} + +void TimelineSelectionTool::mouseMoveEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + + if (event->buttons() == Qt::LeftButton) { + auto endPoint = event->scenePos(); + if (endPoint.x() < 0) + endPoint.rx() = 0; + if (endPoint.y() < 0) + endPoint.ry() = 0; + m_selectionRect->setRect(QRectF(startPosition(), endPoint).normalized()); + m_selectionRect->show(); + + aboutToSelect(selectionMode(event), + scene()->items(m_selectionRect->rect(), Qt::ContainsItemShape)); + } +} + +void TimelineSelectionTool::mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + commitSelection(selectionMode(event)); + + reset(); +} + +void TimelineSelectionTool::mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(item); + Q_UNUSED(event); + + reset(); +} + +void TimelineSelectionTool::keyPressEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +void TimelineSelectionTool::keyReleaseEvent(QKeyEvent *keyEvent) +{ + Q_UNUSED(keyEvent); +} + +void TimelineSelectionTool::deselect() +{ + resetHighlights(); + scene()->clearSelection(); + delegate()->clearSelection(); +} + +void TimelineSelectionTool::reset() +{ + m_selectionRect->hide(); + m_selectionRect->setRect(0, 0, 0, 0); + resetHighlights(); +} + +void TimelineSelectionTool::resetHighlights() +{ + for (auto *keyframe : m_aboutToSelectBuffer) + if (scene()->isKeyframeSelected(keyframe)) + keyframe->setHighlighted(true); + else + keyframe->setHighlighted(false); + + m_aboutToSelectBuffer.clear(); +} + +void TimelineSelectionTool::aboutToSelect(SelectionMode mode, QList<QGraphicsItem *> items) +{ + resetHighlights(); + + for (auto *item : items) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (mode == SelectionMode::Remove) + keyframe->setHighlighted(false); + else if (mode == SelectionMode::Toggle) + if (scene()->isKeyframeSelected(keyframe)) + keyframe->setHighlighted(false); + else + keyframe->setHighlighted(true); + else + keyframe->setHighlighted(true); + + m_aboutToSelectBuffer << keyframe; + } + } +} + +void TimelineSelectionTool::commitSelection(SelectionMode mode) +{ + scene()->selectKeyframes(mode, m_aboutToSelectBuffer); + m_aboutToSelectBuffer.clear(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h new file mode 100644 index 0000000000..3485e087ec --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineselectiontool.h @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineabstracttool.h" + +#include <QList> + +QT_FORWARD_DECLARE_CLASS(QGraphicsItem) +QT_FORWARD_DECLARE_CLASS(QGraphicsRectItem) + +namespace QmlDesigner { + +class TimelineToolDelegate; + +class TimelineKeyframeItem; + +class TimelineGraphicsScene; + +enum class SelectionMode { New, Add, Remove, Toggle }; + +class TimelineSelectionTool : public TimelineAbstractTool +{ +public: + explicit TimelineSelectionTool(TimelineGraphicsScene *scene, TimelineToolDelegate *delegate); + + ~TimelineSelectionTool() override; + + static SelectionMode selectionMode(QGraphicsSceneMouseEvent *event); + + void mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event) override; + + void mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + + void keyReleaseEvent(QKeyEvent *keyEvent) override; + +private: + void deselect(); + + void reset(); + + void resetHighlights(); + + void aboutToSelect(SelectionMode mode, QList<QGraphicsItem *> items); + + void commitSelection(SelectionMode mode); + +private: + QGraphicsRectItem *m_selectionRect; + + QList<TimelineKeyframeItem *> m_aboutToSelectBuffer; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp new file mode 100644 index 0000000000..960c409553 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.cpp @@ -0,0 +1,272 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinesettingsdialog.h" +#include "ui_timelinesettingsdialog.h" + +#include "timelineanimationform.h" +#include "timelineform.h" +#include "timelineicons.h" +#include "timelinesettingsmodel.h" +#include "timelineview.h" + +#include <abstractview.h> +#include <bindingproperty.h> +#include <exception> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <variantproperty.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +#include <QKeyEvent> +#include <QToolBar> + +namespace QmlDesigner { + +static void deleteAllTabs(QTabWidget *tabWidget) +{ + while (tabWidget->count() > 0) { + QWidget *w = tabWidget->widget(0); + tabWidget->removeTab(0); + delete w; + } +} + +static ModelNode getAnimationFromTabWidget(QTabWidget *tabWidget) +{ + QWidget *w = tabWidget->currentWidget(); + if (w) + return qobject_cast<TimelineAnimationForm *>(w)->animation(); + return ModelNode(); +} + +static QmlTimeline getTimelineFromTabWidget(QTabWidget *tabWidget) +{ + QWidget *w = tabWidget->currentWidget(); + if (w) + return qobject_cast<TimelineForm *>(w)->timeline(); + return QmlTimeline(); +} + +static void setTabForTimeline(QTabWidget *tabWidget, const QmlTimeline &timeline) +{ + for (int i = 0; i < tabWidget->count(); ++i) { + QWidget *w = tabWidget->widget(i); + if (qobject_cast<TimelineForm *>(w)->timeline() == timeline) { + tabWidget->setCurrentIndex(i); + return; + } + } +} + +static void setTabForAnimation(QTabWidget *tabWidget, const ModelNode &animation) +{ + for (int i = 0; i < tabWidget->count(); ++i) { + QWidget *w = tabWidget->widget(i); + if (qobject_cast<TimelineAnimationForm *>(w)->animation() == animation) { + tabWidget->setCurrentIndex(i); + return; + } + } +} + +TimelineSettingsDialog::TimelineSettingsDialog(QWidget *parent, TimelineView *view) + : QDialog(parent) + , ui(new Ui::TimelineSettingsDialog) + , m_timelineView(view) +{ + m_timelineSettingsModel = new TimelineSettingsModel(this, view); + + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); + + auto *timelineCornerWidget = new QToolBar; + + auto *timelineAddAction = new QAction(TimelineIcons::ADD_TIMELINE.icon(), tr("Add Timeline")); + auto *timelineRemoveAction = new QAction(TimelineIcons::REMOVE_TIMELINE.icon(), + tr("Remove Timeline")); + + connect(timelineAddAction, &QAction::triggered, this, [this]() { + setupTimelines(m_timelineView->addNewTimeline()); + }); + + connect(timelineRemoveAction, &QAction::triggered, this, [this]() { + QmlTimeline timeline = getTimelineFromTabWidget(ui->timelineTab); + if (timeline.isValid()) { + timeline.destroy(); + setupTimelines(QmlTimeline()); + } + }); + + timelineCornerWidget->addAction(timelineAddAction); + timelineCornerWidget->addAction(timelineRemoveAction); + + ui->timelineTab->setCornerWidget(timelineCornerWidget, Qt::TopRightCorner); + + auto *animationCornerWidget = new QToolBar; + + auto *animationAddAction = new QAction(TimelineIcons::ADD_TIMELINE.icon(), tr("Add Animation")); + auto *animationRemoveAction = new QAction(TimelineIcons::REMOVE_TIMELINE.icon(), + tr("Remove Animation")); + + animationCornerWidget->addAction(animationAddAction); + animationCornerWidget->addAction(animationRemoveAction); + + connect(animationAddAction, &QAction::triggered, this, [this]() { + setupAnimations(m_timelineView->addAnimation(m_currentTimeline)); + }); + + connect(animationRemoveAction, &QAction::triggered, this, [this]() { + ModelNode node = getAnimationFromTabWidget(ui->animationTab); + if (node.isValid()) { + node.destroy(); + setupAnimations(m_currentTimeline); + } + }); + + ui->animationTab->setCornerWidget(animationCornerWidget, Qt::TopRightCorner); + ui->buttonBox->clearFocus(); + + setupTimelines(QmlTimeline()); + setupAnimations(m_currentTimeline); + + connect(ui->timelineTab, &QTabWidget::currentChanged, this, [this]() { + m_currentTimeline = getTimelineFromTabWidget(ui->timelineTab); + setupAnimations(m_currentTimeline); + }); + setupTableView(); +} + +void TimelineSettingsDialog::setCurrentTimeline(const QmlTimeline &timeline) +{ + m_currentTimeline = timeline; + setTabForTimeline(ui->timelineTab, m_currentTimeline); +} + +TimelineSettingsDialog::~TimelineSettingsDialog() +{ + delete ui; +} + +void TimelineSettingsDialog::keyPressEvent(QKeyEvent *event) +{ + switch (event->key()) { + case Qt::Key_Return: + case Qt::Key_Enter: + /* ignore */ + break; + + default: + QDialog::keyPressEvent(event); + } +} + +void TimelineSettingsDialog::setupTableView() +{ + ui->tableView->setModel(m_timelineSettingsModel); + m_timelineSettingsModel->setupDelegates(ui->tableView); + ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + ui->tableView->verticalHeader()->hide(); + ui->tableView->setSelectionMode(QAbstractItemView::NoSelection); + m_timelineSettingsModel->resetModel(); +} + +void TimelineSettingsDialog::setupTimelines(const QmlTimeline &timeline) +{ + deleteAllTabs(ui->timelineTab); + + const QList<QmlTimeline> &timelines = m_timelineView->getTimelines(); + + if (timelines.isEmpty()) { + m_currentTimeline = QmlTimeline(); + auto timelineForm = new TimelineForm(this); + timelineForm->setDisabled(true); + ui->timelineTab->addTab(timelineForm, tr("No Timeline")); + return; + } + + for (const auto &timeline : timelines) + addTimelineTab(timeline); + + if (timeline.isValid()) { + m_currentTimeline = timeline; + } else { + m_currentTimeline = timelines.constFirst(); + } + + setTabForTimeline(ui->timelineTab, m_currentTimeline); + setupAnimations(m_currentTimeline); + m_timelineSettingsModel->resetModel(); +} + +void TimelineSettingsDialog::setupAnimations(const ModelNode &animation) +{ + deleteAllTabs(ui->animationTab); + + const QList<ModelNode> animations = m_timelineView->getAnimations(m_currentTimeline); + + for (const auto &animation : animations) + addAnimationTab(animation); + + if (animations.isEmpty()) { + auto animationForm = new TimelineAnimationForm(this); + animationForm->setDisabled(true); + ui->animationTab->addTab(animationForm, tr("No Animation")); + if (currentTimelineForm()) + currentTimelineForm()->setHasAnimation(false); + } else { + if (currentTimelineForm()) + currentTimelineForm()->setHasAnimation(true); + } + + if (animation.isValid()) + setTabForAnimation(ui->animationTab, animation); + m_timelineSettingsModel->resetModel(); +} + +void TimelineSettingsDialog::addTimelineTab(const QmlTimeline &node) +{ + auto timelineForm = new TimelineForm(this); + ui->timelineTab->addTab(timelineForm, node.modelNode().displayName()); + timelineForm->setTimeline(node); + setupAnimations(ModelNode()); +} + +void TimelineSettingsDialog::addAnimationTab(const ModelNode &node) +{ + auto timelineAnimationForm = new TimelineAnimationForm(this); + ui->animationTab->addTab(timelineAnimationForm, node.displayName()); + timelineAnimationForm->setup(node); +} + +TimelineForm *TimelineSettingsDialog::currentTimelineForm() const +{ + return qobject_cast<TimelineForm *>(ui->timelineTab->currentWidget()); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h new file mode 100644 index 0000000000..da4ddac4f6 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QDialog> + +QT_FORWARD_DECLARE_CLASS(QSpinBox) + +namespace QmlDesigner { + +class TimelineForm; +class TimelineAnimationForm; +class TimelineView; +class TimelineSettingsModel; + +namespace Ui { +class TimelineSettingsDialog; +} + +class TimelineSettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TimelineSettingsDialog(QWidget *parent, TimelineView *view); + void setCurrentTimeline(const QmlTimeline &timeline); + ~TimelineSettingsDialog() override; + +protected: + void keyPressEvent(QKeyEvent *event) override; + +private: + void setupTableView(); + void setupTimelines(const QmlTimeline &node); + void setupAnimations(const ModelNode &node); + + void addTimelineTab(const QmlTimeline &node); + void addAnimationTab(const ModelNode &node); + + TimelineForm *currentTimelineForm() const; + + Ui::TimelineSettingsDialog *ui; + + TimelineView *m_timelineView; + QmlTimeline m_currentTimeline; + TimelineSettingsModel *m_timelineSettingsModel; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui new file mode 100644 index 0000000000..f3dfa6f094 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsdialog.ui @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>QmlDesigner::TimelineSettingsDialog</class> + <widget class="QDialog" name="QmlDesigner::TimelineSettingsDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>519</width> + <height>582</height> + </rect> + </property> + <property name="windowTitle"> + <string>Timeline Settings</string> + </property> + <property name="modal"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QTabWidget" name="timelineTab"> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + <item> + <widget class="QTabWidget" name="animationTab"> + <property name="currentIndex"> + <number>-1</number> + </property> + </widget> + </item> + <item> + <widget class="QTableView" name="tableView"/> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>QmlDesigner::TimelineSettingsDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>QmlDesigner::TimelineSettingsDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui> diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp new file mode 100644 index 0000000000..f75d129983 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.cpp @@ -0,0 +1,484 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinesettingsmodel.h" + +#include "timelineview.h" + +#include <modelnode.h> +#include <variantproperty.h> +#include <qmlitemnode.h> + +#include <utils/qtcassert.h> + +#include <QComboBox> +#include <QItemEditorFactory> +#include <QMessageBox> +#include <QStyledItemDelegate> +#include <QTimer> + +namespace QmlDesigner { + +class CustomDelegate : public QStyledItemDelegate +{ +public: + explicit CustomDelegate(QWidget *parent = nullptr); + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +CustomDelegate::CustomDelegate(QWidget *parent) + : QStyledItemDelegate(parent) +{} + +void CustomDelegate::paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + opt.state &= ~QStyle::State_HasFocus; + QStyledItemDelegate::paint(painter, opt, index); +} + +class TimelineEditorDelegate : public CustomDelegate +{ +public: + TimelineEditorDelegate(QWidget *parent = nullptr); + QWidget *createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +TimelineEditorDelegate::TimelineEditorDelegate(QWidget *parent) + : CustomDelegate(parent) +{ + static QItemEditorFactory *factory = nullptr; + if (factory == nullptr) { + factory = new QItemEditorFactory; + QItemEditorCreatorBase *creator = new QItemEditorCreator<QComboBox>("currentText"); + factory->registerEditor(QVariant::String, creator); + } + + setItemEditorFactory(factory); +} + +QWidget *TimelineEditorDelegate::createEditor(QWidget *parent, + const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QWidget *widget = QStyledItemDelegate::createEditor(parent, option, index); + + const auto timelineSettingsModel = qobject_cast<const TimelineSettingsModel *>(index.model()); + + auto comboBox = qobject_cast<QComboBox *>(widget); + + QTC_ASSERT(timelineSettingsModel, return widget); + QTC_ASSERT(timelineSettingsModel->timelineView(), return widget); + + QmlTimeline qmlTimeline = timelineSettingsModel->timelineForRow(index.row()); + + switch (index.column()) { + case TimelineSettingsModel::TimelineRow: { + QTC_ASSERT(comboBox, return widget); + comboBox->addItem(TimelineSettingsModel::tr("None")); + for (const auto &timeline : timelineSettingsModel->timelineView()->getTimelines()) { + if (!timeline.modelNode().id().isEmpty()) + comboBox->addItem(timeline.modelNode().id()); + } + } break; + case TimelineSettingsModel::AnimationRow: { + QTC_ASSERT(comboBox, return widget); + comboBox->addItem(TimelineSettingsModel::tr("None")); + for (const auto &animation : + timelineSettingsModel->timelineView()->getAnimations(qmlTimeline)) { + if (!animation.id().isEmpty()) + comboBox->addItem(animation.id()); + } + } break; + case TimelineSettingsModel::FixedFrameRow: { + } break; + + default: + qWarning() << "TimelineEditorDelegate::createEditor column" << index.column(); + } + + if (comboBox) { + connect(comboBox, QOverload<int>::of(&QComboBox::activated), this, [=]() { + auto delegate = const_cast<TimelineEditorDelegate *>(this); + emit delegate->commitData(comboBox); + }); + } + + return widget; +} + +TimelineSettingsModel::TimelineSettingsModel(QObject *parent, TimelineView *view) + : QStandardItemModel(parent) + , m_timelineView(view) +{ + connect(this, &QStandardItemModel::dataChanged, this, &TimelineSettingsModel::handleDataChanged); +} + +void TimelineSettingsModel::resetModel() +{ + beginResetModel(); + clear(); + setHorizontalHeaderLabels( + QStringList({tr("State"), tr("Timeline"), tr("Animation"), tr("Fixed Frame")})); + + if (timelineView()->isAttached() && timelineView()->rootModelNode().hasId()) { + addState(ModelNode()); + for (const QmlModelState &state : + QmlItemNode(timelineView()->rootModelNode()).states().allStates()) + addState(state); + } + + endResetModel(); +} + +TimelineView *TimelineSettingsModel::timelineView() const +{ + return m_timelineView; +} + +void TimelineSettingsModel::setupDelegates(QAbstractItemView *view) +{ + view->setItemDelegate(new TimelineEditorDelegate); +} + +static int propertyValueForState(const ModelNode &modelNode, + QmlModelState state, + const PropertyName &propertyName) +{ + if (!modelNode.isValid()) + return -1; + + if (state.isBaseState()) { + if (modelNode.hasVariantProperty(propertyName)) + return modelNode.variantProperty(propertyName).value().toInt(); + return -1; + } + + if (state.hasPropertyChanges(modelNode)) { + QmlPropertyChanges propertyChanges(state.propertyChanges(modelNode)); + if (propertyChanges.modelNode().hasVariantProperty(propertyName)) + return propertyChanges.modelNode().variantProperty(propertyName).value().toInt(); + } + + return -1; +} + +static QStandardItem *createStateItem(const ModelNode &state) +{ + if (state.isValid()) + return new QStandardItem(state.variantProperty("name").value().toString()); + else + return new QStandardItem(TimelineSettingsModel::tr("Base State")); +} + +void TimelineSettingsModel::addState(const ModelNode &state) +{ + QList<QStandardItem *> items; + + QmlTimeline timeline = timelineView()->timelineForState(state); + const QString timelineId = timeline.isValid() ? timeline.modelNode().id() : QString(""); + ModelNode animation = animationForTimelineAndState(timeline, state); + const QString animationId = animation.isValid() ? animation.id() : QString(""); + + QStandardItem *stateItem = createStateItem(state); + auto *timelinelItem = new QStandardItem(timelineId); + auto *animationItem = new QStandardItem(animationId); + auto *fixedFrameItem = new QStandardItem(""); + + stateItem->setData(state.internalId()); + stateItem->setFlags(Qt::ItemIsEnabled); + + int fixedValue = propertyValueForState(timeline, state, "currentFrame"); + fixedFrameItem->setData(fixedValue, Qt::EditRole); + + items.append(stateItem); + items.append(timelinelItem); + items.append(animationItem); + items.append(fixedFrameItem); + + appendRow(items); +} + +void TimelineSettingsModel::handleException() +{ + QMessageBox::warning(nullptr, tr("Error"), m_exceptionError); + resetModel(); +} + +ModelNode TimelineSettingsModel::animationForTimelineAndState(const QmlTimeline &timeline, + const ModelNode &state) +{ + QmlModelState modelState(state); + + if (!timeline.isValid()) + return ModelNode(); + + const QList<ModelNode> &animations = timelineView()->getAnimations(timeline); + + if (modelState.isBaseState()) { + for (const auto &animation : animations) { + if (animation.hasVariantProperty("running") + && animation.variantProperty("running").value().toBool()) + return animation; + } + return ModelNode(); + } + + for (const auto &animation : animations) { + if (modelState.affectsModelNode(animation)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(animation)); + + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("running") + && propertyChanges.modelNode().variantProperty("running").value().toBool()) + return animation; + } + } + return ModelNode(); +} + +void TimelineSettingsModel::updateTimeline(int row) +{ + + timelineView()->executeInTransaction("TimelineSettingsModel::updateTimeline", [this, row](){ + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + ModelNode animation(animationForRow(row)); + QmlTimeline oldTimeline = timelineView()->timelineForState(modelState); + + if (modelState.isBaseState()) { + if (oldTimeline.isValid()) + oldTimeline.modelNode().variantProperty("enabled").setValue(false); + if (timeline.isValid()) + timeline.modelNode().variantProperty("enabled").setValue(true); + } else { + if (oldTimeline.isValid() && modelState.affectsModelNode(oldTimeline)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(oldTimeline)); + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("enabled")) + propertyChanges.modelNode().removeProperty("enabled"); + } + + QmlTimeline baseTimeline(timelineForRow(0)); + + if (baseTimeline.isValid()) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(baseTimeline)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("enabled").setValue(false); + } + + if (timeline.isValid()) { /* If timeline is invalid 'none' was selected */ + QmlPropertyChanges propertyChanges(modelState.propertyChanges(timeline)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("enabled").setValue(true); + } + } + }); + + resetRow(row); +} + +void TimelineSettingsModel::updateAnimation(int row) +{ + timelineView()->executeInTransaction("TimelineSettingsModel::updateAnimation", [this, row](){ + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + ModelNode animation(animationForRow(row)); + QmlTimeline oldTimeline = timelineView()->timelineForState(modelState); + ModelNode oldAnimation = animationForTimelineAndState(oldTimeline, modelState); + + if (modelState.isBaseState()) { + if (oldAnimation.isValid()) + oldAnimation.variantProperty("running").setValue(false); + if (animation.isValid()) + animation.variantProperty("running").setValue(true); + if (timeline.isValid() && timeline.modelNode().hasProperty("currentFrame")) + timeline.modelNode().removeProperty("currentFrame"); + } else { + if (oldAnimation.isValid() && modelState.affectsModelNode(oldAnimation)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(oldAnimation)); + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("running")) + propertyChanges.modelNode().removeProperty("running"); + } + + ModelNode baseAnimation(animationForRow(0)); + + if (baseAnimation.isValid()) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(baseAnimation)); + if (propertyChanges.isValid()) { + propertyChanges.modelNode().variantProperty("running").setValue(false); + if (propertyChanges.modelNode().hasProperty("currentFrame")) + propertyChanges.modelNode().removeProperty("currentFrame"); + } + } + + if (animation.isValid()) { /* If animation is invalid 'none' was selected */ + QmlPropertyChanges propertyChanges(modelState.propertyChanges(animation)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("running").setValue(true); + } + } + }); + resetRow(row); +} + +void TimelineSettingsModel::updateFixedFrameRow(int row) +{ + timelineView()->executeInTransaction("TimelineSettingsModel::updateFixedFrameRow", [this, row](){ + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + + ModelNode animation = animationForTimelineAndState(timeline, modelState); + + int fixedFrame = fixedFrameForRow(row); + + if (modelState.isBaseState()) { + if (animation.isValid()) + animation.variantProperty("running").setValue(false); + if (timeline.isValid()) + timeline.modelNode().variantProperty("currentFrame").setValue(fixedFrame); + } else { + if (animation.isValid() && modelState.affectsModelNode(animation)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(animation)); + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("running")) + propertyChanges.modelNode().removeProperty("running"); + } + + QmlPropertyChanges propertyChanges(modelState.propertyChanges(timeline)); + if (propertyChanges.isValid()) + propertyChanges.modelNode().variantProperty("currentFrame").setValue(fixedFrame); + } + + }); + + resetRow(row); +} + +void TimelineSettingsModel::resetRow(int row) +{ + m_lock = true; + QStandardItem *animationItem = item(row, AnimationRow); + QStandardItem *fixedFrameItem = item(row, FixedFrameRow); + + QmlModelState modelState(stateForRow(row)); + QmlTimeline timeline(timelineForRow(row)); + ModelNode animation = animationForTimelineAndState(timeline, modelState); + + if (animationItem) { + const QString animationId = animation.isValid() ? animation.id() : QString(); + animationItem->setText(animationId); + } + + if (fixedFrameItem) { + int fixedValue = propertyValueForState(timeline, modelState, "currentFrame"); + if (fixedFrameItem->data(Qt::EditRole).toInt() != fixedValue) + fixedFrameItem->setData(fixedValue, Qt::EditRole); + } + + m_lock = false; +} + +QmlTimeline TimelineSettingsModel::timelineForRow(int row) const +{ + QStandardItem *standardItem = item(row, TimelineRow); + + if (standardItem) + return QmlTimeline(timelineView()->modelNodeForId(standardItem->text())); + + return QmlTimeline(); +} + +ModelNode TimelineSettingsModel::animationForRow(int row) const +{ + QStandardItem *standardItem = item(row, AnimationRow); + + if (standardItem) + return timelineView()->modelNodeForId(standardItem->text()); + + return ModelNode(); +} + +ModelNode TimelineSettingsModel::stateForRow(int row) const +{ + QStandardItem *standardItem = item(row, StateRow); + + if (standardItem) + return timelineView()->modelNodeForInternalId(standardItem->data().toInt()); + + return ModelNode(); +} + +int TimelineSettingsModel::fixedFrameForRow(int row) const +{ + QStandardItem *standardItem = item(row, FixedFrameRow); + + if (standardItem) + return standardItem->data(Qt::EditRole).toInt(); + + return -1; +} + +void TimelineSettingsModel::handleDataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight) +{ + if (topLeft != bottomRight) { + qWarning() << "TimelineSettingsModel::handleDataChanged multi edit?"; + return; + } + + if (m_lock) + return; + + m_lock = true; + + int currentColumn = topLeft.column(); + int currentRow = topLeft.row(); + + switch (currentColumn) { + case StateRow: { + /* read only */ + } break; + case TimelineRow: { + updateTimeline(currentRow); + } break; + case AnimationRow: { + updateAnimation(currentRow); + } break; + case FixedFrameRow: { + updateFixedFrameRow(currentRow); + } break; + + default: + qWarning() << "ConnectionModel::handleDataChanged column" << currentColumn; + } + + m_lock = false; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h new file mode 100644 index 0000000000..afd4b58e1b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinesettingsmodel.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <qmltimeline.h> + +#include <QAbstractItemView> +#include <QStandardItemModel> + +namespace QmlDesigner { + +class ModelNode; +class BindingProperty; +class SignalHandlerProperty; +class VariantProperty; + +class TimelineView; + +class TimelineSettingsModel : public QStandardItemModel +{ + Q_OBJECT +public: + enum ColumnRoles { StateRow = 0, TimelineRow = 1, AnimationRow = 2, FixedFrameRow = 3 }; + TimelineSettingsModel(QObject *parent, TimelineView *view); + void resetModel(); + + TimelineView *timelineView() const; + + QStringList getSignalsForRow(int row) const; + ModelNode getTargetNodeForConnection(const ModelNode &connection) const; + + void addConnection(); + + void bindingPropertyChanged(const BindingProperty &bindingProperty); + void variantPropertyChanged(const VariantProperty &variantProperty); + + void setupDelegates(QAbstractItemView *view); + + QmlTimeline timelineForRow(int row) const; + ModelNode animationForRow(int row) const; + ModelNode stateForRow(int row) const; + int fixedFrameForRow(int row) const; + +protected: + void addState(const ModelNode &modelNode); + +private: + void handleDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void handleException(); + ModelNode animationForTimelineAndState(const QmlTimeline &timeline, const ModelNode &state); + + void updateTimeline(int row); + void updateAnimation(int row); + void updateFixedFrameRow(int row); + + void resetRow(int row); + +private: + TimelineView *m_timelineView; + bool m_lock = false; + QString m_exceptionError; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp new file mode 100644 index 0000000000..adc49a97b1 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.cpp @@ -0,0 +1,460 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinetoolbar.h" + +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" + +#include "timelinewidget.h" + +#include <designeractionmanager.h> +#include <nodelistproperty.h> +#include <theme.h> +#include <variantproperty.h> +#include <qmltimeline.h> +#include <qmltimelinekeyframegroup.h> + +#include <coreplugin/actionmanager/actionmanager.h> +#include <coreplugin/actionmanager/command.h> + +#include <utils/algorithm.h> + +#include <QApplication> +#include <QLabel> +#include <QLineEdit> +#include <QResizeEvent> +#include <QSlider> +#include <QIntValidator> + +namespace QmlDesigner { + +bool isSpacer(QObject *object) +{ + return object->property("spacer_widget").toBool(); +} + +QWidget *createSpacer() +{ + QWidget *spacer = new QWidget(); + spacer->setProperty("spacer_widget", true); + return spacer; +} + +int controlWidth(QToolBar *bar, QObject *control) +{ + QWidget *widget = nullptr; + + if (auto *action = qobject_cast<QAction *>(control)) + widget = bar->widgetForAction(action); + + if (widget == nullptr) + widget = qobject_cast<QWidget *>(control); + + if (widget) + return widget->width(); + + return 0; +} + +QAction *createAction(const Core::Id &id, + const QIcon &icon, + const QString &name, + const QKeySequence &shortcut) +{ + QString text = QString("%1 (%2)").arg(name).arg(shortcut.toString()); + + Core::Context context(TimelineConstants::C_QMLTIMELINE); + + auto *action = new QAction(icon, text); + auto *command = Core::ActionManager::registerAction(action, id, context); + command->setDefaultKeySequence(shortcut); + + return action; +} + +TimelineToolBar::TimelineToolBar(QWidget *parent) + : QToolBar(parent) + , m_grp() +{ + setContentsMargins(0, 0, 0, 0); + createLeftControls(); + createCenterControls(); + createRightControls(); +} + +void TimelineToolBar::reset() +{ + if (recording()) + m_recording->setChecked(false); +} + +bool TimelineToolBar::recording() const +{ + if (m_recording) + return m_recording->isChecked(); + + return false; +} + +int TimelineToolBar::scaleFactor() const +{ + if (m_scale) + return m_scale->value(); + return 0; +} + +QString TimelineToolBar::currentTimelineId() const +{ + return m_timelineLabel->text(); +} + +void TimelineToolBar::setCurrentState(const QString &name) +{ + if (name.isEmpty()) + m_stateLabel->setText(tr("Base State")); + else + m_stateLabel->setText(name); +} + +void TimelineToolBar::setCurrentTimeline(const QmlTimeline &timeline) +{ + if (timeline.isValid()) { + setStartFrame(timeline.startKeyframe()); + setEndFrame(timeline.endKeyframe()); + m_timelineLabel->setText(timeline.modelNode().id()); + } else { + m_timelineLabel->setText(""); + } +} + +void TimelineToolBar::setStartFrame(qreal frame) +{ + auto text = QString::number(frame, 'f', 0); + m_firstFrame->setText(text); + setupCurrentFrameValidator(); +} + +void TimelineToolBar::setCurrentFrame(qreal frame) +{ + auto text = QString::number(frame, 'f', 0); + m_currentFrame->setText(text); +} + +void TimelineToolBar::setEndFrame(qreal frame) +{ + auto text = QString::number(frame, 'f', 0); + m_lastFrame->setText(text); + setupCurrentFrameValidator(); +} + +void TimelineToolBar::setScaleFactor(int factor) +{ + const QSignalBlocker blocker(m_scale); + m_scale->setValue(factor); +} + +void TimelineToolBar::setActionEnabled(const QString &name, bool enabled) +{ + for (auto *action : actions()) + if (action->objectName() == name) + action->setEnabled(enabled); +} + +void TimelineToolBar::removeTimeline(const QmlTimeline &timeline) +{ + if (timeline.modelNode().id() == m_timelineLabel->text()) + setCurrentTimeline(QmlTimeline()); +} + +void TimelineToolBar::createLeftControls() +{ + auto addActionToGroup = [&](QAction *action) { + addAction(action); + m_grp << action; + }; + + auto addWidgetToGroup = [&](QWidget *widget) { + addWidget(widget); + m_grp << widget; + }; + + auto addSpacingToGroup = [&](int width) { + auto *widget = new QWidget; + widget->setFixedWidth(width); + addWidget(widget); + m_grp << widget; + }; + + addSpacingToGroup(5); + + auto *settingsAction = createAction(TimelineConstants::C_SETTINGS, + TimelineIcons::ANIMATION.icon(), + tr("Timeline Settings"), + QKeySequence(Qt::Key_S)); + + connect(settingsAction, &QAction::triggered, this, &TimelineToolBar::settingDialogClicked); + + addActionToGroup(settingsAction); + + addWidgetToGroup(createSpacer()); + + m_timelineLabel = new QLabel(this); + m_timelineLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft); + addWidgetToGroup(m_timelineLabel); +} + +static QLineEdit *createToolBarLineEdit(QWidget *parent) +{ + auto lineEdit = new QLineEdit(parent); + lineEdit->setStyleSheet("* { background-color: rgba(0, 0, 0, 0); }"); + lineEdit->setFixedWidth(48); + lineEdit->setAlignment(Qt::AlignCenter); + + QPalette pal = parent->palette(); + pal.setColor(QPalette::Text, Theme::instance()->color(Utils::Theme::PanelTextColorLight)); + lineEdit->setPalette(pal); + QValidator *validator = new QIntValidator(-100000, 100000, lineEdit); + lineEdit->setValidator(validator); + + return lineEdit; +} + +void TimelineToolBar::createCenterControls() +{ + addSpacing(5); + + auto *toStart = createAction(TimelineConstants::C_TO_START, + TimelineIcons::TO_FIRST_FRAME.icon(), + tr("To Start"), + QKeySequence(Qt::Key_Home)); + + connect(toStart, &QAction::triggered, this, &TimelineToolBar::toFirstFrameTriggered); + addAction(toStart); + + addSpacing(2); + + auto *previous = createAction(TimelineConstants::C_PREVIOUS, + TimelineIcons::BACK_ONE_FRAME.icon(), + tr("Previous"), + QKeySequence(Qt::Key_Comma)); + + connect(previous, &QAction::triggered, this, &TimelineToolBar::previousFrameTriggered); + addAction(previous); + + addSpacing(2); + + auto *play = createAction(TimelineConstants::C_PLAY, + TimelineIcons::START_PLAYBACK.icon(), + tr("Play"), + QKeySequence(Qt::Key_Space)); + + connect(play, &QAction::triggered, this, &TimelineToolBar::playTriggered); + addAction(play); + + addSpacing(2); + + auto *next = createAction(TimelineConstants::C_NEXT, + TimelineIcons::FORWARD_ONE_FRAME.icon(), + tr("Next"), + QKeySequence(Qt::Key_Period)); + + connect(next, &QAction::triggered, this, &TimelineToolBar::nextFrameTriggered); + addAction(next); + + addSpacing(2); + + auto *toEnd = createAction(TimelineConstants::C_TO_END, + TimelineIcons::TO_LAST_FRAME.icon(), + tr("To End"), + QKeySequence(Qt::Key_End)); + + connect(toEnd, &QAction::triggered, this, &TimelineToolBar::toLastFrameTriggered); + addAction(toEnd); + +#if 0 + auto *loop = new QAction(TimelineIcons::LOOP_PLAYBACK.icon(), tr("Loop"), this); + addAction(loop); +#endif + + addSpacing(5); + + addSeparator(); + + m_currentFrame = createToolBarLineEdit(this); + addWidget(m_currentFrame); + + auto emitCurrentChanged = [this]() { emit currentFrameChanged(m_currentFrame->text().toInt()); }; + connect(m_currentFrame, &QLineEdit::editingFinished, emitCurrentChanged); + + addSeparator(); + + addSpacing(10); + + QIcon autoKeyIcon = TimelineUtils::mergeIcons(TimelineIcons::GLOBAL_RECORD_KEYFRAMES, + TimelineIcons::GLOBAL_RECORD_KEYFRAMES_OFF); + + m_recording = createAction(TimelineConstants::C_AUTO_KEYFRAME, + autoKeyIcon, + tr("Auto Key"), + QKeySequence(Qt::Key_K)); + + m_recording->setCheckable(true); + connect(m_recording, &QAction::toggled, [&](bool value) { emit recordToggled(value); }); + + addAction(m_recording); + + addSpacing(10); + + addSeparator(); + + addSpacing(10); + + auto *curvePicker = createAction(TimelineConstants::C_CURVE_PICKER, + TimelineIcons::CURVE_EDITOR.icon(), + tr("Curve Picker"), + QKeySequence(Qt::Key_C)); + + curvePicker->setObjectName("Curve Picker"); + connect(curvePicker, &QAction::triggered, this, &TimelineToolBar::openEasingCurveEditor); + addAction(curvePicker); + + addSpacing(10); + +#if 0 + addSeparator(); + + addSpacing(10); + + auto *curveEditor = new QAction(TimelineIcons::CURVE_PICKER.icon(), tr("Curve Editor")); + addAction(curveEditor); +#endif +} + +void TimelineToolBar::createRightControls() +{ + auto *spacer = createSpacer(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + addWidget(spacer); + + addSeparator(); + + m_firstFrame = createToolBarLineEdit(this); + addWidget(m_firstFrame); + + auto emitStartChanged = [this]() { emit startFrameChanged(m_firstFrame->text().toInt()); }; + connect(m_firstFrame, &QLineEdit::editingFinished, emitStartChanged); + + addSeparator(); + + addSpacing(10); + + auto *zoomOut = createAction(TimelineConstants::C_ZOOM_OUT, + TimelineIcons::ZOOM_SMALL.icon(), + tr("Zoom Out"), + QKeySequence(QKeySequence::ZoomOut)); + + connect(zoomOut, &QAction::triggered, [this]() { + m_scale->setValue(m_scale->value() - m_scale->pageStep()); + }); + addAction(zoomOut); + + addSpacing(10); + + m_scale = new QSlider(this); + m_scale->setOrientation(Qt::Horizontal); + m_scale->setMaximumWidth(200); + m_scale->setMinimumWidth(100); + m_scale->setMinimum(0); + m_scale->setMaximum(100); + m_scale->setValue(0); + + connect(m_scale, &QSlider::valueChanged, this, &TimelineToolBar::scaleFactorChanged); + addWidget(m_scale); + + addSpacing(10); + + auto *zoomIn = createAction(TimelineConstants::C_ZOOM_IN, + TimelineIcons::ZOOM_BIG.icon(), + tr("Zoom In"), + QKeySequence(QKeySequence::ZoomIn)); + + connect(zoomIn, &QAction::triggered, [this]() { + m_scale->setValue(m_scale->value() + m_scale->pageStep()); + }); + addAction(zoomIn); + + addSpacing(10); + + addSeparator(); + + m_lastFrame = createToolBarLineEdit(this); + addWidget(m_lastFrame); + + auto emitEndChanged = [this]() { emit endFrameChanged(m_lastFrame->text().toInt()); }; + connect(m_lastFrame, &QLineEdit::editingFinished, emitEndChanged); + + addSeparator(); + + m_stateLabel = new QLabel(this); + m_stateLabel->setFixedWidth(80); + m_stateLabel->setAlignment(Qt::AlignVCenter | Qt::AlignHCenter); + addWidget(m_stateLabel); +} + +void TimelineToolBar::addSpacing(int width) +{ + auto *widget = new QWidget; + widget->setFixedWidth(width); + addWidget(widget); +} + +void TimelineToolBar::setupCurrentFrameValidator() +{ + auto validator = static_cast<const QIntValidator*>(m_currentFrame->validator()); + const_cast<QIntValidator*>(validator)->setRange(m_firstFrame->text().toInt(), m_lastFrame->text().toInt()); +} + +void TimelineToolBar::resizeEvent(QResizeEvent *event) +{ + Q_UNUSED(event) + + int width = 0; + QWidget *spacer = nullptr; + for (auto *object : m_grp) { + if (isSpacer(object)) + spacer = qobject_cast<QWidget *>(object); + else + width += controlWidth(this, object); + } + + if (spacer) { + int spacerWidth = TimelineConstants::sectionWidth - width - 12; + spacer->setFixedWidth(spacerWidth > 0 ? spacerWidth : 0); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h new file mode 100644 index 0000000000..43d42b83f9 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbar.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QToolBar> + +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QLineEdit) +QT_FORWARD_DECLARE_CLASS(QObject) +QT_FORWARD_DECLARE_CLASS(QResizeEvent) +QT_FORWARD_DECLARE_CLASS(QSlider) +QT_FORWARD_DECLARE_CLASS(QWidget) + +namespace QmlDesigner { + +class TimelineWidget; + +class QmlTimeline; + +class TimelineToolBar : public QToolBar +{ + Q_OBJECT + +signals: + void settingDialogClicked(); + //void addTimelineClicked(); + + void openEasingCurveEditor(); + + void playTriggered(); + void previousFrameTriggered(); + void nextFrameTriggered(); + void toFirstFrameTriggered(); + void toLastFrameTriggered(); + + void recordToggled(bool val); + void loopPlaybackToggled(bool val); + + void scaleFactorChanged(int value); + void startFrameChanged(int value); + void currentFrameChanged(int value); + void endFrameChanged(int value); + +public: + explicit TimelineToolBar(QWidget *parent = nullptr); + + void reset(); + + bool recording() const; + int scaleFactor() const; + QString currentTimelineId() const; + + void setCurrentState(const QString &name); + void setCurrentTimeline(const QmlTimeline &timeline); + void setStartFrame(qreal frame); + void setCurrentFrame(qreal frame); + void setEndFrame(qreal frame); + void setScaleFactor(int factor); + + void setActionEnabled(const QString &name, bool enabled); + void removeTimeline(const QmlTimeline &timeline); + +protected: + void resizeEvent(QResizeEvent *event) override; + +private: + void createLeftControls(); + void createCenterControls(); + void createRightControls(); + void addSpacing(int width); + void setupCurrentFrameValidator(); + + QList<QObject *> m_grp; + + QLabel *m_timelineLabel = nullptr; + QLabel *m_stateLabel = nullptr; + QSlider *m_scale = nullptr; + QLineEdit *m_firstFrame = nullptr; + QLineEdit *m_currentFrame = nullptr; + QLineEdit *m_lastFrame = nullptr; + + QAction *m_recording = nullptr; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp new file mode 100644 index 0000000000..e6a9454186 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.cpp @@ -0,0 +1,164 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinetoolbutton.h" + +#include "timelineconstants.h" + +#include <QGraphicsSceneMouseEvent> +#include <QPainter> + +#include <utils/stylehelper.h> +#include <utils/theme/theme.h> + +#include <QAction> +#include <QDebug> + +namespace QmlDesigner { + +TimelineToolButton::TimelineToolButton(QAction *action, QGraphicsItem *parent) + : QGraphicsWidget(parent) + , m_action(action) +{ + resize(TimelineConstants::toolButtonSize, TimelineConstants::toolButtonSize); + setPreferredSize(size()); + setAcceptHoverEvents(true); + connect(action, &QAction::changed, this, [this]() { + setVisible(m_action->isVisible()); + update(); + }); + + connect(this, &TimelineToolButton::clicked, action, &QAction::trigger); + + setEnabled(m_action->isEnabled()); + setVisible(m_action->isVisible()); + setCursor(Qt::ArrowCursor); +} + +void TimelineToolButton::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) +{ + painter->save(); + + if (m_state == Normal) + setOpacity(0.8); + else if (m_state == Pressed) + setOpacity(0.3); + else + setOpacity(1.0); + + if (!isEnabled()) + setOpacity(0.5); + + if (isCheckable()) { + if (isChecked() || isDisabled()) + m_action->icon().paint(painter, + rect().toRect(), + Qt::AlignCenter, + QIcon::Normal, + QIcon::On); + else + m_action->icon().paint(painter, + rect().toRect(), + Qt::AlignCenter, + QIcon::Normal, + QIcon::Off); + } else + m_action->icon().paint(painter, rect().toRect()); + + painter->restore(); +} + +QRectF TimelineToolButton::boundingRect() const +{ + return QRectF(0, 0, TimelineConstants::toolButtonSize, TimelineConstants::toolButtonSize); +} + +bool TimelineToolButton::isCheckable() const +{ + return m_action->isCheckable(); +} + +bool TimelineToolButton::isChecked() const +{ + return m_action->isChecked(); +} + +bool TimelineToolButton::isDisabled() const +{ + return !m_action->isEnabled(); +} + +void TimelineToolButton::setChecked(bool b) +{ + m_action->setChecked(b); + update(); +} + +void TimelineToolButton::setDisabled(bool b) +{ + m_action->setDisabled(b); +} + +void TimelineToolButton::hoverEnterEvent(QGraphicsSceneHoverEvent *event) +{ + m_state = Hovered; + + QGraphicsObject::hoverEnterEvent(event); + event->accept(); + update(); +} + +void TimelineToolButton::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) +{ + m_state = Normal; + + QGraphicsWidget::hoverLeaveEvent(event); + event->accept(); + update(); +} + +void TimelineToolButton::hoverMoveEvent(QGraphicsSceneHoverEvent *event) +{ + m_state = Hovered; + QGraphicsWidget::hoverMoveEvent(event); + update(); +} + +void TimelineToolButton::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + m_state = Pressed; + event->accept(); + update(); +} + +void TimelineToolButton::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + m_state = Normal; + + event->accept(); + emit clicked(); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h new file mode 100644 index 0000000000..cdf024c0b2 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetoolbutton.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <QGraphicsWidget> +#include <QIcon> + +QT_BEGIN_NAMESPACE +class QAction; +QT_END_NAMESPACE + +namespace QmlDesigner { + +class TimelineToolButton : public QGraphicsWidget +{ + Q_OBJECT + +public: + explicit TimelineToolButton(QAction *action, QGraphicsItem *parent = nullptr); + void paint(QPainter *painter, + const QStyleOptionGraphicsItem *option, + QWidget *widget = nullptr) override; + QRectF boundingRect() const override; + + bool isCheckable() const; + bool isChecked() const; + bool isDisabled() const; + void setChecked(bool b); + void setDisabled(bool b); + +signals: + void clicked(); + +protected: + void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + +private: + enum State { Pressed, Hovered, Normal }; + + State m_state = Normal; + QAction *m_action; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp new file mode 100644 index 0000000000..51c5a7d088 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.cpp @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinetooldelegate.h" + +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelinemovableabstractitem.h" +#include "timelinemovetool.h" +#include "timelineselectiontool.h" + +#include <QGraphicsSceneMouseEvent> + +#include "timelinepropertyitem.h" +#include <QDebug> + +namespace QmlDesigner { + +TimelineToolDelegate::TimelineToolDelegate(TimelineGraphicsScene *scene) + : m_scene(scene) + , m_start() + , m_moveTool(new TimelineMoveTool(scene, this)) + , m_selectTool(new TimelineSelectionTool(scene, this)) +{} + +QPointF TimelineToolDelegate::startPoint() const +{ + return m_start; +} + +TimelineMovableAbstractItem *TimelineToolDelegate::item() const +{ + return m_item; +} + +void TimelineToolDelegate::mousePressEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (event->buttons() == Qt::LeftButton && hitCanvas(event)) { + m_start = event->scenePos(); + + if (item) { + setItem(item, event->modifiers()); + m_currentTool = m_moveTool.get(); + } else + m_currentTool = m_selectTool.get(); + } else + m_currentTool = nullptr; + + if (m_currentTool) + m_currentTool->mousePressEvent(item, event); +} + +void TimelineToolDelegate::mouseMoveEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (m_currentTool) + m_currentTool->mouseMoveEvent(item, event); +} + +void TimelineToolDelegate::mouseReleaseEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (m_currentTool) + m_currentTool->mouseReleaseEvent(item, event); + + reset(); +} + +void TimelineToolDelegate::mouseDoubleClickEvent(TimelineMovableAbstractItem *item, + QGraphicsSceneMouseEvent *event) +{ + if (m_currentTool) + m_currentTool->mouseDoubleClickEvent(item, event); + + reset(); +} + +void TimelineToolDelegate::clearSelection() +{ + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(m_item)) + keyframe->setHighlighted(false); + + m_item = nullptr; +} + +void TimelineToolDelegate::setItem(TimelineMovableAbstractItem *item, + const Qt::KeyboardModifiers &modifiers) +{ + if (item) { + setItem(nullptr); + + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(item)) { + if (modifiers.testFlag(Qt::ControlModifier)) { + if (m_scene->isKeyframeSelected(keyframe)) + m_scene->selectKeyframes(SelectionMode::Remove, {keyframe}); + else + m_scene->selectKeyframes(SelectionMode::Add, {keyframe}); + } else { + if (!m_scene->isKeyframeSelected(keyframe)) + m_scene->selectKeyframes(SelectionMode::New, {keyframe}); + } + } + + } else if (m_item) { + if (auto *keyframe = TimelineMovableAbstractItem::asTimelineKeyframeItem(m_item)) + if (!m_scene->isKeyframeSelected(keyframe)) + keyframe->setHighlighted(false); + } + + m_item = item; +} + +bool TimelineToolDelegate::hitCanvas(QGraphicsSceneMouseEvent *event) +{ + return event->scenePos().x() > TimelineConstants::sectionWidth; +} + +void TimelineToolDelegate::reset() +{ + setItem(nullptr); + + m_currentTool = nullptr; + + m_start = QPointF(); +} + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h new file mode 100644 index 0000000000..f945c1a61b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinetooldelegate.h @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelinemovetool.h" +#include "timelineselectiontool.h" + +#include <memory> + +namespace QmlDesigner +{ + +class TimelineGraphicsScene; + +class TimelineToolDelegate +{ +public: + TimelineToolDelegate(TimelineGraphicsScene* scene); + + QPointF startPoint() const; + + TimelineMovableAbstractItem* item() const; + +public: + void mousePressEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void mouseMoveEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void mouseReleaseEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void mouseDoubleClickEvent(TimelineMovableAbstractItem *item, QGraphicsSceneMouseEvent *event); + + void clearSelection(); + +private: + bool hitCanvas(QGraphicsSceneMouseEvent *event); + + void reset(); + + void setItem(TimelineMovableAbstractItem *item, const Qt::KeyboardModifiers& modifiers = Qt::NoModifier); + +private: + static const int dragDistance = 20; + + TimelineGraphicsScene* m_scene; + + QPointF m_start; + + TimelineMovableAbstractItem *m_item = nullptr; + + std::unique_ptr< TimelineMoveTool > m_moveTool; + + std::unique_ptr< TimelineSelectionTool > m_selectTool; + + TimelineAbstractTool *m_currentTool = nullptr; +}; + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp new file mode 100644 index 0000000000..6873fc0b65 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.cpp @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineutils.h" + +#include <QEvent> + +namespace QmlDesigner { + +namespace TimelineUtils { + +DisableContextMenu::DisableContextMenu(QObject *parent) + : QObject(parent) +{} + +bool DisableContextMenu::eventFilter(QObject *watched, QEvent *event) +{ + if (event->type() == QEvent::ContextMenu) + return true; + + return QObject::eventFilter(watched, event); +} + +} // End namespace TimelineUtils. + +} // End namespace QmlDesigner. diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineutils.h b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.h new file mode 100644 index 0000000000..0758733769 --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineutils.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <istream> +#include <utils/icon.h> +#include <vector> +#include <QDataStream> +#include <QObject> + +QT_FORWARD_DECLARE_CLASS(QEvent) + +namespace QmlDesigner { + +namespace TimelineUtils { + +enum class Side { Top, Right, Bottom, Left }; + +template<typename T> +inline T clamp(const T &value, const T &lo, const T &hi) +{ + return value < lo ? lo : value > hi ? hi : value; +} + +template<typename T> +inline T lerp(const T &blend, const T &lhs, const T &rhs) +{ + static_assert(std::is_floating_point<T>::value, + "TimelineUtils::lerp: For floating point types only!"); + return blend * lhs + (1.0 - blend) * rhs; +} + +template<typename T> +inline T reverseLerp(const T &val, const T &lhs, const T &rhs) +{ + static_assert(std::is_floating_point<T>::value, + "TimelineUtils::reverseLerp: For floating point types only!"); + return (val - rhs) / (lhs - rhs); +} + +inline QIcon mergeIcons(const Utils::Icon &on, const Utils::Icon &off) +{ + QIcon out; + out.addPixmap(on.pixmap(), QIcon::Normal, QIcon::On); + out.addPixmap(off.pixmap(), QIcon::Normal, QIcon::Off); + return out; +} + +class DisableContextMenu : public QObject +{ + Q_OBJECT + +public: + explicit DisableContextMenu(QObject *parent = nullptr); + + bool eventFilter(QObject *watched, QEvent *event) override; +}; + +} // End namespace TimelineUtils. + +template<typename T> +inline std::istream &operator>>(std::istream &stream, std::vector<T> &vec) +{ + quint64 s; + stream >> s; + + vec.clear(); + vec.reserve(s); + + T val; + for (quint64 i = 0; i < s; ++i) { + stream >> val; + vec.push_back(val); + } + return stream; +} + +template<typename T> +inline QDataStream &operator<<(QDataStream &stream, const std::vector<T> &vec) +{ + stream << static_cast<quint64>(vec.size()); + for (const auto &elem : vec) + stream << elem; + + return stream; +} + +template<typename T> +inline QDataStream &operator>>(QDataStream &stream, std::vector<T> &vec) +{ + quint64 s; + stream >> s; + + vec.clear(); + vec.reserve(s); + + T val; + for (quint64 i = 0; i < s; ++i) { + stream >> val; + vec.push_back(val); + } + return stream; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp new file mode 100644 index 0000000000..f39a17db2f --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineview.cpp @@ -0,0 +1,610 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelineview.h" + +#include "easingcurve.h" +#include "timelineactions.h" +#include "timelineconstants.h" +#include "timelinecontext.h" +#include "timelinewidget.h" + +#include "timelinegraphicsscene.h" +#include "timelinesettingsdialog.h" +#include "timelinetoolbar.h" + +#include <bindingproperty.h> +#include <exception.h> +#include <modelnodecontextmenu_helper.h> +#include <nodeabstractproperty.h> +#include <nodelistproperty.h> +#include <nodemetainfo.h> +#include <rewritertransaction.h> +#include <variantproperty.h> +#include <qmldesignericons.h> +#include <qmldesignerplugin.h> +#include <qmlitemnode.h> +#include <qmlobjectnode.h> +#include <qmlstate.h> +#include <qmltimeline.h> +#include <qmltimelinekeyframegroup.h> +#include <viewmanager.h> + +#include <coreplugin/icore.h> + +#include <utils/qtcassert.h> + +#include <designmodecontext.h> + +#include <utils/algorithm.h> +#include <utils/qtcassert.h> + +#include <QTimer> + +namespace QmlDesigner { + +TimelineView::TimelineView(QObject *parent) + : AbstractView(parent) +{ + EasingCurve::registerStreamOperators(); +} + +TimelineView::~TimelineView() = default; + +void TimelineView::modelAttached(Model *model) +{ + AbstractView::modelAttached(model); + if (m_timelineWidget) + m_timelineWidget->init(); +} + +void TimelineView::modelAboutToBeDetached(Model *model) +{ + m_timelineWidget->reset(); + setTimelineRecording(false); + AbstractView::modelAboutToBeDetached(model); +} + +void TimelineView::nodeCreated(const ModelNode & /*createdNode*/) {} + +void TimelineView::nodeAboutToBeRemoved(const ModelNode &removedNode) +{ + if (removedNode.isValid()) { + if (QmlTimeline::isValidQmlTimeline(removedNode)) { + auto *toolBar = widget()->toolBar(); + + QString lastId = toolBar->currentTimelineId(); + toolBar->removeTimeline(QmlTimeline(removedNode)); + QString currentId = toolBar->currentTimelineId(); + + removedNode.setAuxiliaryData("removed@Internal", true); + + if (currentId.isEmpty()) + m_timelineWidget->graphicsScene()->clearTimeline(); + if (lastId != currentId) + m_timelineWidget->setTimelineId(currentId); + } else if (removedNode.parentProperty().isValid() + && QmlTimeline::isValidQmlTimeline(removedNode.parentProperty().parentModelNode())) { + if (removedNode.hasBindingProperty("target")) { + const ModelNode target = removedNode.bindingProperty("target").resolveToModelNode(); + if (target.isValid()) { + QmlTimeline timeline(removedNode.parentProperty().parentModelNode()); + if (timeline.hasKeyframeGroupForTarget(target)) + QTimer::singleShot(0, [this, target, timeline]() { + if (timeline.hasKeyframeGroupForTarget(target)) + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(target); + else + m_timelineWidget->graphicsScene()->invalidateScene(); + }); + } + } + } + } +} + +void TimelineView::nodeRemoved(const ModelNode & /*removedNode*/, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags /*propertyChange*/) +{ + if (parentProperty.isValid() + && QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup( + parentProperty.parentModelNode())) { + QmlTimelineKeyframeGroup frames(parentProperty.parentModelNode()); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } +} + +void TimelineView::nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty & /*oldPropertyParent*/, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + if (newPropertyParent.isValid() + && QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup( + newPropertyParent.parentModelNode())) { + QmlTimelineKeyframeGroup frames(newPropertyParent.parentModelNode()); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } else if (QmlTimelineKeyframeGroup::checkKeyframesType( + node)) { /* During copy and paste type info might be incomplete */ + QmlTimelineKeyframeGroup frames(node); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } +} + +void TimelineView::instancePropertyChanged(const QList<QPair<ModelNode, PropertyName>> &propertyList) +{ + QmlTimeline timeline = currentTimeline(); + bool updated = false; + for (const auto &pair : propertyList) { + if (pair.second == "startFrame" || pair.second == "endFrame") { + if (QmlTimeline::isValidQmlTimeline(pair.first)) { + m_timelineWidget->invalidateTimelineDuration(pair.first); + } + } else if (pair.second == "currentFrame") { + if (QmlTimeline::isValidQmlTimeline(pair.first)) { + m_timelineWidget->invalidateTimelinePosition(pair.first); + } + } else if (!updated && timeline.hasTimeline(pair.first, pair.second)) { + m_timelineWidget->graphicsScene()->invalidateCurrentValues(); + updated = true; + } + } +} + +void TimelineView::variantPropertiesChanged(const QList<VariantProperty> &propertyList, + AbstractView::PropertyChangeFlags /*propertyChange*/) +{ + for (const auto &property : propertyList) { + if (property.name() == "frame" + && property.parentModelNode().type() == "QtQuick.Timeline.Keyframe" + && property.parentModelNode().isValid() + && property.parentModelNode().hasParentProperty()) { + const ModelNode framesNode + = property.parentModelNode().parentProperty().parentModelNode(); + if (QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup(framesNode)) { + QmlTimelineKeyframeGroup frames(framesNode); + m_timelineWidget->graphicsScene()->invalidateKeyframesForTarget(frames.target()); + } + } + } +} + +void TimelineView::selectedNodesChanged(const QList<ModelNode> & /*selectedNodeList*/, + const QList<ModelNode> & /*lastSelectedNodeList*/) +{ + if (m_timelineWidget) + m_timelineWidget->graphicsScene()->update(); +} + +void TimelineView::propertiesAboutToBeRemoved(const QList<AbstractProperty> &propertyList) +{ + for (const auto &property : propertyList) { + if (property.isNodeListProperty()) { + for (const auto &node : property.toNodeListProperty().toModelNodeList()) { + nodeAboutToBeRemoved(node); + } + } + } +} + +void TimelineView::propertiesRemoved(const QList<AbstractProperty> &propertyList) +{ + for (const auto &property : propertyList) { + if (property.name() == "keyframes" && property.parentModelNode().isValid()) { + if (QmlTimelineKeyframeGroup::isValidQmlTimelineKeyframeGroup( + property.parentModelNode())) { + QmlTimelineKeyframeGroup frames(property.parentModelNode()); + m_timelineWidget->graphicsScene()->invalidateSectionForTarget(frames.target()); + } else if (QmlTimeline::isValidQmlTimeline(property.parentModelNode())) { + m_timelineWidget->graphicsScene()->invalidateScene(); + } + } + } +} + +bool TimelineView::hasWidget() const +{ + return true; +} + +void TimelineView::nodeIdChanged(const ModelNode &node, const QString &, const QString &) +{ + if (QmlTimeline::isValidQmlTimeline(node)) + m_timelineWidget->init(); +} + +void TimelineView::currentStateChanged(const ModelNode &) +{ + if (m_timelineWidget) + m_timelineWidget->init(); +} + +TimelineWidget *TimelineView::widget() const +{ + return m_timelineWidget; +} + +const QmlTimeline TimelineView::addNewTimeline() +{ + const TypeName timelineType = "QtQuick.Timeline.Timeline"; + + QTC_ASSERT(isAttached(), return QmlTimeline()); + + try { + ensureQtQuickTimelineImport(); + } catch (const Exception &e) { + e.showException(); + } + + NodeMetaInfo metaInfo = model()->metaInfo(timelineType); + + QTC_ASSERT(metaInfo.isValid(), return QmlTimeline()); + + ModelNode timelineNode; + + executeInTransaction("TimelineView::addNewTimeline", [=, &timelineNode](){ + bool hasTimelines = getTimelines().isEmpty(); + + timelineNode = createModelNode(timelineType, + metaInfo.majorVersion(), + metaInfo.minorVersion()); + timelineNode.validId(); + + timelineNode.variantProperty("startFrame").setValue(0); + timelineNode.variantProperty("endFrame").setValue(1000); + timelineNode.variantProperty("enabled").setValue(hasTimelines); + + rootModelNode().defaultNodeListProperty().reparentHere(timelineNode); + }); + + return QmlTimeline(timelineNode); +} + +ModelNode TimelineView::addAnimation(QmlTimeline timeline) +{ + const TypeName animationType = "QtQuick.Timeline.TimelineAnimation"; + + QTC_ASSERT(timeline.isValid(), return ModelNode()); + + QTC_ASSERT(isAttached(), return ModelNode()); + + NodeMetaInfo metaInfo = model()->metaInfo(animationType); + + QTC_ASSERT(metaInfo.isValid(), return ModelNode()); + + ModelNode animationNode; + + executeInTransaction("TimelineView::addAnimation", [=, &animationNode](){ + animationNode = createModelNode(animationType, + metaInfo.majorVersion(), + metaInfo.minorVersion()); + animationNode.variantProperty("duration").setValue(timeline.duration()); + animationNode.validId(); + + animationNode.variantProperty("from").setValue(timeline.startKeyframe()); + animationNode.variantProperty("to").setValue(timeline.endKeyframe()); + + animationNode.variantProperty("loops").setValue(1); + + animationNode.variantProperty("running").setValue(getAnimations(timeline).isEmpty()); + + timeline.modelNode().nodeListProperty("animations").reparentHere(animationNode); + + if (timeline.modelNode().hasProperty("currentFrame")) + timeline.modelNode().removeProperty("currentFrame"); + }); + + return animationNode; +} + +void TimelineView::addNewTimelineDialog() +{ + auto timeline = addNewTimeline(); + addAnimation(timeline); + openSettingsDialog(); +} + +void TimelineView::openSettingsDialog() +{ + auto dialog = new TimelineSettingsDialog(Core::ICore::dialogParent(), this); + + auto timeline = m_timelineWidget->graphicsScene()->currentTimeline(); + if (timeline.isValid()) + dialog->setCurrentTimeline(timeline); + + QObject::connect(dialog, &TimelineSettingsDialog::rejected, [this, dialog]() { + m_timelineWidget->init(); + dialog->deleteLater(); + }); + + QObject::connect(dialog, &TimelineSettingsDialog::accepted, [this, dialog]() { + m_timelineWidget->init(); + dialog->deleteLater(); + }); + + dialog->show(); +} + +void TimelineView::setTimelineRecording(bool value) +{ + ModelNode node = widget()->graphicsScene()->currentTimeline(); + + QTC_ASSERT(node.isValid(), return ); + + if (value) { + activateTimelineRecording(node); + } else { + deactivateTimelineRecording(); + activateTimeline(node); + } +} + +void TimelineView::customNotification(const AbstractView * /*view*/, + const QString &identifier, + const QList<ModelNode> &nodeList, + const QList<QVariant> &data) +{ + if (identifier == QStringLiteral("reset QmlPuppet")) { + QmlTimeline timeline = widget()->graphicsScene()->currentTimeline(); + if (timeline.isValid()) + timeline.modelNode().removeAuxiliaryData("currentFrame@NodeInstance"); + } else if (identifier == "INSERT_KEYFRAME" && !nodeList.isEmpty() && !data.isEmpty()) { + insertKeyframe(nodeList.constFirst(), data.constFirst().toString().toUtf8()); + } +} + +void TimelineView::insertKeyframe(const ModelNode &target, const PropertyName &propertyName) +{ + QmlTimeline timeline = widget()->graphicsScene()->currentTimeline(); + ModelNode targetNode = target; + if (timeline.isValid() && targetNode.isValid() + && QmlObjectNode::isValidQmlObjectNode(targetNode)) { + executeInTransaction("TimelineView::insertKeyframe", [=, &timeline, &targetNode](){ + + targetNode.validId(); + + QmlTimelineKeyframeGroup timelineFrames( + timeline.keyframeGroup(targetNode, propertyName)); + + QTC_ASSERT(timelineFrames.isValid(), return ); + + const qreal frame + = timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal(); + const QVariant value = QmlObjectNode(targetNode).instanceValue(propertyName); + + timelineFrames.setValue(value, frame); + + }); + } +} + +QList<QmlTimeline> TimelineView::getTimelines() const +{ + QList<QmlTimeline> timelines; + + if (!isAttached()) + return timelines; + + for (const ModelNode &modelNode : allModelNodes()) { + if (QmlTimeline::isValidQmlTimeline(modelNode) && !modelNode.hasAuxiliaryData("removed@Internal")) { + timelines.append(modelNode); + } + } + return timelines; +} + +QList<ModelNode> TimelineView::getAnimations(const QmlTimeline &timeline) +{ + if (!timeline.isValid()) + return QList<ModelNode>(); + + if (isAttached()) { + return Utils::filtered(timeline.modelNode().directSubModelNodes(), + [timeline](const ModelNode &node) { + if (node.metaInfo().isValid() && node.hasParentProperty() + && (node.parentProperty().parentModelNode() + == timeline.modelNode())) + return node.metaInfo().isSubclassOf( + "QtQuick.Timeline.TimelineAnimation"); + return false; + }); + } + return {}; +} + +QmlTimeline TimelineView::timelineForState(const ModelNode &state) const +{ + QmlModelState modelState(state); + + const QList<QmlTimeline> &timelines = getTimelines(); + + if (modelState.isBaseState()) { + for (const auto &timeline : timelines) { + if (timeline.modelNode().hasVariantProperty("enabled") + && timeline.modelNode().variantProperty("enabled").value().toBool()) + return timeline; + } + return QmlTimeline(); + } + + for (const auto &timeline : timelines) { + if (modelState.affectsModelNode(timeline)) { + QmlPropertyChanges propertyChanges(modelState.propertyChanges(timeline)); + + if (propertyChanges.isValid() && propertyChanges.modelNode().hasProperty("enabled") + && propertyChanges.modelNode().variantProperty("enabled").value().toBool()) + return timeline; + } + } + return QmlTimeline(); +} + +QmlModelState TimelineView::stateForTimeline(const QmlTimeline &timeline) +{ + if (timeline.modelNode().hasVariantProperty("enabled") + && timeline.modelNode().variantProperty("enabled").value().toBool()) { + return QmlModelState(rootModelNode()); + } + + for (const QmlModelState &state : QmlItemNode(rootModelNode()).states().allStates()) { + if (timelineForState(state) == timeline) + return state; + } + + return QmlModelState(); +} + +void TimelineView::registerActions() +{ + auto &actionManager = QmlDesignerPlugin::instance()->viewManager().designerActionManager(); + + SelectionContextPredicate timelineEnabled = [this](const SelectionContext &context) { + return context.singleNodeIsSelected() + && widget()->graphicsScene()->currentTimeline().isValid(); + }; + + SelectionContextPredicate timelineHasKeyframes = + [this](const SelectionContext &context) { + auto timeline = widget()->graphicsScene()->currentTimeline(); + return !timeline.keyframeGroupsForTarget(context.currentSingleSelectedNode()).isEmpty(); + }; + + SelectionContextPredicate timelineHasClipboard = [](const SelectionContext &context) { + return !context.fastUpdate() && TimelineActions::clipboardContainsKeyframes(); + }; + + SelectionContextOperation deleteKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::deleteAllKeyframesForTarget(context.currentSingleSelectedNode(), + mutator); + }; + + SelectionContextOperation insertKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::insertAllKeyframesForTarget(context.currentSingleSelectedNode(), + mutator); + }; + + SelectionContextOperation copyKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::copyAllKeyframesForTarget(context.currentSingleSelectedNode(), mutator); + }; + + SelectionContextOperation pasteKeyframes = [this](const SelectionContext &context) { + auto mutator = widget()->graphicsScene()->currentTimeline(); + if (mutator.isValid()) + TimelineActions::pasteKeyframesToTarget(context.currentSingleSelectedNode(), mutator); + }; + + actionManager.addDesignerAction(new ActionGroup(TimelineConstants::timelineCategoryDisplayName, + TimelineConstants::timelineCategory, + TimelineConstants::priorityTimelineCategory, + timelineEnabled, + &SelectionContextFunctors::always)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline delete", + TimelineConstants::timelineDeleteKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 160, + deleteKeyframes, + timelineHasKeyframes)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline insert", + TimelineConstants::timelineInsertKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 140, + insertKeyframes, + timelineHasKeyframes)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline copy", + TimelineConstants::timelineCopyKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 120, + copyKeyframes, + timelineHasKeyframes)); + + actionManager.addDesignerAction( + new ModelNodeContextMenuAction("commandId timeline paste", + TimelineConstants::timelinePasteKeyframesDisplayName, + {}, + TimelineConstants::timelineCategory, + QKeySequence(), + 100, + pasteKeyframes, + timelineHasClipboard)); +} + +TimelineWidget *TimelineView::createWidget() +{ + if (!m_timelineWidget) + m_timelineWidget = new TimelineWidget(this); + + auto *timelineContext = new TimelineContext(m_timelineWidget); + Core::ICore::addContextObject(timelineContext); + + return m_timelineWidget; +} + +WidgetInfo TimelineView::widgetInfo() +{ + return createWidgetInfo(createWidget(), + nullptr, + QStringLiteral("Timelines"), + WidgetInfo::BottomPane, + 0, + tr("Timeline")); +} + +bool TimelineView::hasQtQuickTimelineImport() +{ + if (isAttached()) { + Import import = Import::createLibraryImport("QtQuick.Timeline", "1.0"); + return model()->hasImport(import, true, true); + } + + return false; +} + +void TimelineView::ensureQtQuickTimelineImport() +{ + if (!hasQtQuickTimelineImport()) { + Import timelineImport = Import::createLibraryImport("QtQuick.Timeline", "1.0"); + model()->changeImports({timelineImport}, {}); + } +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelineview.h b/src/plugins/qmldesigner/components/timelineeditor/timelineview.h new file mode 100644 index 0000000000..057ff3047b --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelineview.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include <abstractview.h> + +#include <QPointer> + +namespace QmlDesigner { + +class TimelineWidget; + +class TimelineView : public AbstractView +{ + Q_OBJECT + +public: + explicit TimelineView(QObject *parent = nullptr); + ~TimelineView() override; + //Abstract View + WidgetInfo widgetInfo() override; + void modelAttached(Model *model) override; + void modelAboutToBeDetached(Model *model) override; + void nodeCreated(const ModelNode &createdNode) override; + void nodeAboutToBeRemoved(const ModelNode &removedNode) override; + void nodeRemoved(const ModelNode &removedNode, + const NodeAbstractProperty &parentProperty, + PropertyChangeFlags propertyChange) override; + void nodeReparented(const ModelNode &node, + const NodeAbstractProperty &newPropertyParent, + const NodeAbstractProperty &oldPropertyParent, + PropertyChangeFlags propertyChange) override; + void instancePropertyChanged(const QList<QPair<ModelNode, PropertyName>> &propertyList) override; + void variantPropertiesChanged(const QList<VariantProperty> &propertyList, + PropertyChangeFlags propertyChange) override; + void selectedNodesChanged(const QList<ModelNode> &selectedNodeList, + const QList<ModelNode> &lastSelectedNodeList) override; + + void propertiesAboutToBeRemoved(const QList<AbstractProperty> &propertyList) override; + void propertiesRemoved(const QList<AbstractProperty> &propertyList) override; + + bool hasWidget() const override; + + void nodeIdChanged(const ModelNode &node, const QString &, const QString &) override; + + void currentStateChanged(const ModelNode &node) override; + + TimelineWidget *widget() const; + + const QmlTimeline addNewTimeline(); + ModelNode addAnimation(QmlTimeline timeline); + void addNewTimelineDialog(); + void openSettingsDialog(); + + void setTimelineRecording(bool b); + + void customNotification(const AbstractView *view, + const QString &identifier, + const QList<ModelNode> &nodeList, + const QList<QVariant> &data) override; + void insertKeyframe(const ModelNode &target, const PropertyName &propertyName); + + QList<QmlTimeline> getTimelines() const; + QList<ModelNode> getAnimations(const QmlTimeline &timeline); + QmlTimeline timelineForState(const ModelNode &state) const; + QmlModelState stateForTimeline(const QmlTimeline &timeline); + + void registerActions(); + +private: + TimelineWidget *createWidget(); + TimelineWidget *m_timelineWidget = nullptr; + bool hasQtQuickTimelineImport(); + void ensureQtQuickTimelineImport(); +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp new file mode 100644 index 0000000000..8a58eb7dcf --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.cpp @@ -0,0 +1,443 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "timelinewidget.h" +#include "easingcurvedialog.h" +#include "timelineconstants.h" +#include "timelinegraphicsscene.h" +#include "timelineicons.h" +#include "timelinepropertyitem.h" +#include "timelinetoolbar.h" +#include "timelineview.h" + +#include <qmldesignerplugin.h> +#include <qmlstate.h> +#include <qmltimeline.h> + +#include <theme.h> +#include <utils/algorithm.h> +#include <utils/fileutils.h> + +#include <QApplication> +#include <QComboBox> +#include <QGraphicsView> +#include <QHBoxLayout> +#include <QLabel> +#include <QMargins> +#include <QPushButton> +#include <QResizeEvent> +#include <QScrollBar> +#include <QShowEvent> +#include <QSlider> +#include <QVBoxLayout> +#include <QtGlobal> + +namespace QmlDesigner { + +class Eventfilter : public QObject +{ +public: + Eventfilter(QObject *parent) + : QObject(parent) + {} + + bool eventFilter(QObject *, QEvent *event) override + { + if (event->type() == QEvent::Wheel) { + event->accept(); + return true; + } + return false; + } +}; + +static qreal next(const QVector<qreal> &vector, qreal current) +{ + auto iter = std::find_if(vector.cbegin(), vector.cend(), [&](qreal val) { + return val > current; + }); + if (iter != vector.end()) + return *iter; + return current; +} + +static qreal previous(const QVector<qreal> &vector, qreal current) +{ + auto iter = std::find_if(vector.rbegin(), vector.rend(), [&](qreal val) { + return val < current; + }); + if (iter != vector.rend()) + return *iter; + return current; +} + +static qreal getcurrentFrame(const QmlTimeline &timeline) +{ + if (!timeline.isValid()) + return 0; + + if (timeline.modelNode().hasAuxiliaryData("currentFrame@NodeInstance")) + return timeline.modelNode().auxiliaryData("currentFrame@NodeInstance").toReal(); + return timeline.currentKeyframe(); +} + +TimelineWidget::TimelineWidget(TimelineView *view) + : QWidget() + , m_toolbar(new TimelineToolBar(this)) + , m_rulerView(new QGraphicsView(this)) + , m_graphicsView(new QGraphicsView(this)) + , m_scrollbar(new QScrollBar(this)) + , m_statusBar(new QLabel(this)) + , m_timelineView(view) + , m_graphicsScene(new TimelineGraphicsScene(this)) + , m_addButton(new QPushButton(this)) +{ + setWindowTitle(tr("Timeline", "Title of timeline view")); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + const QString css = Theme::replaceCssColors(QString::fromUtf8( + Utils::FileReader::fetchQrc(QLatin1String(":/qmldesigner/scrollbar.css")))); + + m_scrollbar->setStyleSheet(css); + m_scrollbar->setOrientation(Qt::Horizontal); + + QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Preferred); + sizePolicy1.setHorizontalStretch(0); + sizePolicy1.setVerticalStretch(0); + sizePolicy1.setHeightForWidth(m_graphicsView->sizePolicy().hasHeightForWidth()); + + m_rulerView->setObjectName("RulerView"); + m_rulerView->setScene(graphicsScene()); + m_rulerView->setFixedHeight(TimelineConstants::rulerHeight); + m_rulerView->setAlignment(Qt::AlignLeft | Qt::AlignTop); + m_rulerView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_rulerView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_rulerView->viewport()->installEventFilter(new Eventfilter(this)); + m_rulerView->viewport()->setFocusPolicy(Qt::NoFocus); + + m_graphicsView->setStyleSheet(css); + m_graphicsView->setObjectName("SceneView"); + m_graphicsView->setFrameShape(QFrame::NoFrame); + m_graphicsView->setFrameShadow(QFrame::Plain); + m_graphicsView->setLineWidth(0); + m_graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + m_graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + m_graphicsView->setSizePolicy(sizePolicy1); + m_graphicsView->setScene(graphicsScene()); + m_graphicsView->setAlignment(Qt::AlignLeft | Qt::AlignTop); + m_graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); + + auto *scrollBarLayout = new QHBoxLayout; + scrollBarLayout->addSpacing(TimelineConstants::sectionWidth); + scrollBarLayout->addWidget(m_scrollbar); + + QMargins margins(0, 0, 0, QApplication::style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); + + auto *contentLayout = new QVBoxLayout; + contentLayout->setContentsMargins(margins); + contentLayout->addWidget(m_rulerView); + contentLayout->addWidget(m_graphicsView); + contentLayout->addLayout(scrollBarLayout); + contentLayout->addWidget(m_statusBar); + m_statusBar->setIndent(2); + m_statusBar->setFixedHeight(TimelineConstants::rulerHeight); + + auto *widgetLayout = new QVBoxLayout; + widgetLayout->setContentsMargins(0, 0, 0, 0); + widgetLayout->setSpacing(0); + widgetLayout->addWidget(m_toolbar); + widgetLayout->addWidget(m_addButton); + + m_addButton->setIcon(TimelineIcons::ADD_TIMELINE.icon()); + m_addButton->setToolTip(tr("Add Timeline")); + m_addButton->setFlat(true); + m_addButton->setFixedSize(32, 32); + + widgetLayout->addLayout(contentLayout); + this->setLayout(widgetLayout); + + connectToolbar(); + + auto setScrollOffset = [this]() { graphicsScene()->setScrollOffset(m_scrollbar->value()); }; + connect(m_scrollbar, &QSlider::valueChanged, this, setScrollOffset); + + connect(graphicsScene(), + &TimelineGraphicsScene::statusBarMessageChanged, + this, + [this](const QString &message) { m_statusBar->setText(message); }); + + connect(m_addButton, &QPushButton::clicked, this, [this]() { + m_timelineView->addNewTimelineDialog(); + }); +} + +void TimelineWidget::connectToolbar() +{ + connect(graphicsScene(), + &TimelineGraphicsScene::selectionChanged, + this, + &TimelineWidget::selectionChanged); + + connect(graphicsScene(), &TimelineGraphicsScene::scroll, this, &TimelineWidget::scroll); + + auto setRulerScaling = [this](int val) { m_graphicsScene->setRulerScaling(val); }; + connect(m_toolbar, &TimelineToolBar::scaleFactorChanged, setRulerScaling); + + auto setToFirstFrame = [this]() { + graphicsScene()->setCurrentFrame(graphicsScene()->startFrame()); + }; + connect(m_toolbar, &TimelineToolBar::toFirstFrameTriggered, setToFirstFrame); + + auto setToLastFrame = [this]() { + graphicsScene()->setCurrentFrame(graphicsScene()->endFrame()); + }; + connect(m_toolbar, &TimelineToolBar::toLastFrameTriggered, setToLastFrame); + + auto setToPreviousFrame = [this]() { + graphicsScene()->setCurrentFrame(adjacentFrame(&previous)); + }; + connect(m_toolbar, &TimelineToolBar::previousFrameTriggered, setToPreviousFrame); + + auto setToNextFrame = [this]() { graphicsScene()->setCurrentFrame(adjacentFrame(&next)); }; + connect(m_toolbar, &TimelineToolBar::nextFrameTriggered, setToNextFrame); + + auto setCurrentFrame = [this](int frame) { graphicsScene()->setCurrentFrame(frame); }; + connect(m_toolbar, &TimelineToolBar::currentFrameChanged, setCurrentFrame); + + auto setStartFrame = [this](int start) { graphicsScene()->setStartFrame(start); }; + connect(m_toolbar, &TimelineToolBar::startFrameChanged, setStartFrame); + + auto setEndFrame = [this](int end) { graphicsScene()->setEndFrame(end); }; + connect(m_toolbar, &TimelineToolBar::endFrameChanged, setEndFrame); + + + connect(m_toolbar, &TimelineToolBar::recordToggled, + this, + &TimelineWidget::setTimelineRecording); + + connect(m_toolbar, + &TimelineToolBar::openEasingCurveEditor, + this, + &TimelineWidget::openEasingCurveEditor); + + connect(m_toolbar, + &TimelineToolBar::settingDialogClicked, + m_timelineView, + &TimelineView::openSettingsDialog); + + for (auto action : QmlDesignerPlugin::instance()->designerActionManager().designerActions()) { + if (action->menuId() == "LivePreview") { + QObject::connect(m_toolbar, + &TimelineToolBar::playTriggered, + action->action(), + [action]() { + action->action()->setChecked(false); + action->action()->triggered(true); + }); + } + } + + setTimelineActive(false); +} + +int TimelineWidget::adjacentFrame(const std::function<qreal(const QVector<qreal> &, qreal)> &fun) const +{ + auto positions = graphicsScene()->keyframePositions(); + std::sort(positions.begin(), positions.end()); + return qRound(fun(positions, graphicsScene()->currentFramePosition())); +} + +void TimelineWidget::changeScaleFactor(int factor) +{ + m_toolbar->setScaleFactor(factor); +} + +void TimelineWidget::scroll(const TimelineUtils::Side &side) +{ + if (side == TimelineUtils::Side::Left) + m_scrollbar->setValue(m_scrollbar->value() - m_scrollbar->singleStep()); + else if (side == TimelineUtils::Side::Right) + m_scrollbar->setValue(m_scrollbar->value() + m_scrollbar->singleStep()); +} + +void TimelineWidget::selectionChanged() +{ + if (graphicsScene()->hasSelection()) + m_toolbar->setActionEnabled("Curve Picker", true); + else + m_toolbar->setActionEnabled("Curve Picker", false); +} + +void TimelineWidget::openEasingCurveEditor() +{ + if (graphicsScene()->hasSelection()) { + QList<ModelNode> frames; + for (auto *item : graphicsScene()->selectedKeyframes()) + frames.append(item->frameNode()); + EasingCurveDialog::runDialog(frames); + } +} + +void TimelineWidget::setTimelineRecording(bool value) +{ + ModelNode node = timelineView()->modelNodeForId(m_toolbar->currentTimelineId()); + + if (value) { + timelineView()->activateTimelineRecording(node); + } else { + timelineView()->deactivateTimelineRecording(); + timelineView()->activateTimeline(node); + } + + graphicsScene()->invalidateRecordButtonsStatus(); +} + +void TimelineWidget::contextHelp(const Core::IContext::HelpCallback &callback) const +{ + if (timelineView()) + timelineView()->contextHelp(callback); + else + callback({}); +} + +void TimelineWidget::init() +{ + QmlTimeline currentTimeline = m_timelineView->timelineForState(m_timelineView->currentState()); + if (currentTimeline.isValid()) + setTimelineId(currentTimeline.modelNode().id()); + else + setTimelineId({}); + + invalidateTimelineDuration(graphicsScene()->currentTimeline()); + + graphicsScene()->setWidth(m_graphicsView->viewport()->width()); + + // setScaleFactor uses QSignalBlocker. + m_toolbar->setScaleFactor(0); + m_graphicsScene->setRulerScaling(0); +} + +void TimelineWidget::reset() +{ + graphicsScene()->clearTimeline(); + m_toolbar->reset(); + m_statusBar->clear(); +} + +TimelineGraphicsScene *TimelineWidget::graphicsScene() const +{ + return m_graphicsScene; +} + +TimelineToolBar *TimelineWidget::toolBar() const +{ + return m_toolbar; +} + +void TimelineWidget::invalidateTimelineDuration(const QmlTimeline &timeline) +{ + if (timelineView() && timelineView()->model()) { + QmlTimeline currentTimeline = graphicsScene()->currentTimeline(); + if (currentTimeline.isValid() && currentTimeline == timeline) { + m_toolbar->setCurrentTimeline(timeline); + graphicsScene()->setTimeline(timeline); + graphicsScene()->setCurrenFrame(timeline, getcurrentFrame(timeline)); + } + } +} + +void TimelineWidget::invalidateTimelinePosition(const QmlTimeline &timeline) +{ + if (timelineView() && timelineView()->model()) { + QmlTimeline currentTimeline = graphicsScene()->currentTimeline(); + if (currentTimeline.isValid() && currentTimeline == timeline) { + qreal frame = getcurrentFrame(timeline); + m_toolbar->setCurrentFrame(frame); + graphicsScene()->setCurrenFrame(timeline, frame); + } + } +} + +void TimelineWidget::setupScrollbar(int min, int max, int current) +{ + bool b = m_scrollbar->blockSignals(true); + m_scrollbar->setMinimum(min); + m_scrollbar->setMaximum(max); + m_scrollbar->setValue(current); + m_scrollbar->setSingleStep((max - min) / 10); + m_scrollbar->blockSignals(b); +} + +void TimelineWidget::setTimelineId(const QString &id) +{ + setTimelineActive(!m_timelineView->getTimelines().isEmpty()); + if (m_timelineView->isAttached()) { + m_toolbar->setCurrentTimeline(m_timelineView->modelNodeForId(id)); + m_toolbar->setCurrentState(m_timelineView->currentState().name()); + m_timelineView->setTimelineRecording(false); + } +} + +void TimelineWidget::setTimelineActive(bool b) +{ + if (b) { + m_toolbar->setVisible(true); + m_graphicsView->setVisible(true); + m_rulerView->setVisible(true); + m_scrollbar->setVisible(true); + m_addButton->setVisible(false); + m_graphicsView->update(); + m_rulerView->update(); + } else { + m_toolbar->setVisible(false); + m_graphicsView->setVisible(false); + m_rulerView->setVisible(false); + m_scrollbar->setVisible(false); + m_addButton->setVisible(true); + } +} + +void TimelineWidget::showEvent(QShowEvent *event) +{ + Q_UNUSED(event) + graphicsScene()->setWidth(m_graphicsView->viewport()->width()); + graphicsScene()->invalidateLayout(); + graphicsScene()->invalidate(); + graphicsScene()->onShow(); +} + +void TimelineWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + graphicsScene()->setWidth(m_graphicsView->viewport()->width()); +} + +TimelineView *TimelineWidget::timelineView() const +{ + return m_timelineView; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h new file mode 100644 index 0000000000..3fd299a88a --- /dev/null +++ b/src/plugins/qmldesigner/components/timelineeditor/timelinewidget.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "timelineutils.h" +#include <coreplugin/icontext.h> + +#include <QWidget> + +#include <functional> + +QT_FORWARD_DECLARE_CLASS(QComboBox) +QT_FORWARD_DECLARE_CLASS(QGraphicsView) +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QResizeEvent) +QT_FORWARD_DECLARE_CLASS(QScrollBar) +QT_FORWARD_DECLARE_CLASS(QShowEvent) +QT_FORWARD_DECLARE_CLASS(QString) +QT_FORWARD_DECLARE_CLASS(QPushButton) + +namespace QmlDesigner { + +class TimelineToolBar; +class TimelineView; +class TimelineGraphicsScene; +class QmlTimeline; + +class TimelineWidget : public QWidget +{ + Q_OBJECT + +public: + explicit TimelineWidget(TimelineView *view); + void contextHelp(const Core::IContext::HelpCallback &callback) const; + + TimelineGraphicsScene *graphicsScene() const; + TimelineView *timelineView() const; + TimelineToolBar *toolBar() const; + + void init(); + void reset(); + + void invalidateTimelineDuration(const QmlTimeline &timeline); + void invalidateTimelinePosition(const QmlTimeline &timeline); + void setupScrollbar(int min, int max, int current); + void setTimelineId(const QString &id); + + void setTimelineActive(bool b); + +public slots: + void selectionChanged(); + void openEasingCurveEditor(); + void setTimelineRecording(bool value); + void changeScaleFactor(int factor); + void scroll(const TimelineUtils::Side &side); + +protected: + void showEvent(QShowEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + +private: + void connectToolbar(); + + int adjacentFrame(const std::function<qreal(const QVector<qreal> &, qreal)> &fun) const; + + TimelineToolBar *m_toolbar = nullptr; + + QGraphicsView *m_rulerView = nullptr; + + QGraphicsView *m_graphicsView = nullptr; + + QScrollBar *m_scrollbar = nullptr; + + QLabel *m_statusBar = nullptr; + + TimelineView *m_timelineView = nullptr; + + TimelineGraphicsScene *m_graphicsScene; + + QPushButton *m_addButton = nullptr; +}; + +} // namespace QmlDesigner |