From cae7176827eb3c23428cfcd8f6dabb00f8dcaef3 Mon Sep 17 00:00:00 2001 From: Sebastian Sauer Date: Tue, 12 Aug 2014 18:53:00 +0700 Subject: [PATCH] QSM: Reintroduce guard argument evaluation Implements the suggestion from Simon Hausmann (codereview 89716 from 08-05 14:46) to use QQmlScriptString rather then the previous used MetaObject-manipulation. This also introduces comparison operators for QQmlScriptString to be able to determinate if a QQmlScriptString changed what is needed cause there is otherwise no way to access (all) the needed details within QQmlScriptStringPrivate. Change-Id: I198479eac8fd37cbdd98a99aacdd8eebf7b75d21 Reviewed-by: Simon Hausmann --- src/imports/statemachine/signaltransition.cpp | 41 ++++++-- src/imports/statemachine/signaltransition.h | 10 +- .../snippets/qml/statemachine/guardcondition.qml | 69 +++++++++++++ src/qml/qml/qqmlscriptstring.cpp | 38 +++++++ src/qml/qml/qqmlscriptstring.h | 3 + tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp | 82 ++++++++++++++++ .../qmltest/statemachine/tst_guardcondition.qml | 109 +++++++++++++++++++++ 7 files changed, 340 insertions(+), 12 deletions(-) create mode 100644 src/qml/doc/snippets/qml/statemachine/guardcondition.qml create mode 100644 tests/auto/qmltest/statemachine/tst_guardcondition.qml diff --git a/src/imports/statemachine/signaltransition.cpp b/src/imports/statemachine/signaltransition.cpp index 9b64550..634ec5a 100644 --- a/src/imports/statemachine/signaltransition.cpp +++ b/src/imports/statemachine/signaltransition.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -46,7 +47,6 @@ SignalTransition::SignalTransition(QState *parent) : QSignalTransition(this, SIGNAL(invokeYourself()), parent) - , m_guard(true) { connect(this, SIGNAL(signalChanged()), SIGNAL(qmlSignalChanged())); } @@ -57,7 +57,23 @@ bool SignalTransition::eventTest(QEvent *event) if (!QSignalTransition::eventTest(event)) return false; - return m_guard; + if (m_guard.isEmpty()) + return true; + + QQmlContext context(QQmlEngine::contextForObject(this)); + + QStateMachine::SignalEvent *e = static_cast(event); + + // Set arguments as context properties + int count = e->arguments().count(); + QMetaMethod metaMethod = e->sender()->metaObject()->method(e->signalIndex()); + for (int i = 0; i < count; i++) + context.setContextProperty(metaMethod.parameterNames()[i], QVariant::fromValue(e->arguments().at(i))); + + QQmlExpression expr(m_guard, &context, this); + QVariant result = expr.evaluate(); + + return result.toBool(); } const QJSValue& SignalTransition::signal() @@ -89,17 +105,18 @@ void SignalTransition::setSignal(const QJSValue &signal) QSignalTransition::setSignal(metaMethod.methodSignature()); } -bool SignalTransition::guard() const +QQmlScriptString SignalTransition::guard() const { return m_guard; } -void SignalTransition::setGuard(bool guard) +void SignalTransition::setGuard(const QQmlScriptString &guard) { - if (guard != m_guard) { - m_guard = guard; - emit guardChanged(); - } + if (m_guard == guard) + return; + + m_guard = guard; + emit guardChanged(); } void SignalTransition::invoke() @@ -227,4 +244,12 @@ void SignalTransition::invoke() Guard conditions affect the behavior of a state machine by enabling transitions only when they evaluate to true and disabling them when they evaluate to false. + + When the signal associated with this signal transition is emitted the + guard condition is evaluated. In the guard condition the arguments + of the signal can be used as demonstrated in the example below. + + \snippet qml/statemachine/guardcondition.qml document + + \sa signal */ diff --git a/src/imports/statemachine/signaltransition.h b/src/imports/statemachine/signaltransition.h index 311195e..3ac563b 100644 --- a/src/imports/statemachine/signaltransition.h +++ b/src/imports/statemachine/signaltransition.h @@ -38,19 +38,21 @@ #include #include +#include + QT_BEGIN_NAMESPACE class SignalTransition : public QSignalTransition { Q_OBJECT Q_PROPERTY(QJSValue signal READ signal WRITE setSignal NOTIFY qmlSignalChanged) - Q_PROPERTY(bool guard READ guard WRITE setGuard NOTIFY guardChanged) + Q_PROPERTY(QQmlScriptString guard READ guard WRITE setGuard NOTIFY guardChanged) public: explicit SignalTransition(QState *parent = Q_NULLPTR); - bool guard() const; - void setGuard(bool guard); + QQmlScriptString guard() const; + void setGuard(const QQmlScriptString &guard); bool eventTest(QEvent *event); @@ -70,7 +72,7 @@ Q_SIGNALS: private: QByteArray m_data; QJSValue m_signal; - bool m_guard; + QQmlScriptString m_guard; }; QT_END_NAMESPACE diff --git a/src/qml/doc/snippets/qml/statemachine/guardcondition.qml b/src/qml/doc/snippets/qml/statemachine/guardcondition.qml new file mode 100644 index 0000000..f9ecf45 --- /dev/null +++ b/src/qml/doc/snippets/qml/statemachine/guardcondition.qml @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Ford Motor Company +** Contact: http://www.qt-project.org/legal +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +//! [document] +import QtQml.StateMachine 1.0 +import QtQuick 2.0 + +Rectangle { + Button { + anchors.fill: parent + id: button + StateMachine { + StateBase { + SignalTransition { + targetState: finalState + signal: button.mysignal + // the guard condition uses the mystr string argument from mysignal + guard: mystr == "test" + } + } + FinalState { + id: finalState + } + } + // define the signal the SignalTransition is connected with + signal mysignal(string mystr) + // on clicking the button emit the signal with a single string argument + onClicked: button.mysignal("test") + } +} +//! [document] + diff --git a/src/qml/qml/qqmlscriptstring.cpp b/src/qml/qml/qqmlscriptstring.cpp index b18bd4d..c822707 100644 --- a/src/qml/qml/qqmlscriptstring.cpp +++ b/src/qml/qml/qqmlscriptstring.cpp @@ -114,6 +114,44 @@ QQmlScriptString &QQmlScriptString::operator=(const QQmlScriptString &other) } /*! +Returns \c true if this and the \a other QQmlScriptString objects are equal. + +\sa operator!=() +*/ +bool QQmlScriptString::operator==(const QQmlScriptString &other) const +{ + if (d == other.d) + return true; + + if (d->isNumberLiteral || other.d->isNumberLiteral) + return d->isNumberLiteral && other.d->isNumberLiteral && d->numberValue == other.d->numberValue; + + if (d->isStringLiteral || other.d->isStringLiteral) + return d->isStringLiteral && other.d->isStringLiteral && d->script == other.d->script; + + if (d->script == QStringLiteral("true") || + d->script == QStringLiteral("false") || + d->script == QStringLiteral("undefined") || + d->script == QStringLiteral("null")) + return d->script == other.d->script; + + return d->context == other.d->context && + d->scope == other.d->scope && + d->script == other.d->script && + d->bindingId == other.d->bindingId; +} + +/*! +Returns \c true if this and the \a other QQmlScriptString objects are different. + +\sa operator==() +*/ +bool QQmlScriptString::operator!=(const QQmlScriptString &other) const +{ + return !operator==(other); +} + +/*! Returns whether the QQmlScriptString is empty. */ bool QQmlScriptString::isEmpty() const diff --git a/src/qml/qml/qqmlscriptstring.h b/src/qml/qml/qqmlscriptstring.h index 1ff8903..ccbe905 100644 --- a/src/qml/qml/qqmlscriptstring.h +++ b/src/qml/qml/qqmlscriptstring.h @@ -58,6 +58,9 @@ public: QQmlScriptString &operator=(const QQmlScriptString &); + bool operator==(const QQmlScriptString &) const; + bool operator!=(const QQmlScriptString &) const; + bool isEmpty() const; bool isUndefinedLiteral() const; diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp index d6d1cef..05cdccf 100644 --- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp +++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp @@ -140,6 +140,7 @@ private slots: void scriptString(); void scriptStringJs(); void scriptStringWithoutSourceCode(); + void scriptStringComparison(); void defaultPropertyListOrder(); void declaredPropertyValues(); void dontDoubleCallClassBegin(); @@ -2065,6 +2066,87 @@ void tst_qqmllanguage::scriptStringWithoutSourceCode() } } +// Test the QQmlScriptString comparison operators. The script strings are considered +// equal if there evaluation would produce the same result. +void tst_qqmllanguage::scriptStringComparison() +{ + QQmlComponent component1(&engine, testFileUrl("scriptString.qml")); + QVERIFY(!component1.isError() && component1.errors().isEmpty()); + MyTypeObject *object1 = qobject_cast(component1.create()); + QVERIFY(object1 != 0); + + QQmlComponent component2(&engine, testFileUrl("scriptString2.qml")); + QVERIFY(!component2.isError() && component2.errors().isEmpty()); + MyTypeObject *object2 = qobject_cast(component2.create()); + QVERIFY(object2 != 0); + + QQmlComponent component3(&engine, testFileUrl("scriptString3.qml")); + QVERIFY(!component3.isError() && component3.errors().isEmpty()); + MyTypeObject *object3 = qobject_cast(component3.create()); + QVERIFY(object3 != 0); + + //QJSValue inst1 = engine.newQObject(object1); + QJSValue inst2 = engine.newQObject(object2); + QJSValue inst3 = engine.newQObject(object3); + QJSValue func = engine.evaluate("function(value) { this.scriptProperty = value }"); + + const QString s = "hello\\n\\\"world\\\""; + const qreal n = 12.345; + bool ok; + + QVERIFY(object2->scriptProperty().stringLiteral() == s); + QVERIFY(object3->scriptProperty().numberLiteral(&ok) == n && ok); + QVERIFY(object1->scriptProperty() == object1->scriptProperty()); + QVERIFY(object2->scriptProperty() == object2->scriptProperty()); + QVERIFY(object3->scriptProperty() == object3->scriptProperty()); + QVERIFY(object2->scriptProperty() != object3->scriptProperty()); + QVERIFY(object1->scriptProperty() != object2->scriptProperty()); + QVERIFY(object1->scriptProperty() != object3->scriptProperty()); + + func.callWithInstance(inst2, QJSValueList() << n); + QVERIFY(object2->scriptProperty() == object3->scriptProperty()); + + func.callWithInstance(inst2, QJSValueList() << s); + QVERIFY(object2->scriptProperty() != object3->scriptProperty()); + func.callWithInstance(inst3, QJSValueList() << s); + QVERIFY(object2->scriptProperty() == object3->scriptProperty()); + + func.callWithInstance(inst2, QJSValueList() << QJSValue::UndefinedValue); + QVERIFY(object2->scriptProperty() != object3->scriptProperty()); + func.callWithInstance(inst3, QJSValueList() << QJSValue::UndefinedValue); + QVERIFY(object2->scriptProperty() == object3->scriptProperty()); + + func.callWithInstance(inst2, QJSValueList() << QJSValue::NullValue); + QVERIFY(object2->scriptProperty() != object3->scriptProperty()); + func.callWithInstance(inst3, QJSValueList() << QJSValue::NullValue); + QVERIFY(object2->scriptProperty() == object3->scriptProperty()); + + func.callWithInstance(inst2, QJSValueList() << false); + QVERIFY(object2->scriptProperty() != object3->scriptProperty()); + func.callWithInstance(inst3, QJSValueList() << false); + QVERIFY(object2->scriptProperty() == object3->scriptProperty()); + + func.callWithInstance(inst2, QJSValueList() << true); + QVERIFY(object2->scriptProperty() != object3->scriptProperty()); + func.callWithInstance(inst3, QJSValueList() << true); + QVERIFY(object2->scriptProperty() == object3->scriptProperty()); + + QVERIFY(object1->scriptProperty() != object2->scriptProperty()); + object2->setScriptProperty(object1->scriptProperty()); + QVERIFY(object1->scriptProperty() == object2->scriptProperty()); + + QVERIFY(object1->scriptProperty() != object3->scriptProperty()); + func.callWithInstance(inst3, QJSValueList() << engine.toScriptValue(object1->scriptProperty())); + QVERIFY(object1->scriptProperty() == object3->scriptProperty()); + + // While this are two instances of the same object they are still considered different + // because the (none literal) script string may access variables which have different + // values in both instances and hence evaluated to different results. + MyTypeObject *object1_2 = qobject_cast(component1.create()); + QVERIFY(object1_2 != 0); + QVERIFY(object1->scriptProperty() != object1_2->scriptProperty()); +} + // Check that default property assignments are correctly spliced into explicit // property assignments void tst_qqmllanguage::defaultPropertyListOrder() diff --git a/tests/auto/qmltest/statemachine/tst_guardcondition.qml b/tests/auto/qmltest/statemachine/tst_guardcondition.qml new file mode 100644 index 0000000..71171b2 --- /dev/null +++ b/tests/auto/qmltest/statemachine/tst_guardcondition.qml @@ -0,0 +1,109 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Ford Motor Company +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQml.StateMachine 1.0 +import QtTest 1.0 + +TestCase { + id: testCase + StateMachine { + id: machine + initialState: startState + StateBase { + id: startState + SignalTransition { + id: signalTrans + signal: testCase.mysignal + guard: mystr == "test1" + targetState: finalState + } + } + FinalState { + id: finalState + } + } + + SignalSpy { + id: finalStateActive + target: finalState + signalName: "activeChanged" + } + + signal mysignal(string mystr, bool mybool, int myint) + + name: "testGuardCondition" + function test_guardCondition() + { + // Start statemachine, should not have reached finalState yet. + machine.start() + tryCompare(finalStateActive, "count", 0) + tryCompare(machine, "running", true) + + // Emit the signalTrans.signal which will evaluate the guard. The + // guard should return true, finalState be reached and the + // statemachine be stopped. + testCase.mysignal("test1", true, 2) + tryCompare(finalStateActive, "count", 1) + tryCompare(machine, "running", false) + + // Restart machine. + machine.start() + tryCompare(machine, "running", true) + tryCompare(finalStateActive, "count", 2) + + // Emit signal that makes the signalTrans.guard return false. The + // finalState should not have been triggered. + testCase.mysignal("test2", true, 2) + tryCompare(finalStateActive, "count", 2) + tryCompare(machine, "running", true) + + // Change the guard in javascript to test that boolean true/false + // works as expected. + signalTrans.guard = false; + testCase.mysignal("test1", true, 2) + tryCompare(finalStateActive, "count", 2) + tryCompare(machine, "running", true) + signalTrans.guard = true; + testCase.mysignal("test1", true, 2) + tryCompare(finalStateActive, "count", 3) + tryCompare(machine, "running", false) + } +} -- 2.7.4