Skip to content

Commit

Permalink
cxx-qt-gen: move signals away from an enum to an extern "C++" block
Browse files Browse the repository at this point in the history
Related to KDAB#557
  • Loading branch information
ahayzen-kdab committed Jun 2, 2023
1 parent e56b9ce commit 305b2f1
Show file tree
Hide file tree
Showing 37 changed files with 1,447 additions and 1,002 deletions.
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ SPDX-License-Identifier: MIT OR Apache-2.0
- [QObject](./qobject/index.md)
- [`#[cxx_qt::bridge]` - Bridge Macro](./qobject/bridge-macro.md)
- [`#[cxx_qt::qobject]` - Defining QObjects](./qobject/qobject_struct.md)
- [`#[cxx_qt::qsignals]` - Signals enum](./qobject/signals_enum.md)
- [`#[cxx_qt::qsignals]` - Signals macro](./qobject/signals.md)
- [`qobject::T` - The generated QObject](./qobject/generated-qobject.md)
- [CxxQtThread](./qobject/cxxqtthread.md)
- [Concepts](./concepts/index.md)
Expand Down
2 changes: 1 addition & 1 deletion book/src/getting-started/1-qobjects-in-rust.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Then you can use the afformentioned features with the help of more macros.
- `#[cxx_qt::qobject]` - Expose a Rust struct to Qt as a QObject subclass.
- `#[qproperty]` - Expose a field of the Rust struct to QML/C++ as a [`Q_PROPERTY`](https://doc.qt.io/qt-6/qtqml-cppintegration-exposecppattributes.html#exposing-properties).
- `#[qinvokable]` - Expose a function on the QObject to QML and C++ as a [`Q_INVOKABLE`](https://doc.qt.io/qt-6/qtqml-cppintegration-exposecppattributes.html#exposing-methods-including-qt-slots).
- `#[cxx_qt::qsignals(T)]` - Use an enum to define the [Signals](https://doc.qt.io/qt-6/signalsandslots.html#signals) of a QObject T.
- `#[cxx_qt::qsignals]` - Use an enum to define the [Signals](https://doc.qt.io/qt-6/signalsandslots.html#signals) of a QObject T.

CXX-Qt will then expand this Rust module into two separate parts:
- C++ files that define a QObject subclass for each `#[cxx_qt::qobject]` marked struct.
Expand Down
2 changes: 1 addition & 1 deletion book/src/qobject/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ For a simpler introduction, take a look at our [Getting Started guide](../gettin
QObject Features and Parts:
* [`#[cxx_qt::bridge]` - The macro around the module](./bridge-macro.md)
* [`#[cxx_qt::qobject]` - Marking a Rust struct as a QObject](./qobject_struct.md)
* [`#[cxx_qt::qsignals(T)]` - An enum for defining signals](./signals_enum.md)
* [`#[cxx_qt::qsignals]` - A macro for defining signals](./signals.md)
* [`qobject:T` - The generated QObject](./generated-qobject.md)
* [`CxxQtThread` - Queueing closures onto the Qt event loop](./cxxqtthread.md)

Expand Down
25 changes: 10 additions & 15 deletions book/src/qobject/signals_enum.md → book/src/qobject/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ SPDX-License-Identifier: MIT OR Apache-2.0

# Signals enum

The `cxx_qt::qsignals(T)` attribute is used on an [enum](https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html) to define [signals](https://doc.qt.io/qt-6/signalsandslots.html) for the QObject `T`.
The `cxx_qt::qsignals` attribute is used on an `extern "C++"` block to define [signals](https://doc.qt.io/qt-6/signalsandslots.html) for the a QObject.

```rust,ignore,noplayground
{{#include ../../../examples/qml_features/rust/src/signals.rs:book_signals_enum}}
{{#include ../../../examples/qml_features/rust/src/signals.rs:book_signals_block}}
```

For every enum variant, CXX-Qt will generate a signal on the corresponding QObject.
If the enum variant has members, they will become the parameters for the corresponding signal.

Because CXX-Qt needs to know the names of each parameter, only enum variants with named members are supported.
The signal parameters are generated in order of appearance in the enum variant.
For every function signature in the extern block, CXX-Qt will generate a signal on the corresponding QObject.
If the function has parameters, they will become the parameters for the corresponding signal.

If a signal is defined on the base class of the QObject then `#[inherit]` can be used to indicate to CXX-Qt that the `Q_SIGNAL` does not need to be created in C++.

Expand Down Expand Up @@ -60,18 +57,16 @@ In this case, it is no longer possible to disconnect later.

## Emitting a signal

For every generated QObject [`qobject::T`](./generated-qobject.md) that has a signals enum, CXX-Qt will generate an `emit` function:
``` rust,ignore,noplayground
fn emit(self: Pin<&mut Self>, signal: /*Signals enum*/)
```
`emit` can therefore be called from any mutable `#[qinvokable]`.
Call the function signature defined in the `extern "C++` block to emit the signal.

Note that these are defined on the generated QObject [`qobject::T`](./generated-qobject.md), so can be called from any mutable `#[qinvokable]`.

The `emit` function will immediately emit the signal.
The function will immediately emit the signal.
Depending on the connection type, the connected slots will be called either immediately or from the event loop (See [the different connection types](https://doc.qt.io/qt-6/qt.html#ConnectionType-enum)).
To queue the call to `emit` until the next cycle of the Qt event loop, you can use the [`CxxQtThread`](./cxxqtthread.md).
To queue the call until the next cycle of the Qt event loop, you can use the [`CxxQtThread`](./cxxqtthread.md).

### [Example](https://github.com/KDAB/cxx-qt/blob/main/examples/qml_features/rust/src/signals.rs)

```rust,ignore,noplayground
{{#include ../../../examples/qml_features/rust/src/signals.rs:book_macro_code}}
```

165 changes: 152 additions & 13 deletions crates/cxx-qt-gen/src/generator/cpp/property/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::generator::{
cpp::{qobject::GeneratedCppQObjectBlocks, types::CppType},
cpp::{qobject::GeneratedCppQObjectBlocks, signal::generate_cpp_signals, types::CppType},
naming::{property::QPropertyName, qobject::QObjectName},
};
use crate::parser::{cxxqtdata::ParsedCxxMappings, property::ParsedQProperty};
Expand All @@ -21,7 +21,9 @@ pub fn generate_cpp_properties(
cxx_mappings: &ParsedCxxMappings,
) -> Result<GeneratedCppQObjectBlocks> {
let mut generated = GeneratedCppQObjectBlocks::default();
let mut signals = vec![];
let qobject_ident = qobject_idents.cpp_class.cpp.to_string();

for property in properties {
// Cache the idents as they are used in multiple places
let idents = QPropertyName::from(property);
Expand All @@ -34,9 +36,15 @@ pub fn generate_cpp_properties(
generated
.methods
.push(setter::generate(&idents, &qobject_ident, &cxx_ty));
generated.methods.push(signal::generate(&idents));
signals.push(signal::generate(&idents, qobject_idents));
}

generated.append(&mut generate_cpp_signals(
&signals,
qobject_idents,
cxx_mappings,
)?);

Ok(generated)
}

Expand Down Expand Up @@ -77,7 +85,7 @@ mod tests {
assert_str_eq!(generated.metaobjects[1], "Q_PROPERTY(::std::unique_ptr<QColor> opaqueProperty READ getOpaqueProperty WRITE setOpaqueProperty NOTIFY opaquePropertyChanged)");

// methods
assert_eq!(generated.methods.len(), 6);
assert_eq!(generated.methods.len(), 10);
let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[0] {
(header, source)
} else {
Expand Down Expand Up @@ -116,13 +124,8 @@ mod tests {
}
"#}
);
let header = if let CppFragment::Header(header) = &generated.methods[2] {
header
} else {
panic!("Expected header!")
};
assert_str_eq!(header, "Q_SIGNAL void trivialPropertyChanged();");
let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[3] {

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[2] {
(header, source)
} else {
panic!("Expected pair!")
Expand All @@ -143,7 +146,7 @@ mod tests {
"#}
);

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[4] {
let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[3] {
(header, source)
} else {
panic!("Expected pair!")
Expand All @@ -164,12 +167,105 @@ mod tests {
"#}
);

let header = if let CppFragment::Header(header) = &generated.methods[5] {
let header = if let CppFragment::Header(header) = &generated.methods[4] {
header
} else {
panic!("Expected header!")
};
assert_str_eq!(header, "Q_SIGNAL void trivialPropertyChanged();");

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[5] {
(header, source)
} else {
panic!("Expected Pair")
};
assert_str_eq!(header, "void emitTrivialPropertyChanged();");
assert_str_eq!(
source,
indoc! {r#"
void
MyObject::emitTrivialPropertyChanged()
{
Q_EMIT trivialPropertyChanged();
}
"#}
);

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[6] {
(header, source)
} else {
panic!("Expected Pair")
};
assert_str_eq!(
header,
"::QMetaObject::Connection trivialPropertyChangedConnect(::rust::Fn<void(MyObject&)> func, ::Qt::ConnectionType type);"
);
assert_str_eq!(
source,
indoc! {r#"
::QMetaObject::Connection
MyObject::trivialPropertyChangedConnect(::rust::Fn<void(MyObject&)> func, ::Qt::ConnectionType type)
{
return ::QObject::connect(this,
&MyObject::trivialPropertyChanged,
this,
[&, func = ::std::move(func)]() {
const ::std::lock_guard<::std::recursive_mutex> guard(*m_rustObjMutex);
func(*this);
}, type);
}
"#}
);

let header = if let CppFragment::Header(header) = &generated.methods[7] {
header
} else {
panic!("Expected header!")
};
assert_str_eq!(header, "Q_SIGNAL void opaquePropertyChanged();");

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[8] {
(header, source)
} else {
panic!("Expected Pair")
};
assert_str_eq!(header, "void emitOpaquePropertyChanged();");
assert_str_eq!(
source,
indoc! {r#"
void
MyObject::emitOpaquePropertyChanged()
{
Q_EMIT opaquePropertyChanged();
}
"#}
);

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[9] {
(header, source)
} else {
panic!("Expected Pair")
};
assert_str_eq!(
header,
"::QMetaObject::Connection opaquePropertyChangedConnect(::rust::Fn<void(MyObject&)> func, ::Qt::ConnectionType type);"
);
assert_str_eq!(
source,
indoc! {r#"
::QMetaObject::Connection
MyObject::opaquePropertyChangedConnect(::rust::Fn<void(MyObject&)> func, ::Qt::ConnectionType type)
{
return ::QObject::connect(this,
&MyObject::opaquePropertyChanged,
this,
[&, func = ::std::move(func)]() {
const ::std::lock_guard<::std::recursive_mutex> guard(*m_rustObjMutex);
func(*this);
}, type);
}
"#}
);
}

#[test]
Expand All @@ -194,7 +290,7 @@ mod tests {
assert_str_eq!(generated.metaobjects[0], "Q_PROPERTY(A1 mappedProperty READ getMappedProperty WRITE setMappedProperty NOTIFY mappedPropertyChanged)");

// methods
assert_eq!(generated.methods.len(), 3);
assert_eq!(generated.methods.len(), 5);
let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[0] {
(header, source)
} else {
Expand Down Expand Up @@ -236,5 +332,48 @@ mod tests {
panic!("Expected header!")
};
assert_str_eq!(header, "Q_SIGNAL void mappedPropertyChanged();");

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[3] {
(header, source)
} else {
panic!("Expected Pair")
};
assert_str_eq!(header, "void emitMappedPropertyChanged();");
assert_str_eq!(
source,
indoc! {r#"
void
MyObject::emitMappedPropertyChanged()
{
Q_EMIT mappedPropertyChanged();
}
"#}
);

let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[4] {
(header, source)
} else {
panic!("Expected Pair")
};
assert_str_eq!(
header,
"::QMetaObject::Connection mappedPropertyChangedConnect(::rust::Fn<void(MyObject&)> func, ::Qt::ConnectionType type);"
);
assert_str_eq!(
source,
indoc! {r#"
::QMetaObject::Connection
MyObject::mappedPropertyChangedConnect(::rust::Fn<void(MyObject&)> func, ::Qt::ConnectionType type)
{
return ::QObject::connect(this,
&MyObject::mappedPropertyChanged,
this,
[&, func = ::std::move(func)]() {
const ::std::lock_guard<::std::recursive_mutex> guard(*m_rustObjMutex);
func(*this);
}, type);
}
"#}
);
}
}
29 changes: 22 additions & 7 deletions crates/cxx-qt-gen/src/generator/cpp/property/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::generator::naming::property::QPropertyName;
use crate::CppFragment;
use syn::ForeignItemFn;

pub fn generate(idents: &QPropertyName) -> CppFragment {
CppFragment::Header(format!(
"Q_SIGNAL void {ident_notify}();",
ident_notify = idents.notify.cpp
))
use crate::{
generator::naming::{property::QPropertyName, qobject::QObjectName},
parser::signals::ParsedSignal,
};

pub fn generate(idents: &QPropertyName, qobject_idents: &QObjectName) -> ParsedSignal {
// We build our signal in the generation phase as we need to use the naming
// structs to build the signal name
let cpp_class_rust = &qobject_idents.cpp_class.rust;
let notify_cpp = &idents.notify.cpp;
let notify_rust_str = idents.notify.rust.to_string();
let method: ForeignItemFn = syn::parse_quote! {
#[doc = "Notify for the Q_PROPERTY"]
#[rust_name = #notify_rust_str]
fn #notify_cpp(self: Pin<&mut #cpp_class_rust>);
};
ParsedSignal::from_property_method(
method,
idents.notify.clone(),
qobject_idents.cpp_class.rust.clone(),
)
}
12 changes: 5 additions & 7 deletions crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,11 @@ impl GeneratedCppQObject {
&qobject_idents,
cxx_mappings,
)?);
if let Some(signals_enum) = &qobject.signals {
generated.blocks.append(&mut generate_cpp_signals(
&signals_enum.signals,
&qobject_idents,
cxx_mappings,
)?);
}
generated.blocks.append(&mut generate_cpp_signals(
&qobject.signals,
&qobject_idents,
cxx_mappings,
)?);
generated.blocks.append(&mut inherit::generate(
&qobject.inherited_methods,
&qobject.base_class,
Expand Down
Loading

0 comments on commit 305b2f1

Please sign in to comment.