From 1cd623d24116863b6219fda9a340b83a5d2b3f20 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Tue, 29 Jul 2025 08:53:20 +0200 Subject: [PATCH 1/2] Add support for setting a Makie scene and proper scaling Issue https://github.com/JuliaGraphics/QMLMakie.jl/issues/4 --- CMakeLists.txt | 2 +- jlqml.hpp | 19 ++++++++++-- makie_viewport.cpp | 45 ++++++++++++++++++++++++---- makie_viewport.hpp | 9 ++++++ opengl_viewport.hpp | 2 +- qmltests/crash.jl | 73 +++++++++++++++++++++++++++++++++++++++++++++ wrap_qml.cpp | 31 +++++++------------ 7 files changed, 151 insertions(+), 30 deletions(-) create mode 100644 qmltests/crash.jl diff --git a/CMakeLists.txt b/CMakeLists.txt index 7f94c7e..938f507 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16.0) project(JlQML) -set(JlQML_VERSION 0.7.0) +set(JlQML_VERSION 0.7.1) message(STATUS "Project version: v${JlQML_VERSION}") set(CMAKE_MACOSX_RPATH 1) diff --git a/jlqml.hpp b/jlqml.hpp index 8517efd..8bbd7d5 100644 --- a/jlqml.hpp +++ b/jlqml.hpp @@ -4,8 +4,21 @@ #include #include -Q_DECLARE_METATYPE(jlcxx::SafeCFunction) -Q_DECLARE_OPAQUE_POINTER(jl_value_t*) -Q_DECLARE_METATYPE(jl_value_t*) +namespace qmlwrap +{ + +// Helper to store a Julia value of type Any in a GC-safe way +struct QVariantAny +{ + QVariantAny(jl_value_t* v); + ~QVariantAny(); + jl_value_t* value; +}; + +using qvariant_any_t = std::shared_ptr; + +} + +Q_DECLARE_METATYPE(qmlwrap::qvariant_any_t) #endif diff --git a/makie_viewport.cpp b/makie_viewport.cpp index b15bd48..dad58c9 100644 --- a/makie_viewport.cpp +++ b/makie_viewport.cpp @@ -13,8 +13,12 @@ namespace qmlwrap class MakieRenderFunction : public RenderFunction { public: - MakieRenderFunction(jl_value_t* const & screen_ptr) : m_screen_ptr(screen_ptr) + MakieRenderFunction(jl_value_t* const & screen_ptr, jl_value_t*& scene) : m_screen_ptr(screen_ptr), m_scene(scene) { + if(MakieViewport::m_default_render_function.fptr != nullptr) + { + m_scene_render_function = jlcxx::make_function_pointer(MakieViewport::m_default_render_function); + } } void setRenderFunction(jlcxx::SafeCFunction f) override @@ -23,12 +27,22 @@ class MakieRenderFunction : public RenderFunction } void render() override { - m_render_function(m_screen_ptr); + if(m_scene != nullptr) + { + m_scene_render_function(m_screen_ptr, m_scene); + } + else if(m_render_function != nullptr) + { + m_render_function(m_screen_ptr); + } } private: typedef void (*render_callback_t)(jl_value_t*); - render_callback_t m_render_function; + render_callback_t m_render_function = nullptr; + typedef void (*scene_render_callback_t)(jl_value_t*, jl_value_t*); // Default render function takes an extra scene argument + scene_render_callback_t m_scene_render_function; jl_value_t* const & m_screen_ptr; + jl_value_t*& m_scene; }; jl_module_t* get_makie_support_module() @@ -73,7 +87,7 @@ struct MakieSupport jlcxx::JuliaFunction on_context_destroy; }; -MakieViewport::MakieViewport(QQuickItem *parent) : OpenGLViewport(parent, new MakieRenderFunction(m_screen)) +MakieViewport::MakieViewport(QQuickItem *parent) : OpenGLViewport(parent, new MakieRenderFunction(m_screen, m_scene)) { get_makie_support_module(); // Throw the possible error early QObject::connect(this, &QQuickItem::windowChanged, [this] (QQuickWindow* w) @@ -96,13 +110,33 @@ MakieViewport::~MakieViewport() { jlcxx::unprotect_from_gc(m_screen); } + if(m_scene != nullptr) + { + jlcxx::unprotect_from_gc(m_scene); + } +} + +qvariant_any_t MakieViewport::scene() +{ + return std::make_shared(m_scene); +} + +void MakieViewport::setScene(qvariant_any_t scene) +{ + jl_value_t* scene_val = scene->value; + jlcxx::protect_from_gc(scene_val); + if(m_scene != nullptr) + { + jlcxx::unprotect_from_gc(m_scene); + } + m_scene = scene_val; } void MakieViewport::setup_buffer(QOpenGLFramebufferObject* fbo) { if(m_screen == nullptr) { - m_screen = MakieSupport::instance().setup_screen(std::forward(fbo)); + m_screen = MakieSupport::instance().setup_screen(std::forward(fbo), window()); jlcxx::protect_from_gc(m_screen); } else @@ -113,5 +147,6 @@ void MakieViewport::setup_buffer(QOpenGLFramebufferObject* fbo) } jl_module_t* MakieViewport::m_qmlmakie_mod = nullptr; +jlcxx::SafeCFunction MakieViewport::m_default_render_function = {nullptr, nullptr, nullptr}; } // namespace qmlwrap diff --git a/makie_viewport.hpp b/makie_viewport.hpp index 20e4275..141dd0a 100644 --- a/makie_viewport.hpp +++ b/makie_viewport.hpp @@ -11,14 +11,23 @@ class MakieViewport : public OpenGLViewport { Q_OBJECT QML_ELEMENT + Q_PROPERTY(qvariant_any_t scene READ scene WRITE setScene NOTIFY sceneChanged) public: MakieViewport(QQuickItem* parent = 0); virtual ~MakieViewport(); + qvariant_any_t scene(); + void setScene(qvariant_any_t scene); + static jl_module_t* m_qmlmakie_mod; + static jlcxx::SafeCFunction m_default_render_function; + +signals: + void sceneChanged(); private: // Screen created and used on the Julia side jl_value_t* m_screen = nullptr; + jl_value_t* m_scene = nullptr; virtual void setup_buffer(QOpenGLFramebufferObject* fbo) override; }; diff --git a/opengl_viewport.hpp b/opengl_viewport.hpp index c22cad4..3d2f210 100644 --- a/opengl_viewport.hpp +++ b/opengl_viewport.hpp @@ -8,7 +8,7 @@ #include #include -// #include "jlqml.hpp" +#include "jlqml.hpp" namespace qmlwrap { diff --git a/qmltests/crash.jl b/qmltests/crash.jl new file mode 100644 index 0000000..d4becd9 --- /dev/null +++ b/qmltests/crash.jl @@ -0,0 +1,73 @@ +using QML + +# const qml_file = joinpath(pwd(), "code", "qml", "test.qml") + +function update_props() + props["fruits"] = fruits2 +end + +function main(qml_file, props) + loadqml(qml_file, props=props) + exec() +end + +mutable struct Fruit + name::String + cost::Float64 +end + +fruits = JuliaItemModel([Fruit("apple", 1.0), Fruit("orange", 2.0)]) +fruits2 = JuliaItemModel([Fruit("banana", 1.0), Fruit("pear", 2.0)]) + +props = JuliaPropertyMap("fruits" => fruits) + +@qmlfunction update_props + +# main(qml_file, props) + + +mktempdir() do folder + path = joinpath(folder, "test.qml") + write( + path, + """ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.julialang + +ApplicationWindow { + id: mainWin + title: "My Application" + width: 400 + height: 400 + visible: true + + ColumnLayout{ + anchors.fill: parent + + Button { + height: 200 + Layout.fillWidth: true + text: "Update Fruit" + onClicked: { + Julia.update_props() + console.log("Value:", props.fruits) + } + } + ListView { + model: props.fruits + Layout.fillHeight: true + Layout.fillWidth: true + delegate: Row { + Text { + text: name + } + } + } + } +} +""" + ) + main(path, props) +end \ No newline at end of file diff --git a/wrap_qml.cpp b/wrap_qml.cpp index 31dd65f..5e8f9df 100644 --- a/wrap_qml.cpp +++ b/wrap_qml.cpp @@ -51,29 +51,15 @@ template<> struct SuperType { using type = QAbstractTab namespace qmlwrap { -// Helper to store a Julia value of type Any in a GC-safe way -struct QVariantAny +QVariantAny::QVariantAny(jl_value_t* v) : value(v) { - QVariantAny(jl_value_t* v) : value(v) - { - assert(v != nullptr); - jlcxx::protect_from_gc(value); - } - ~QVariantAny() - { - jlcxx::unprotect_from_gc(value); - } - jl_value_t* value; -}; - -using qvariant_any_t = std::shared_ptr; - + assert(v != nullptr); + jlcxx::protect_from_gc(value); } - -Q_DECLARE_METATYPE(qmlwrap::qvariant_any_t) - -namespace qmlwrap +QVariantAny::~QVariantAny() { + jlcxx::unprotect_from_gc(value); +} using qvariant_types = jlcxx::ParameterList, JuliaDisplay*, JuliaCanvas*, JuliaPropertyMap*, QObject*>; @@ -296,6 +282,11 @@ JLCXX_MODULE define_julia_module(jlcxx::Module& qml_module) qmlwrap::MakieViewport::m_qmlmakie_mod = reinterpret_cast(mod); }); + qml_module.method("set_default_makie_renderfunction", [](jlcxx::SafeCFunction renderFunction) + { + qmlwrap::MakieViewport::m_default_render_function = renderFunction; + }); + // Enums qml_module.add_bits("Orientation", jlcxx::julia_type("CppEnum")); qml_module.set_const("Horizontal", Qt::Horizontal); From 78a105c08fc0d1efe8de14f5922de07a29124293 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Tue, 29 Jul 2025 09:33:28 +0200 Subject: [PATCH 2/2] Update CI --- .github/workflows/test.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da2a57e..415ddaa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,15 +15,21 @@ jobs: fail-fast: false matrix: version: - - '1.8' - - '1.9' + - '1.10' + - '1' - 'nightly' os: - ubuntu-latest - - macOS-latest + - macos-latest - windows-latest - arch: - - x64 + arch: [x64, arm64] + exclude: + - os: ubuntu-latest + arch: arm64 + - os: windows-latest + arch: arm64 + - os: macos-latest + arch: x64 steps: - uses: actions/checkout@v3 with: @@ -34,18 +40,13 @@ jobs: repository: JuliaInterop/libcxxwrap-julia path: libcxxwrap - name: Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: '6.4.2' + uses: jurplel/install-qt-action@v4 - uses: julia-actions/setup-julia@latest with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - name: Build libcxxwrap-julia run: | - if [[ "$OSTYPE" != "darwin"* ]]; then - rm -f /opt/hostedtoolcache/julia/1.6*/x64/lib/julia/libstdc++.so.6 - fi mkdir build-libcxxwrap && cd build-libcxxwrap cmake -DCMAKE_INSTALL_PREFIX=$HOME/install -DJLCXX_BUILD_EXAMPLES=OFF -DCMAKE_BUILD_TYPE=Debug ../libcxxwrap VERBOSE=ON cmake --build . --config Debug --target install