Skip to content

Commit 0ed19c6

Browse files
Allow custom setters/getters/notify for qproperty (#994)
This adds support for three flags to the qproperty macro: `read`,`write`,`notify`. If any of them is used, the user is in control of what is generated by CXX-Qt. Using the flags without an argument will cause CXX-Qt to auto-generate that part of the property. This means that `read, write, notify` is equivalent to the current behavior, which continues to be supported for convenience. If the user specifies a value (e.g. `read=my_getter`), CXX-Qt will no longer auto-generate anything, but simply link to that function in the definition of the Q_PROPERTY macro in C++. The corresponding function/signal must already be exported to C++ by the user with the correct name & signature. This allows user complete control over their signals. We will likely follow this up with the remaining valid flags on Q_PROPERTY.
1 parent 02e5be6 commit 0ed19c6

File tree

19 files changed

+907
-213
lines changed

19 files changed

+907
-213
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- Add cxx-qt-lib-extras crate which contains: `QCommandLineOption`, `QCommandLineParser`, `QElapsedTimer`, `QApplication`
2727
- Serde support for `QString` (requires "serde" feature on cxx-qt-lib)
2828
- A new QuickControls module, which exposes `QQuickStyle`. This module is enabled by default and is behind the `qt_quickcontrols` feature.
29+
- Add support for specifying read write and notify in qproperty macro, including support for custom user defined functions
2930

3031
### Changed
3132

book/src/bridge/extern_rustqt.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,23 @@ Where `<Property>` is the name of the property.
131131

132132
These setters and getters assure that the changed signal is emitted every time the property is edited.
133133

134-
> Note that in the future it will be possible to specify custom getters and setters
134+
It is also possible to specify custom getters, setters and notify signals, using flags passed like so:
135+
`#[qproperty(TYPE, NAME, read = myGetter, write = mySetter, notify = myOnChanged)]`
136+
137+
> Note: currently the rust name must be in camel case or specified like `#[cxx_name = "my_getter"]` if not
138+
139+
It is also possible to use any combination of custom functions or omit them entirely, but if flags are specified, read must be included as all properties need to be able to be read.
140+
141+
Using the read flag will cause CXX-Qt to generate a getter function with an automatic name based off the property. e.g. `#[qproperty(i32, num, read)]` will have a getter function generated called get_num in Rust, and getNum in C++.
142+
143+
If a custom function is specified, an implementation both in qobject::MyObject and and export in the bridge is expected.
144+
145+
### Examples
146+
147+
- `#[qproperty(TYPE, NAME, read)]` A read only prop
148+
- `#[qproperty(TYPE, NAME, read = myGetter, write, notify)]` custom getter provided but will generate setter and on-changed
149+
- `#[qproperty(TYPE, NAME)]` is syntactic sugar for `#[qproperty(TYPE, NAME, read, write, notify)]`
150+
- `#[qproperty(TYPE, NAME, write)]` is an error as read was not explicitly passed
135151

136152
## Methods
137153

crates/cxx-qt-gen/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
[package]
77
name = "cxx-qt-gen"
88
version.workspace = true
9-
authors = ["Andrew Hayzen <[email protected]>", "Gerhard de Clercq <[email protected]>", "Leon Matthes <[email protected]>"]
9+
authors = [
10+
"Andrew Hayzen <[email protected]>",
11+
"Gerhard de Clercq <[email protected]>",
12+
"Leon Matthes <[email protected]>",
13+
]
1014
edition.workspace = true
1115
license.workspace = true
1216
description = "Code generation for integrating `cxx-qt` into higher level tools"

crates/cxx-qt-gen/src/generator/cpp/property/getter.rs

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,47 @@
33
//
44
// SPDX-License-Identifier: MIT OR Apache-2.0
55

6-
use crate::generator::{cpp::fragment::CppFragment, naming::property::QPropertyNames};
6+
use crate::generator::{
7+
cpp::fragment::CppFragment,
8+
naming::property::{NameState, QPropertyNames},
9+
};
710
use indoc::formatdoc;
811

9-
pub fn generate(idents: &QPropertyNames, qobject_ident: &str, return_cxx_ty: &str) -> CppFragment {
10-
CppFragment::Pair {
11-
header: format!(
12-
"{return_cxx_ty} const& {ident_getter}() const;",
13-
ident_getter = idents.getter.cxx_unqualified()
14-
),
15-
source: formatdoc!(
16-
r#"
17-
{return_cxx_ty} const&
18-
{qobject_ident}::{ident_getter}() const
19-
{{
20-
const ::rust::cxxqt1::MaybeLockGuard<{qobject_ident}> guard(*this);
21-
return {ident_getter_wrapper}();
22-
}}
23-
"#,
24-
ident_getter = idents.getter.cxx_unqualified(),
25-
ident_getter_wrapper = idents.getter_wrapper.cxx_unqualified(),
26-
),
12+
pub fn generate(
13+
idents: &QPropertyNames,
14+
qobject_ident: &str,
15+
return_cxx_ty: &str,
16+
) -> Option<CppFragment> {
17+
if let (NameState::Auto(name), Some(getter_wrapper)) = (&idents.getter, &idents.getter_wrapper)
18+
{
19+
Some(CppFragment::Pair {
20+
header: format!(
21+
"{return_cxx_ty} const& {ident_getter}() const;",
22+
ident_getter = name.cxx_unqualified()
23+
),
24+
source: formatdoc!(
25+
r#"
26+
{return_cxx_ty} const&
27+
{qobject_ident}::{ident_getter}() const
28+
{{
29+
const ::rust::cxxqt1::MaybeLockGuard<{qobject_ident}> guard(*this);
30+
return {ident_getter_wrapper}();
31+
}}
32+
"#,
33+
ident_getter = name.cxx_unqualified(),
34+
ident_getter_wrapper = getter_wrapper.cxx_unqualified(),
35+
),
36+
})
37+
} else {
38+
None
2739
}
2840
}
2941

30-
pub fn generate_wrapper(idents: &QPropertyNames, cxx_ty: &str) -> CppFragment {
31-
CppFragment::Header(format!(
32-
"{cxx_ty} const& {ident_getter_wrapper}() const noexcept;",
33-
ident_getter_wrapper = idents.getter_wrapper.cxx_unqualified()
34-
))
42+
pub fn generate_wrapper(idents: &QPropertyNames, cxx_ty: &str) -> Option<CppFragment> {
43+
idents.getter_wrapper.as_ref().map(|name| {
44+
CppFragment::Header(format!(
45+
"{cxx_ty} const& {ident_getter_wrapper}() const noexcept;",
46+
ident_getter_wrapper = name.cxx_unqualified()
47+
))
48+
})
3549
}

crates/cxx-qt-gen/src/generator/cpp/property/meta.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ use crate::generator::naming::property::QPropertyNames;
77

88
/// Generate the metaobject line for a given property
99
pub fn generate(idents: &QPropertyNames, cxx_ty: &str) -> String {
10+
let mut parts = vec![format!(
11+
"READ {ident_getter}",
12+
ident_getter = idents.getter.cxx_unqualified()
13+
)];
14+
15+
if let Some(setter) = &idents.setter {
16+
parts.push(format!("WRITE {}", setter.cxx_unqualified()));
17+
}
18+
19+
if let Some(notify) = &idents.notify {
20+
parts.push(format!("NOTIFY {}", notify.cxx_unqualified()));
21+
}
22+
1023
format!(
11-
"Q_PROPERTY({ty} {ident} READ {ident_getter} WRITE {ident_setter} NOTIFY {ident_notify})",
24+
"Q_PROPERTY({ty} {ident} {meta_parts})",
1225
ty = cxx_ty,
1326
ident = idents.name.cxx_unqualified(),
14-
ident_getter = idents.getter.cxx_unqualified(),
15-
ident_setter = idents.setter.cxx_unqualified(),
16-
ident_notify = idents.notify.cxx_unqualified()
27+
meta_parts = parts.join(" ")
1728
)
1829
}

crates/cxx-qt-gen/src/generator/cpp/property/mod.rs

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,26 @@ pub fn generate_cpp_properties(
3232
let cxx_ty = syn_type_to_cpp_type(&property.ty, type_names)?;
3333

3434
generated.metaobjects.push(meta::generate(&idents, &cxx_ty));
35-
generated
36-
.methods
37-
.push(getter::generate(&idents, &qobject_ident, &cxx_ty));
38-
generated
39-
.private_methods
40-
.push(getter::generate_wrapper(&idents, &cxx_ty));
41-
generated
42-
.methods
43-
.push(setter::generate(&idents, &qobject_ident, &cxx_ty));
44-
generated
45-
.private_methods
46-
.push(setter::generate_wrapper(&idents, &cxx_ty));
47-
signals.push(signal::generate(&idents, qobject_idents));
35+
36+
if let Some(getter) = getter::generate(&idents, &qobject_ident, &cxx_ty) {
37+
generated.methods.push(getter);
38+
}
39+
40+
if let Some(getter_wrapper) = getter::generate_wrapper(&idents, &cxx_ty) {
41+
generated.private_methods.push(getter_wrapper);
42+
}
43+
44+
if let Some(setter) = setter::generate(&idents, &qobject_ident, &cxx_ty) {
45+
generated.methods.push(setter)
46+
}
47+
48+
if let Some(setter_wrapper) = setter::generate_wrapper(&idents, &cxx_ty) {
49+
generated.private_methods.push(setter_wrapper)
50+
}
51+
52+
if let Some(notify) = signal::generate(&idents, qobject_idents) {
53+
signals.push(notify)
54+
}
4855
}
4956

5057
generated.append(&mut generate_cpp_signals(
@@ -60,25 +67,75 @@ pub fn generate_cpp_properties(
6067
mod tests {
6168
use super::*;
6269

70+
use crate::parser::property::QPropertyFlags;
71+
6372
use crate::generator::naming::qobject::tests::create_qobjectname;
6473
use crate::CppFragment;
6574
use indoc::indoc;
6675
use pretty_assertions::assert_str_eq;
6776
use quote::format_ident;
68-
use syn::parse_quote;
77+
use syn::{parse_quote, ItemStruct};
78+
79+
#[test]
80+
fn test_custom_setter() {
81+
let mut input: ItemStruct = parse_quote! {
82+
#[qproperty(i32, num, read, write = mySetter)]
83+
struct MyStruct;
84+
};
85+
let property = ParsedQProperty::parse(input.attrs.remove(0)).unwrap();
86+
87+
let properties = vec![property];
88+
89+
let qobject_idents = create_qobjectname();
90+
91+
let type_names = TypeNames::mock();
92+
let generated = generate_cpp_properties(&properties, &qobject_idents, &type_names).unwrap();
93+
94+
assert_eq!(generated.metaobjects.len(), 1);
95+
assert_str_eq!(
96+
generated.metaobjects[0],
97+
"Q_PROPERTY(::std::int32_t num READ getNum WRITE mySetter)"
98+
);
99+
100+
// Methods
101+
assert_eq!(generated.methods.len(), 1);
102+
let (header, source) = if let CppFragment::Pair { header, source } = &generated.methods[0] {
103+
(header, source)
104+
} else {
105+
panic!("Expected pair!")
106+
};
107+
108+
assert_str_eq!(header, "::std::int32_t const& getNum() const;");
109+
assert_str_eq!(
110+
source,
111+
indoc! {r#"
112+
::std::int32_t const&
113+
MyObject::getNum() const
114+
{
115+
const ::rust::cxxqt1::MaybeLockGuard<MyObject> guard(*this);
116+
return getNumWrapper();
117+
}
118+
"#}
119+
);
120+
}
69121

70122
#[test]
71123
fn test_generate_cpp_properties() {
124+
let mut input1: ItemStruct = parse_quote! {
125+
#[qproperty(i32, trivial_property, read, write, notify)]
126+
struct MyStruct;
127+
};
128+
129+
let mut input2: ItemStruct = parse_quote! {
130+
#[qproperty(UniquePtr<QColor>, opaque_property)]
131+
struct MyStruct;
132+
};
133+
72134
let properties = vec![
73-
ParsedQProperty {
74-
ident: format_ident!("trivial_property"),
75-
ty: parse_quote! { i32 },
76-
},
77-
ParsedQProperty {
78-
ident: format_ident!("opaque_property"),
79-
ty: parse_quote! { UniquePtr<QColor> },
80-
},
135+
ParsedQProperty::parse(input1.attrs.remove(0)).unwrap(),
136+
ParsedQProperty::parse(input2.attrs.remove(0)).unwrap(),
81137
];
138+
82139
let qobject_idents = create_qobjectname();
83140

84141
let mut type_names = TypeNames::mock();
@@ -97,6 +154,7 @@ mod tests {
97154
} else {
98155
panic!("Expected pair!")
99156
};
157+
100158
assert_str_eq!(header, "::std::int32_t const& getTrivialProperty() const;");
101159
assert_str_eq!(
102160
source,
@@ -136,6 +194,7 @@ mod tests {
136194
} else {
137195
panic!("Expected pair!")
138196
};
197+
139198
assert_str_eq!(
140199
header,
141200
"::std::unique_ptr<QColor> const& getOpaqueProperty() const;"
@@ -358,6 +417,7 @@ mod tests {
358417
let properties = vec![ParsedQProperty {
359418
ident: format_ident!("mapped_property"),
360419
ty: parse_quote! { A },
420+
flags: QPropertyFlags::default(),
361421
}];
362422
let qobject_idents = create_qobjectname();
363423

crates/cxx-qt-gen/src/generator/cpp/property/setter.rs

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,47 @@
33
//
44
// SPDX-License-Identifier: MIT OR Apache-2.0
55

6-
use crate::generator::{cpp::fragment::CppFragment, naming::property::QPropertyNames};
6+
use crate::generator::{
7+
cpp::fragment::CppFragment,
8+
naming::property::{NameState, QPropertyNames},
9+
};
710
use indoc::formatdoc;
811

9-
pub fn generate(idents: &QPropertyNames, qobject_ident: &str, cxx_ty: &str) -> CppFragment {
10-
CppFragment::Pair {
11-
header: format!(
12-
"Q_SLOT void {ident_setter}({cxx_ty} const& value);",
13-
ident_setter = idents.setter.cxx_unqualified(),
14-
),
15-
source: formatdoc! {
16-
r#"
17-
void
18-
{qobject_ident}::{ident_setter}({cxx_ty} const& value)
19-
{{
20-
const ::rust::cxxqt1::MaybeLockGuard<{qobject_ident}> guard(*this);
21-
{ident_setter_wrapper}(value);
22-
}}
23-
"#,
24-
ident_setter = idents.setter.cxx_unqualified(),
25-
ident_setter_wrapper = idents.setter_wrapper.cxx_unqualified(),
26-
},
12+
pub fn generate(idents: &QPropertyNames, qobject_ident: &str, cxx_ty: &str) -> Option<CppFragment> {
13+
// Only generates setter code if the state provided is Auto (not custom provided by user)
14+
if let (Some(NameState::Auto(setter)), Some(setter_wrapper)) =
15+
(&idents.setter, &idents.setter_wrapper)
16+
{
17+
Some(CppFragment::Pair {
18+
header: format!(
19+
"Q_SLOT void {ident_setter}({cxx_ty} const& value);",
20+
ident_setter = setter.cxx_unqualified(),
21+
),
22+
source: formatdoc! {
23+
r#"
24+
void
25+
{qobject_ident}::{ident_setter}({cxx_ty} const& value)
26+
{{
27+
const ::rust::cxxqt1::MaybeLockGuard<{qobject_ident}> guard(*this);
28+
{ident_setter_wrapper}(value);
29+
}}
30+
"#,
31+
ident_setter = setter.cxx_unqualified(),
32+
ident_setter_wrapper = setter_wrapper.cxx_unqualified(),
33+
},
34+
})
35+
} else {
36+
None
2737
}
2838
}
2939

30-
pub fn generate_wrapper(idents: &QPropertyNames, cxx_ty: &str) -> CppFragment {
31-
CppFragment::Header(format!(
32-
// Note that we pass T not const T& to Rust so that it is by-value
33-
// https://github.com/KDAB/cxx-qt/issues/463
34-
"void {ident_setter_wrapper}({cxx_ty} value) noexcept;",
35-
ident_setter_wrapper = idents.setter_wrapper.cxx_unqualified()
36-
))
40+
pub fn generate_wrapper(idents: &QPropertyNames, cxx_ty: &str) -> Option<CppFragment> {
41+
idents.setter_wrapper.as_ref().map(|setter_wrapper| {
42+
CppFragment::Header(format!(
43+
// Note that we pass T not const T& to Rust so that it is by-value
44+
// https://github.com/KDAB/cxx-qt/issues/463
45+
"void {ident_setter_wrapper}({cxx_ty} value) noexcept;",
46+
ident_setter_wrapper = setter_wrapper.cxx_unqualified()
47+
))
48+
})
3749
}

0 commit comments

Comments
 (0)