diff --git a/src/qtitchescontrols/qtitchesblockdroparea.cpp b/src/qtitchescontrols/qtitchesblockdroparea.cpp index 1bc534a..cd45aa6 100644 --- a/src/qtitchescontrols/qtitchesblockdroparea.cpp +++ b/src/qtitchescontrols/qtitchesblockdroparea.cpp @@ -36,22 +36,16 @@ QString mimeTypeForTypeCategory(Core::Block::TypeCategory typeCategory) return {}; } -QString mimeTypeForAction(BlockDropArea::DropAction dropAction) +BlockDropArea::DropActions actionsForMimeType(const QString &mimeType) { - switch (dropAction) { - case BlockDropArea::PrependBlock: - case BlockDropArea::AppendBlock: - return s_mimeTypeBlockType; - - case BlockDropArea::ApplyBooleanExpression: - return s_mimeTypeBooleanExpression; - - case BlockDropArea::ApplyNumberExpression: - return s_mimeTypeNumberExpression; - - case BlockDropArea::ApplyStringExpression: - return s_mimeTypeStringExpression; - } + if (mimeType == s_mimeTypeBlockType) + return BlockDropArea::PrependBlock | BlockDropArea::AppendBlock; + if (mimeType == s_mimeTypeBooleanExpression) + return BlockDropArea::ApplyBooleanExpression; + if (mimeType == s_mimeTypeNumberExpression) + return BlockDropArea::ApplyNumberExpression; + if (mimeType == s_mimeTypeStringExpression) + return BlockDropArea::ApplyStringExpression; return {}; } @@ -78,14 +72,14 @@ BlockDropArea::DropActions BlockDropArea::acceptedDropActions() const return m_acceptedDropActions; } -BlockDropArea::DropAction BlockDropArea::pendingDropAction() const +BlockDropArea::DropActions BlockDropArea::floatingDropActions() const { - return m_pendingDropSuspended ? DropAction{} : m_pendingDropAction; + return m_floatingDropActions; } -QByteArray BlockDropArea::typeInfo() const +BlockDropArea::DropAction BlockDropArea::pendingDropAction() const { - return m_typeInfo; + return m_pendingDropAction; } QVariantMap BlockDropArea::createMimeData(const QJsonObject &typeInfo) const @@ -102,36 +96,42 @@ QVariantMap BlockDropArea::createMimeData(const QJsonObject &typeInfo) const void BlockDropArea::dragEnterEvent(QDragEnterEvent *event) { const auto mimeData = event->mimeData(); - for (const auto &member: QMetaEnum::fromType()) { - const auto action = member.value(); + if (!mimeData) + return; - if (eventViolatesAction(event, action)) - continue; + m_typeInfo.clear(); + m_floatingDropActions = {}; - if ((m_acceptedDropActions & action) == action) { - m_typeInfo = mimeData->data(mimeTypeForAction(action)); + for (const auto &format: mimeData->formats()) { + if (const auto actions = (actionsForMimeType(format) & m_acceptedDropActions)) { + if (m_typeInfo.isEmpty()) + m_typeInfo = mimeData->data(format); - if (!m_typeInfo.isEmpty()) { - event->accept(); - m_pendingDropAction = action; - emit pendingDropActionChanged(m_pendingDropAction); - return; - } + m_floatingDropActions |= actions; } } - cancelDrop(); + if ((m_pendingDropAction = findActionForEvent(event)) != 0) { + event->accept(answerRect(m_pendingDropAction)); + emit pendingDropActionChanged(m_pendingDropAction); + } + QQuickItem::dragEnterEvent(event); } void BlockDropArea::dragMoveEvent(QDragMoveEvent *event) { - if (m_pendingDropAction) { - const auto suspendRequired = eventViolatesAction(event, m_pendingDropAction); - if (m_pendingDropSuspended != suspendRequired) { - m_pendingDropSuspended = suspendRequired; - emit pendingDropActionChanged(pendingDropAction()); - } + const auto action = findActionForEvent(event); + + if (m_pendingDropAction != action) { + m_pendingDropAction = action; + + if (m_pendingDropAction) + event->accept(answerRect(m_pendingDropAction)); + else + event->ignore(); + + emit pendingDropActionChanged(m_pendingDropAction); } QQuickItem::dragMoveEvent(event); @@ -139,11 +139,8 @@ void BlockDropArea::dragMoveEvent(QDragMoveEvent *event) void BlockDropArea::dragLeaveEvent(QDragLeaveEvent *event) { - if (m_pendingDropAction) { - cancelDrop(); - } else { - QQuickItem::dragLeaveEvent(event); - } + resetDropActionState(); + QQuickItem::dragLeaveEvent(event); } void BlockDropArea::dropEvent(QDropEvent *event) @@ -151,27 +148,52 @@ void BlockDropArea::dropEvent(QDropEvent *event) if (m_pendingDropAction) { event->accept(); emit typeInfoDropped(m_pendingDropAction, m_typeInfo); - cancelDrop(); - } else { - QQuickItem::dropEvent(event); } + + resetDropActionState(); + QQuickItem::dropEvent(event); } -void BlockDropArea::cancelDrop() +QRect BlockDropArea::answerRect(BlockDropArea::DropAction action) const { - m_typeInfo.clear(); - m_pendingDropAction = {}; - emit pendingDropActionChanged(m_pendingDropAction); + const auto center = height()/2; + + switch(action) { + case PrependBlock: + return {0, 0, qRound(width()), qRound(center)}; + + case AppendBlock: + return {0, qRound(height() - center), qRound(width()), qRound(center)}; + + case ApplyBooleanExpression: + case ApplyNumberExpression: + case ApplyStringExpression: + break; + } + + return {}; } -bool BlockDropArea::eventViolatesAction(QDropEvent *event, BlockDropArea::DropAction action) const +BlockDropArea::DropAction BlockDropArea::findActionForEvent(QDropEvent *event) const { - if (action == PrependBlock && event->pos().y() >= height()/2) - return true; - if (action == AppendBlock && event->pos().y() < height()/2) - return true; + for (const auto &member: QMetaEnum::fromType()) { + const auto action = member.value(); + + if ((m_floatingDropActions & action) == action + && answerRect(action).contains(event->pos())) + return action; + } + + return {}; +} + +void BlockDropArea::resetDropActionState() +{ + m_typeInfo.clear(); + m_floatingDropActions = {}; - return false; + if (std::exchange(m_pendingDropAction, {})) + emit pendingDropActionChanged(m_pendingDropAction); } } // namespace Controls diff --git a/src/qtitchescontrols/qtitchesblockdroparea.h b/src/qtitchescontrols/qtitchesblockdroparea.h index f9ae283..1edd7b5 100644 --- a/src/qtitchescontrols/qtitchesblockdroparea.h +++ b/src/qtitchescontrols/qtitchesblockdroparea.h @@ -32,9 +32,8 @@ class QTITCHES_CONTROLS_EXPORT BlockDropArea : public QQuickItem void setAcceptedDropActions(DropActions acceptedDropActions); DropActions acceptedDropActions() const; - + DropActions floatingDropActions() const; DropAction pendingDropAction() const; - QByteArray typeInfo() const; Q_INVOKABLE virtual QVariantMap createMimeData(const QJsonObject &typeInfo) const; @@ -49,21 +48,23 @@ class QTITCHES_CONTROLS_EXPORT BlockDropArea : public QQuickItem void dragLeaveEvent(QDragLeaveEvent *event) override; void dropEvent(QDropEvent *event) override; +private: void setAcceptedDropActions(int acceptedDropActions) { setAcceptedDropActions(static_cast(acceptedDropActions)); } -private: - void cancelDrop(); - bool eventViolatesAction(QDropEvent *event, DropAction action) const; + QRect answerRect(DropAction action) const; + DropAction findActionForEvent(QDropEvent *event) const; + void resetDropActionState(); DropActions m_acceptedDropActions; + DropActions m_floatingDropActions; DropAction m_pendingDropAction = {}; - bool m_pendingDropSuspended = false; QByteArray m_typeInfo; }; } // namespace Controls } // namespace QtItches +Q_DECLARE_METATYPE(QtItches::Controls::BlockDropArea::DropActions) Q_DECLARE_OPERATORS_FOR_FLAGS(QtItches::Controls::BlockDropArea::DropActions) #endif // QTITCHESBLOCKDROPAREA_H diff --git a/src/qtitchescore/qtitchesblocklibrary.cpp b/src/qtitchescore/qtitchesblocklibrary.cpp index 52de577..d4bdb02 100644 --- a/src/qtitchescore/qtitchesblocklibrary.cpp +++ b/src/qtitchescore/qtitchesblocklibrary.cpp @@ -39,7 +39,7 @@ class BlockLibrary::Private return type.module() + ' ' + QString::number(type.majorVersion()) + '.' + QString::number(type.majorVersion()); } - QJsonObject createTypeInfo() + QJsonObject createTypeInfo() const { return { {s_typeId, type.index()}, @@ -150,6 +150,21 @@ Block::TypeCategory BlockLibrary::typeCategory(const QJsonObject &typeInfo) return static_cast(typeInfo.value(s_typeCategory).toInt()); } +QJsonObject BlockLibrary::typeInfo(const QString &uri, int majorVersion, int minorVersion, const QString &name) +{ + const QHashedString hashedUri{uri}; + + for (const auto &row: d->m_rows) { + if (row.type.module() == hashedUri + && row.type.majorVersion() == majorVersion + && row.type.minorVersion() == minorVersion + && row.type.elementName() == name) + return row.createTypeInfo(); + } + + return {}; +} + Block *BlockLibrary::Private::createBlock(QQmlEngine *engine, const QQmlType &type) const { if (type.metaObject() && type.metaObject()->inherits(&Block::staticMetaObject)) { diff --git a/src/qtitchescore/qtitchesblocklibrary.h b/src/qtitchescore/qtitchesblocklibrary.h index cca3847..271e82d 100644 --- a/src/qtitchescore/qtitchesblocklibrary.h +++ b/src/qtitchescore/qtitchesblocklibrary.h @@ -50,6 +50,7 @@ class QTITCHES_CORE_EXPORT BlockLibrary : public QAbstractListModel, public QQml void componentComplete() override; static Block::TypeCategory typeCategory(const QJsonObject &typeInfo); + QJsonObject typeInfo(const QString &uri, int majorVersion, int minorVersion, const QString &name); Q_INVOKABLE QtItches::Core::Block *createBlock(const QByteArray &typeInfo, QObject *parent) const; diff --git a/src/qtitchescore/qtitchesexpression.cpp b/src/qtitchescore/qtitchesexpression.cpp index 88c6cfa..e4e9a09 100644 --- a/src/qtitchescore/qtitchesexpression.cpp +++ b/src/qtitchescore/qtitchesexpression.cpp @@ -5,6 +5,12 @@ namespace Core { //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +Expression::Expression(QObject *parent) + : Block{parent} +{ + setConnectors(Connectors{}); +} + void Expression::setParameterType(Parameter::Type parameterType) { if (m_parameterType == parameterType) diff --git a/src/qtitchescore/qtitchesexpression.h b/src/qtitchescore/qtitchesexpression.h index 921ae44..262063b 100644 --- a/src/qtitchescore/qtitchesexpression.h +++ b/src/qtitchescore/qtitchesexpression.h @@ -29,7 +29,7 @@ class QTITCHES_CORE_EXPORT Expression : public Block Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged FINAL) public: - using Block::Block; + explicit Expression(QObject *parent = {}); void setParameterType(Parameter::Type parameterType); Parameter::Type parameterType() const { return m_parameterType; } diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index 0bc1e3f..e0f6b20 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,4 +1,2 @@ TEMPLATE = subdirs - -SUBDIRS = \ - tst_expressions.pro +SUBDIRS = $$files(tst_*.pro) diff --git a/tests/auto/tst_blockview.cpp b/tests/auto/tst_blockview.cpp new file mode 100644 index 0000000..a5d39a1 --- /dev/null +++ b/tests/auto/tst_blockview.cpp @@ -0,0 +1,191 @@ +#include "qtitchesblocklibrary.h" +#include "qtitchesblockview.h" + +#include +#include +#include +#include +#include +#include + +static void initResources() +{ + Q_INIT_RESOURCE(qtitchescontrolsplugin); + Q_INIT_RESOURCE(qtitchescoreplugin); +} + +Q_IMPORT_PLUGIN(qtitchescontrolsplugin) +Q_IMPORT_PLUGIN(qtitchescoreplugin) + +namespace QTest { + +template<> inline char *toString(const QtItches::Controls::BlockView::DropActions &actions) +{ + QString buffer; + QDebug(&buffer) << actions; + return qstrdup(buffer.toLocal8Bit().constData()); +} + +} + +namespace QtItches { +namespace Controls { + +class BlockViewTest : public QObject +{ + Q_OBJECT + +public: + BlockViewTest() + { + initResources(); + } + +private slots: + void testDropBehavior_data() + { + QTest::addColumn("blockQml"); + QTest::addColumn>("pendingDropActionChanges"); + QTest::addColumn("acceptedDropActions"); + QTest::addColumn("topRegionActions"); + QTest::addColumn("bottomRegionActions"); + + QTest::newRow("GotoXY") + << "GotoXY {}" + << QVector{0, 1, 2, 3, 4, 5, 6} + << BlockView::DropActions{BlockView::PrependBlock | BlockView::AppendBlock} + << BlockView::PrependBlock + << BlockView::AppendBlock; + + QTest::newRow("DoForever") + << "DoForever {}" + << QVector{0, 1, 2, 2, 2, 3, 4} + << BlockView::DropActions{BlockView::PrependBlock} + << BlockView::PrependBlock + << BlockView::DropAction{}; + + QTest::newRow("WhenFlagClicked") + << "WhenFlagClicked {}" + << QVector{0, 0, 1, 2, 3, 4, 4} + << BlockView::DropActions{BlockView::AppendBlock} + << BlockView::DropAction{} + << BlockView::AppendBlock; + + QTest::newRow("Not") + << "Not {}" + << QVector{0, 0, 0, 0, 0, 0, 0} + << BlockView::DropActions{} + << BlockView::DropAction{} + << BlockView::DropAction{}; + } + + void testDropBehavior() + { + QFETCH(QString, blockQml); + QFETCH(QVector, pendingDropActionChanges); + QFETCH(BlockView::DropActions, acceptedDropActions); + QFETCH(BlockView::DropAction, topRegionActions); + QFETCH(BlockView::DropAction, bottomRegionActions); + + QQmlEngine engine; + engine.addImportPath("qrc:/imports"); + + QQmlComponent component{&engine}; + component.setData("import QtItches.Core 1.0\n" + "import QtItches.Controls 1.0\n" + "BlockView {\n" + " block: " + blockQml.toUtf8() + "\n" + " library: BlockLibrary {}\n" + "}", {}); + + QVERIFY2(component.status() == QQmlComponent::Ready, qPrintable(component.errorString())); + const auto blockView = dynamic_cast(component.create()); + QVERIFY(blockView); + + QSignalSpy pendingDropActionChanged{blockView, &BlockView::pendingDropActionChanged}; + + QVERIFY(qmlContext(blockView)); + QVERIFY(blockView->block()); + QVERIFY(blockView->library()); + QVERIFY(!blockView->size().isEmpty()); + QCOMPARE(blockView->acceptedDropActions(), acceptedDropActions); + QCOMPARE(blockView->pendingDropAction(), BlockView::DropAction{}); + QCOMPARE(pendingDropActionChanged.count(), pendingDropActionChanges.takeFirst()); + + const auto typeInfo = blockView->library()->typeInfo("QtItches.Core", 1, 0, "TurnLeft"); + QVERIFY(!typeInfo.isEmpty()); + + const auto qmlMimeData = blockView->createMimeData(typeInfo); + const auto mimeData = std::make_unique(); + for (auto it = qmlMimeData.begin(), last = qmlMimeData.end(); it != last; ++it) + mimeData->setData(it.key(), it.value().toByteArray()); + QVERIFY(!mimeData->text().isEmpty()); + +#define VERIFY_DRAG_EVENT_WITHOUT_RECT(expectedFloatingActions, expectedPendingActions, expectAccepted) do { \ + QCOMPARE(pendingDropActionChanged.count(), pendingDropActionChanges.takeFirst()); \ + QCOMPARE(blockView->floatingDropActions(), (expectedFloatingActions)); \ + QCOMPARE(blockView->pendingDropAction(), (expectedPendingActions)); \ + QCOMPARE(event.isAccepted(), (expectAccepted)); \ + } while(false) + +#define VERIFY_DRAG_EVENT(expectedFloatingActions, expectedPendingActions, expectAccepted) do { \ + VERIFY_DRAG_EVENT_WITHOUT_RECT(expectedFloatingActions, expectedPendingActions, expectAccepted); \ + QCOMPARE(event.answerRect().height() > 1, (expectAccepted)); \ + } while(false) + + const QPoint topPoint{1, 1}; + const QPoint bottomPoint{1, qRound(blockView->height()) - 2}; + + { // enter from top + QDragEnterEvent event{topPoint, Qt::CopyAction, mimeData.get(), Qt::LeftButton, {}}; + qApp->sendEvent(blockView, &event); + + VERIFY_DRAG_EVENT(bottomRegionActions | topRegionActions, + topRegionActions, !!topRegionActions); + } + + { // move to bottom + QDragMoveEvent event{bottomPoint, Qt::CopyAction, mimeData.get(), Qt::LeftButton, {}}; + qApp->sendEvent(blockView, &event); + + VERIFY_DRAG_EVENT(bottomRegionActions | topRegionActions, + bottomRegionActions, !!bottomRegionActions); + } + + { // leave + QDragLeaveEvent event; + qApp->sendEvent(blockView, &event); + VERIFY_DRAG_EVENT_WITHOUT_RECT(0, 0, true); + } + + { // enter from bottom + QDragEnterEvent event{bottomPoint, Qt::CopyAction, mimeData.get(), Qt::LeftButton, {}}; + qApp->sendEvent(blockView, &event); + + VERIFY_DRAG_EVENT(bottomRegionActions | topRegionActions, + bottomRegionActions, !!bottomRegionActions); + } + + { // move to top + QDragMoveEvent event{topPoint, Qt::CopyAction, mimeData.get(), Qt::LeftButton, {}}; + qApp->sendEvent(blockView, &event); + + VERIFY_DRAG_EVENT(bottomRegionActions | topRegionActions, + topRegionActions, !!topRegionActions); + } + + { // drop + QDropEvent event{topPoint, Qt::CopyAction, mimeData.get(), Qt::LeftButton, {}}; + qApp->sendEvent(blockView, &event); + + VERIFY_DRAG_EVENT_WITHOUT_RECT(0, 0, !!topRegionActions); + } + } +}; + +} // namespace Controls +} // namespace QtItches + +QTEST_MAIN(QtItches::Controls::BlockViewTest) + +#include "tst_blockview.moc" diff --git a/tests/auto/tst_blockview.pro b/tests/auto/tst_blockview.pro new file mode 100644 index 0000000..8dd9a13 --- /dev/null +++ b/tests/auto/tst_blockview.pro @@ -0,0 +1,3 @@ +include(testcase.pri) +include(../../src/qtitchescore/qtitchescorestaticplugin.pri) +include(../../src/qtitchescontrols/qtitchescontrolsstaticplugin.pri)