diff --git a/examples/qml_features/cpp/custom_object.cpp b/examples/qml_features/cpp/custom_object.cpp index c79424e0a..1ea057d31 100644 --- a/examples/qml_features/cpp/custom_object.cpp +++ b/examples/qml_features/cpp/custom_object.cpp @@ -11,3 +11,15 @@ qvariantCanConvertCustomStruct(const QVariant& variant) { return variant.canConvert(); } + +CustomObject::CustomObject(QObject* parent) + : QObject(parent) + , m_value(0) +{ +} + +CustomStruct +CustomObject::asStruct() const +{ + return CustomStruct{ m_value }; +} diff --git a/examples/qml_features/cpp/custom_object.h b/examples/qml_features/cpp/custom_object.h index d9acf0c2a..fc03c53d9 100644 --- a/examples/qml_features/cpp/custom_object.h +++ b/examples/qml_features/cpp/custom_object.h @@ -24,13 +24,9 @@ class CustomObject : public QObject Q_PROPERTY(int value MEMBER m_value) public: - CustomObject(QObject* parent = nullptr) - : QObject(parent) - , m_value(0) - { - } + explicit CustomObject(QObject* parent = nullptr); - Q_INVOKABLE CustomStruct asStruct() const { return CustomStruct{ m_value }; } + Q_INVOKABLE CustomStruct asStruct() const; private: int m_value; diff --git a/examples/qml_features/cpp/external_qobject.cpp b/examples/qml_features/cpp/external_qobject.cpp new file mode 100644 index 000000000..d3b051b01 --- /dev/null +++ b/examples/qml_features/cpp/external_qobject.cpp @@ -0,0 +1,21 @@ +// clang-format off +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#include "external_qobject.h" + +ExternalQObject::ExternalQObject(QObject* parent) + : QObject(parent) +{ +} + +void +ExternalQObject::trigger(::std::uint32_t amount) +{ + for (::std::uint32_t i = 0; i < amount; i++) { + Q_EMIT triggered(); + Q_EMIT triggeredPrivateSignal(QPrivateSignal()); + } +} diff --git a/examples/qml_features/cpp/external_qobject.h b/examples/qml_features/cpp/external_qobject.h new file mode 100644 index 000000000..c4a4b712f --- /dev/null +++ b/examples/qml_features/cpp/external_qobject.h @@ -0,0 +1,25 @@ +// clang-format off +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// clang-format on +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +#pragma once + +#include + +#include + +class ExternalQObject : public QObject +{ + Q_OBJECT + +public: + explicit ExternalQObject(QObject* parent = nullptr); + + Q_INVOKABLE void trigger(::std::uint32_t amount); + +Q_SIGNALS: + void triggered(); + void triggeredPrivateSignal(QPrivateSignal); +}; diff --git a/examples/qml_features/cpp/main.cpp b/examples/qml_features/cpp/main.cpp index 0e5827993..081e463fb 100644 --- a/examples/qml_features/cpp/main.cpp +++ b/examples/qml_features/cpp/main.cpp @@ -9,6 +9,7 @@ #include #include "custom_object.h" +#include "external_qobject.h" int main(int argc, char* argv[]) @@ -35,6 +36,8 @@ main(int argc, char* argv[]) // the elements generated by Rust will be available to the QML engine. qmlRegisterType( "com.kdab.cxx_qt.demo_cpp", 1, 0, "CustomObject"); + qmlRegisterType( + "com.kdab.cxx_qt.demo_cpp", 1, 0, "ExternalQObject"); engine.load(url); diff --git a/examples/qml_features/qml/main.qml b/examples/qml_features/qml/main.qml index 896f17e6a..c7df1143e 100644 --- a/examples/qml_features/qml/main.qml +++ b/examples/qml_features/qml/main.qml @@ -116,6 +116,10 @@ ApplicationWindow { name: "Custom Parent Class" source: "pages/CustomParentClassPage.qml" } + ListElement { + name: "ExternCxxQt" + source: "pages/ExternCxxQtPage.qml" + } } } } diff --git a/examples/qml_features/qml/pages/ExternCxxQtPage.qml b/examples/qml_features/qml/pages/ExternCxxQtPage.qml new file mode 100644 index 000000000..b7d3371a0 --- /dev/null +++ b/examples/qml_features/qml/pages/ExternCxxQtPage.qml @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQuick.Window 2.12 + +import com.kdab.cxx_qt.demo 1.0 +import com.kdab.cxx_qt.demo_cpp 1.0 + +Page { + property int amount: 5 + + header: ToolBar { + RowLayout { + anchors.fill: parent + + ToolButton { + text: qsTr("Trigger") + + onClicked: rustExternCxxQt.triggerOnExternal(externalQObject, amountSpinBox.value) + } + + Item { + Layout.fillWidth: true + } + } + } + + ExternalQObject { + id: externalQObject + } + + ExternalCxxQtHelper { + id: rustExternCxxQt + } + + ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: qsTr("Connecting to signals in external QObject can run closures as normal.") + wrapMode: Text.Wrap + } + + SpinBox { + id: amountSpinBox + Layout.alignment: Qt.AlignHCenter + from: 1 + to: 10 + value: 5 + } + + Label { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: qsTr("Count: %1").arg(rustExternCxxQt.count) + wrapMode: Text.Wrap + } + } + + Component.onCompleted: rustExternCxxQt.connectToExternal(externalQObject) +} diff --git a/examples/qml_features/rust/build.rs b/examples/qml_features/rust/build.rs index 11e119fd8..c2e9b549c 100644 --- a/examples/qml_features/rust/build.rs +++ b/examples/qml_features/rust/build.rs @@ -15,6 +15,7 @@ fn main() { "src/containers.rs", "src/custom_base_class.rs", "src/custom_parent_class.rs", + "src/externcxxqt.rs", "src/invokables.rs", "src/multiple_qobjects.rs", "src/nested_qobjects.rs", @@ -31,6 +32,7 @@ fn main() { "../qml/pages/ContainersPage.qml", "../qml/pages/CustomBaseClassPage.qml", "../qml/pages/CustomParentClassPage.qml", + "../qml/pages/ExternCxxQtPage.qml", "../qml/pages/InvokablesPage.qml", "../qml/pages/MultipleQObjectsPage.qml", "../qml/pages/NestedQObjectsPage.qml", @@ -48,8 +50,10 @@ fn main() { .cc_builder(|cc| { cc.include("../cpp"); cc.file("../cpp/custom_object.cpp"); + cc.file("../cpp/external_qobject.cpp"); }) .qobject_header("../cpp/custom_object.h") + .qobject_header("../cpp/external_qobject.h") // Ensure that Quick module is linked, so that cargo test can work. // In a CMake project this isn't required as the linking happens in CMake. .qt_module("Quick") diff --git a/examples/qml_features/rust/src/externcxxqt.rs b/examples/qml_features/rust/src/externcxxqt.rs new file mode 100644 index 000000000..7fa064725 --- /dev/null +++ b/examples/qml_features/rust/src/externcxxqt.rs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2022 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! This example shows how an external QObject with signals can be used + +/// A CXX-Qt bridge which shows how an external QObject with signals can be used +// ANCHOR: book_cxx_file_stem +#[cxx_qt::bridge(cxx_file_stem = "externcxxqt")] +pub mod ffi { + unsafe extern "C++Qt" { + include!("external_qobject.h"); + /// ExternalQObject C++ class + type ExternalQObject; + + /// Trigger emitting the signal "amount" times + fn trigger(self: Pin<&mut ExternalQObject>, amount: u32); + + /// Signal that is emitted when trigger is fired + #[qsignal] + fn triggered(self: Pin<&mut ExternalQObject>); + + /// Private signal that is emitted when trigger is fired + #[qsignal] + #[rust_name = "triggered_private_signal"] + pub(self) fn triggeredPrivateSignal(self: Pin<&mut ExternalQObject>); + } + + unsafe extern "RustQt" { + #[qobject] + #[qml_element] + #[qproperty(u32, count)] + #[qproperty(u32, private_count)] + type ExternalCxxQtHelper = super::ExternalCxxQtHelperRust; + + #[qinvokable] + unsafe fn connect_to_external( + self: Pin<&mut ExternalCxxQtHelper>, + external: *mut ExternalQObject, + ); + + #[qinvokable] + unsafe fn trigger_on_external( + self: Pin<&mut ExternalCxxQtHelper>, + external: *mut ExternalQObject, + amount: u32, + ); + } + + impl cxx_qt::Threading for ExternalCxxQtHelper {} +} + +use core::pin::Pin; +use cxx_qt::Threading; + +/// Test struct +#[derive(Default)] +pub struct ExternalCxxQtHelperRust { + count: u32, + private_count: u32, +} + +impl ffi::ExternalCxxQtHelper { + unsafe fn connect_to_external(self: Pin<&mut Self>, external: *mut ffi::ExternalQObject) { + if let Some(external) = external.as_mut() { + let qt_thread = self.qt_thread(); + let mut pinned_external = Pin::new_unchecked(external); + pinned_external + .as_mut() + .on_triggered(move |_| { + qt_thread + .queue(|mut qobject| { + let new_count = qobject.as_ref().count() + 1; + qobject.as_mut().set_count(new_count); + }) + .unwrap(); + }) + .release(); + + let qt_thread = self.qt_thread(); + pinned_external + .as_mut() + .on_triggered_private_signal(move |_| { + qt_thread + .queue(|mut qobject| { + let new_private_count = qobject.as_ref().private_count() + 1; + qobject.as_mut().set_private_count(new_private_count); + }) + .unwrap(); + }) + .release(); + } + } + + unsafe fn trigger_on_external( + self: Pin<&mut Self>, + external: *mut ffi::ExternalQObject, + amount: u32, + ) { + if let Some(external) = external.as_mut() { + let pinned_external = Pin::new_unchecked(external); + pinned_external.trigger(amount); + } + } +} diff --git a/examples/qml_features/rust/src/lib.rs b/examples/qml_features/rust/src/lib.rs index 241f244a8..994023e05 100644 --- a/examples/qml_features/rust/src/lib.rs +++ b/examples/qml_features/rust/src/lib.rs @@ -13,6 +13,7 @@ pub mod containers; pub mod custom_base_class; pub mod custom_parent_class; +pub mod externcxxqt; pub mod invokables; pub mod multiple_qobjects; pub mod nested_qobjects; diff --git a/examples/qml_features/tests/main.cpp b/examples/qml_features/tests/main.cpp index 0fffd4f0b..693fdd0f8 100644 --- a/examples/qml_features/tests/main.cpp +++ b/examples/qml_features/tests/main.cpp @@ -9,6 +9,7 @@ #include #include "custom_object.h" +#include "external_qobject.h" class Setup : public QObject { @@ -24,6 +25,8 @@ class Setup : public QObject // of the elements generated by Rust will be available to the QML engine. qmlRegisterType( "com.kdab.cxx_qt.demo_cpp", 1, 0, "CustomObject"); + qmlRegisterType( + "com.kdab.cxx_qt.demo_cpp", 1, 0, "ExternalQObject"); } }; diff --git a/examples/qml_features/tests/tst_externcxxqt.qml b/examples/qml_features/tests/tst_externcxxqt.qml new file mode 100644 index 000000000..85ffe2477 --- /dev/null +++ b/examples/qml_features/tests/tst_externcxxqt.qml @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2023 Klarälvdalens Datakonsult AB, a KDAB Group company +// SPDX-FileContributor: Andrew Hayzen +// +// SPDX-License-Identifier: MIT OR Apache-2.0 +import QtQuick 2.12 +import QtTest 1.12 + +import com.kdab.cxx_qt.demo 1.0 +import com.kdab.cxx_qt.demo_cpp 1.0 + +TestCase { + name: "ExternCxxQtTests" + + Component { + id: componentExternalQObject + + ExternalQObject { + + } + } + + Component { + id: componentExternalCxxQtHelper + + ExternalCxxQtHelper { + + } + } + + Component { + id: componentSpy + + SignalSpy { + + } + } + + function test_connect_to_external() { + const obj = createTemporaryObject(componentExternalQObject, null, {}); + const helper = createTemporaryObject(componentExternalCxxQtHelper, null, {}); + const triggeredSpy = createTemporaryObject(componentSpy, null, { + signalName: "triggered", + target: obj, + }); + const triggeredPrivateSpy = createTemporaryObject(componentSpy, null, { + signalName: "triggeredPrivateSignal", + target: obj, + }); + + helper.connectToExternal(obj); + + compare(triggeredSpy.count, 0); + compare(triggeredPrivateSpy.count, 0); + compare(helper.count, 0); + compare(helper.privateCount, 0); + + helper.triggerOnExternal(obj, 2); + + tryCompare(triggeredSpy, "count", 2); + tryCompare(triggeredPrivateSpy, "count", 2); + compare(helper.count, 2); + compare(helper.privateCount, 2); + } +}