From cb39d7b90a0aeb4df25c2d257923b3ac1067c301 Mon Sep 17 00:00:00 2001 From: Bart Janssens Date: Fri, 1 Sep 2017 23:45:03 +0200 Subject: [PATCH] Deprecate @qmlapp, remove @qmlget and @qmlset Issues #43 and #44 --- README.md | 147 +++++++++++------------ deps/src/qmlwrap/application_manager.hpp | 3 - deps/src/qmlwrap/type_conversion.cpp | 8 +- deps/src/qmlwrap/wrap_qml.cpp | 37 +++--- example/gui.jl | 34 +++--- example/observable.jl | 19 +++ example/qml/observable.qml | 37 ++++++ src/QML.jl | 95 +++++++++------ test/functions.jl | 2 +- test/julia_arrays.jl | 11 +- test/julia_object.jl | 73 ++++------- test/julia_signal.jl | 2 +- test/listviews.jl | 2 +- test/properties.jl | 9 +- test/qml/julia_object.qml | 29 +++-- test/qml_propertymap.jl | 11 +- test/qqmlcomponent.jl | 7 +- test/qquickview.jl | 7 +- test/qtimer.jl | 4 +- test/runtests.jl | 2 +- 20 files changed, 295 insertions(+), 244 deletions(-) create mode 100644 example/observable.jl create mode 100644 example/qml/observable.qml diff --git a/README.md b/README.md index 9f8825b..8ffcef9 100644 --- a/README.md +++ b/README.md @@ -51,24 +51,14 @@ And additionally, ### Loading a QML file We support three methods of loading a QML file: `QQmlApplicationEngine`, `QQuickView` and `QQmlComponent`. These behave equivalently to the corresponding Qt classes. #### QQmlApplicationEngine -The easiest way to run the QML file `main.qml` from the current directory is using the `@qmlapp` macro: +The easiest way to run the QML file `main.qml` from the current directory is using the `load` function, which will create and return a `QQmlApplicationEngine` and load the supplied QML file: ```julia using QML -@qmlapp "main.qml" +load("main.qml") exec() ``` -The QML must have an `ApplicationWindow` as top component. It is also possible to default-construct the `QQmlApplicationEngine` and call `load` to load the QML separately: -```julia -qml_engine = init_qmlapplicationengine() -# ... -# set properties, ... -# ... -load(qml_engine, "main.qml") -``` - -This is useful to set context properties before loading the QML, for example. -Note we use `init_` functions rather than calling the constructor for the Qt type directly. The init methods have the advantage that cleanup (calling delete etc.) happens in C++ automatically. Calling the constructor directly requires manually finalizing the corresponding components in the correct order and has a high risk of causing crashes on exit. +The lifetime of the `QQmlApplicationEngine` is managed from C++ and it gets cleaned up when the application quits. This means it is not necessary to keep a reference to the engine to prevent it from being garbage collected prematurely. #### QQuickView The `QQuickView` creates a window, so it's not necessary to wrap the QML in `ApplicationWindow`. A QML file is loaded as follows: @@ -139,87 +129,95 @@ Julia.my_other_function(arg1, arg2) ``` ### Context properties -The entry point for setting context properties is the root context of the engine, available using the `qmlcontext()` function. It is defined once the `@qmlapp` macro or one of the init functions has been called. +Context properties are set using the context object method. To dynamically add properties from Julia, a `QQmlPropertyMap` is used, setting e.g. a property named `a`: ```julia -@qmlset qmlcontext().property_name = property_value +propmap = QML.QQmlPropertyMap() +propmap["a"] = 1 ``` -This sets the QML context property named `property_name` to value `julia_value`. Any time the `@qmlset` macro is called on such a property, QML is notified of the change and updates any dependent values. +This sets the QML context property named `property_name` to value `julia_value`. The value of a property can be queried from Julia like this: ```julia -@qmlget qmlcontext().property_name +@test propmap["a"] == 1 ``` -At application initialization, it is also possible to pass context properties as additional arguments to the `@qmlapp` macro: +To pass these properties to the QML side, the property map can be the second argument to `load`: ```julia -my_prop = 2. -@qmlapp "main.qml" my_prop +load(qml_file, propmap) ``` -This will initialize a context property named `my_prop` with the value 2. -#### Type conversion -Most fundamental types are converted implicitly. Mind that the default integer type in QML corresponds to `Int32` in Julia. +There is also a shorthand notation using keywords: +```julia +load(qml_file, a=1, b=2) +``` +This will create context properties `a` and `b`, initialized to `1` and `2`. -We also convert `QVariantMap`, exposing the indexing operator `[]` to access element by a string key. This mostly to deal with arguments passed to the QML `append` function in list models. +#### Observable properties +When an [`Observable`](https://github.com/JuliaGizmos/Observables.jl) is set in a `QQmlPropertyMap`, bi-directional change notification is enabled. For example, using the Julia code: +```julia +using QML +using Observables -#### Composite types -Setting a composite type as a context property maps the type fields into a `JuliaObject`, which derives from `QQmlPropertyMap`. Example: +const qml_file = "observable.qml" +const input = Observable(1.0) +const output = Observable(0.0) -```julia -type JuliaTestType - a::Int32 +on(output) do x + println("Output changed to ", x) end -jobj = JuliaTestType(0.) -@qmlset qmlcontext().julia_object = jobj -@qmlset qmlcontext().julia_object.a = 2 -@test @qmlget(root_ctx.julia_object.a) == 2 -@test jobj.a == 2 +load(qml_file, input=input, output=output) +exec_async() # run from REPL for async execution ``` -Access from QML: +In QML we add a slider for the input and display the output, which is twice the input (computed in QML here): ```qml import QtQuick 2.0 +import QtQuick.Controls 1.0 +import QtQuick.Layouts 1.0 -Timer { - interval: 0; running: true; repeat: false - onTriggered: { - julia_object.a = 1 - Qt.quit() - } - } -``` - -When passing a `JuliaObject` object from QML to a Julia function, it is automatically converted to the Julia value, so on the Julia side it can be manipulated as normal. To get the QML side to see the changes the `update` function must be called: +ApplicationWindow { + id: root + title: "Observables" + width: 512 + height: 200 + visible: true + + ColumnLayout { + spacing: 6 + anchors.fill: parent + + Slider { + value: input + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + minimumValue: 0.0 + maximumValue: 100.0 + stepSize: 1.0 + tickmarksEnabled: true + onValueChanged: { + input = value; + output = 2*input; + } + } -```julia -type JuliaTestType - a::Int32 - i::InnerType -end + Text { + Layout.alignment: Qt.AlignCenter + text: output + font.pixelSize: 0.1*root.height + } + } -# passed as context property -julia_object2 = JuliaTestType(0, InnerType(0.0)) +} +``` -function setthree(x::JuliaTestType) - x.a = 3 - x.i.x = 3.0 -end +Moving the slider will print the output on Julia. The input can also be set from the REPL using e.g. `input[] = 3.0`, and the slider will move accordingly and call QML to compute the output, which can be queried using `output[]`. -function testthree(a,x) - @test a == 3 - @test x == 3.0 -end -``` +#### Type conversion +Most fundamental types are converted implicitly. Mind that the default integer type in QML corresponds to `Int32` in Julia. -```qml -// ... -Julia.setthree(julia_object2) -julia_object2.update() -Julia.testthree(julia_object2.a, julia_object2.i.x) -// ... -``` +We also convert `QVariantMap`, exposing the indexing operator `[]` to access element by a string key. This mostly to deal with arguments passed to the QML `append` function in list models. ### Emitting signals from Julia Defining signals must be done in QML in the JuliaSignals block, following the instructions from the [QML manual](http://doc.qt.io/qt-5/qtqml-syntax-objectattributes.html#signal-attributes). Example signal with connection: @@ -254,7 +252,7 @@ adds the role named `myrole` to `array_model`, using the function `myrole` to ac To use the model from QML, it can be exposed as a context attribute, e.g: ```julia -@qmlapp qml_file array_model +load(qml_file, array_model=array_model) ``` And then in QML: @@ -293,7 +291,7 @@ fruitlist = [ Fruit("Durian", 9.95, ListModel([Attribute("Tropical"), Attribute("Smelly")]))] # Set a context property with our listmodel -@qmlset qmlcontext().fruitModel = ListModel(fruitlist) +propmap["fruitModel"] = ListModel(fruitlist) ``` See the full example for more details, including the addition of an extra constructor to deal with the nested `ListModel` for the attributes. @@ -301,19 +299,16 @@ See the full example for more details, including the addition of an extra constr `QTimer` can be used to simulate running Julia code in the background. Excerpts from [`test/gui.jl`](test/gui.jl): ```julia -bg_counter = 0 +const bg_counter = Observable(0) function counter_slot() global bg_counter - bg_counter += 1 - @qmlset qmlcontext().bg_counter = bg_counter + bg_counter[] += 1 end @qmlfunction counter_slot -timer = QTimer() -@qmlset qmlcontext().bg_counter = bg_counter -@qmlset qmlcontext().timer = timer +load(qml_file, timer=QTimer(), bg_counter=bg_counter) ``` Use in QML like this: diff --git a/deps/src/qmlwrap/application_manager.hpp b/deps/src/qmlwrap/application_manager.hpp index 220fd0d..25c2fbd 100644 --- a/deps/src/qmlwrap/application_manager.hpp +++ b/deps/src/qmlwrap/application_manager.hpp @@ -16,9 +16,6 @@ namespace qmlwrap { -/// Helper to set context properties -//void set_context_property(QQmlContext* ctx, const QString& name, jl_value_t* v); - /// Manage creation and destruction of the application and the QML engine, class ApplicationManager { diff --git a/deps/src/qmlwrap/type_conversion.cpp b/deps/src/qmlwrap/type_conversion.cpp index 2bf75ea..92e836c 100644 --- a/deps/src/qmlwrap/type_conversion.cpp +++ b/deps/src/qmlwrap/type_conversion.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include "julia_display.hpp" @@ -114,7 +115,7 @@ jl_value_t* convert_to_julia(const QVariant& v) if(v.canConvert()) { // Add new types here - return try_qobject_cast(v.value()); + return try_qobject_cast(v.value()); } return nullptr; @@ -124,6 +125,11 @@ jl_value_t* convert_to_julia(const QVariant& v) template jl_value_t* try_convert_to_julia(const QVariant& v) { + if(!v.isValid()) + { + return jl_nothing; + } + for(auto&& jval : {convert_to_julia(v)...}) { if(jval != nullptr) diff --git a/deps/src/qmlwrap/wrap_qml.cpp b/deps/src/qmlwrap/wrap_qml.cpp index a14970f..98ffb38 100644 --- a/deps/src/qmlwrap/wrap_qml.cpp +++ b/deps/src/qmlwrap/wrap_qml.cpp @@ -24,7 +24,9 @@ namespace jlcxx { +template<> struct SuperType { typedef QQmlEngine type; }; template<> struct SuperType { typedef QObject type; }; +template<> struct SuperType { typedef QObject type; }; template<> struct SuperType { typedef QObject type; }; template<> struct SuperType { typedef QQuickWindow type; }; template<> struct SuperType { typedef QObject type; }; @@ -51,14 +53,15 @@ JULIA_CPP_MODULE_BEGIN(registry) .method("context_property", &QQmlContext::contextProperty) .method("set_context_object", &QQmlContext::setContextObject) .method("set_context_property", static_cast(&QQmlContext::setContextProperty)) - .method("set_context_property", static_cast(&QQmlContext::setContextProperty)); + .method("set_context_property", static_cast(&QQmlContext::setContextProperty)) + .method("context_object", &QQmlContext::contextObject); qml_module.add_type("QQmlEngine", julia_type()) .method("root_context", &QQmlEngine::rootContext); qml_module.add_type("QQmlApplicationEngine", julia_type()) .constructor() // Construct with path to QML - .method("load", [] (QQmlApplicationEngine* e, const QString& qmlpath) + .method("load_into_engine", [] (QQmlApplicationEngine* e, const QString& qmlpath) { bool success = false; auto conn = QObject::connect(e, &QQmlApplicationEngine::objectCreated, [&] (QObject* obj, const QUrl& url) { success = (obj != nullptr); }); @@ -148,23 +151,21 @@ JULIA_CPP_MODULE_BEGIN(registry) }); }); - // Emit signals helper - qml_module.method("emit", [](const char* signal_name, jlcxx::ArrayRef args) - { - using namespace qmlwrap; - JuliaSignals* julia_signals = JuliaAPI::instance()->juliaSignals(); - if(julia_signals == nullptr) - { - throw std::runtime_error("No signals available"); - } - julia_signals->emit_signal(signal_name, args); - }); + // Emit signals helper + qml_module.method("emit", [](const char *signal_name, jlcxx::ArrayRef args) { + using namespace qmlwrap; + JuliaSignals *julia_signals = JuliaAPI::instance()->juliaSignals(); + if (julia_signals == nullptr) + { + throw std::runtime_error("No signals available"); + } + julia_signals->emit_signal(signal_name, args); + }); - // Function to register a function - qml_module.method("qmlfunction", [](const QString& name, jl_function_t* f) - { - qmlwrap::JuliaAPI::instance()->register_function(name, f); - }); + // Function to register a function + qml_module.method("qmlfunction", [](const QString &name, jl_function_t *f) { + qmlwrap::JuliaAPI::instance()->register_function(name, f); + }); qml_module.add_type("JuliaDisplay", julia_type("CppDisplay")) .method("load_png", &qmlwrap::JuliaDisplay::load_png) diff --git a/example/gui.jl b/example/gui.jl index 11ea303..b9a30ed 100644 --- a/example/gui.jl +++ b/example/gui.jl @@ -1,13 +1,15 @@ using Base.Test using QML +using Observables hello() = "Hello from Julia" counter = 0 +const oldcounter = Observable(0) function increment_counter() - global counter - @qmlset qmlcontext().oldcounter = counter + global counter, oldcounter + oldcounter[] = counter counter += 1 end @@ -16,12 +18,19 @@ function counter_value() return counter end -bg_counter = 0 +const bg_counter = Observable(0) function counter_slot() global bg_counter - bg_counter += 1 - @qmlset qmlcontext().bg_counter = bg_counter + bg_counter[] += 1 +end + +# This slows down the bg_counter display. It counts a *lot* faster this way, proving the main overhead is in the GUI update and not in the callback mechanism to Julia +const bg_counter_slow = Observable(0) +on(bg_counter) do newcount + if newcount % 100 == 0 + bg_counter_slow[] = newcount + end end @qmlfunction counter_slot hello increment_counter uppercase string @@ -29,22 +38,11 @@ end # absolute path in case working dir is overridden qml_file = joinpath(dirname(@__FILE__), "qml", "gui.qml") -# Initialize app and engine. Lifetime managed by C++ -qml_engine = init_qmlapplicationengine() - -# Set up a timer -timer = QTimer() - -# Set context properties -@qmlset qmlcontext().oldcounter = counter -@qmlset qmlcontext().bg_counter = bg_counter -@qmlset qmlcontext().timer = timer - # Load the QML file -load(qml_engine, qml_file) +load(qml_file, timer=QTimer(), oldcounter=oldcounter, bg_counter=bg_counter_slow) # Run the application exec() println("Button was pressed $counter times") -println("Background counter now at $bg_counter") +println("Background counter now at $(bg_counter[])") diff --git a/example/observable.jl b/example/observable.jl new file mode 100644 index 0000000..6c19b7c --- /dev/null +++ b/example/observable.jl @@ -0,0 +1,19 @@ +using QML +using Observables + +qml_file = joinpath(dirname(@__FILE__), "qml", "observable.qml") + +const input = Observable(1.0) +const output = Observable(0.0) + +on(output) do x + println("Output changed to ", x) +end + +load(qml_file, input=input, output=output) + +if isinteractive() + exec_async() +else + exec() +end \ No newline at end of file diff --git a/example/qml/observable.qml b/example/qml/observable.qml new file mode 100644 index 0000000..83e8ed4 --- /dev/null +++ b/example/qml/observable.qml @@ -0,0 +1,37 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0 +import QtQuick.Layouts 1.0 + +ApplicationWindow { + id: root + title: "Observables" + width: 512 + height: 200 + visible: true + + ColumnLayout { + spacing: 6 + anchors.fill: parent + + Slider { + value: input + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + minimumValue: 0.0 + maximumValue: 100.0 + stepSize: 1.0 + tickmarksEnabled: true + onValueChanged: { + input = value; + output = 2*input; + } + } + + Text { + Layout.alignment: Qt.AlignCenter + text: output + font.pixelSize: 0.1*root.height + } + } + +} diff --git a/src/QML.jl b/src/QML.jl index 541d92a..e21adb4 100644 --- a/src/QML.jl +++ b/src/QML.jl @@ -1,6 +1,6 @@ module QML -export @qmlget, @qmlset, @emit, @qmlfunction, @qmlapp, qmlfunction, QVariant +export @emit, @qmlfunction, @qmlapp, qmlfunction, QVariant, load, QQmlPropertyMap, set_context_object @static if is_windows() ENV["QML_PREFIX_PATH"] = joinpath(dirname(dirname(@__FILE__)),"deps","usr") @@ -8,6 +8,9 @@ end using CxxWrap using Observables +using FileIO + +FileIO.add_format(format"QML", (), ".qml") const depsfile = joinpath(dirname(dirname(@__FILE__)), "deps", "deps.jl") if !isfile(depsfile) @@ -29,6 +32,42 @@ end wrap_module(_l_qml_wrap, QML) +load +function FileIO.load(f::FileIO.File{format"QML"}, ctxobj::QObject) + qml_engine = init_qmlapplicationengine() + ctx = root_context(qml_engine) + set_context_object(ctx, ctxobj) + if !load_into_engine(qml_engine, filename(f)) + error("Failed to load QML file ", filename(f)) + end + gcprotect(ctxobj) + return qml_engine +end + +""" + +load(qml_file, prop1=x, prop2=y, ...) + +Load a QML file, creating a QQmlApplicationEngine and setting the context properties supplied in the keyword arguments. Returns the created engine. + +load(qml_file, context_object) + +Load a QML file, creating a QQmlApplicationEngine and setting the context object to the supplied QObject +""" +function FileIO.load(f::FileIO.File{format"QML"}; kwargs...) + qml_engine = init_qmlapplicationengine() + ctx = root_context(qml_engine) + propmap = QQmlPropertyMap(ctx) + set_context_object(ctx, propmap) + for (key,value) in kwargs + propmap[String(key)] = value + end + if !load_into_engine(qml_engine, filename(f)) + error("Failed to load QML file ", filename(f)) + end + return qml_engine +end + function __init__() # Make sure we have an application at module load, so any QObject is created after this @static if is_windows() @@ -62,17 +101,18 @@ end # QQmlPropertyMap indexing interface Base.getindex(propmap::QQmlPropertyMap, key::AbstractString) = value(propmap, key).value -Base.setindex!(propmap::QQmlPropertyMap, val, key::AbstractString) = insert(propmap, key, QVariant(val)) +function Base.setindex!{T}(propmap::QQmlPropertyMap, val::T, key::AbstractString) + if !isbits(T) && !isimmutable(T) + gcprotect(val) + end + insert(propmap, key, QVariant(val)) +end Base.setindex!(propmap::QQmlPropertyMap, val::QVariant, key::AbstractString) = insert(propmap, key, val) function Base.setindex!(propmap::QQmlPropertyMap, val::Observable, key::AbstractString) insert_observable(propmap, key, val) on(QmlPropertyUpdater(propmap, key), val) end - -""" -Overloads for getting a property value based on its name for any base class -""" -generic_property_get(ctx::QQmlContext, key::AbstractString) = context_property(ctx, key).value +Base.setindex!(propmap::QQmlPropertyMap, val::Irrational, key::AbstractString) = (propmap[key] = convert(Float64, val)) """ Expand an expression of the form a.b.c to replace the dot operator by function calls: @@ -88,17 +128,6 @@ macro expand_dots(source_expr, func) return esc(source_expr) end -""" -Get a property from the Qt hierarchy using ".": -``` -@qmlget o.a.b -``` -returns the value of property with name "b" of property with name "a" of the root object o -""" -macro qmlget(dots_expr) - :(@expand_dots($(esc(dots_expr)), generic_property_get)) -end - """ Set a property on the given context """ @@ -117,21 +146,6 @@ function set_context_property(ctx::QML.QQmlContext, key::AbstractString, value:: return invoke(set_context_property, Tuple{Union{CxxWrap.SmartPointer{T2}, T2} where T2<:QML.QQmlContext, AbstractString, QObject}, ctx, key, value) end -""" -Overloads for setting a property value based on its name for any base class -""" -generic_property_set(ctx::QQmlContext, key::AbstractString, value::Any) = set_context_property(ctx, key, value) - -""" -Setter version of `@qmlget`, use in the form: -``` -@qmlset o.a.b = value -``` -""" -macro qmlset(assign_expr) - :(generic_property_set(@expand_dots($(esc(assign_expr.args[1].args[1])), generic_property_get), $(string(assign_expr.args[1].args[2].args[1])), $(esc(assign_expr.args[2])))) -end - """ Emit a signal in the form: ``` @@ -158,19 +172,25 @@ macro qmlfunction(fnames...) end """ -Load the given QML path using a QQmlApplicationEngine, initializing the context with the given properties +Load the given QML path using a QQmlApplicationEngine, initializing the context with the given properties. Deprecated """ macro qmlapp(path, context_properties...) + Base.depwarn("The qmlapp macro is deprecated. Please use the qmlapp function instead and add properties separately using either @contextproperties or the global context object.", :qmlapp) result = quote qml_engine = init_qmlapplicationengine() end for p in context_properties push!(result.args, :(set_context_property(qmlcontext(), $(esc(string(p))), $(esc(p))))) end - push!(result.args, :(load(qml_engine, $(esc(path))))) + push!(result.args, :(load_into_engine(qml_engine, $(esc(path))))) return result end +function qmlapp(path::AbstractString) + qml_engine = init_qmlapplicationengine() + return load_into_engine(qml_engine, path) +end + function Base.display(d::JuliaDisplay, x) buf = IOBuffer() supported_types = (MIME"image/svg+xml"(), MIME"image/png"()) @@ -254,11 +274,6 @@ You can now use `my_property` in QML and every time `set_context_property` is ca @doc "Equivalent to [`QQmlEngine::rootContext`](http://doc.qt.io/qt-5/qqmlengine.html#rootContext)" root_context -@doc """ -Equivalent to [`&QQmlApplicationEngine::load`](http://doc.qt.io/qt-5/qqmlapplicationengine.html#load-1). The first argument is -the application engine, the second is a string containing a path to the local QML file to load. -""" load - @doc "Equivalent to `QLibraryInfo::location(QLibraryInfo::PrefixPath)`" qt_prefix_path @doc "Equivalent to `QQuickWindow::contentItem`" content_item @doc "Equivalent to `QQuickView::setSource`" set_source diff --git a/test/functions.jl b/test/functions.jl index 7c9dce1..4cce728 100644 --- a/test/functions.jl +++ b/test/functions.jl @@ -70,7 +70,7 @@ set_state2 = TestModuleFunction.set_state2 qmlfunction("unexported_return_two", UnExported.return_two) qmlfunction("unexported_check", UnExported.check) -@qmlapp joinpath(dirname(@__FILE__), "qml", "functions.qml") +load(joinpath(dirname(@__FILE__), "qml", "functions.qml")) exec() stringresult = VERSION < v"0.5-dev" ? ASCIIString : String diff --git a/test/julia_arrays.jl b/test/julia_arrays.jl index 20afb6b..ce98093 100644 --- a/test/julia_arrays.jl +++ b/test/julia_arrays.jl @@ -37,7 +37,16 @@ custom_list = [ListElem("a",1), ListElem("b", 2)] custom_model = ListModel(custom_list) @qmlfunction get_array -@qmlapp qml_file julia_array array_model array_model2 move_model resize_typed_model insert_model clear_model custom_model +load(qml_file, + julia_array=julia_array, + array_model=array_model, + array_model2=array_model2, + move_model=move_model, + resize_typed_model=resize_typed_model, + insert_model=insert_model, + clear_model=clear_model, + custom_model=custom_model) + # Run the application exec() diff --git a/test/julia_object.jl b/test/julia_object.jl index 6a3fc5b..3b9dc4f 100644 --- a/test/julia_object.jl +++ b/test/julia_object.jl @@ -1,5 +1,6 @@ using Base.Test using QML +using Observables type InnerType x::Float64 @@ -11,67 +12,39 @@ type JuliaTestType i::InnerType end -# absolute path in case working dir is overridden -qml_file = joinpath(dirname(@__FILE__), "qml", "julia_object.qml") - -julia_object = JuliaTestType(0, InnerType(0.0)) -julia_object2 = JuliaTestType(0, InnerType(0.0)) - -function test_string(s) - try - @test s == string(julia_object) - return - catch e - exit(1) - end +function modify_julia_object(jo::JuliaTestType) + jo.a = 3 + jo.i.x = 2.0 + return end -function jlobj_callback(o::JuliaTestType) - try - @test o == julia_object - return - catch e - exit(1) - end -end +replace_julia_object() = JuliaTestType(1, InnerType(2.0)) -innertwo() = InnerType(2.0) +geta(jo) = jo.a +getx(jo) = jo.i.x -function check_inner_x(x) - try - @test x == 2.0 - return - catch e - exit(1) - end +function julia_object_check(b::Bool) + @test b end -function setthree(x::JuliaTestType) - x.a = 3 - x.i.x = 3.0 -end +logged_x = 0.0 -function testthree(a,x) - try - @test a == 3 - @test x == 3.0 - catch - exit(1) - end +function logx(x) + global logged_x + logged_x = x + return end -@qmlfunction test_string jlobj_callback innertwo check_inner_x setthree testthree - -# Run with qml file and one context property -@qmlapp qml_file julia_object julia_object2 +# absolute path in case working dir is overridden +qml_file = joinpath(dirname(@__FILE__), "qml", "julia_object.qml") +observed_object = Observable(JuliaTestType(0,InnerType(0.0))) +@qmlfunction modify_julia_object replace_julia_object julia_object_check geta getx logx -@test (@qmlget qmlcontext().julia_object.a) == 0 -@test (@qmlget qmlcontext().julia_object.i.x) == 0.0 -@test QML.julia_value(@qmlget qmlcontext().julia_object).a == 0 +load(qml_file, julia_object=JuliaTestType(1, InnerType(2.0)), observed_object=observed_object) # Run the application exec() - -@test julia_object.a == 1 -@test julia_object.i.x == 2.0 +@test observed_object[].a == 1 +@test observed_object[].i.x == 2.0 +@test logged_x == 2.0 \ No newline at end of file diff --git a/test/julia_signal.jl b/test/julia_signal.jl index b93284c..7fb712f 100644 --- a/test/julia_signal.jl +++ b/test/julia_signal.jl @@ -27,7 +27,7 @@ end # absolute path in case working dir is overridden qml_file = joinpath(dirname(@__FILE__), "qml", "julia_signal.qml") -@qmlapp qml_file +load(qml_file) # Run the application exec() diff --git a/test/listviews.jl b/test/listviews.jl index 885582b..87ffd59 100644 --- a/test/listviews.jl +++ b/test/listviews.jl @@ -35,7 +35,7 @@ removerole_c() = removerole(tablemodel, "c") setrole_a() = setrole(tablemodel, 0, "abc", (x::TableItem) -> x.a*x.b*x.c) @qmlfunction testfail removerole_b removerole_c setrole_a -@qmlapp qml_file array_model array_model2 tablemodel +load(qml_file, array_model=array_model, array_model2=array_model2, tablemodel=tablemodel) exec() diff --git a/test/properties.jl b/test/properties.jl index ee0e271..5356ff7 100644 --- a/test/properties.jl +++ b/test/properties.jl @@ -3,8 +3,12 @@ using QML # Test context properties +propmap = QQmlPropertyMap() +propmap["my_prop"] = 1 +propmap["φ"] = φ + function check_property(x) - @test x == @qmlget qmlcontext().my_prop + @test x == propmap["my_prop"] nothing end @@ -16,6 +20,5 @@ end @qmlfunction check_property check_golden qmlfile = joinpath(dirname(@__FILE__), "qml", "properties.qml") -my_prop = 1 -@qmlapp qmlfile my_prop φ +load(qmlfile, propmap) exec() diff --git a/test/qml/julia_object.qml b/test/qml/julia_object.qml index 3c47042..d9768ef 100644 --- a/test/qml/julia_object.qml +++ b/test/qml/julia_object.qml @@ -1,18 +1,17 @@ import QtQuick 2.0 import org.julialang 1.0 -Timer { - interval: 200; running: true; repeat: false - onTriggered: { - julia_object.a += parseInt("1") - julia_object.b = 0 - Julia.test_string(julia_object.julia_string()) - Julia.jlobj_callback(julia_object) - julia_object.i = Julia.innertwo() - Julia.check_inner_x(julia_object.i.x) - Julia.setthree(julia_object2) - julia_object2.update() - Julia.testthree(julia_object2.a, julia_object2.i.x) - Qt.quit() - } - } +Item { + Timer { + interval: 200; running: true; repeat: false + onTriggered: { + Julia.julia_object_check(Julia.geta(julia_object) == 1) + Julia.julia_object_check(Julia.getx(julia_object) == 2.0) + observed_object = Julia.replace_julia_object() + Qt.quit() + } + } + + property var someNumber: Julia.getx(observed_object) + onSomeNumberChanged: Julia.logx(someNumber) // should be triggered when replacing observed_object +} diff --git a/test/qml_propertymap.jl b/test/qml_propertymap.jl index 2ca590c..dcf901e 100644 --- a/test/qml_propertymap.jl +++ b/test/qml_propertymap.jl @@ -26,10 +26,7 @@ end @qmlfunction propertymap_test set_expected_ob do_ob_update -# Run with qml file and one context property -@qmlapp qml_file - -propmap = QML.QQmlPropertyMap(qmlcontext()) +propmap = QML.QQmlPropertyMap() propmap["a"] = 1 @test propmap["a"] == 1 @@ -48,11 +45,13 @@ expected_ob = 3 println("setting ob from Julia to value $expected_ob") ob[] = expected_ob -QML.set_context_object(qmlcontext(), propmap) +@test propmap["ob"] == expected_ob + +# Load the QML file, setting the property map as context object +load(qml_file, propmap) # Run the application exec() @test ob[] == expected_ob - @test ob_handler_calls == 3 \ No newline at end of file diff --git a/test/qqmlcomponent.jl b/test/qqmlcomponent.jl index c673440..511b078 100644 --- a/test/qqmlcomponent.jl +++ b/test/qqmlcomponent.jl @@ -1,8 +1,6 @@ using Base.Test using QML -hi = "Hi from Julia" - # absolute path in case working dir is overridden qml_data = QByteArray(""" import QtQuick 2.0 @@ -31,7 +29,10 @@ ApplicationWindow { """) qengine = init_qmlengine() -@qmlset qmlcontext().hi = hi +ctx = root_context(qengine) +ctxobj = QQmlPropertyMap(ctx) +set_context_object(ctx, ctxobj) +ctxobj["hi"] = "Hi from Julia" qcomp = QQmlComponent(qengine) set_data(qcomp, qml_data, "") diff --git a/test/qquickview.jl b/test/qquickview.jl index 32fc3ca..98302c5 100644 --- a/test/qquickview.jl +++ b/test/qquickview.jl @@ -1,13 +1,14 @@ using Base.Test using QML -hi = "Hi from Julia" - # absolute path in case working dir is overridden qml_file = joinpath(dirname(@__FILE__), "qml", "qquickview.qml") qview = init_qquickview() -@qmlset qmlcontext().hi = hi +ctx = root_context(QML.engine(qview)) +ctxobj = QQmlPropertyMap(ctx) +set_context_object(ctx, ctxobj) +ctxobj["hi"] = "Hi from Julia" # Load QML after setting context properties, to avoid errors set_source(qview, qml_file) diff --git a/test/qtimer.jl b/test/qtimer.jl index 6e431f9..387a581 100644 --- a/test/qtimer.jl +++ b/test/qtimer.jl @@ -12,10 +12,8 @@ end @qmlfunction counter_slot -timer = QTimer() - qmlfile = joinpath(dirname(@__FILE__), "qml", "qtimer.qml") -@qmlapp qmlfile timer +load(qmlfile, timer=QTimer()) exec() @test bg_counter > 100 diff --git a/test/runtests.jl b/test/runtests.jl index 8bfec1e..d5e2196 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ using Base.Test -excluded = ["runtests.jl", "qml", "include", "julia_object.jl"] +excluded = ["runtests.jl", "qml", "include"] # OpenGL on Linux travis is excessively old, causing a crash when attempting display of a window if get(ENV, "QML_SKIP_GUI_TESTS", "0") != "0"