diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54df889..165537a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,8 +13,9 @@ jobs: matrix: os: [ubuntu-latest, ubuntu-20.04, windows-latest, macos-latest, macos-10.15] build_type: ['Release', 'Debug'] - shared_libs: ['ON', 'OFF'] qt_version: [[5, 12, 12], [5, 15, 2], [6, 2, 3]] + shared_libs: ['ON', 'OFF'] + tests: ['ON', 'OFF'] include: - os: ubuntu-latest triplet: 'x64-linux' @@ -35,7 +36,12 @@ jobs: triplet: 'x64-osx' cmake_flags: '' exclude: - # Disabled until https://github.com/sgieseking/anyrpc/pull/43 is in place + # tests won't build with shared libs due to private symbols + - shared_libs: 'ON' + tests: 'ON' + - shared_libs: 'OFF' + tests: 'OFF' + # Disabled until https://github.com/sgieseking/anyrpc/pull/43 is in place - os: windows-latest shared_libs: 'ON' steps: @@ -65,7 +71,7 @@ jobs: - name: "Configure" run: | mkdir build - cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DSPIX_BUILD_TESTS=ON -DSPIX_BUILD_EXAMPLES=ON ${{ matrix.cmake_flags}} -DBUILD_SHARED_LIBS=${{ matrix.shared_libs }} -DSPIX_QT_MAJOR=${{ matrix.qt_version[0] }} . + cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DSPIX_BUILD_TESTS=${{ matrix.tests }} -DSPIX_BUILD_EXAMPLES=ON ${{ matrix.cmake_flags }} -DBUILD_SHARED_LIBS=${{ matrix.shared_libs }} -DSPIX_QT_MAJOR=${{ matrix.qt_version[0] }} . - name: "Print cmake compile commands" if: ${{ !contains(matrix.os, 'windows') }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aea3f6..d104c9a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ option(SPIX_BUILD_TESTS "Build Spix unit tests." OFF) set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake/modules") -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) # Hide symbols unless explicitly flagged with SPIX_EXPORT set(CMAKE_CXX_VISIBILITY_PRESET hidden) diff --git a/README.md b/README.md index c7f839e..5221325 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ generate and update screenshots for your documentation. * Enter text * Check existence and visibility of items * Get property values of items (text, position, color, ...) +* Invoke a method on an object * Take and save a screenshot * Quit the app * Remote control, also of embedded devices / iOS @@ -146,7 +147,7 @@ resultText = s.getStringProperty("root/results", "text") You can also use the XMLRPC client to list the available methods. The complete list of methods are also available in the [source](lib/src/AnyRpcServer.cpp). ```python print(s.system.listMethods()) -# ['command', 'enterKey', 'existsAndVisible', 'getBoundingBox', 'getErrors', 'getStringProperty', 'inputText', 'mouseBeginDrag', 'mouseClick', 'mouseDropUrls', 'mouseEndDrag', 'quit', 'setStringProperty', 'system.listMethods', 'system.methodHelp', 'takeScreenshot', 'wait'] +# ['command', 'enterKey', 'existsAndVisible', 'getBoundingBox', 'getErrors', 'getStringProperty', 'inputText', 'invokeMethod', 'mouseBeginDrag', 'mouseClick', 'mouseDropUrls', 'mouseEndDrag', 'quit', 'setStringProperty', 'system.listMethods', 'system.methodHelp', 'takeScreenshot', 'wait'] print(s.system.methodHelp('mouseClick')) # Click on the object at the given path ``` @@ -168,6 +169,67 @@ More specifically, Spix's matching processes works as follows: * `` matches a top-level [`QQuickWindow`](https://doc-snapshots.qt.io/qt6-dev/qquickwindow.html) whose `objectName` (or `id` if `objectName` is empty) matches the specified string. Top-level windows are enumerated by [`QGuiApplication::topLevelWindows`](https://doc.qt.io/qt-6/qguiapplication.html#topLevelWindows). * `` matches the first child object whose `objectName` (or `id` if `objectName` is empty) matches the specified string using a recursive search of all children and subchildren of the root. This process repeats for every subsequent child path entry. +### Invoking QML methods + +Spix can directly invoke both internal and custom methods in QML objects: this can be a handy way to automate interactions that Spix doesn't support normally. For example, we can control the cursor in a `TextArea` by calling [`TextArea.select`](https://doc-snapshots.qt.io/qt6-6.2/qml-qtquick-textedit.html#select-method): +```qml +TextArea { + id: textArea +} +``` +```python +# select characters 100-200 +s.invokeMethod("root/textArea", "select", [100, 200]) +``` + +In addition, you can use custom functions in the QML to implement more complicated interactions, and have Spix interact with the function: +```qml +TextArea { + id: textArea + function customFunction(arg1, arg2) { + // insert QML interactions here + return {'key1': true, 'key2': false} + } +} +``` +```python +# invoke the custom function +result = s.invokeMethod("root/textArea", "customFunction", ['a string', 34]) +# prints {'key1': True, 'key2': False} +print(result) +``` + +Spix supports the following types as arguments/return values: +| Python Type | XMLRPC Type | QML Type(s) | JavaScript Type(s)| Notes | +|-------------------|----------------------|-----------------|-------------------|--------------------------------------------------| +| int | \ | int | number | Values over/under int max are upcasted to double | +| bool | \ | bool | boolean | | +| str | \ | string | string | | +| float | \ | double, real | number | Defaults to double | +| datetime.datetime | \ | date | Date | No timezone support (always uses local timezone) | +| dict | \ | var | object | String keys only | +| list | \ | var | Array | | +| None | no type | null, undefined | object, undefined | Defaults to null | | + +In general Spix will attempt to coerce the arguments and return value to the correct types to match the method being invoked. Valid conversion are listed under the [`QVariant` docs](https://doc.qt.io/qt-5/qvariant.html#canConvert). If Spix cannot find a valid conversion it will generate an error. +```qml +Item { + id: item + function test(arg1: bool) { + ... + } +} +``` +```python +# ok +s.invokeMethod("root/item", "test", [False]) + +# argument will implicitly be converted to a boolean (True) to match the declaration type +s.invokeMethod("root/item", "test", [34]) + +# no conversion from object to boolean, so an error is thrown +s.invokeMethod("root/item", "test", [{}]) +``` ## Two modes of operation In general, Spix can be used in two ways, which are different in how events are generated and sent @@ -176,7 +238,7 @@ to your application: ### Generate Qt events directly You can use Spix to directly create Qt events, either from C++ as a unit test, or from an external script via the network through RPC. Since the Qt events are generated directly inside the -app, and do not come from the system, the mouse coursor will not actually move and interaction +app, and do not come from the system, the mouse cursor will not actually move and interaction with other applications is limited. On the plus side, this mechanism is independent from the system your app is running on and can easily be used to control software on an embedded device via the network (RPC). diff --git a/examples/Basic/CMakeLists.txt b/examples/Basic/CMakeLists.txt index 23885d3..8018713 100644 --- a/examples/Basic/CMakeLists.txt +++ b/examples/Basic/CMakeLists.txt @@ -1,7 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED) diff --git a/examples/BasicStandalone/CMakeLists.txt b/examples/BasicStandalone/CMakeLists.txt index 13dfeb7..9e0d296 100644 --- a/examples/BasicStandalone/CMakeLists.txt +++ b/examples/BasicStandalone/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../../cmake/modules") set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # diff --git a/examples/GTest/CMakeLists.txt b/examples/GTest/CMakeLists.txt index 385bcdf..8225d32 100644 --- a/examples/GTest/CMakeLists.txt +++ b/examples/GTest/CMakeLists.txt @@ -1,7 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") diff --git a/examples/ListGridView/CMakeLists.txt b/examples/ListGridView/CMakeLists.txt index a253032..e6d288a 100644 --- a/examples/ListGridView/CMakeLists.txt +++ b/examples/ListGridView/CMakeLists.txt @@ -1,7 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") diff --git a/examples/RemoteCtrl/CMakeLists.txt b/examples/RemoteCtrl/CMakeLists.txt index 1675293..b59d021 100644 --- a/examples/RemoteCtrl/CMakeLists.txt +++ b/examples/RemoteCtrl/CMakeLists.txt @@ -1,7 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") diff --git a/examples/RepeaterLoader/CMakeLists.txt b/examples/RepeaterLoader/CMakeLists.txt index 5df7ef5..434d2f6 100644 --- a/examples/RepeaterLoader/CMakeLists.txt +++ b/examples/RepeaterLoader/CMakeLists.txt @@ -1,7 +1,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against") diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 723de5e..7a270d3 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -54,6 +54,8 @@ set(SOURCES src/Commands/GetTestStatus.h src/Commands/InputText.cpp src/Commands/InputText.h + src/Commands/InvokeMethod.cpp + src/Commands/InvokeMethod.h src/Commands/Quit.cpp src/Commands/Quit.h src/Commands/Screenshot.cpp @@ -92,8 +94,10 @@ set(SOURCES src/Scene/Qt/QtScene.cpp src/Scene/Qt/QtScene.h src/Scene/Scene.h - + + src/Utils/AnyRpcUtils.cpp src/Utils/AnyRpcUtils.h + src/Utils/AnyRpcFunction.h src/Utils/DebugDump.cpp src/Utils/DebugDump.h src/Utils/QtEventRecorder.cpp diff --git a/lib/include/Spix/Data/Variant.h b/lib/include/Spix/Data/Variant.h new file mode 100644 index 0000000..3e0fd45 --- /dev/null +++ b/lib/include/Spix/Data/Variant.h @@ -0,0 +1,61 @@ +/*** + * Copyright (C) Noah Koontz. All rights reserved. + * Licensed under the MIT license. + * See LICENSE.txt file in the project root for full license information. + ****/ + +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace spix { + +struct Variant; + +namespace { +using VariantBaseType = std::variant, std::vector, std::map>; +} + +/** + * Utility union type that contains a number of RPC-able types, including a list of itself and a map of {std::string: + * itself}. Inherits from std::variant. This variant is used to abstract between RPC union types (ex anyrpc::Value) the + * scene union types (ex. QVariant). + * + * NOTE: std::visit is broken for this variant for GCC <= 11.2 and clang <= 14.0, see + * http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2162r0.html for more info. Instead, type switches must be + * created manually using the index() method and the TypeIndex enum. + */ +struct SPIX_EXPORT Variant : VariantBaseType { + using ListType = std::vector; + using MapType = std::map; + using VariantType = VariantBaseType; + using VariantBaseType::variant; + VariantBaseType const& base() const { return *this; } + VariantBaseType& base() { return *this; } + + enum TypeIndex + { + Nullptr = 0, + Bool, + Int, + Uint, + Double, + String, + Time, + List, + Map, + TypeIndexCount + }; +}; + +static_assert( + Variant::TypeIndexCount == std::variant_size_v, "Variant enum does not cover all Variant types"); + +} // namespace spix diff --git a/lib/include/Spix/TestServer.h b/lib/include/Spix/TestServer.h index 65bd32b..f3da289 100644 --- a/lib/include/Spix/TestServer.h +++ b/lib/include/Spix/TestServer.h @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -57,6 +58,7 @@ class SPIX_EXPORT TestServer { std::string getStringProperty(ItemPath path, std::string propertyName); void setStringProperty(ItemPath path, std::string propertyName, std::string propertyValue); + Variant invokeMethod(ItemPath path, std::string method, std::vector args); Rect getBoundingBox(ItemPath path); bool existsAndVisible(ItemPath path); std::vector getErrors(); diff --git a/lib/src/AnyRpcServer.cpp b/lib/src/AnyRpcServer.cpp index 7e275f4..4551daf 100644 --- a/lib/src/AnyRpcServer.cpp +++ b/lib/src/AnyRpcServer.cpp @@ -5,8 +5,8 @@ ****/ #include -#include -#include +#include +#include #include namespace spix { @@ -70,6 +70,12 @@ AnyRpcServer::AnyRpcServer(int anyrpcPort) setStringProperty(std::move(path), std::move(property), std::move(value)); }); + utils::AddFunctionToAnyRpc)>(methodManager, "invokeMethod", + "Invoke a method on a QML object | invokeMethod(string path, string method, any[] args)", + [this](std::string path, std::string method, std::vector args) { + return invokeMethod(std::move(path), std::move(method), std::move(args)); + }); + utils::AddFunctionToAnyRpc(std::string)>(methodManager, "getBoundingBox", "Return the bounding box of an item in screen coordinates | getBoundingBox(string path) : (doubles) " "[topLeft.x, topLeft.y , width, height]", diff --git a/lib/src/Commands/InvokeMethod.cpp b/lib/src/Commands/InvokeMethod.cpp new file mode 100644 index 0000000..1ec0ad8 --- /dev/null +++ b/lib/src/Commands/InvokeMethod.cpp @@ -0,0 +1,39 @@ +/*** + * Copyright (C) Falko Axmann. All rights reserved. + * Licensed under the MIT license. + * See LICENSE.txt file in the project root for full license information. + ****/ + +#include "InvokeMethod.h" + +#include + +namespace spix { +namespace cmd { + +InvokeMethod::InvokeMethod(ItemPath path, std::string method, std::vector args, std::promise promise) +: m_path(std::move(path)) +, m_method(std::move(method)) +, m_args(std::move(args)) +, m_promise(std::move(promise)) +{ +} + +void InvokeMethod::execute(CommandEnvironment& env) +{ + auto item = env.scene().itemAtPath(m_path); + + if (item) { + Variant ret; + bool success = item->invokeMethod(m_method, m_args, ret); + if (!success) + env.state().reportError("InvokeMethod: Failed to invoke method: " + m_method); + m_promise.set_value(ret); + } else { + env.state().reportError("InvokeMethod: Item not found: " + m_path.string()); + m_promise.set_value(Variant(nullptr)); + } +} + +} // namespace cmd +} // namespace spix diff --git a/lib/src/Commands/InvokeMethod.h b/lib/src/Commands/InvokeMethod.h new file mode 100644 index 0000000..e269215 --- /dev/null +++ b/lib/src/Commands/InvokeMethod.h @@ -0,0 +1,34 @@ +/*** + * Copyright (C) Falko Axmann. All rights reserved. + * Licensed under the MIT license. + * See LICENSE.txt file in the project root for full license information. + ****/ + +#pragma once + +#include + +#include "Command.h" +#include +#include + +#include + +namespace spix { +namespace cmd { + +class SPIX_EXPORT InvokeMethod : public Command { +public: + InvokeMethod(ItemPath path, std::string method, std::vector args, std::promise promise); + + void execute(CommandEnvironment& env) override; + +private: + ItemPath m_path; + std::string m_method; + std::vector m_args; + std::promise m_promise; +}; + +} // namespace cmd +} // namespace spix diff --git a/lib/src/QtQmlBot.cpp b/lib/src/QtQmlBot.cpp index 46f8e57..b3f67cd 100644 --- a/lib/src/QtQmlBot.cpp +++ b/lib/src/QtQmlBot.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -24,6 +25,7 @@ QtQmlBot::~QtQmlBot() = default; void QtQmlBot::runTestServer(TestServer& server) { + qInfo() << "Spix server is enabled. Only use this in a safe environment."; server.setCommandExecuter(m_cmdExec.get()); server.start(); } diff --git a/lib/src/Scene/Item.h b/lib/src/Scene/Item.h index c76779d..0d3e887 100644 --- a/lib/src/Scene/Item.h +++ b/lib/src/Scene/Item.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include @@ -29,6 +30,7 @@ class Item { virtual Rect bounds() const = 0; virtual std::string stringProperty(const std::string& name) const = 0; virtual void setStringProperty(const std::string& name, const std::string& value) = 0; + virtual bool invokeMethod(const std::string& method, const std::vector& args, Variant& ret) = 0; virtual bool visible() const = 0; }; diff --git a/lib/src/Scene/Mock/MockItem.cpp b/lib/src/Scene/Mock/MockItem.cpp index e7a266d..1267a57 100644 --- a/lib/src/Scene/Mock/MockItem.cpp +++ b/lib/src/Scene/Mock/MockItem.cpp @@ -40,6 +40,12 @@ void MockItem::setStringProperty(const std::string& name, const std::string& val { } +bool MockItem::invokeMethod(const std::string& method, const std::vector& args, Variant& ret) +{ + ret = Variant(nullptr); + return true; +} + bool MockItem::visible() const { return true; diff --git a/lib/src/Scene/Mock/MockItem.h b/lib/src/Scene/Mock/MockItem.h index fa89092..09ef388 100644 --- a/lib/src/Scene/Mock/MockItem.h +++ b/lib/src/Scene/Mock/MockItem.h @@ -10,8 +10,6 @@ #include -#include - namespace spix { class SPIX_EXPORT MockItem : public Item { @@ -24,6 +22,7 @@ class SPIX_EXPORT MockItem : public Item { Rect bounds() const override; std::string stringProperty(const std::string& name) const override; void setStringProperty(const std::string& name, const std::string& value) override; + bool invokeMethod(const std::string& method, const std::vector& args, Variant& ret) override; bool visible() const override; // MockItem specials diff --git a/lib/src/Scene/Qt/QtItem.cpp b/lib/src/Scene/Qt/QtItem.cpp index cd364aa..45e0226 100644 --- a/lib/src/Scene/Qt/QtItem.cpp +++ b/lib/src/Scene/Qt/QtItem.cpp @@ -6,8 +6,11 @@ #include "QtItem.h" +#include #include +#include + namespace spix { QtItem::QtItem(QQuickItem* item) @@ -49,6 +52,33 @@ void QtItem::setStringProperty(const std::string& name, const std::string& value m_item->setProperty(name.c_str(), value.c_str()); } +bool QtItem::invokeMethod(const std::string& method, const std::vector& args, Variant& ret) +{ + if (args.size() > 10) + return false; + + std::vector qtVars; + for (auto arg : args) + qtVars.push_back(qt::VariantToQVariant(arg)); + + QMetaMethod match; + bool matched = spix::qt::GetMethodMetaForArgs(*m_item, method, qtVars, match); + if (!matched) + return false; + + qt::QMLReturnVariant retVar; + QGenericReturnArgument retArg = qt::GetReturnArgForQMetaType(match.returnType(), retVar); + std::vector qtArgs = qt::ConvertAndCreateQArgumentsForMethod(match, qtVars); + + bool success = match.invoke(m_item, Qt::ConnectionType::DirectConnection, retArg, qtArgs[0], qtArgs[1], qtArgs[2], + qtArgs[3], qtArgs[4], qtArgs[5], qtArgs[6], qtArgs[7], qtArgs[8], qtArgs[9]); + if (success) { + ret = qt::QMLReturnVariantToVariant(retVar); + return true; + } + return false; +} + bool QtItem::visible() const { return m_item->isVisible(); diff --git a/lib/src/Scene/Qt/QtItem.h b/lib/src/Scene/Qt/QtItem.h index 134e1a2..1ee4711 100644 --- a/lib/src/Scene/Qt/QtItem.h +++ b/lib/src/Scene/Qt/QtItem.h @@ -22,6 +22,7 @@ class QtItem : public Item { Rect bounds() const override; std::string stringProperty(const std::string& name) const override; void setStringProperty(const std::string& name, const std::string& value) override; + bool invokeMethod(const std::string& method, const std::vector& args, Variant& ret) override; bool visible() const override; QQuickItem* qquickitem(); diff --git a/lib/src/Scene/Qt/QtItemTools.cpp b/lib/src/Scene/Qt/QtItemTools.cpp index ef54c33..d42dbe1 100644 --- a/lib/src/Scene/Qt/QtItemTools.cpp +++ b/lib/src/Scene/Qt/QtItemTools.cpp @@ -6,8 +6,10 @@ #include "QtItemTools.h" +#include #include #include +#include namespace spix { namespace qt { @@ -85,5 +87,214 @@ QObject* FindChildItem(QObject* object, const QString& name) return nullptr; } +QGenericReturnArgument GetReturnArgForQMetaType(int type, QMLReturnVariant& retVar) +{ + switch (type) { + case QMetaType::Type::Void: + retVar = nullptr; + return QGenericReturnArgument(); + case QMetaType::Type::Bool: + retVar = bool(); + return Q_RETURN_ARG(bool, std::get(retVar)); + case QMetaType::Type::Int: + retVar = int(); + return Q_RETURN_ARG(int, std::get(retVar)); + case QMetaType::Type::Float: + retVar = float(); + return Q_RETURN_ARG(float, std::get(retVar)); + case QMetaType::Type::Double: + retVar = double(); + return Q_RETURN_ARG(double, std::get(retVar)); + case QMetaType::Type::QString: + retVar = QString(); + return Q_RETURN_ARG(QString, std::get(retVar)); + case QMetaType::Type::QDateTime: + retVar = QDateTime(); + return Q_RETURN_ARG(QDateTime, std::get(retVar)); + default: + retVar = QVariant(); + return Q_RETURN_ARG(QVariant, std::get(retVar)); + } +} + +QVariant VariantToQVariant(const Variant& var) +{ + static_assert(Variant::TypeIndexCount == 9, "VariantToQVariant does not cover all Variant types"); + + switch (var.index()) { + case Variant::Nullptr: + return QVariant(); + case Variant::Bool: + return QVariant(std::get(var)); + case Variant::Int: + return QVariant(std::get(var)); + case Variant::Uint: + return QVariant(std::get(var)); + case Variant::Double: + return QVariant(std::get(var)); + case Variant::String: + return QVariant(QString::fromStdString(std::get(var))); + case Variant::Time: { + auto time = std::get>(var); + std::time_t timet = std::chrono::system_clock::to_time_t(time); + return QVariant(QDateTime::fromSecsSinceEpoch(timet)); + } + case Variant::List: { + QVariantList list; + for (const auto& elem : std::get(var)) + list.push_back(VariantToQVariant(elem)); + return QVariant(list); + } + case Variant::Map: { + QVariantMap map; + for (const auto& [key, value] : std::get(var)) + map.insert(QString::fromStdString(key), VariantToQVariant(value)); + return QVariant(map); + } + default: + throw std::runtime_error("VariantToQVariant received Variant with unknown type"); + } +} + +Variant QVariantToVariant(const QVariant& var) +{ + // in Qt5 QJSValue is a user type + int varType = var.userType(); + switch (static_cast(varType)) { + case QMetaType::Type::Bool: + return Variant(var.toBool()); + case QMetaType::Type::Char: + case QMetaType::Type::SChar: + case QMetaType::Type::Short: + case QMetaType::Type::Int: + case QMetaType::Type::Long: + case QMetaType::Type::LongLong: + return Variant(var.toLongLong()); + case QMetaType::Type::UChar: + case QMetaType::Type::UShort: + case QMetaType::Type::UInt: + case QMetaType::Type::ULong: + case QMetaType::Type::ULongLong: + return Variant(var.toULongLong()); + case QMetaType::Type::Float: + case QMetaType::Type::Double: + return Variant(var.toDouble()); + case QMetaType::Type::QDateTime: { + std::time_t time = var.toDateTime().toSecsSinceEpoch(); + return Variant(std::chrono::system_clock::from_time_t(time)); + } + case QMetaType::Type::QString: + return Variant(var.toString().toStdString()); + case QMetaType::Type::Nullptr: + case QMetaType::Type::Void: + case QMetaType::Type::UnknownType: + return Variant(nullptr); + default: + break; + } + + if (varType == qMetaTypeId()) { + QJSValue jsval = var.value(); + return QVariantToVariant(jsval.toVariant()); + } + + if (var.canConvert(QMetaType::Type::QVariantList)) { + const QVariantList& list = var.toList(); + Variant::ListType ret; + for (const QVariant& elem : list) { + Variant convertedElem = QVariantToVariant(elem); + ret.push_back(convertedElem); + } + return Variant(ret); + } + + if (var.canConvert(QMetaType::Type::QVariantMap)) { + const QVariantMap& map = var.toMap(); + Variant::MapType ret; + for (auto ptr = map.constBegin(); ptr != map.constEnd(); ptr++) { + Variant convertedElem = QVariantToVariant(ptr.value()); + ret[ptr.key().toStdString()] = convertedElem; + } + return Variant(ret); + } + + if (var.canConvert(QMetaType::Type::QString)) { + return Variant(var.toString().toStdString()); + } + + std::string error = std::string("QVariantToVariant: Received unknown type for return value: ") + var.typeName(); + throw std::runtime_error(error); +} + +Variant QMLReturnVariantToVariant(const QMLReturnVariant& var) +{ + return std::visit( + [](auto&& arg) { + using T = std::decay_t; + + if constexpr (std::is_integral_v && !std::is_same_v) { + return Variant(static_cast(arg)); + } else if constexpr (std::is_unsigned_v && !std::is_same_v) { + return Variant(static_cast(arg)); + } else if constexpr (std::is_floating_point_v) { + return Variant(static_cast(arg)); + } else if constexpr (std::is_same_v) { + return Variant(arg.toStdString()); + } else if constexpr (std::is_same_v) { + return Variant(std::chrono::system_clock::from_time_t(arg.toLocalTime().toSecsSinceEpoch())); + } else if constexpr (std::is_same_v) { + return QVariantToVariant(arg); + } else { + return Variant(arg); + } + }, + var); +} + +bool CanConvertArgTypes(const QMetaMethod& metaMethod, const std::vector& varargs) +{ + if (metaMethod.parameterCount() != varargs.size()) + return false; + for (size_t i = 0; i < metaMethod.parameterCount(); i++) { + int targetType = metaMethod.parameterType(i); + if (targetType != QMetaType::Type::QVariant && !varargs[i].canConvert(targetType)) + return false; + } + return true; +} + +bool GetMethodMetaForArgs( + const QObject& obj, const std::string& method, const std::vector& varargs, QMetaMethod& ret) +{ + const QMetaObject* itemMeta = obj.metaObject(); + for (size_t i = 0; i < itemMeta->methodCount(); i++) { + const QMetaMethod methodMeta = itemMeta->method(i); + if (methodMeta.name().compare(method.data()) == 0 && CanConvertArgTypes(methodMeta, varargs)) { + ret = methodMeta; + return true; + } + } + return false; +} + +std::vector ConvertAndCreateQArgumentsForMethod( + const QMetaMethod& metaMethod, std::vector& varargs) +{ + std::vector qtArgs; + for (size_t i = 0; i < 10; i++) { + if (i < varargs.size()) { + int targetType = metaMethod.parameterType(i); + if (targetType != QMetaType::Type::QVariant) { + varargs[i].convert(targetType); + qtArgs.push_back(QGenericArgument(varargs[i].typeName(), varargs[i].data())); + } else { + qtArgs.push_back(Q_ARG(QVariant, varargs[i])); + } + } else + qtArgs.push_back(QGenericArgument()); + } + return qtArgs; +} + } // namespace qt } // namespace spix diff --git a/lib/src/Scene/Qt/QtItemTools.h b/lib/src/Scene/Qt/QtItemTools.h index e125f87..dce53ef 100644 --- a/lib/src/Scene/Qt/QtItemTools.h +++ b/lib/src/Scene/Qt/QtItemTools.h @@ -6,8 +6,12 @@ #pragma once +#include + +#include #include #include +#include class QString; @@ -37,5 +41,18 @@ T FindChildItem(QObject* object, const QString& name) return qobject_cast(FindChildItem(object, name)); } +using QMLReturnVariant = std::variant; +QGenericReturnArgument GetReturnArgForQMetaType(int type, QMLReturnVariant& toInitialize); + +QVariant VariantToQVariant(const Variant& var); +Variant QVariantToVariant(const QVariant& var); +Variant QMLReturnVariantToVariant(const QMLReturnVariant& var); + +bool CanConvertArgTypes(const QMetaMethod& metaMethod, const std::vector& varargs); +bool GetMethodMetaForArgs( + const QObject& obj, const std::string& method, const std::vector& varargs, QMetaMethod& ret); +std::vector ConvertAndCreateQArgumentsForMethod( + const QMetaMethod& metaMethod, std::vector& varargs); + } // namespace qt } // namespace spix diff --git a/lib/src/TestServer.cpp b/lib/src/TestServer.cpp index c2ccc84..45eee1e 100644 --- a/lib/src/TestServer.cpp +++ b/lib/src/TestServer.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -119,6 +120,16 @@ void TestServer::setStringProperty(ItemPath path, std::string propertyName, std: m_cmdExec->enqueueCommand(path, std::move(propertyName), std::move(propertyValue)); } +Variant TestServer::invokeMethod(ItemPath path, std::string method, std::vector args) +{ + std::promise promise; + auto result = promise.get_future(); + auto cmd = std::make_unique(path, std::move(method), std::move(args), std::move(promise)); + m_cmdExec->enqueueCommand(std::move(cmd)); + + return result.get(); +} + Rect TestServer::getBoundingBox(ItemPath path) { std::promise promise; diff --git a/lib/src/Utils/AnyRpcFunction.h b/lib/src/Utils/AnyRpcFunction.h new file mode 100644 index 0000000..e3870be --- /dev/null +++ b/lib/src/Utils/AnyRpcFunction.h @@ -0,0 +1,190 @@ +/*** + * Copyright (C) Falko Axmann. All rights reserved. + * Licensed under the MIT license. + * See LICENSE.txt file in the project root for full license information. + ****/ + +#pragma once + +#include +#include +#include +#include + +/** + * Utility type traits + */ + +namespace { +template class Template> +struct is_specialization : std::false_type { +}; + +template