Dynamically load/assign objects in QML - qml

I've just started with QML game development using V-Play and coming from java, I stil have problems understanding some basics.
I know how I'd do it in Java, but don't know how it works in QML, so I'll explain it in Java terms:
I have a "base class" with some properties and methods. Then I have modules extending this base class with more specific features. (Like "class Module1 extends BaseModule")
Then I have an object ("ModuleContainer") that contains a Module, but I don't know which one - it's loaded dynamically at runtime. Thus, in Java i'd create a new object like "BaseModule someModule = new Module1()", and can later access it and also replace it (like "someModule = new Module2()").
How could I do that in QML?
I've tried properties, but I haven't found a way to create a new object and then use it. Dynamic creation of objects with an entityManager doesn't quit seem to work for this either.

I played around with components a bit and together with a Loader, it's pretty much exactly what I wanted.
Here's my code if anyone else needs something like this:
Basically, I create a baseclass (BaseModule), create two Modules that extend that class (java terms).
In a new class (ModuleSlot), I create two components containing a Module each, that can be dynamically loaded and replaced in the Main code.
Important parts
//define a component and make it accessible from the outside
property Component cmodule1: module1
Component {
id: module1
Module1 {
}
}
//define a component to hold the component to use (for easier changing)
property Component dynamicModule: dynamicModuleHolder.cmodule1
ModuleSlot {
id:dynamicModuleHolder
}
//the magic happens here: the defined component is loaded dynamically on runtime,
when changed, the old one is removed and the new one loaded
Loader {
sourceComponent:dynamicModule
}
Full Code
Main.qml:
GameWindow {
id: gameWindow
//licenseKey: "<generate one from http://v-play.net/licenseKey>"
activeScene: scene
width: 640
height: 960
property Component dynamicModule: dynamicModuleHolder.cmodule1
property int activeElement: 1
Scene {
id: scene
width: 640
height: 960
Loader{
sourceComponent:dynamicModule
}
ModuleSlot {
id:dynamicModuleHolder
}
MouseArea {
anchors.fill: parent
onClicked: {
if(activeElement == 1) {
dynamicModule = dynamicModuleHolder.cmodule2
activeElement = 2
} else {
dynamicModule = dynamicModuleHolder.cmodule1
activeElement = 1
}
}
}
}
}
BaseModule.qml:
Item {
property int someAttribute: 0
}
Module1.qml/Module2.qml (Module2 only has a different Rectangle)
BaseModule {
someAttribute: 5 // just to show that Module1 inherits from BaseModule
Rectangle {
color: "red"
width: 50
height: 50
x: 200
y: 200
}
}
ModuleSlot
Item {
property Component cmodule1: module1
property Component cmodule2: module2
QtObject {
id: internalSettings
property color color: "green"
}
Component {
id: module1
Module1 {
}
}
Component {
id: module2
Module2 {
}
}
}

Related

Change TextArea Text When String Variable Changes TornadoFX

I have a controller with a string variable, and I would like the text value of a TextArea to change when the controller's string variable changes.
class MyView: View() {
...
button("Run Test").action {
runAsync {
for(test in testList){
controller.updateText = "running" + test.name
run(test)
}
}
}
...
scriptRanArea = textarea {
text = controller.updateText
}
...
}
This is the fastest way I know how to accomplish this, but I don't really know which design pattern you want to use:
class MyView: View() {
val controller: MyController by inject()
override val root = vbox {
textarea(controller.myTextProperty)
}
}
class MyController: Controller() {
val myTextProperty = SimpleStringProperty()
}
The inject method automatically finds the controller within the TornadoFX Scope, or creates one if not found, when it is first referenced. The TornadoFX text area builder function binds the string property from the controller to the TextArea when it is passed in as a param. Keep in mind though, writing in the text area will now automatically change the value in the controller's property and vice versa. If you don't want that functionality, you will have to update your question to be more specific to your needs.

TornadoFX: How to preserve `ItemViewModel`'s property?

Good day.
I am trying to preserve a property of ItemViewModel via config helper. I am able to successfully save the property (conf directory with appropriate .properties file is generated), however upon next start, the property does not restore its value, just remains null. Here's a sample code to demonstrate my issue:
import javafx.beans.property.SimpleStringProperty
import tornadofx.*
data class Foo(val doNotPreserveMe: String, val preserveMe: String)
class FooModel : ItemViewModel<Foo>() {
val doNotPreserveMe = bind { item?.doNotPreserveMe?.toProperty() }
val preserveMe = bind { SimpleStringProperty(item?.preserveMe, "pm", config.string("pm")) }
}
class FooApp : App(FooView::class)
class FooView : View() {
private val model = FooModel()
override val root = form {
fieldset {
field("Do not preserve me") { textfield(model.doNotPreserveMe).required() }
field("Preserve me") { textfield(model.preserveMe).required() }
button("Do something") {
enableWhen(model.valid)
action {
model.commit {
// ...
with(config) {
set("pm" to model.preserveMe.value)
save()
}
}
}
}
}
}
}
Any ideas on why the model is not restoring the value?
Each Component has it's own config store, which is backed by a separate file. Either make sure to use the same config file, or the app global config file.
You can refer to other component's config store, so one solution would be to let the View access the ViewModel's config store like this:
button("Do something") {
enableWhen(model.valid)
action {
model.commit {
// ...
with(model.config) {
set("pm" to model.preserveMe.value)
save()
}
}
}
}
However, there is a much simpler and more contained solution, which is simply to handle save in the FooModel's onCommit callback
override fun onCommit() {
with(config) {
set("pm" to preserveMe.value)
save()
}
}
In this case you'd simply call model.commit() in the button callback.
You can also use a common, or even global config object. Either use a Controller's config store, or the global store. To use the global config object, just refer to app.config in both the model and the view.

TableView and Fragment to edit Details with tornadofx

I use kotlinx.serialization for my models.
I'd like the idea of them to not depend on JavaFX, so they do not expose properties.
Given a model, I want a tableview for a quick representation of a list of instances, and additionally a more detailed Fragment as an editor.
consider the following model:
#Serializable
data class Person(
var name: String,
var firstname: String,
var complex: Stuff)
the view containing the tableview contains
private val personlist = mutableListOf<Person>().observable()
with a tableview that opens an instance of PersonEditor for the selected row when Enter is pressed:
tableview(personlist) {
column("name", Person::name)
column("first name", Person::firstname)
setOnKeyPressed { ev ->
selectedItem?.apply {
when (ev.code) {
KeyCode.ENTER -> PersonEditor(this).openModal()
}
}
}
}
I followed this gitbook section (but do not want the modelview to be rebound on selection of another row within the tableview)
The editor looks about like this:
class PersonEditor(person: Person) : ItemFragment<Person>() {
val model: Model = Model()
override val root = form {
fieldset("Personal information") {
field("Name") {
textfield(model.name)
}
field("Vorname") {
textfield(model.firstname)
}
}
fieldset("complex stuff") {
//... more complex stuff here
}
fieldset {
button("Save") {
enableWhen(model.dirty)
action { model.commit() }
}
button("Reset") { action { model.rollback() } }
}
}
class Model : ItemViewModel<Person>() {
val name = bind(Person::name)
val firstname = bind(Person::firstname)
//... complex stuff
}
init {
itemProperty.value = mieter
model.bindTo(this)
}
}
When I save the edited values in the detail view, the tableview is not updated.
Whats the best practize to solve this?
Also I'm unsure, if what I'm doing can be considered good practize, so i'd be happy for some advice on that too.
The best practice in a JavaFX application is to use observable properties. Not doing so is an uphill battle. You can keep your lean domain objects, but add a JavaFX/TornadoFX specific version with observable properties. This object can know how to copy data to/from your "lean" domain objects.
With this approach, especially in combination with ItemViewModel wrappers will make sure that your data is always updated.
The setOnKeyPressed code you posted can be changed to:
setOnUserSelect {
PersonEditor(it).openModal()
}
Notice though, that you are not supposed to instantiate Views and Fragments directly, as doing so skips certain steps in the TornadoFX life cycle. Instead you should pass the person as a parameter, or create a new scope and inject a PersonModel into that scope before opening the editor in that scope:
setOnUserSelect {
find<PersonEditor>(Scope(PersonEditor(it)))
}

questions about DI, ViewModel etc

I have the following code:
class ExampleView :View("My Example view") {
val model:ExampleModel by inject()
override val root= vbox {
textfield(model.data)
button("Commit") {
setOnAction {
model.commit()
closeModal()
}
}
button("Rollback") {
setOnAction {
model.rollback()
closeModal()
}
}
button("Just quit") {
setOnAction {
closeModal()
}
}
}
}
class Example() {
var data by property<String>()
fun dataProperty() = getProperty(Example::data)
}
class ExampleModel(example: Example) : ItemViewModel<Example>() {
init {
item = example
}
val data = bind { item?.dataProperty() }
}
class MainView : View() {
val example:Example
override val root = BorderPane()
init {
example = Example()
example.data = "Data for example"
val exampleModel = ExampleModel(example)
with(root){
top {
menubar {
menu("Test") {
menuitem("Example - 1") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
menuitem("Example - 2") {
val scope = Scope()
setInScope(exampleModel, scope)
find<ExampleView>(scope).openWindow()
}
}
}
}
}
}
}
I have two questions for this example:
1) If i change the value and close the window without a commit (User can do this with help [X] button) then only ViewModel will store changes (and it will be displayed in GUI even after the re-opening ), but model POJO object will keep the old data.
if I used instance of Example class (without DI) then this instance received all the changes at once.
For example i don't want commit/rollback functionality but i want DI and immediate updating. What i should do? (ofcource i can call "commit" for "textfield change value event")
2) ViewModel has constructor with parameter and if i try open ExampleView like this
find<ExampleView>(Scope()).openWindow()
then I got an an obvious RuntimeException. Can I avoid this for example, by a compiler warnings (or by something else)?
1) This is the correct default behavior of the ViewModel. If you bind a property of the view model to an input, the changes are immediately reflected in that bound property, but will only be flushed into the underlying model object once you commit it.
If you want to autocommit changes in the view model properties back into the underlying model object, you can create the binding with the autocommit property set to true:
val data = bind(true) { item?.dataProperty() }
You can also write bind(autocommit = true) if that looks clearer to you. This will cause any changes to be automatically flushed back into the underlying object.
I also want to make you aware that by requiring an item in the constructor of your view model, you're effectively preventing it from being used with injection unless you prime it like you do using setInScope. This might be fine for your use case, but worth noting.
2) The upcoming TornadoFX 1.5.10 will give you a better runtime error message if you forget to pass a parameter. It also introduces default values for parameters. See https://github.com/edvin/tornadofx/pull/227 for more info.

TypeScript modules

I am wondering if it is possible somehow to have two or more classes in two or more files added to the same module in TypeScript. Something like this:
//src/gui/uielement.ts
module mylib {
module gui {
export interface UIElement {
public draw() : void;
}
}
}
//src/gui/button.ts
///<reference path='uielement.ts'/>
module mylib {
module gui {
export class Button implements UIElement {
constructor(public str : string) { }
draw() : void { }
}
}
}
There will probably be dozens of GUI classes, so having them all in the same file will not be possible. And all my classes will be in the 'mylib' module.
But how do I do that?
If the module mylib {...} is translated into a function then all content of all mylib blocks in all files should be contained within the same function.
Is this at all possible?
When I compile I get this:
$ tsc src/gui/button.ts
src/gui/button.ts(4,39): The name 'UIElement' does not exist in the current scope
This is exactly how it works! If you look at the generated javascript code, it add as an anonymous function that accepts an object, the "the module object":
var mylib;
(function (mylib) {
var Button = (function () {
function Button(x) {
this.x = x;
}
return Button;
})();
mylib.Button = Button;
})(mylib || (mylib = {}));
If you look at the last line (})(mylib || (mylib = {}));) you see that it instantiates a new ojbect (mylib = {}) only if the existing variable is false (or something that evaluates to false, like null).
That way, all "modules" that are named the same will be merged to the same object.
Therefore, internal modules extend each other. I have to note that I have not quite figured out what happens to nested modules.
Update: Your code works for me if I do not use the nested module syntax, but change it to the dot syntax. e.g.:
module mylib.gui {
}
instead of
module mylib {
module gui {
}
}
I'll try to investigate in why this is happening, as far as I have read the spec, both ways should be equal.
Update: if the nested referenced module is marked as exported, it works:
module mylib {
export module gui {
}
}