Skip to content

Commit 8921b37

Browse files
authored
Add imageprovider and improve threading support (#235)
1 parent a4e81e6 commit 8921b37

6 files changed

Lines changed: 229 additions & 12 deletions

File tree

Project.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "QML"
22
uuid = "2db162a6-7e43-52c3-8d84-290c1c42d82a"
3-
version = "0.12.1"
3+
version = "0.13.0"
44
authors = ["Bart Janssens <bart@bartjanssens.org>"]
55

66
[deps]
@@ -28,9 +28,9 @@ ColorTypes = "0.11, 0.12"
2828
CxxWrap = "0.17.5"
2929
MacroTools = "0.5"
3030
Observables = "0.5"
31-
Qt6Svg_jll = "6.8"
32-
Qt6Wayland_jll = "6.8"
33-
jlqml_jll = "0.9.0"
31+
Qt6Svg_jll = "6.10"
32+
Qt6Wayland_jll = "6.10"
33+
jlqml_jll = "0.10.0"
3434
julia = "1.10"
3535

3636
[extras]

src/QML.jl

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ Load a QML file, creating a [`QML.QQmlApplicationEngine`](@ref), and setting the
8888
"""
8989
function loadqml(qmlfilename; kwargs...)
9090
qml_engine = init_qmlapplicationengine()
91+
return loadqml(qml_engine, qmlfilename; kwargs...)
92+
end
93+
function loadqml(qml_engine,qmlfilename; kwargs...)
9194
ctx = root_context(CxxRef(qml_engine))
9295
for (key,value) in kwargs
9396
set_context_property(ctx, String(key), value)
@@ -208,6 +211,7 @@ function Base.iterate(s::QString, i::Integer=1)
208211
end
209212
Base.convert(::Type{<:QString}, s::String) = QString(s)
210213
QString(u::QUrl) = toString(u)
214+
@cxxdereference Base.show(io::IO, u::QUrl) = print(io, toString(u))
211215

212216
# QByteArray
213217
Base.convert(::Type{QByteArray}, s::AbstractString) = QByteArray(s)
@@ -310,6 +314,14 @@ Base.iterate(h::QMap, state::QMapIterator) = _qmap_iteration_tuple(h, iteratorne
310314
Base.values(h::QMap) = QML.values(h)
311315
Base.keys(h::QMap) = QML.keys(h)
312316

317+
#QTimer helper
318+
function QTimer(f, interval)
319+
timer = QTimer()
320+
setInterval(timer, interval)
321+
callOnTimeout(timer, f)
322+
start(timer)
323+
end
324+
313325
# Helper to call a julia function
314326
function julia_call(f, argptr::Ptr{Cvoid})
315327
arglist = CxxRef{QVariantList}(argptr)[]
@@ -409,17 +421,26 @@ JuliaPropertyMap(dict::Dict{<:AbstractString,<:Any}) = JuliaPropertyMap(dict...)
409421

410422
@cxxdereference value(::Type{JuliaPropertyMap}, qvar::QVariant) = getpropertymap(qvar)
411423

412-
const _queued_properties = []
413-
414424
# Functor to update a QML property when an Observable is changed in Julia
415425
struct QmlPropertyUpdater
416426
propertymap::QQmlPropertyMap
417427
key::String
418428
active::Bool
419429
end
430+
431+
const _queued_properties = Dict{QmlPropertyUpdater,Any}()
432+
const _queue_lock = ReentrantLock()
433+
_called_update = false;
434+
420435
function (updater::QmlPropertyUpdater)(x)
421436
if Base.current_task() != Base.roottask
422-
push!(_queued_properties, (updater, x))
437+
@lock _queue_lock begin
438+
_queued_properties[updater] = x
439+
if !_called_update
440+
queue_process_eventloop_updates()
441+
global _called_update = true
442+
end
443+
end
423444
return
424445
end
425446
updater.propertymap[updater.key] = x
@@ -445,6 +466,10 @@ macro deferredcall(expr)
445466
$(esc(expr))
446467
else
447468
put!(_deferred_calls, () -> $(esc(expr)))
469+
@lock _queue_lock if !_called_update
470+
queue_process_eventloop_updates()
471+
global _called_update = true
472+
end
448473
nothing
449474
end
450475
end
@@ -669,6 +694,7 @@ function Base.displayable(d::JuliaDisplay, mime::AbstractString)
669694
end
670695

671696
include("itemmodel.jl")
697+
include("imageprovider.jl")
672698

673699
function exec()
674700
# We redirect to the Core stdout/err in case threading is used
@@ -687,7 +713,21 @@ function exec()
687713
return
688714
end
689715

716+
# Runs all observable and model updates that need to be done on the main event loop
717+
# Updates are cached here when they were made from a task different from the Julia root task
718+
function process_eventloop_updates()
719+
@lock _queue_lock begin
720+
for (updater, x) in _queued_properties
721+
updater.propertymap[updater.key] = x
722+
end
723+
empty!(_queued_properties)
724+
run_deferred_calls()
725+
global _called_update = false
726+
end
727+
end
728+
690729
function exec_async()
730+
global _called_update = true # No need to queue event loop updates to the main thread
691731
lastdisplay = popdisplay()
692732
if VERSION >= v"1.12-"
693733
newrepl = @async Base.run_main_repl(true,true,:yes,true)
@@ -701,11 +741,8 @@ function exec_async()
701741
pushdisplay(lastdisplay)
702742

703743
while !istaskdone(newrepl)
704-
for (updater, x) in _queued_properties
705-
updater.propertymap[updater.key] = x
706-
end
707-
empty!(_queued_properties)
708-
run_deferred_calls()
744+
process_eventloop_updates()
745+
global _called_update = true
709746
process_events()
710747
sleep(0.015)
711748
end

src/imageprovider.jl

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export ImageProvider, QImage, QPixmap, QColor, QSize, setcallback, addImageProvider
2+
3+
# Wrapper for the C++ type for easier construction and
4+
# keeping a reference to the callback to prevent GC
5+
mutable struct ImageProvider
6+
provider::JuliaImageProvider # The C++ object
7+
callback
8+
9+
function ImageProvider(imagetype, callback)
10+
provider_cpp = JuliaImageProvider(imagetype)
11+
provider = new(provider_cpp)
12+
setcallback(provider, callback)
13+
return provider
14+
end
15+
end
16+
17+
function setcallback(provider, callback)
18+
callback_imageresult(id, w, h) = ImageResult(callback(id, w, h)...)
19+
callback_c = @CxxWrap.safe_cfunction($callback_imageresult, Any, (ConstCxxRef{QString},Cint,Cint))
20+
set_callback(provider.provider, callback_c)
21+
provider.callback = callback_c
22+
return
23+
end
24+
25+
Base.deepcopy(image::QImage) = QML.copy(image)
26+
27+
@cxxdereference addImageProvider(engine, id, provider::ImageProvider) = QML.addImageProvider(engine, id, CxxPtr(provider.provider))
28+
29+
ImageResult(image::QImage, width, height) = QML.ImageResult{QImage}(image, Int32(width), Int32(height))
30+
ImageResult(image::QPixmap, width, height) = QML.ImageResult{QPixmap}(image, Int32(width), Int32(height))

test/imageprovider.jl

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using QML
2+
using Test
3+
4+
qmlfile = joinpath(dirname(@__FILE__), "qml", "imageprovider.qml")
5+
6+
"""
7+
make_qimage_rgb888(rgb::NTuple{3, Integer}, width::Integer, height::Integer)
8+
-> (buf::Vector{UInt8}, bytes_per_line::Int)
9+
10+
Create a flat `Vector{UInt8}` containing pixel data for a solid-color image in
11+
**QImage::Format_RGB888** (3 bytes/pixel in R, G, B order), laid out row-major.
12+
13+
Returns the buffer and `bytes_per_line` (stride) which you pass to `QImage`.
14+
15+
Notes:
16+
- The buffer is tightly packed: `bytes_per_line == 3 * width`.
17+
- Keep a Julia reference to `buf` alive for as long as Qt may read it,
18+
unless your C++ side deep-copies the image data.
19+
"""
20+
function make_qimage_rgb888(rgb::NTuple{3, Integer}, width::Integer, height::Integer)
21+
w = Int(width); h = Int(height)
22+
(w > 0 && h > 0) || throw(ArgumentError("width and height must be positive"))
23+
24+
r = UInt8(clamp(rgb[1], 0, 255))
25+
g = UInt8(clamp(rgb[2], 0, 255))
26+
b = UInt8(clamp(rgb[3], 0, 255))
27+
28+
bytes_per_line = 3 * w
29+
buf = Vector{UInt8}(undef, h * bytes_per_line)
30+
31+
# Build one scanline and copy it h times — fast and branch-free
32+
@inbounds begin
33+
row = Vector{UInt8}(undef, bytes_per_line)
34+
for x in 0:w-1
35+
i = 3x + 1
36+
row[i] = r
37+
row[i+1] = g
38+
row[i+2] = b
39+
end
40+
for y in 0:h-1
41+
dest = y * bytes_per_line + 1
42+
copyto!(buf, dest, row, 1, bytes_per_line)
43+
end
44+
end
45+
46+
return buf, bytes_per_line
47+
end
48+
49+
function image_callback(id, requestedwidth, requestedheight)
50+
width = requestedwidth <= 0 ? 100 : requestedwidth
51+
height = requestedheight <= 0 ? 100 : requestedheight
52+
53+
color = (0,0,0)
54+
if id[] == "yellow"
55+
color = (255,255,0)
56+
elseif id[] == "red"
57+
color = (255,0,0)
58+
end
59+
60+
buf, stride = make_qimage_rgb888(color, width, height)
61+
image = QImage(pointer(buf), width, height, stride, QML.Format_RGB888)
62+
return deepcopy(image), width, height
63+
end
64+
65+
function pixmap_callback(id, requestedwidth, requestedheight)
66+
width = requestedwidth <= 0 ? 100 : requestedwidth
67+
height = requestedheight <= 0 ? 100 : requestedheight
68+
69+
pixmap = QPixmap(width, height)
70+
QML.fill(pixmap, QColor(id))
71+
72+
return pixmap, width, height
73+
end
74+
75+
imageprovider = ImageProvider(QML.Image, image_callback)
76+
pixmapprovider = ImageProvider(QML.Pixmap, pixmap_callback)
77+
engine = init_qmlapplicationengine()
78+
79+
addImageProvider(engine, "images", imageprovider)
80+
addImageProvider(engine, "pixmaps", pixmapprovider)
81+
82+
loadqml(engine, qmlfile)
83+
exec()
84+

test/qml/imageprovider.qml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import QtQuick
2+
import QtQuick.Controls
3+
import QtQuick.Layouts
4+
5+
ApplicationWindow {
6+
visible: true
7+
width: 320
8+
height: 240
9+
10+
RowLayout {
11+
anchors.fill: parent
12+
ColumnLayout {
13+
Layout.fillWidth: true
14+
Layout.fillHeight: true
15+
Image {
16+
source: "image://images/yellow"
17+
Layout.fillWidth: true
18+
Layout.fillHeight: true
19+
sourceSize.width: width
20+
sourceSize.height: height
21+
}
22+
Image {
23+
source: "image://images/red"
24+
Layout.fillWidth: true
25+
Layout.fillHeight: true
26+
sourceSize.width: width
27+
sourceSize.height: height
28+
}
29+
}
30+
Image {
31+
source: "image://pixmaps/black"
32+
Layout.fillWidth: true
33+
Layout.fillHeight: true
34+
sourceSize.width: width
35+
sourceSize.height: height
36+
}
37+
Image {
38+
source: "image://images/yellow"
39+
Layout.fillWidth: true
40+
Layout.fillHeight: true
41+
sourceSize.width: width
42+
sourceSize.height: height
43+
}
44+
Image {
45+
source: "image://images/red"
46+
Layout.fillWidth: true
47+
Layout.fillHeight: true
48+
sourceSize.width: width
49+
sourceSize.height: height
50+
}
51+
}
52+
53+
Timer {
54+
interval: 1000; running: true; repeat: false
55+
onTriggered: Qt.exit(0)
56+
}
57+
58+
}

test/qstring.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,11 @@ let strings = ["TestStr", "😁😃😆abc😎😈☹"]
1616

1717
@test strings == qsl
1818
end
19+
20+
let filename = "test.txt"
21+
uri = QUrlFromLocalFile(filename)
22+
uri_repr = repr(uri)
23+
@test startswith(uri_repr, "file")
24+
@test endswith(uri_repr, filename)
25+
@test QML.toLocalFile(uri) == filename
26+
end

0 commit comments

Comments
 (0)