Skip to content

Commit 4de4407

Browse files
add the ability to invoke QML functions (#56)
1 parent 368793a commit 4de4407

33 files changed

+1870
-251
lines changed

.github/workflows/build.yml

+9-3
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ jobs:
1313
matrix:
1414
os: [ubuntu-latest, ubuntu-20.04, windows-latest, macos-latest, macos-10.15]
1515
build_type: ['Release', 'Debug']
16-
shared_libs: ['ON', 'OFF']
1716
qt_version: [[5, 12, 12], [5, 15, 2], [6, 2, 3]]
17+
shared_libs: ['ON', 'OFF']
18+
tests: ['ON', 'OFF']
1819
include:
1920
- os: ubuntu-latest
2021
triplet: 'x64-linux'
@@ -35,7 +36,12 @@ jobs:
3536
triplet: 'x64-osx'
3637
cmake_flags: ''
3738
exclude:
38-
# Disabled until https://github.com/sgieseking/anyrpc/pull/43 is in place
39+
# tests won't build with shared libs due to private symbols
40+
- shared_libs: 'ON'
41+
tests: 'ON'
42+
- shared_libs: 'OFF'
43+
tests: 'OFF'
44+
# Disabled until https://github.com/sgieseking/anyrpc/pull/43 is in place
3945
- os: windows-latest
4046
shared_libs: 'ON'
4147
steps:
@@ -65,7 +71,7 @@ jobs:
6571
- name: "Configure"
6672
run: |
6773
mkdir build
68-
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] }} .
74+
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] }} .
6975
7076
- name: "Print cmake compile commands"
7177
if: ${{ !contains(matrix.os, 'windows') }}

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ option(SPIX_BUILD_TESTS "Build Spix unit tests." OFF)
66
set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against")
77

88
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_LIST_DIR}/cmake/modules")
9-
set(CMAKE_CXX_STANDARD 14)
9+
set(CMAKE_CXX_STANDARD 17)
1010

1111
# Hide symbols unless explicitly flagged with SPIX_EXPORT
1212
set(CMAKE_CXX_VISIBILITY_PRESET hidden)

README.md

+64-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ generate and update screenshots for your documentation.
5454
* Enter text
5555
* Check existence and visibility of items
5656
* Get property values of items (text, position, color, ...)
57+
* Invoke a method on an object
5758
* Take and save a screenshot
5859
* Quit the app
5960
* Remote control, also of embedded devices / iOS
@@ -146,7 +147,7 @@ resultText = s.getStringProperty("root/results", "text")
146147
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).
147148
```python
148149
print(s.system.listMethods())
149-
# ['command', 'enterKey', 'existsAndVisible', 'getBoundingBox', 'getErrors', 'getStringProperty', 'inputText', 'mouseBeginDrag', 'mouseClick', 'mouseDropUrls', 'mouseEndDrag', 'quit', 'setStringProperty', 'system.listMethods', 'system.methodHelp', 'takeScreenshot', 'wait']
150+
# ['command', 'enterKey', 'existsAndVisible', 'getBoundingBox', 'getErrors', 'getStringProperty', 'inputText', 'invokeMethod', 'mouseBeginDrag', 'mouseClick', 'mouseDropUrls', 'mouseEndDrag', 'quit', 'setStringProperty', 'system.listMethods', 'system.methodHelp', 'takeScreenshot', 'wait']
150151
print(s.system.methodHelp('mouseClick'))
151152
# Click on the object at the given path
152153
```
@@ -168,6 +169,67 @@ More specifically, Spix's matching processes works as follows:
168169
* `<root>` 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).
169170
* `<child>` 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.
170171

172+
### Invoking QML methods
173+
174+
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):
175+
```qml
176+
TextArea {
177+
id: textArea
178+
}
179+
```
180+
```python
181+
# select characters 100-200
182+
s.invokeMethod("root/textArea", "select", [100, 200])
183+
```
184+
185+
In addition, you can use custom functions in the QML to implement more complicated interactions, and have Spix interact with the function:
186+
```qml
187+
TextArea {
188+
id: textArea
189+
function customFunction(arg1, arg2) {
190+
// insert QML interactions here
191+
return {'key1': true, 'key2': false}
192+
}
193+
}
194+
```
195+
```python
196+
# invoke the custom function
197+
result = s.invokeMethod("root/textArea", "customFunction", ['a string', 34])
198+
# prints {'key1': True, 'key2': False}
199+
print(result)
200+
```
201+
202+
Spix supports the following types as arguments/return values:
203+
| Python Type | XMLRPC Type | QML Type(s) | JavaScript Type(s)| Notes |
204+
|-------------------|----------------------|-----------------|-------------------|--------------------------------------------------|
205+
| int | \<int\> | int | number | Values over/under int max are upcasted to double |
206+
| bool | \<boolean\> | bool | boolean | |
207+
| str | \<string\> | string | string | |
208+
| float | \<double\> | double, real | number | Defaults to double |
209+
| datetime.datetime | \<dateTime.iso8601\> | date | Date | No timezone support (always uses local timezone) |
210+
| dict | \<struct\> | var | object | String keys only |
211+
| list | \<array\> | var | Array | |
212+
| None | no type | null, undefined | object, undefined | Defaults to null | |
213+
214+
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.
215+
```qml
216+
Item {
217+
id: item
218+
function test(arg1: bool) {
219+
...
220+
}
221+
}
222+
```
223+
```python
224+
# ok
225+
s.invokeMethod("root/item", "test", [False])
226+
227+
# argument will implicitly be converted to a boolean (True) to match the declaration type
228+
s.invokeMethod("root/item", "test", [34])
229+
230+
# no conversion from object to boolean, so an error is thrown
231+
s.invokeMethod("root/item", "test", [{}])
232+
```
171233

172234
## Two modes of operation
173235
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:
176238
### Generate Qt events directly
177239
You can use Spix to directly create Qt events, either from C++ as a unit test, or from
178240
an external script via the network through RPC. Since the Qt events are generated directly inside the
179-
app, and do not come from the system, the mouse coursor will not actually move and interaction
241+
app, and do not come from the system, the mouse cursor will not actually move and interaction
180242
with other applications is limited. On the plus side, this mechanism is independent from
181243
the system your app is running on and can easily be used to control software on an embedded
182244
device via the network (RPC).

examples/Basic/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
set(CMAKE_INCLUDE_CURRENT_DIR ON)
22
set(CMAKE_AUTOMOC ON)
33
set(CMAKE_AUTORCC ON)
4-
set(CMAKE_CXX_STANDARD 11)
4+
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

77
find_package(Qt${SPIX_QT_MAJOR} COMPONENTS Core Quick REQUIRED)

examples/BasicStandalone/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../../cmake/modules")
88
set(CMAKE_INCLUDE_CURRENT_DIR ON)
99
set(CMAKE_AUTOMOC ON)
1010
set(CMAKE_AUTORCC ON)
11-
set(CMAKE_CXX_STANDARD 11)
11+
set(CMAKE_CXX_STANDARD 17)
1212
set(CMAKE_CXX_STANDARD_REQUIRED ON)
1313

1414
#

examples/GTest/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
set(CMAKE_INCLUDE_CURRENT_DIR ON)
22
set(CMAKE_AUTOMOC ON)
33
set(CMAKE_AUTORCC ON)
4-
set(CMAKE_CXX_STANDARD 11)
4+
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

77
set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against")

examples/ListGridView/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
set(CMAKE_INCLUDE_CURRENT_DIR ON)
22
set(CMAKE_AUTOMOC ON)
33
set(CMAKE_AUTORCC ON)
4-
set(CMAKE_CXX_STANDARD 11)
4+
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

77
set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against")

examples/RemoteCtrl/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
set(CMAKE_INCLUDE_CURRENT_DIR ON)
22
set(CMAKE_AUTOMOC ON)
33
set(CMAKE_AUTORCC ON)
4-
set(CMAKE_CXX_STANDARD 11)
4+
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

77
set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against")

examples/RepeaterLoader/CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
set(CMAKE_INCLUDE_CURRENT_DIR ON)
22
set(CMAKE_AUTOMOC ON)
33
set(CMAKE_AUTORCC ON)
4-
set(CMAKE_CXX_STANDARD 11)
4+
set(CMAKE_CXX_STANDARD 17)
55
set(CMAKE_CXX_STANDARD_REQUIRED ON)
66

77
set(SPIX_QT_MAJOR "6" CACHE STRING "Major Qt version to build Spix against")

lib/CMakeLists.txt

+5-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ set(SOURCES
5454
src/Commands/GetTestStatus.h
5555
src/Commands/InputText.cpp
5656
src/Commands/InputText.h
57+
src/Commands/InvokeMethod.cpp
58+
src/Commands/InvokeMethod.h
5759
src/Commands/Quit.cpp
5860
src/Commands/Quit.h
5961
src/Commands/Screenshot.cpp
@@ -92,8 +94,10 @@ set(SOURCES
9294
src/Scene/Qt/QtScene.cpp
9395
src/Scene/Qt/QtScene.h
9496
src/Scene/Scene.h
95-
97+
98+
src/Utils/AnyRpcUtils.cpp
9699
src/Utils/AnyRpcUtils.h
100+
src/Utils/AnyRpcFunction.h
97101
src/Utils/DebugDump.cpp
98102
src/Utils/DebugDump.h
99103
src/Utils/QtEventRecorder.cpp

lib/include/Spix/Data/Variant.h

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/***
2+
* Copyright (C) Noah Koontz. All rights reserved.
3+
* Licensed under the MIT license.
4+
* See LICENSE.txt file in the project root for full license information.
5+
****/
6+
7+
#pragma once
8+
9+
#include <chrono>
10+
#include <map>
11+
#include <string>
12+
#include <variant>
13+
#include <vector>
14+
15+
#include <Spix/spix_export.h>
16+
17+
namespace spix {
18+
19+
struct Variant;
20+
21+
namespace {
22+
using VariantBaseType = std::variant<std::nullptr_t, bool, long long, unsigned long long, double, std::string,
23+
std::chrono::time_point<std::chrono::system_clock>, std::vector<Variant>, std::map<std::string, Variant>>;
24+
}
25+
26+
/**
27+
* Utility union type that contains a number of RPC-able types, including a list of itself and a map of {std::string:
28+
* itself}. Inherits from std::variant. This variant is used to abstract between RPC union types (ex anyrpc::Value) the
29+
* scene union types (ex. QVariant).
30+
*
31+
* NOTE: std::visit is broken for this variant for GCC <= 11.2 and clang <= 14.0, see
32+
* http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2162r0.html for more info. Instead, type switches must be
33+
* created manually using the index() method and the TypeIndex enum.
34+
*/
35+
struct SPIX_EXPORT Variant : VariantBaseType {
36+
using ListType = std::vector<Variant>;
37+
using MapType = std::map<std::string, Variant>;
38+
using VariantType = VariantBaseType;
39+
using VariantBaseType::variant;
40+
VariantBaseType const& base() const { return *this; }
41+
VariantBaseType& base() { return *this; }
42+
43+
enum TypeIndex
44+
{
45+
Nullptr = 0,
46+
Bool,
47+
Int,
48+
Uint,
49+
Double,
50+
String,
51+
Time,
52+
List,
53+
Map,
54+
TypeIndexCount
55+
};
56+
};
57+
58+
static_assert(
59+
Variant::TypeIndexCount == std::variant_size_v<VariantBaseType>, "Variant enum does not cover all Variant types");
60+
61+
} // namespace spix

lib/include/Spix/TestServer.h

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
#include <Spix/Data/Geometry.h>
1515
#include <Spix/Data/ItemPath.h>
16+
#include <Spix/Data/Variant.h>
1617
#include <Spix/Events/Identifiers.h>
1718

1819
#include <Spix/spix_export.h>
@@ -57,6 +58,7 @@ class SPIX_EXPORT TestServer {
5758

5859
std::string getStringProperty(ItemPath path, std::string propertyName);
5960
void setStringProperty(ItemPath path, std::string propertyName, std::string propertyValue);
61+
Variant invokeMethod(ItemPath path, std::string method, std::vector<Variant> args);
6062
Rect getBoundingBox(ItemPath path);
6163
bool existsAndVisible(ItemPath path);
6264
std::vector<std::string> getErrors();

lib/src/AnyRpcServer.cpp

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
****/
66

77
#include <Spix/AnyRpcServer.h>
8-
#include <Utils/AnyRpcUtils.h>
9-
#include <anyrpc/anyrpc.h>
8+
#include <Spix/Data/Variant.h>
9+
#include <Utils/AnyRpcFunction.h>
1010
#include <atomic>
1111

1212
namespace spix {
@@ -70,6 +70,12 @@ AnyRpcServer::AnyRpcServer(int anyrpcPort)
7070
setStringProperty(std::move(path), std::move(property), std::move(value));
7171
});
7272

73+
utils::AddFunctionToAnyRpc<Variant(std::string, std::string, std::vector<Variant>)>(methodManager, "invokeMethod",
74+
"Invoke a method on a QML object | invokeMethod(string path, string method, any[] args)",
75+
[this](std::string path, std::string method, std::vector<Variant> args) {
76+
return invokeMethod(std::move(path), std::move(method), std::move(args));
77+
});
78+
7379
utils::AddFunctionToAnyRpc<std::vector<double>(std::string)>(methodManager, "getBoundingBox",
7480
"Return the bounding box of an item in screen coordinates | getBoundingBox(string path) : (doubles) "
7581
"[topLeft.x, topLeft.y , width, height]",

lib/src/Commands/InvokeMethod.cpp

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/***
2+
* Copyright (C) Falko Axmann. All rights reserved.
3+
* Licensed under the MIT license.
4+
* See LICENSE.txt file in the project root for full license information.
5+
****/
6+
7+
#include "InvokeMethod.h"
8+
9+
#include <Scene/Scene.h>
10+
11+
namespace spix {
12+
namespace cmd {
13+
14+
InvokeMethod::InvokeMethod(ItemPath path, std::string method, std::vector<Variant> args, std::promise<Variant> promise)
15+
: m_path(std::move(path))
16+
, m_method(std::move(method))
17+
, m_args(std::move(args))
18+
, m_promise(std::move(promise))
19+
{
20+
}
21+
22+
void InvokeMethod::execute(CommandEnvironment& env)
23+
{
24+
auto item = env.scene().itemAtPath(m_path);
25+
26+
if (item) {
27+
Variant ret;
28+
bool success = item->invokeMethod(m_method, m_args, ret);
29+
if (!success)
30+
env.state().reportError("InvokeMethod: Failed to invoke method: " + m_method);
31+
m_promise.set_value(ret);
32+
} else {
33+
env.state().reportError("InvokeMethod: Item not found: " + m_path.string());
34+
m_promise.set_value(Variant(nullptr));
35+
}
36+
}
37+
38+
} // namespace cmd
39+
} // namespace spix

lib/src/Commands/InvokeMethod.h

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/***
2+
* Copyright (C) Falko Axmann. All rights reserved.
3+
* Licensed under the MIT license.
4+
* See LICENSE.txt file in the project root for full license information.
5+
****/
6+
7+
#pragma once
8+
9+
#include <Spix/spix_export.h>
10+
11+
#include "Command.h"
12+
#include <Spix/Data/ItemPath.h>
13+
#include <Spix/Data/Variant.h>
14+
15+
#include <future>
16+
17+
namespace spix {
18+
namespace cmd {
19+
20+
class SPIX_EXPORT InvokeMethod : public Command {
21+
public:
22+
InvokeMethod(ItemPath path, std::string method, std::vector<Variant> args, std::promise<Variant> promise);
23+
24+
void execute(CommandEnvironment& env) override;
25+
26+
private:
27+
ItemPath m_path;
28+
std::string m_method;
29+
std::vector<Variant> m_args;
30+
std::promise<Variant> m_promise;
31+
};
32+
33+
} // namespace cmd
34+
} // namespace spix

0 commit comments

Comments
 (0)