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 7, 2023
1 parent e235c03 commit 3c51560
Show file tree
Hide file tree
Showing 40 changed files with 1,453 additions and 1,037 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/concepts/qt.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ Properties can be defined using the [QObject struct](../qobject/qobject_struct.m

## Signals

Signals can be defined using the [Signals enum](../qobject/signals_enum.md), these will be exposed as `Q_SIGNALS` on the C++ class and therefore to QML as well.
Signals can be defined using the [QSignals macros](../qobject/signals.md), these will be exposed as `Q_SIGNALS` on the C++ class and therefore to QML as well.
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]` - 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
8 changes: 0 additions & 8 deletions book/src/qobject/generated-qobject.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,6 @@ The closure also takes a pinned mutable reference to the QObject, so that it can

See the [CxxQtThread page](./cxxqtthread.md) for more details.

### Signal emission
``` rust,ignore,noplayground
fn emit(self: Pin<&mut Self>, signal: /*Your Signals enum goes here*/)
```
If there is a [Signals enum](./signals_enum.md) defined, CXX-Qt will generate the appropriate `emit` function to allow you to emit signals.

See the [Signals enum page](./signals_enum.md) for more details.

### Access to internal Rust struct
For every field in the Rust struct, CXX-Qt will generate appropriate getters and setters.
See the [QObject page](./qobject_struct.md#properties) for details.
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
2 changes: 1 addition & 1 deletion book/src/qobject/qobject_struct.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The macro does multiple other things for you though:
- Expose the generated QObject subclass to Rust as [`qobject::MyObject`](./generated-qobject.md)
- Generate getters/setters for all fields.
- Generate `Q_PROPERTY`s for all fields that are marked as `#[qproperty]`.
- Generate signals if paired with a [`#[cxx_qt::qsignals]` enum](./signals_enum.md).
- Generate signals if paired with a [`#[cxx_qt::qsignals]` macro](./signals.md).

## Exposing to QML
`#[cxx_qt::qobject]` supports registering the Qt object as a QML type directly at build time.
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}}
```

166 changes: 153 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 @@ -22,7 +22,9 @@ pub fn generate_cpp_properties(
lock_guard: Option<&str>,
) -> 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 @@ -41,9 +43,16 @@ pub fn generate_cpp_properties(
&cxx_ty,
lock_guard,
));
generated.methods.push(signal::generate(&idents));
signals.push(signal::generate(&idents, qobject_idents));
}

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

Ok(generated)
}

Expand Down Expand Up @@ -88,7 +97,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 @@ -127,13 +136,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 @@ -154,7 +158,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 @@ -175,12 +179,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)]() {
// ::std::lock_guard
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)]() {
// ::std::lock_guard
func(*this);
}, type);
}
"#}
);
}

#[test]
Expand Down Expand Up @@ -210,7 +307,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 @@ -252,5 +349,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)]() {
// ::std::lock_guard
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(),
)
}
14 changes: 6 additions & 8 deletions crates/cxx-qt-gen/src/generator/cpp/qobject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,12 @@ impl GeneratedCppQObject {
cxx_mappings,
lock_guard,
)?);
if let Some(signals_enum) = &qobject.signals {
generated.blocks.append(&mut generate_cpp_signals(
&signals_enum.signals,
&qobject_idents,
cxx_mappings,
lock_guard,
)?);
}
generated.blocks.append(&mut generate_cpp_signals(
&qobject.signals,
&qobject_idents,
cxx_mappings,
lock_guard,
)?);
generated.blocks.append(&mut inherit::generate(
&qobject.inherited_methods,
&qobject.base_class,
Expand Down
Loading

0 comments on commit 3c51560

Please sign in to comment.