woboq / qmetaobject-rs

Integrate Qml and Rust by building the QMetaObject at compile time.
MIT License
648 stars 89 forks source link

[help needed] using rust-based enums as qml properties #264

Open xzz53 opened 2 years ago

xzz53 commented 2 years ago

Hi, I'm a new qmetaobject-rs user, and I'm trying to use QEnum-deriving enum as a property type. My code compiles, but at runtime qml sees the property type Object instead of int. Any hints are welcome. The minimal (broken) example and output are below.

use cstr::cstr;
use qmetaobject::{prelude::*, QMetaType};

#[derive(Copy, Clone, Debug, Eq, PartialEq, QEnum)]
#[repr(C)]
pub enum OpMode {
    Generator = 0,
    Wav,
    I2C,
    Spi,
    Uart,
}

impl Default for OpMode {
    fn default() -> Self {
        OpMode::Generator
    }
}

impl QMetaType for OpMode {}

#[allow(non_snake_case)]
#[derive(Default, QObject)]
struct Backend {
    base: qt_base_class!(trait QObject),

    opMode: qt_property!(OpMode; NOTIFY opmode_changed WRITE set_opmode READ get_opmode),
    opmode_changed: qt_signal!(),
}

impl Backend {
    fn get_opmode(&self) -> OpMode {
        println!("get_opmode()");
        self.opMode
    }

    fn set_opmode(&mut self, mode: OpMode) {
        println!("set_opmode(): old={:?}, new={:?}", self.opMode, mode);
        self.opMode = mode;
    }
}

fn main() {
    qml_register_enum::<OpMode>(cstr!("Backend"), 1, 0, cstr!("OpMode"));
    qml_register_type::<Backend>(cstr!("Backend"), 1, 0, cstr!("Backend"));

    let backend = QObjectBox::new(Backend::default());
    let mut engine = QmlEngine::new();

    engine.set_object_property(QString::from("backend"), backend.pinned());

    engine.load_data(
        r#"import QtQuick 2.15
import QtQuick.Window 2.15
import Backend 1.0

Window {
    id: window
    property int testprop

    visible: true
    width: 450
    height: 580

    Component.onCompleted: {
        console.log(OpMode.Wav)
        console.log(backend.OpMode, typeof(backend.OpMode), JSON.stringify(backend.OpMode))

        window.testprop = backend.OpMode
        console.log(window.testprop)
    }
}
"#
        .into(),
    );
    engine.exec();
}
qml: 1
qml: [object Object] object {}
<Unknown File>:17: Error: Cannot assign QObject* to int
andrew-otiv commented 1 year ago

Thanks for sharing your code; it helped me progress on a similar enum vs. int related typing issue while trying to set an enum property. When I try to set my property with "my_property = MyEnum.MyVariant" in a callback, I hit ":57: Error: Cannot assign int to TypeId{t:11890792827600742819}"

You ~did declare "property int testprop"... did you already try "property var testprop" or "property backend.OpMode"?

I believe this has to do with enums in C not being types; they're "tags" and have type int. So the type of the enum is conflated with the type of the variant, and enums aren't really first class types. I am still looking for a workaround, but the answer might just be to give up on enums in qt and resort to stringly or intly typed code.

andrew-otiv commented 1 year ago

I changed my property that was previously my enum to be an i32: stateMachineQt: qt_property!(i32; READ getState WRITE setState NOTIFY state_changed), Then I implemented TryFrom for it following:

https://stackoverflow.com/questions/28028854/how-do-i-match-enum-values-with-an-integer

And I use that to validate the int in the setter with something like:

    fn setState(&mut self, new_state: i32) {
        QtStateMachineStateEnum::try_from(new_state).unwrap();
        self.stateMachineQt = new_state;
    }

This alone wouldn't provide much type safety, since I could still use the wrong enum in the QML, but I mitigated it using large random numbers for my enum variants so they ~probably won't collide with the variants of any other enums:

#[derive(Copy, Clone, Debug, Eq, PartialEq, QEnum)]
#[repr(C)]
enum QtStateMachineStateEnum {
    Disabled = 13739,
    Enabled = 23639,
}