diff --git a/Project.toml b/Project.toml index 282efbd..47b697e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "QML" uuid = "2db162a6-7e43-52c3-8d84-290c1c42d82a" -version = "0.12.1" +version = "0.13.0" authors = ["Bart Janssens "] [deps] @@ -28,9 +28,9 @@ ColorTypes = "0.11, 0.12" CxxWrap = "0.17.5" MacroTools = "0.5" Observables = "0.5" -Qt6Svg_jll = "6.8" -Qt6Wayland_jll = "6.8" -jlqml_jll = "0.9.0" +Qt6Svg_jll = "6.10" +Qt6Wayland_jll = "6.10" +jlqml_jll = "0.10.0" julia = "1.10" [extras] diff --git a/src/QML.jl b/src/QML.jl index 6f7c1ab..f595832 100644 --- a/src/QML.jl +++ b/src/QML.jl @@ -88,6 +88,9 @@ Load a QML file, creating a [`QML.QQmlApplicationEngine`](@ref), and setting the """ function loadqml(qmlfilename; kwargs...) qml_engine = init_qmlapplicationengine() + return loadqml(qml_engine, qmlfilename; kwargs...) +end +function loadqml(qml_engine,qmlfilename; kwargs...) ctx = root_context(CxxRef(qml_engine)) for (key,value) in kwargs set_context_property(ctx, String(key), value) @@ -208,6 +211,7 @@ function Base.iterate(s::QString, i::Integer=1) end Base.convert(::Type{<:QString}, s::String) = QString(s) QString(u::QUrl) = toString(u) +@cxxdereference Base.show(io::IO, u::QUrl) = print(io, toString(u)) # QByteArray Base.convert(::Type{QByteArray}, s::AbstractString) = QByteArray(s) @@ -310,6 +314,14 @@ Base.iterate(h::QMap, state::QMapIterator) = _qmap_iteration_tuple(h, iteratorne Base.values(h::QMap) = QML.values(h) Base.keys(h::QMap) = QML.keys(h) +#QTimer helper +function QTimer(f, interval) + timer = QTimer() + setInterval(timer, interval) + callOnTimeout(timer, f) + start(timer) +end + # Helper to call a julia function function julia_call(f, argptr::Ptr{Cvoid}) arglist = CxxRef{QVariantList}(argptr)[] @@ -409,17 +421,26 @@ JuliaPropertyMap(dict::Dict{<:AbstractString,<:Any}) = JuliaPropertyMap(dict...) @cxxdereference value(::Type{JuliaPropertyMap}, qvar::QVariant) = getpropertymap(qvar) -const _queued_properties = [] - # Functor to update a QML property when an Observable is changed in Julia struct QmlPropertyUpdater propertymap::QQmlPropertyMap key::String active::Bool end + +const _queued_properties = Dict{QmlPropertyUpdater,Any}() +const _queue_lock = ReentrantLock() +_called_update = false; + function (updater::QmlPropertyUpdater)(x) if Base.current_task() != Base.roottask - push!(_queued_properties, (updater, x)) + @lock _queue_lock begin + _queued_properties[updater] = x + if !_called_update + queue_process_eventloop_updates() + global _called_update = true + end + end return end updater.propertymap[updater.key] = x @@ -445,6 +466,10 @@ macro deferredcall(expr) $(esc(expr)) else put!(_deferred_calls, () -> $(esc(expr))) + @lock _queue_lock if !_called_update + queue_process_eventloop_updates() + global _called_update = true + end nothing end end @@ -669,6 +694,7 @@ function Base.displayable(d::JuliaDisplay, mime::AbstractString) end include("itemmodel.jl") +include("imageprovider.jl") function exec() # We redirect to the Core stdout/err in case threading is used @@ -687,7 +713,21 @@ function exec() return end +# Runs all observable and model updates that need to be done on the main event loop +# Updates are cached here when they were made from a task different from the Julia root task +function process_eventloop_updates() + @lock _queue_lock begin + for (updater, x) in _queued_properties + updater.propertymap[updater.key] = x + end + empty!(_queued_properties) + run_deferred_calls() + global _called_update = false + end +end + function exec_async() + global _called_update = true # No need to queue event loop updates to the main thread lastdisplay = popdisplay() if VERSION >= v"1.12-" newrepl = @async Base.run_main_repl(true,true,:yes,true) @@ -701,11 +741,8 @@ function exec_async() pushdisplay(lastdisplay) while !istaskdone(newrepl) - for (updater, x) in _queued_properties - updater.propertymap[updater.key] = x - end - empty!(_queued_properties) - run_deferred_calls() + process_eventloop_updates() + global _called_update = true process_events() sleep(0.015) end diff --git a/src/imageprovider.jl b/src/imageprovider.jl new file mode 100644 index 0000000..e26276d --- /dev/null +++ b/src/imageprovider.jl @@ -0,0 +1,30 @@ +export ImageProvider, QImage, QPixmap, QColor, QSize, setcallback, addImageProvider + +# Wrapper for the C++ type for easier construction and +# keeping a reference to the callback to prevent GC +mutable struct ImageProvider + provider::JuliaImageProvider # The C++ object + callback + + function ImageProvider(imagetype, callback) + provider_cpp = JuliaImageProvider(imagetype) + provider = new(provider_cpp) + setcallback(provider, callback) + return provider + end +end + +function setcallback(provider, callback) + callback_imageresult(id, w, h) = ImageResult(callback(id, w, h)...) + callback_c = @CxxWrap.safe_cfunction($callback_imageresult, Any, (ConstCxxRef{QString},Cint,Cint)) + set_callback(provider.provider, callback_c) + provider.callback = callback_c + return +end + +Base.deepcopy(image::QImage) = QML.copy(image) + +@cxxdereference addImageProvider(engine, id, provider::ImageProvider) = QML.addImageProvider(engine, id, CxxPtr(provider.provider)) + +ImageResult(image::QImage, width, height) = QML.ImageResult{QImage}(image, Int32(width), Int32(height)) +ImageResult(image::QPixmap, width, height) = QML.ImageResult{QPixmap}(image, Int32(width), Int32(height)) diff --git a/test/imageprovider.jl b/test/imageprovider.jl new file mode 100644 index 0000000..f9aa4f8 --- /dev/null +++ b/test/imageprovider.jl @@ -0,0 +1,84 @@ +using QML +using Test + +qmlfile = joinpath(dirname(@__FILE__), "qml", "imageprovider.qml") + +""" + make_qimage_rgb888(rgb::NTuple{3, Integer}, width::Integer, height::Integer) + -> (buf::Vector{UInt8}, bytes_per_line::Int) + +Create a flat `Vector{UInt8}` containing pixel data for a solid-color image in +**QImage::Format_RGB888** (3 bytes/pixel in R, G, B order), laid out row-major. + +Returns the buffer and `bytes_per_line` (stride) which you pass to `QImage`. + +Notes: +- The buffer is tightly packed: `bytes_per_line == 3 * width`. +- Keep a Julia reference to `buf` alive for as long as Qt may read it, + unless your C++ side deep-copies the image data. +""" +function make_qimage_rgb888(rgb::NTuple{3, Integer}, width::Integer, height::Integer) + w = Int(width); h = Int(height) + (w > 0 && h > 0) || throw(ArgumentError("width and height must be positive")) + + r = UInt8(clamp(rgb[1], 0, 255)) + g = UInt8(clamp(rgb[2], 0, 255)) + b = UInt8(clamp(rgb[3], 0, 255)) + + bytes_per_line = 3 * w + buf = Vector{UInt8}(undef, h * bytes_per_line) + + # Build one scanline and copy it h times — fast and branch-free + @inbounds begin + row = Vector{UInt8}(undef, bytes_per_line) + for x in 0:w-1 + i = 3x + 1 + row[i] = r + row[i+1] = g + row[i+2] = b + end + for y in 0:h-1 + dest = y * bytes_per_line + 1 + copyto!(buf, dest, row, 1, bytes_per_line) + end + end + + return buf, bytes_per_line +end + +function image_callback(id, requestedwidth, requestedheight) + width = requestedwidth <= 0 ? 100 : requestedwidth + height = requestedheight <= 0 ? 100 : requestedheight + + color = (0,0,0) + if id[] == "yellow" + color = (255,255,0) + elseif id[] == "red" + color = (255,0,0) + end + + buf, stride = make_qimage_rgb888(color, width, height) + image = QImage(pointer(buf), width, height, stride, QML.Format_RGB888) + return deepcopy(image), width, height +end + +function pixmap_callback(id, requestedwidth, requestedheight) + width = requestedwidth <= 0 ? 100 : requestedwidth + height = requestedheight <= 0 ? 100 : requestedheight + + pixmap = QPixmap(width, height) + QML.fill(pixmap, QColor(id)) + + return pixmap, width, height +end + +imageprovider = ImageProvider(QML.Image, image_callback) +pixmapprovider = ImageProvider(QML.Pixmap, pixmap_callback) +engine = init_qmlapplicationengine() + +addImageProvider(engine, "images", imageprovider) +addImageProvider(engine, "pixmaps", pixmapprovider) + +loadqml(engine, qmlfile) +exec() + diff --git a/test/qml/imageprovider.qml b/test/qml/imageprovider.qml new file mode 100644 index 0000000..4913b62 --- /dev/null +++ b/test/qml/imageprovider.qml @@ -0,0 +1,58 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ApplicationWindow { + visible: true + width: 320 + height: 240 + + RowLayout { + anchors.fill: parent + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Image { + source: "image://images/yellow" + Layout.fillWidth: true + Layout.fillHeight: true + sourceSize.width: width + sourceSize.height: height + } + Image { + source: "image://images/red" + Layout.fillWidth: true + Layout.fillHeight: true + sourceSize.width: width + sourceSize.height: height + } + } + Image { + source: "image://pixmaps/black" + Layout.fillWidth: true + Layout.fillHeight: true + sourceSize.width: width + sourceSize.height: height + } + Image { + source: "image://images/yellow" + Layout.fillWidth: true + Layout.fillHeight: true + sourceSize.width: width + sourceSize.height: height + } + Image { + source: "image://images/red" + Layout.fillWidth: true + Layout.fillHeight: true + sourceSize.width: width + sourceSize.height: height + } + } + + Timer { + interval: 1000; running: true; repeat: false + onTriggered: Qt.exit(0) + } + +} \ No newline at end of file diff --git a/test/qstring.jl b/test/qstring.jl index 20b69d3..3416b94 100644 --- a/test/qstring.jl +++ b/test/qstring.jl @@ -16,3 +16,11 @@ let strings = ["TestStr", "😁😃😆abc😎😈☹"] @test strings == qsl end + +let filename = "test.txt" + uri = QUrlFromLocalFile(filename) + uri_repr = repr(uri) + @test startswith(uri_repr, "file") + @test endswith(uri_repr, filename) + @test QML.toLocalFile(uri) == filename +end