QML Loader not shows changes on .qml file - caching

I have main.qml and dynamic.qml files that i want to load dynamic.qml on main.qml using Loader {}.
Content of dynamic.qml file is dynamic and another program may change its content and overwrite it.
So i wrote some C++ code for detecting changes on file and fires Signal.
My problem is that I don't know how can i force Loader to reload file.
This is my current work:
MainController {
id: mainController
onInstallationHelpChanged: {
helpLoader.source = "";
helpLoader.source = "../dynamic.qml";
}
}
Loader {
id: helpLoader
anchors.fill: parent
anchors.margins: 60
source: "../dynamic.qml"
}
I think that QML Engine caches dynamic.qml file. So whenever I want to reload Loader, it shows old content. Any suggestion?

You need to call trimComponentCache() on QQmlEngine after you have set the Loaders source property to an empty string. In other words:
helpLoader.source = "";
// call trimComponentCache() here!!!
helpLoader.source = "../dynamic.qml";
In order to do that, you'll need to expose some C++ object to QML which has a reference to your QQmlEngine (lots of examples in Qt and on StackOverflow to help with that).
trimComponentCache tells QML to forget about all the components it's not current using and does just what you want.
Update - explaining in a bit more detail:
For example, somewhere you define a class that takes a pointer to your QQmlEngine and exposes the trimComponentCache method:
class ComponentCacheManager : public QObject {
Q_OBJECT
public:
ComponentCacheManager(QQmlEngine *engine) : engine(engine) { }
Q_INVOKABLE void trim() { engine->trimComponentCache(); }
private:
QQmlEngine *engine;
};
Then when you create your QQuickView, bind one of the above as a context property:
QQuickView *view = new QQuickView(...);
...
view->rootContext()->setContextProperty(QStringLiteral("componentCache", new ComponentCacheManager(view->engine());
Then in your QML you can do something like:
helpLoader.source = "";
componentCache.trim();
helpLoader.source = "../dynamic.qml";

I was hoping for a pure QML solution. I noticed that loader.source is a url (file:///) and remembered how with HTML, you can avoid HTTP caching using ?t=Date.now() in your requests. Tried adding ?t=1234 to the end of loader.source, and sure enough, it works.
import QtQuick 2.0
Item {
Loader {
id: loader
anchors.fill: parent
property string filename: "User.qml"
source: filename
function reload() {
source = filename + "?t=" + Date.now()
}
}
Timer {
id: reloadTimer
interval: 2000
repeat: true
running: true
onTriggered: {
loader.reload();
}
}
}
I also wrote another example that will check for changes in the file contents before triggering a reload using an XMLHttpRequest.
import QtQuick 2.0
Item {
Loader {
id: loader
anchors.fill: parent
property string filename: "AppletUser.qml"
property string fileContents: ""
source: ""
function reload() {
source = filename + "?t=" + Date.now()
}
function checkForChange() {
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState === 4) {
if (loader.fileContents != req.responseText) {
loader.fileContents = req.responseText;
loader.reload();
}
}
}
req.open("GET", loader.filename, true);
req.send();
}
onLoaded: {
console.log(source)
}
Timer {
id: reloadTimer
interval: 2000
repeat: true
running: true
onTriggered: loader.checkForChange()
}
Component.onCompleted: {
loader.checkForChange()
}
}
}

Related

Tornadofx: Trying to reload/refresh MainView

I've only really just started on Tornadofx and was having a bit of trouble trying to figure out how to reload a view so the controls in that view are refreshed.
Below is a simplified version of the code I'm working with. I've got a loop to generate radio-button controls based on strings in a list.
class MainView: View("MainView") {
override val root = vbox {
for(x in radioText) {
radiobutton(x, radioGroup) {
action {
radioSelected = this#radiobutton.text
}
}
}
button("Next") {
action {
// Reload View to update radiobuttons with new values
}
}
}
}
In the program I need to go through several sets of these radio buttons, and so the idea was that each time the user presses the "Next" button, the items in the radioText list would be updated to match the next set of radio-buttons. Then I was looking for a way to get the view to update with these new values.
I tried using openWindow() to open a new instance of the view, but then when I used close() to get rid of the previous instance and ended up closing both windows.
button("Next") {
action {
MainView().openWindow()
close()
}
}
Any help with this would be much appreciated,
Thanks.
If I understood correctly, you are trying to have a list of string and generate radiobuttons with it. So, by adding the variables to your example, would be something like this:
class MainView: View("MainView") {
val radioText = ArrayList<String>()
var radioGroup : ToggleGroup by singleAssign()
lateinit var radioSelected : String
override val root = vbox {
radioText.addAll(arrayListOf("One","Two","Three","Four"))
radioGroup = togglegroup(){}
for(x in radioText) {
radiobutton(x,radioGroup) {
action {
radioSelected = text //You don't need this#radiobutton.text
}
}
}
button("Next") {
action {
// Reload View to update radiobuttons with new values
}
}
}
}
I thing is a better idea having your radiobutton created by a listview, wich would be updated by a observable list of string, like I do bellow:
class MainView2: View("MainView") {
// this is a list of observable string, so when the items on his list change
// the listview is updated
val radioText = FXCollections.observableArrayList<String>()
var radioGroup : ToggleGroup by singleAssign()
lateinit var radioSelected : String
override val root = vbox() {
prefWidth = 200.0
prefHeight = 300.0
radioText.setAll("One","Two","Three","Four")
radioGroup = togglegroup(){}
listview<String>(radioText){
// Setting listview height dinamically
fixedCellSize = 25.0
prefHeightProperty().bind(radioText.sizeProperty.multiply(fixedCellSizeProperty().add(2)))
// Generating the radiobutton acording to strings on radioText
cellFormat {
graphic = cache(it){
radiobutton(it,radioGroup){
action {
radioSelected = text
}
}
}
}
}
button("Next") {
action {
radioText.clear()
radioText.setAll("Five","Six","Seven","Eight","Nine","Ten")
}
}
}
}
Please let me know if there is something you don't understand on my aproach.

How do I initialise a NativeScript app fully programmatically (without XML)?

Here's what I have so far. The background goes green (the colour of the Page), but I'd expect a purple ContentView with some text inside to fill the page, too.
Is there anything further I'm missing?
import { on, run, launchEvent } from "tns-core-modules/application";
import { Frame } from "tns-core-modules/ui/frame/frame";
import { ContentView } from "tns-core-modules/ui/content-view/content-view";
import { TextBase } from "tns-core-modules/ui/text-base/text-base";
import { Page } from "tns-core-modules/ui/page/page";
on(launchEvent, (data) => {
const frame = new Frame();
const page = new Page();
page.backgroundColor = "green";
const contentView = new ContentView();
const textBase = new TextBase();
contentView.height = 100;
contentView.width = 100;
contentView.backgroundColor = "purple";
textBase.text = "Hello, world!";
contentView._addView(textBase);
page.bindingContext = contentView;
frame.navigate({ create: () => page });
data.root = page; // Incidentally, should this be the frame or the page?
});
run();
You are almost on track, you just need slight modification on your code.
import { on, run, launchEvent } from 'tns-core-modules/application';
import { Frame } from 'tns-core-modules/ui/frame/frame';
import { ContentView } from 'tns-core-modules/ui/content-view/content-view';
import { TextField } from 'tns-core-modules/ui/text-field';
import { Page } from 'tns-core-modules/ui/page/page';
run({
create: () => {
const frame = new Frame();
frame.navigate({
create: () => {
const page = new Page();
page.backgroundColor = "green";
const contentView = new ContentView();
const textField = new TextField();
contentView.height = 100;
contentView.width = 100;
contentView.backgroundColor = "purple";
textField.text = "Hello, world!";
contentView.content = textField;
page.content = contentView;
return page;
}
});
return frame;
}
});
You don't have to wait for launch event, you could set the root frame in run method itself.
In your code, you were creating the frame but never adding it to root UI element or mark the frame itself as root element
It's recommended to use .content to add child for a ContentView / Page as they are originally designed to hold one child element only.
Use TextField / TextView for input text, TextBase is just a base class.
It seems to me that you try to overcomplicate. You can replace XML with code just by implementing createPage method - Create a page via code.
I just modified default NS + TypeScript Playground template to operate without XML - NS + TypeScript template without XML.
I think you can't leave run as empty as it is expecting an entry to start the app. From {NS} website,
You can use this file to perform app-level initializations, but the
primary purpose of the file is to pass control to the app's root
module. To do this, you need to call the application.run() method and
pass a NavigationEntry with the desired moduleName as the path to the
root module relative to your /app folder.
if you look for run code in "tns-core-modules/application"
function run(entry) {
createRootFrame.value = false;
start(entry);
}
exports.run = run;
and
function start(entry) {
if (started) {
throw new Error("Application is already started.");
}
started = true;
mainEntry = typeof entry === "string" ? { moduleName: entry } : entry;
if (!androidApp.nativeApp) {
var nativeApp = getNativeApplication();
androidApp.init(nativeApp);
}
}

Vaadin - run client side javascript after image fully loaded

I need to print a picture on client side. I used this as a template. My PrintUI looks like this:
#Override
protected void init(VaadinRequest request) {
Item item = ..get item ..
StreamResource imageStream = ... build image dynamically ...
Image image = new Image(item.getName(), imageStream);
image.setWidth("100%");
setContent(image);
setWidth("100%");
// Print automatically when the window opens
JavaScript.getCurrent().execute("setTimeout(function() {print(); self.close();}, 0);");
}
This works so far in IE but in chrome it opens the printing preview showing an empty page. The problem is that the image is loaded in some way that chrome does not wait for it and starts the printing preview immideatly.
To verify this, I tried: (setting a 5sec timeout)
JavaScript.getCurrent().execute("setTimeout(function() {print(); self.close();}, 0);");
Then it works in IE and Chrome, but its of course an ugly hack, and if the connection is slower than 5sec, then again it will fail.
In pure JS it would work like this, but Im not sure how to reference the element from vaadin in cient-side js. Any ideas?
You can use AbstractJavascriptExtension.
Example extension class:
#JavaScript({ "vaadin://scripts/connector/wait_for_image_load_connector.js" })
public class WaitForImageLoadExtension extends AbstractJavaScriptExtension {
private List<ImageLoadedListener> imageLoadedListeners = new ArrayList<>();
public interface ImageLoadedListener {
void onImageLoaded();
}
public void extend(Image image) {
super.extend(image);
addFunction("onImageLoaded", new JavaScriptFunction() {
#Override
public void call(JsonArray arguments) {
for (ImageLoadedListener imageLoadedListener : imageLoadedListeners) {
if (imageLoadedListener != null) {
imageLoadedListener.onImageLoaded();
}
}
}
});
}
public void addImageLoadedListener(ImageLoadedListener listener) {
imageLoadedListeners.add(listener);
}
}
and javascript connector (placed in wait_for_image_load_connector.js) with the waiting method you have linked:
window.your_package_WaitForImageLoadExtension = function() {
var connectorId = this.getParentId();
var img = this.getElement(connectorId);
if (img.complete) {
this.onImageLoaded();
} else {
img.addEventListener('load', this.onImageLoaded)
img.addEventListener('error', function() {
alert('error');
})
}
}
Then you can do something like that:
Image image = new Image(item.getName(), imageStream);
WaitForImageLoadExtension ext = new WaitForImageLoadExtension();
ext.extend(image);
ext.addImageLoadedListener(new ImageLoadedListener() {
#Override
public void onImageLoaded() {
JavaScript.eval("print()");
}
});
In your case, when calling print() is the only thing you want to do after the image is loaded, you can also do it without server-side listener by just calling it in the connector:
if (img.complete) {
print();
} else {
img.addEventListener('load', print)
img.addEventListener('error', function() {
alert('error');
})
}

how to project data change into child component with angular2?

i'm trying to build angular2 component which draws chart (using jquery plot)
import {Component, ElementRef, Input, OnChanges} from 'angular2/core';
#Component({
selector: 'flot',
template: `<div>loading</div>`
})
export class FlotCmp implements OnChanges{
private width = '100%';
private height = 220;
static chosenInitialized = false;
#Input() private options: any;
#Input() private dataset:any;
#Input() private width:string;
#Input() private height:string;
constructor(public el: ElementRef) {}
ngOnChanges() {
if(!FlotCmp.chosenInitialized) {
let plotArea = $(this.el.nativeElement).find('div').empty();
plotArea.css({
width: this.width,
height: this.height
});
$.plot( plotArea, this.dataset, this.options);
FlotCmp.chosenInitialized = true;
}
}
}
Component getting chart "data" property as input parameter:
<flot [options]="splineOptions" [dataset]="dataset" height="250px" width="100%"></flot>
So far i managed to make it work as long as "dataset" is static variable.
this.dataset = [{label: "line1",color:"blue",data:[[1, 130], [3, 80], [4, 160], [5, 159], [12, 350]]}];
My problem is to make it work when data came as a promise:
export class App implements OnInit {
private dataset:any;
public entries;
getEntries() {
this._flotService.getFlotEntries().then(
entries => this.dataset[0].data = entries,
error => this.errorMessage = <any>error);
}
ngOnInit() {
this.getEntries()
}
constructor(private _flotService:FlotService) {
this.name = 'Angular2'
this.splineOptions = {
series: {
lines: { show: true },
points: {
radius: 3,
show: true
}
}
};
this.dataset = [{label: "line1",color:"blue",data:null]}];
}
}
For some reason data change cannot project to "flot" component
here is link to plunk
Please help
The problem is
entries => this.dataset[0].data = entries,
because only the inner state of the bound value is changed and Angular2 change detection doesn't observe the content only the value or reference itself.
A workaround would be to create a new array with the same content
this._flotService.getFlotEntries().then(
entries => {
this.dataset[0].data = entries;
this.dataset = this.dataset.slice();
},
In your case an additional event could work that notifies the child component that updates have happended.
Besides Günter's answer, another option is to implement your own change detection inside ngDoCheck() which will be called when your data comes back from the server:
ngDoCheck() {
if(this.dataset[0].data !== null && !this.dataPlotted) {
console.log('plotting data');
let plotArea = $(this.el.nativeElement).find('div').empty();
$.plot( plotArea, this.dataset, this.options);
this.dataPlotted = true;
}
}
I feel this is a cleaner approach, since we don't have to write code a specific way just to satisfy/trigger Angular change detection. But alas, it is less efficient. (I hate it when that happens!)
Also, the code you have in ngOnChanges() can be moved to ngOnInit().
Plunker
As Günter already mentioned, ngOnChanges() isn't called because the dataset array reference doesn't change when you fill in your data. So Angular doesn't think any input properties changed, so ngOnChanges() isn't called. ngDoCheck() is always called every change detection cycle, whether or not there are any input property changes.
Yet another option is to use #ViewChild(FlotCmp) in the parent component, which will get a reference to FlotCmp. The parent could then use that reference to call some method, say drawPlot(), on FlotCmp to draw/update the plot when the data arrives.
drawPlot(dataset) {
console.log('plotting data', dataset);
let plotArea = $(this.el.nativeElement).find('div').empty();
$.plot( plotArea, dataset, this.options);
this.dataset = dataset;
}
Plunker
This is more efficient than ngDoCheck(), and it doesn't have the issue I described above with the ngOnChanges() approach.
However, if I were to use this approach, I would rework the code somewhat, since I don't like how dataset is currently an input property, but then drawPlot() gets the data passed in via a function argument.

javafx close window

I have a javafx app, and I was able to spawn another window, but I can't seem to find a way to close the the window i started in.
I used this to load the second window
var design = Launcher {};
javafx.stage.Stage
{
title: "Launcher"
scene: design.getDesignScene ()
}
stage.close(), but you would need a variable that references the original stage.
The way it worked for me:
I have the Main.fx where I instante the window I want to see first.
ex:
var mainWind:MainWindow = MainWindow{ };
MainWindow.fx will extend CustomNode and override create() method.
In the create() method I have the stage
ex:
public class MainWindow extends CustomNode{
...
var stage:Stage;
override function create():Node {
var n:Node;
stage = Stage {
width: 300
height: 180
title: "Login"
scene: Scene {
content:[ userText, userTextBox, passwdText, passwdTextBox, btnsBox ]
}
}
return n;
}
}
In MainWindow.fx I have a button with an event where I close this stage and show the other one .
ex:
Button {
text: "Login"
font:Font{ name:"Arial" size: 12}
layoutInfo:LayoutInfo {
width: loginbtn_width
}
blocksMouse: false
//close main window
onMouseClicked: function(e:MouseEvent):Void { stage.close(); }
//open the other window
action: function(){
// the ConnectionSettings will also extend CustomNode and will have its own stage
var conSetWind:ConnectionSettings = ConnectionSettings{ };
}
}
Iulia

Resources