Skip to content

Commit

Permalink
Deprecate @qmlapp, remove @qmlget and @qmlset
Browse files Browse the repository at this point in the history
Issues #43 and #44
  • Loading branch information
barche committed Aug 11, 2018
1 parent f5efc83 commit cb39d7b
Show file tree
Hide file tree
Showing 20 changed files with 295 additions and 244 deletions.
147 changes: 71 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -293,27 +291,24 @@ 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.

## Using QTimer
`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:
Expand Down
3 changes: 0 additions & 3 deletions deps/src/qmlwrap/application_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
8 changes: 7 additions & 1 deletion deps/src/qmlwrap/type_conversion.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <QDebug>
#include <QFileInfo>
#include <QQmlPropertyMap>
#include <QUrl>

#include "julia_display.hpp"
Expand Down Expand Up @@ -114,7 +115,7 @@ jl_value_t* convert_to_julia<QObject*>(const QVariant& v)
if(v.canConvert<QObject*>())
{
// Add new types here
return try_qobject_cast<JuliaDisplay, ListModel>(v.value<QObject*>());
return try_qobject_cast<JuliaDisplay, ListModel, QQmlPropertyMap>(v.value<QObject*>());
}

return nullptr;
Expand All @@ -124,6 +125,11 @@ jl_value_t* convert_to_julia<QObject*>(const QVariant& v)
template<typename... TypesT>
jl_value_t* try_convert_to_julia(const QVariant& v)
{
if(!v.isValid())
{
return jl_nothing;
}

for(auto&& jval : {convert_to_julia<TypesT>(v)...})
{
if(jval != nullptr)
Expand Down
37 changes: 19 additions & 18 deletions deps/src/qmlwrap/wrap_qml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
namespace jlcxx
{

template<> struct SuperType<QQmlApplicationEngine> { typedef QQmlEngine type; };
template<> struct SuperType<QQmlContext> { typedef QObject type; };
template<> struct SuperType<QQmlEngine> { typedef QObject type; };
template<> struct SuperType<QQmlPropertyMap> { typedef QObject type; };
template<> struct SuperType<QQuickView> { typedef QQuickWindow type; };
template<> struct SuperType<QTimer> { typedef QObject type; };
Expand All @@ -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<void(QQmlContext::*)(const QString&, const QVariant&)>(&QQmlContext::setContextProperty))
.method("set_context_property", static_cast<void(QQmlContext::*)(const QString&, QObject*)>(&QQmlContext::setContextProperty));
.method("set_context_property", static_cast<void(QQmlContext::*)(const QString&, QObject*)>(&QQmlContext::setContextProperty))
.method("context_object", &QQmlContext::contextObject);

qml_module.add_type<QQmlEngine>("QQmlEngine", julia_type<QObject>())
.method("root_context", &QQmlEngine::rootContext);

qml_module.add_type<QQmlApplicationEngine>("QQmlApplicationEngine", julia_type<QQmlEngine>())
.constructor<QString>() // 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); });
Expand Down Expand Up @@ -148,23 +151,21 @@ JULIA_CPP_MODULE_BEGIN(registry)
});
});

// Emit signals helper
qml_module.method("emit", [](const char* signal_name, jlcxx::ArrayRef<jl_value_t*> 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<jl_value_t *> 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<qmlwrap::JuliaDisplay>("JuliaDisplay", julia_type("CppDisplay"))
.method("load_png", &qmlwrap::JuliaDisplay::load_png)
Expand Down
34 changes: 16 additions & 18 deletions example/gui.jl
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -16,35 +18,31 @@ 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

# 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[])")
Loading

0 comments on commit cb39d7b

Please sign in to comment.