Toolbar search with side menu - codenameone

When using the toolbar search feature, I encounter two issues.
The first (esthetical) problem is that when entering or leaving the search mode, the toolbar disappears for a moment, resulting in a flicker on the screen.
The second (functional) problem is that together with a side menu, the pointer dragged event results in a NullPointerException in the actionPerformed method on line 1302 of Toolbar.java. As a result, no scrolling is possible while in search mode.
Both problems can be replicated in the simulator and on Android using the form below.
Kind regards, Frans.
import com.codename1.ui.FontImage;
import com.codename1.ui.Form;
import com.codename1.ui.TextArea;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.layouts.BorderLayout;
public class ToolbarSearchForm extends Form
{
public ToolbarSearchForm()
{
super("Toolbar search", new BorderLayout());
getToolbar().addSearchCommand(e -> filter((String)e.getSource()));
getToolbar().addMaterialCommandToSideMenu("Settings", FontImage.MATERIAL_SETTINGS, this::settingsAction);
TextArea text = new TextArea(100, 100);
text.getAllStyles().setFgColor(0xFF000000);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++)
{
sb.append(i + " Hello World!\n");
}
text.setText(sb.toString());
add(BorderLayout.CENTER, text);
}
private void filter(String filter)
{
System.out.println(filter);
}
private void settingsAction(ActionEvent e)
{
System.out.println(e);
}
}
The complete stack trace is:
java.lang.NullPointerException
at com.codename1.ui.Toolbar$4.actionPerformed(Toolbar.java:1302)
at com.codename1.ui.util.EventDispatcher.fireActionSync(EventDispatcher.java:459)
at com.codename1.ui.util.EventDispatcher.fireActionEvent(EventDispatcher.java:362)
at com.codename1.ui.Form.pointerDragged(Form.java:3146)
at com.codename1.ui.Display.handleEvent(Display.java:2118)
at com.codename1.ui.Display.edtLoopImpl(Display.java:1051)
at com.codename1.ui.Display.mainEDTLoop(Display.java:969)
at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)
java.lang.NullPointerException
at com.codename1.ui.Toolbar$4.actionPerformed(Toolbar.java:1302)
at com.codename1.ui.util.EventDispatcher.fireActionSync(EventDispatcher.java:459)
at com.codename1.ui.util.EventDispatcher.fireActionEvent(EventDispatcher.java:362)
at com.codename1.ui.Form.pointerDragged(Form.java:3146)
at com.codename1.ui.Display.handleEvent(Display.java:2118)
at com.codename1.ui.Display.edtLoopImpl(Display.java:1051)
at com.codename1.ui.Display.flushEdt(Display.java:826)
at com.codename1.ui.Form.showModal(Form.java:2098)
at com.codename1.ui.Dialog.showModal(Dialog.java:1137)
at com.codename1.ui.Dialog.show(Dialog.java:582)
at com.codename1.ui.Dialog.showPackedImpl(Dialog.java:1433)
at com.codename1.ui.Dialog.showPacked(Dialog.java:1349)
at com.codename1.ui.Dialog.showImpl(Dialog.java:1093)
at com.codename1.ui.Dialog.show(Dialog.java:1071)
at com.codename1.ui.Dialog.show(Dialog.java:1027)
at com.codename1.ui.Dialog.show(Dialog.java:793)
at com.codename1.ui.Dialog.show(Dialog.java:746)
at com.codename1.ui.Dialog.show(Dialog.java:711)
at com.codename1.ui.Dialog.show(Dialog.java:652)
at com.codename1.ui.Dialog.show(Dialog.java:807)
at com.codename1.ui.Display.mainEDTLoop(Display.java:982)
at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120)
at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)
The getComponentForm() returns null (because the toolbar has no parent) in this line of Toolbar.java: if (Display.getInstance().getImplementation().isScrollWheeling() || !enableSideMenuSwipe || getComponentForm().findCurrentlyEditingComponent() != null || getComponentForm().isEditing()) {

I added a workaround for this issue in this git commit. It should be in the update tomorrow.

Related

Codename One back command update form data

I have an Android application that uses Back Command to go back to the start screen.
The start screen has a label with a number inside, that I want to update when the back command is used.
I could figure out a solution with the code inside the back command, but I don't know if my approach is the best, since the ClassOne gets sort of loaded twice.
Here is the code I already have:
public class ClassOne {
public ClassOne(ClassPojo classPojo) {
// I want to change the text of this label when calling the back command
labelOne.setText(classPojo.getStringTest());
formOne.show();
}
}
public class ClassTwo {
public ClassTwo(Form a , ClassPojo classPojo) {
Command back = new Command("A") {
#Override
public void actionPerformed(ActionEvent evt) {
// I am adding the new value for the label here inside the back command
classPojo.setStringTest("testing");
a.showBack();
new ClassOne(classPojo);
}
};
formTwo.setBackCommand(back);
}
I'm not sure what the problem is, your example is a bit generic. However, a complete minimal example where the startScreen form instance is not recreated is this one:
Form startScreen = new Form("Start screen", BoxLayout.y());
Wrapper<Integer> count = new Wrapper<>(1);
Label numberLabel = new Label(count.get() + "");
Button button1 = new Button("Go to Form 2");
startScreen.addAll(numberLabel, button1);
startScreen.show();
button1.addActionListener(l -> {
Form form2 = new Form("Form 2", BoxLayout.y());
Label label = new Label("Use the back button");
form2.add(label);
form2.getToolbar().setBackCommand("Back", Toolbar.BackCommandPolicy.ALWAYS, ll -> {
count.set(count.get() + 1);
numberLabel.setText(count.get() + "");
startScreen.showBack();
});
form2.show();
});
If you don't even want to recreate the form2 instance, then you can do so:
Form startScreen = new Form("Start screen", BoxLayout.y());
Wrapper<Integer> count = new Wrapper<>(1);
Label numberLabel = new Label(count.get() + "");
Button button1 = new Button("Go to Form 2");
startScreen.addAll(numberLabel, button1);
startScreen.show();
Form form2 = new Form("Form 2", BoxLayout.y());
Label label = new Label("Use the back button");
form2.add(label);
form2.getToolbar().setBackCommand("Back", Toolbar.BackCommandPolicy.ALWAYS, ll -> {
count.set(count.get() + 1);
numberLabel.setText(count.get() + "");
startScreen.showBack();
});
button1.addActionListener(l -> {
form2.show();
});
In my opinion, whether or not to recreate the instances of a Form should be evaluated on a case-by-case basis. Among the variables between taking into consideration, according to my modest opinion, there is also the readability of the code and what it does, especially in complex cases.
The overhead of recreating a form instance is negligible so that wouldn't be a problem but in recent years we try to reuse form instances more. Not because of the performance.
The benefit is in minor behaviors e.g. scroll position within the form. These are very hard to replicate.
During testing, I found an easy solution that is adding the label to the constructor. I hope this snippet can be helpful.
public ClassTwo(Form a, ClassPojo classPojo, Label label) {
Command back = new Command("A") {
#Override
public void actionPerformed(ActionEvent evt) {
label.setText(classPojo.getStringTest());
a.showBack();
}
};

Crash on iOS when downloading and inserting in Database

I have a problem with my application on iOS only (iPad 9.7" 11.3.1), it works perfectly on the simulator and on Android device.
When the app is launched for the first time, the user has to log in. This works fine. After that, the menu form is launched and a Dialog is shown on onShowCompleted():
if (!Preferences.get("download", false)) {
dialog = new Dialog("...");
dialog.setLayout(new BorderLayout());
SpanLabel label = new SpanLabel("...");
dialog.addComponent(BorderLayout.CENTER, label);
dialog.show(Display.getInstance().getDisplayHeight() / 5, ..., ..., ...);
}
In the constructor, a method that downloads informations is launched :
if (!Preferences.get("download", false)) {
dlReferentiel();
}
The method :
public void dlReferentiel() {
source = DataSource.getInstance();
// I also tried with invokeAndBlock() but doesn't work
/*
Display.getInstance().invokeAndBlock(() -> requestS());
Display.getInstance().invokeAndBlock(() -> requestB());
Display.getInstance().invokeAndBlock(() -> requestZ());
Display.getInstance().invokeAndBlock(() -> requestA());
*/
requestS();
requestB();
requestZ();
requestA();
}
The requestX methods get informations from server with ConnectionRequest :
(Exemple with requestA() which needs to be the last because it disposes the dialog, it is the only one that calls the UI)
public void requestA() {
ConnectionRequest connectionRequest = new ConnectionRequest() {
ArrayList<A> as = new ArrayList<>();
#Override
protected void readResponse(InputStream in) throws IOException {
JSONParser json = new JSONParser();
Reader reader = new InputStreamReader(in, "UTF-8");
Map<String, Object> data = json.parseJSON(reader);
for (String s : data.keySet()) {
as.add(new A(s, (String) data.get(s)));
}
}
#Override
protected void postResponse() {
source.createAllA(as); // makes insertions with Transaction
ToastBar.showErrorMessage("Référentiel téléchargé", 10);
dialog.dispose();
Preferences.set("download", true);
}
};
connectionRequest.setUrl(URL_A);
connectionRequest.setPost(true);
NetworkManager.getInstance().addToQueue(connectionRequest);
}
I was able to see some logs with a Mac. Each time, the app crashes after the Dialog is shown and few insertions are made. The same error message is output :
HW kbd: Failed to set (null) as keyboard focus
Unable to get short BSD proc info for 513: No such process
Unable to get proc info for 513: Undefined error: 0
I search for informations about this issue but nothing interesting with Codename One.
I don't know what to do with this error, I tried some things like using invokeAndBlock() or changing build hints like ios.keyboardOpen (just because there is "keyboard" in it) but nothing worked.
Thanks for the help.

AutoCompleteTextField does not respect minimum length on load

On load, when I focus or select the AutoCompleteTextField , the suggestions are showed right away eventhough I set the minimumLength to 4.
This is what is going on:
1) On load , all suggestions are shown in this example
2) Only after I start typing the suggestions behave correctly and they wouldn't show until we meet the 4 character criteria.
You can recreate this issue with the code below from a barebone hello world.
public void start() {
if (current != null) {
current.show();
return;
}
Form hi = new Form("Hi World");
ListModel<String> suggestionsModel = new DefaultListModel<String>();
suggestionsModel.addItem("Apple");
suggestionsModel.addItem("Banana");
suggestionsModel.addItem("Chocolate");
suggestionsModel.addItem("Elk");
suggestionsModel.addItem("Fish");
AutoCompleteTextField search = new AutoCompleteTextField(suggestionsModel);
search.setMinimumLength(4);
suggestionsModel.addSelectionListener(new SelectionListener() {
#Override
public void selectionChanged(int oldSelected, int newSelected) {
System.out.println("SUGGESTION SELECTED"+suggestionsModel.getSelectedIndex());
}
});
hi.add(search);
hi.show();
}
That is a bug on Codename One side... I experienced the same thing. Here is a link for you to file an issue and it will be fixed
https://github.com/codenameone/CodenameOne/issues/new
EDIT:
According to Shai's answer to your previous question, I doubt if the behavior will be changed due to other actions that rely on it.

Build Server: 'Error! Failed to transfer some classes!'

I wanted to test my App on my phone, so I send it to the Build server (Android). Instead of the expected, successfull build, I got an error:
Error! Failed to transform some classes
java.lang.RuntimeException: java.lang.ClassNotFoundException: com.private.container.projectDetailsContainer.top.center.progCircle.ProgressCircle
at net.orfjackal.retrolambda.lambdas.BackportLambdaInvocations.loadClass(BackportLambdaInvocations.java:116)
at net.orfjackal.retrolambda.lambdas.BackportLambdaInvocations.access$100(BackportLambdaInvocations.java:16)
at net.orfjackal.retrolambda.lambdas.BackportLambdaInvocations$InvokeDynamicInsnConverter.backportLambda(BackportLambdaInvocations.java:101)
at net.orfjackal.retrolambda.lambdas.BackportLambdaInvocations$InvokeDynamicInsnConverter.visitInvokeDynamicInsn(BackportLambdaInvocations.java:94)
at net.orfjackal.retrolambda.asm.ClassReader.readCode(ClassReader.java:1452)
at net.orfjackal.retrolambda.asm.ClassReader.readMethod(ClassReader.java:1017)
at net.orfjackal.retrolambda.asm.ClassReader.accept(ClassReader.java:693)
at net.orfjackal.retrolambda.asm.ClassReader.accept(ClassReader.java:506)
at net.orfjackal.retrolambda.Transformers.lambda$transform$4(Transformers.java:106)
at net.orfjackal.retrolambda.Transformers$$Lambda$8/636718812.accept(Unknown Source)
at net.orfjackal.retrolambda.Transformers.transform(Transformers.java:120)
at net.orfjackal.retrolambda.Transformers.transform(Transformers.java:106)
at net.orfjackal.retrolambda.Transformers.backportClass(Transformers.java:46)
at net.orfjackal.retrolambda.Retrolambda.run(Retrolambda.java:72)
at net.orfjackal.retrolambda.Main.main(Main.java:26)
Caused by: java.lang.ClassNotFoundException: com.private.container.projectDetailsContainer.top.center.progCircle.ProgressCircle
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at net.orfjackal.retrolambda.NonDelegatingClassLoader.loadClass(NonDelegatingClassLoader.java:27)
at net.orfjackal.retrolambda.lambdas.BackportLambdaInvocations.loadClass(BackportLambdaInvocations.java:114)
... 14 more
What am I doing wrong?
Notice
I already used the App on my mobile device and there was no problem with it
The App has no problems running in the simulator
EDIT 1
As you asked for it, here is the way I use the class (the only time I use it):
package com.companyname.mobile.container.projectDetailsContainer.top.center.progCircle;
import java.util.HashMap;
import com.codename1.ui.Container;
import com.codename1.ui.Label;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.ui.layouts.GridLayout;
import com.companyname.mobile.renderer.projectListitem.ProjectListItem;
public class ProgressCircleContainer extends Container {
/* attributes */
private ProgressCircle progressCircle;
/************************/
/**
* Constructor for the Container.
* The given parameter is the clicked listitem
*
* #param clicked List item clicked by the user
*/
public ProgressCircleContainer (ProjectListItem clicked) {
this.clicked = clicked;
init();
}
/*
* initializes the layout of the container
*/
private void init () {
this.layout = new GridLayout (2);
/* left container */
progressCircle = new ProgressCircle(clicked.getStatus());
leftCont = new Container ();
leftContLayout = new BorderLayout();
leftCont.setLayout(leftContLayout);
leftCont.add(BorderLayout.CENTER, progressCircle);
/****************************/
this.add(leftCont); //using gridlayout for the form
}
/* Getter and Setter */
}
The class is much larger, but I left out the unnessecary parts of it.
Here is the ProgCircle class:
package com.companyname.mobile.container.projectDetailsContainer.top.center.progCircle;
import java.io.IOException;
import com.codename1.ui.Component;
import com.codename1.ui.Container;
import com.codename1.ui.Graphics;
import com.codename1.ui.Image;
import com.codename1.ui.Label;
import com.codename1.ui.layouts.FlowLayout;
import com.codename1.ui.layouts.LayeredLayout;
import com.codename1.ui.util.Resources;
import com.codename1.ui.Graphics;
import com.codename1.ui.geom.Rectangle;
/**
* This class partly draws and partly displays the round progress circle
* one can see in the north of ProjectDetailsForm.
*
* It uses a LayeredLayout to paint the progress as a circle in the background
* while concealing the unnecessary part of it with a picture. On the GlassPane
* layer, there is a label showing the progress in percent
*
* {#http://stackoverflow.com/questions/35841377/how-to-make-a-round-progress-bar-in-codename-one}
*
*/
public class ProgressCircle extends Container {
/* attributes */
private Label percent;
private LayeredLayout layout;
/************************/
/* constants */
public static final String ROUND_PROG_BAR = "roundprogbar.png";
/************************/
/**
* Constructor for the round progress bar.
* The parameter given is the color it will be drawn in.
*
* #param statusColor color the progress will be drawn in
*/
public ProgressCircle (int statusColor) {
layout = new LayeredLayout();
this.setLayout(layout);
try {
Resources s = Resources.open("/theme.res");
Image progressOverlayImage = s.getImage(ROUND_PROG_BAR);
int currentProgress360 = 100;
percent = new Label (String.valueOf((int) (((double)currentProgress360 /360)*100))+ "%");
percent.getUnselectedStyle().setFgColor(0x000000);
percent.getUnselectedStyle().setAlignment(Component.CENTER);
this.add(new Label (progressOverlayImage.scaled((int)(progressOverlayImage.getWidth()*0.65), (int)(progressOverlayImage.getHeight()*0.65)), "Container")).
add(FlowLayout.encloseCenterMiddle(percent));
this.getUnselectedStyle().setBgPainter((Graphics g, Rectangle rect) -> {
g.setColor(statusColor);
g.fillArc(this.getX(), this.getY(),(int)(progressOverlayImage.getWidth()*0.65), (int)(progressOverlayImage.getHeight()*0.65), 0, currentProgress360);
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
Another notice:
I was wondering if it could be the too long package name. I replaced my companies name by 'companyname', but their length is equal.
If you need further information, let me know.
If it does not compile like this, I may have deleted a nessecary part, but then it is my fault right now. The app compiles successfully in the simulator and is fully functioning
EDIT 2:
here a link to the full error:
http://pastebin.com/ktCAbxk5
Have you modified your project class path in any way (eg added any jars or other source dirs)?
Do you have a class within the package com.private?
private is a reserved word in Java so I'm actually very surprised this works for you anywhere.

Timer in Javafx Creating FX Thread null pointers and out of bounds exceptions

I was trying to make a simple timer in Javafx with a GUI and Timer class. When the start button is pressed, it is supposed to count down from the inputed time and stop when it reaches 0.
I have the remaining time in milliseconds updating in a TextField on the GUI but it only runs for a random number of milliseconds usually between 100-200 and then it freezes up and throws a VERY large number of exceptions.
I tried to pinpoint where the error was coming from, and found there was also a concurrent modification exception as well.
Here is my Timer class:
import java.sql.Time;
/**
* Created by Josh on 19/8/2015.
*/
public class Timer {
private long endTimeMillis;
public Timer(long hours, long minutes, long seconds){
long currentTime = System.currentTimeMillis();
endTimeMillis = ((seconds*1000 + minutes*1000*60 + hours*1000*60*60) + currentTime);
}
public boolean isFinished(){
long currentTime = System.currentTimeMillis();
if(endTimeMillis - currentTime <= 0){
return true;
}else{
return false;
}
}
public long getRemainingTimeMillis(){
long currentTime = System.currentTimeMillis();
return endTimeMillis - currentTime;
}
}
and this is the GUI
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
/**
* Created by Josh on 19/8/2015.
*/
public class GUI extends Application {
private BorderPane root;
Label hoursLabel, minutesLabel, secondsLabel;
TextField hoursTF, minutesTF, secondsTF;
Button startButton;
Label remainingTimeLabel;
TextField remainingTimeTF;
long remainingTime;
#Override
public void start(Stage primaryStage) throws Exception {
root = new BorderPane();
primaryStage.setScene(new Scene(root, 1300, 125));
primaryStage.show();
primaryStage.setResizable(true);
primaryStage.setTitle("Timer");
primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
#Override
public void handle(WindowEvent event) {
event.consume();
System.exit(0);
}
});
FlowPane center = new FlowPane();
hoursLabel = new Label(" Hours: ");
hoursTF = new TextField();
minutesLabel = new Label(" Minutes: ");
minutesTF = new TextField();
secondsLabel = new Label(" Seconds: ");
secondsTF = new TextField();
center.getChildren().addAll(hoursLabel, hoursTF, minutesLabel, minutesTF, secondsLabel, secondsTF);
root.setCenter(center);
FlowPane bottom = new FlowPane();
bottom.setAlignment(Pos.CENTER);
startButton = new Button("Start");
startButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
start();
}
});
remainingTimeLabel = new Label(" Time Remaining: ");
remainingTimeTF = new TextField();
bottom.getChildren().addAll(startButton, remainingTimeLabel, remainingTimeTF);
root.setBottom(bottom);
}
public void start(){
Task timer = new Task<Void>() {
#Override
protected Void call() throws Exception {
long hours = 0, minutes = 0, seconds = 0;
try{
if(hoursTF.getText() == null || hoursTF.getText().equals("")){
hours = 0;
}else{
hours = Long.parseLong(hoursTF.getText());
}
if(minutesTF.getText() == null || minutesTF.getText().equals("")){
minutes = 0;
}else{
minutes = Long.parseLong(minutesTF.getText());
}
if(secondsTF.getText() == null || secondsTF.getText().equals("")){
seconds = 0;
}else{
seconds = Long.parseLong(secondsTF.getText());
}
}catch(NumberFormatException e){
System.out.println("Error");
}
Timer timer = new Timer(hours, minutes, seconds);
while(!timer.isFinished()){
remainingTimeTF.setText(Long.toString(timer.getRemainingTimeMillis()));
}
return null;
}
};
new Thread(timer).start();
}
public static void main(String[] args) {
launch(args);
}
}
any help would be appreciated!
New stack trace:
x.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
at javafx.beans.property.StringProperty.setValue(StringProperty.java:65)
at javafx.scene.control.Labeled.setText(Labeled.java:145)
at GUI$3.call(GUI.java:109)
at GUI$3.call(GUI.java:80)
at javafx.concurrent.Task$TaskCallable.call(Task.java:1423)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.lang.Thread.run(Thread.java:745)
Exception in thread "Thread-4" java.lang.IllegalStateException: Not on FX application thread; currentThread = Thread-4
at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:204)
at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:438)
at javafx.scene.Parent$2.onProposedChange(Parent.java:364)
at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:113)
at com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:108)
at com.sun.javafx.scene.control.skin.LabeledSkinBase.updateChildren(LabeledSkinBase.java:575)
at com.sun.javafx.scene.control.skin.LabeledSkinBase.handleControlPropertyChanged(LabeledSkinBase.java:204)
at com.sun.javafx.scene.control.skin.LabelSkin.handleControlPropertyChanged(LabelSkin.java:49)
at com.sun.javafx.scene.control.skin.BehaviorSkinBase.lambda$registerChangeListener$61(BehaviorSkinBase.java:197)
at com.sun.javafx.scene.control.skin.BehaviorSkinBase$$Lambda$102/1442917786.call(Unknown Source)
at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler$1.changed(MultiplePropertyChangeListenerHandler.java:55)
at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:89)
at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.StringPropertyBase.fireValueChangedEvent(StringPropertyBase.java:103)
at javafx.beans.property.StringPropertyBase.markInvalid(StringPropertyBase.java:110)
at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:144)
at javafx.beans.property.StringPropertyBase.set(StringPropertyBase.java:49)
at javafx.beans.property.StringProperty.setValue(StringProperty.java:65)
at javafx.scene.control.Labeled.setText(Labeled.java:145)
at GUI$3.call(GUI.java:109)
at GUI$3.call(GUI.java:80)
at javafx.concurrent.Task$TaskCallable.call(Task.java:1423)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.lang.Thread.run(Thread.java:745)
You are violating the rule about not accessing the state of a node in a live scene graph on a background thread. Most JavaFX controls will actually check for this and throw an IllegalStateException when you do this: apparently TextInputControl.setText(...) doesn't (presumably for reasons of performance).
The simplest fix is to update the message property of the task:
while(!timer.isFinished()){
updateMessage(Long.toString(timer.getRemainingTimeMillis()));
// remainingTimeTF.setText(Long.toString(timer.getRemainingTimeMillis()));
}
(this causes an update to the message property to occur on the FX Application Thread), and then bind the text field's text property to it:
remainingTimeTF.textProperty().bind(timer.messageProperty());
new Thread(timer).start();
However, there are a lot of things about your code that violate good practice. Fo example, you should really compute the total time outside of the task (i.e. on the FX Application Thread) and store it in a final variable; and the busy while loop is pretty horrible (though updateMessage(...) will fix most of the issues associated with that for you).
I recommend you read up on multithreading in JavaFX. Start with the Task API docs. Maybe this question will help: Using threads to make database requests
First of all, if you really need to update that TextField in the way you are doing it I would recommend adding a Thread.sleep(100); (10 updates per second) in your while, or even Thread.sleep(1000); (1 update per second) since it's really CPU intensive to update that text field on every CPU cycle and usually you don't need to..
Secondly (and probably the reason of your exceptions) the call remainingTimeTF.setText(); MUST happen IN the FX Thread, try this code instead:
Platform.runLater(new Runnable() {
#Override public void run() {
remainingTimeTF.setText(Long.toString(timer.getRemainingTimeMillis()));
}
});
Right now you are setting the text outside the FX Thead which might lead to several exceptions since update requests are happening in a worker thread and the actual UI modifications are happening in the FX thread. You need to make sure that every time you need to modify the UI it happens in the correct thread, in this case the FX Thread with the Platform.runLater() call.
Also since you are already using the Task class why not bind the text property and use the updateMessage() or updateTitle() methods instead?
Take a look at this article, it has good examples for the correct usage of Task and updating the UI from worker threads.
Hope this helps!

Resources