JavaFX TextArea and autoscroll - java

I am trying to get a TextArea to autoscroll to the bottom with new text which is put in via an event handler. Each new entry is just one long string of text with each entry separated by a line break. I have tried a change handler which sets setscrolltop to Double.MIN_VALUE but to no avail. Any ideas of how this could be done?

You have to add a listener to the TextArea element to scroll to the bottom when it's value is changed:
#FXML private TextArea txa;
...
txa.textProperty().addListener(new ChangeListener<Object>() {
#Override
public void changed(ObservableValue<?> observable, Object oldValue,
Object newValue) {
txa.setScrollTop(Double.MAX_VALUE); //this will scroll to the bottom
//use Double.MIN_VALUE to scroll to the top
}
});
But this listener is not triggered when you use the setText(text) method, so if you want to trigger it after a setText(text) use the appendText(text) right after it:
txa.setText("Text into the textArea"); //does not trigger the listener
txa.appendText(""); //this will trigger the listener and will scroll the
//TextArea to the bottom
This sounds more like a bug, once the setText() should trigger the changed listener, however it doesn't. This is the workaround I use myself and hope it helps you.

txa.appendText("") will scroll to the bottom without a listener. This becomes an issue if you want to scroll back and the text is being constantly updated. txa.setText("") puts the scroll bar back at the top and same issue applies.
My solution was to extend the TextArea class, ammend the FXML tag from textArea to LogTextArea. Where this works, it clearly causes problems in scene builder as it does not know what this component is
import javafx.scene.control.TextArea;
import javafx.scene.text.Font;
public class LogTextArea extends TextArea {
private boolean pausedScroll = false;
private double scrollPosition = 0;
public LogTextArea() {
super();
}
public void setMessage(String data) {
if (pausedScroll) {
scrollPosition = this.getScrollTop();
this.setText(data);
this.setScrollTop(scrollPosition);
} else {
this.setText(data);
this.setScrollTop(Double.MAX_VALUE);
}
}
public void pauseScroll(Boolean pause) {
pausedScroll = pause;
}
}

I don't have enough reputation to comment, but wanted to give some insight for future readers as to why setText doesn't appear to trigger the listener, but appendText does, as in Math's answer.
I Just found this answer while encountering similar issues myself, and looked into the code. This is currently the top result for 'javafx textarea settext scroll' in a google search.
setText does indeed trigger the listener.
According to the javadoc on the doSet method in TextInputControl (TextArea's superclass):
* doSet is called whenever the setText() method was called directly
* on the TextInputControl, or when the text property was bound,
* unbound, or reacted to a binding invalidation. It is *not* called
* when modifications to the content happened indirectly, such as
* through the replaceText / replaceSelection methods.
Inside the doSet method, a call is made to updateText(), which TextArea overrides:
#Override final void textUpdated() {
setScrollTop(0);
setScrollLeft(0);
}
So, when you set the scroll amount in the listener as in Math's answer, the following happens:
The TextProperty is updated
Your listener is called, and the scroll is set
doSet is called
textUpdated is called
The scroll is set back to the top-left
When you then append "",
The TextProperty is updated
Your listener is called, and the scroll is set
The javadoc is above is clear why this is the case - doSet is only called when using setText.
In fact, appendText calls insertText which calls replaceText - and the javadoc further states that replaceText does NOT trigger a call to doSet.
The behaviour is rather irritating, especially since these are all final methods, and not obvious at first glance - but is not a bug.

Alternative to that strange setText bug without using appendText
textArea.selectPositionCaret(textArea.getLength());
textArea.deselect(); //removes the highlighting

One addendum I would add to jamesarbrown's response would be to this would be to use a boolean property instead so you can access it from within FXML.
Something like this.
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.TextArea;
public class LogTextArea extends TextArea {
private final BooleanProperty pausedScrollProperty = new SimpleBooleanProperty(false);
private double scrollPosition = 0;
public LogTextArea() {
super();
}
public void setMessage(String data) {
if (isPausedScroll()) {
scrollPosition = this.getScrollTop();
this.setText(data);
this.setScrollTop(scrollPosition);
} else {
this.setText(data);
this.setScrollTop(Double.MAX_VALUE);
}
}
public final BooleanProperty pausedScrollProperty() { return pausedScrollProperty; }
public final boolean isPausedScroll() { return pausedScrollProperty.getValue(); }
public final void setPausedScroll(boolean value) { pausedScrollProperty.setValue(value); }
}
However, the problem with this answer is that if you get flooded with an unreasonably large amount of input (as can happen when retrieving a log from an IO Stream) the javaFX thread will lock up because the TextArea gets too much data.

As Matthew has posted the setText call is the problem. A easy workaround is to call clear, appendText and then setScrollTop. The other suggestions above did not work well for me, with enough delay it worked but was unreliable behaviour.
textAreaListener = (observable, oldValue, newValue) -> {
textArea.clear();
textArea.appendText(newValue);
textArea.setScrollTop(Double.MAX_VALUE);
};

Related

preventing the backspace key from deleting characters in a javafx textarea

Im trying to prevent backspace from deleting characters in a javafx textarea. KeyEvent.consume should do this but it doesn't prevent the key event from happening. It does set the property of the key event and KeyEvent.isConsumed() gives back true after consuming the event. The backspace still gets executed. I tried other keys and there is the same effect.
This is my current fxcontroller class:
public class Terminal {
#FXML
private TextArea ta_console;
#FXML
private AnchorPane ap_main;
#FXML
protected void initialize(){
ta_console.setText(">");
ta_console.positionCaret(1);
}
public void keyStroke(KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.BACK_SPACE || keyEvent.getCode() == KeyCode.DELETE) {
keyEvent.consume();
}
}
}
The keyStroke method gets executed by the textarea on key pressed.
Am i missing something or is consume bugged or somehow not functional the way the documentation says? Is there a way i can still get the desired outcome?
If you want to prevent the user deleting text, that should be done with a TextFormatter instead of trying to guess which key events the control is handling internally, and in what order all those events are handled, etc. etc., for its default behavior.
It's not clear exactly what you want to achieve, but here is an example, which prevents any deletion (or replacement) of text:
public class Terminal {
#FXML
private TextArea taConsole;
#FXML
protected void initialize(){
taConsole.setText(">");
taConsole.positionCaret(1);
taConsole.setTextFormatter(new TextFormatter(change -> {
if (change.getRangeStart() == change.getRangeEnd()) { // nothing deleted
return change ;
}
// change represents a deletion of some text, veto it:
return null ;
});
}
}
See the Javadocs for TextFormatter.Change for details.

Updating entries of virtual TableViewer on model change

I'm currently recoding a TableViewer to work fully virtual. So far I'm pretty content with the results, but I still have a problem, that all visible elements in the table are refreshed on a fixed timer. The model changes continously though. This means, that if I click on an entry before a periodic update happens, the table loads in the actual value for that position, but leaves all other elements untouched. Since this is how the LazyContentProvider works that is set for the TableViewer this is not much of a problem.
Since my TableViewer is a Live-Viewer of incoming events, with the newest entry shifting all other items one down, I'd like to refresh all visible elements on adding a new event.
I've tried to use TableViewer.refresh() on adding a new item, but that does not seem to do anything.
Since the full code is pretty complex, and part of a bigger piece of code I'll provide a basic representation of the code:
public class MyClass{
public TableViewer liveViewer;
public List<String> myItems=new ArrayList<>();
void init(){
liveViewer = new TableViewer(liveComp, SWT.BORDER | SWT.FULL_SELECTION | SWT.VIRTUAL);
liveViewer.setContentProvider(new LiveViewerContentProvider(liveViewer));
liveViewer.setLabelProvider(someLabelProvider);
liveViewer.setUseHashlookup(true);
ClassThatProvidesItems.addListener(new ItemAddedListener(){
#Override
void itemAdded(String item){
myItems.add(0,item);
}
}
}
}
public class LiveViewerContentProvider implements ILazyContentProvider{
private TableViewer viewer;
private List<String> input;
public LiveViewerContentProvider(TableViewer viewer) {
this.viewer = viewer;
}
#Override
public void dispose() {
}
#Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
this.input = (List<String>) newInput;
}
#Override
public void updateElement(int index) {
viewer.replace(input.get(index), index);
}
}
I'm aware, that an ArrayList is probably not the best choice, for always adding an element at the head, but please ignore it for now. I've tried to perform a liveViewer.refresh(); at the end of the listener callback, but it didn't seem to refresh my elements. What could I do to force a refresh for all visible items on adding a new one?
Thanks in advance.
I've just noticed, that my solution was almost working. The problem was, that the whole code was somewhere within a weird try-catch-block that just silently swallowed Exceptions, and didn't give me the invalid-Thread-access exception that I should have gotten for not performing the liveViewer.refresh within the Display-Thread. Wrapping the line like this fixed the issue:
Display.getDefault().asyncExec(new Runnable() {
#Override
public void run() {
liveViewer.refresh();
}
});

Is this cast okay?

I have an EventHandler that I set as an event filter on TextFields. When I write the class, I get the source TextField by calling getSource() on the event and casting it to a TextField.
The code for the EventHandler:
public class NumberFilter implements EventHandler<KeyEvent> {
public final int maxLength;
public NumberFilter(int maxLength) {
this.maxLength = maxLength;
}
#Override
public void handle(KeyEvent event) {
TextField textField = (TextField) event.getSource(); //<-- is this cast okay?
//consume event if there is too much text or the input isn't a number.
if (textField.getText().length() >= maxLength || !event.getCharacter().matches("[0-9]")) {
event.consume();
}
}
}
Is this cast okay by standard java conventions? How can I write the class so that it can't be used anywhere except as an event filter for a TextField?
Andy Turner's answer provides a robust general approach to allowing event handlers to be added to only one type of Node. However, for the specific case of vetoing changes to the text in a TextField (or other text input control), the approach of using key event handlers is not a good one for the following reasons:
The user can bring up a context menu with the mouse and paste text in. This doesn't involve any key presses at all, so your handler won't be invoked.
You have no control over which type of key events the text field uses internally. Are you registering this filter with KEY_PRESSED, KEY_RELEASED, or KEY_TYPED events? Are you sure the events used internally by the text field will remain the same from one JavaFX release to the next?
You will likely inadvertently veto keyboard shortcuts such as Ctrl-C (for copy) or Ctrl-V (for paste), and similar. (If you don't veto shortcuts for "paste", you allow another loophole for the user to paste invalid text...). Again, it's possible a future release of JavaFX may introduce additional shortcuts, which it's virtually impossible to proof your functionality against.
For completeness, the preferred approach for this particular use case is as follows:
Use a TextFormatter, which is the supported mechanism for vetoing or modifying text entry to a text input control (as well as providing mechanisms to format or parse text in the control). You can make this reusable by implementing the filter in a standalone class:
public class NumberFilter implements UnaryOperator<TextFormatter.Change> {
private final Pattern pattern ;
public NumberFilter(int maxLength) {
pattern = Pattern.compile("[0-9]{0,"+maxLength+"}");
}
#Override
public TextFormatter.Change apply(TextFormatter.Change c) {
String newText = c.getControlNewText() ;
if (pattern.matcher(newText).matches()) {
return c ;
} else {
return null ;
}
}
}
And now you can do
TextField textField = new TextField();
textField.setTextFormatter(new TextFormatter<String>(new NumberFilter(5)));
Just to expand on my comment on #MaxPower's answer:
Don't use inheritance to do something which you can more cleanly do with composition.
I think that #James_D's approach is better in this case; but if in general you want an EventHandler which can only be added to a certain type of field, enforce this through your API:
public class NumberFilter implements EventHandler<KeyEvent> {
public static void addTo(int maxLength, TextField textField) {
textField.addEventHandler(new NumberFilter(maxLength));
}
private NumberFilter(int maxLength) {
// Private ctor means that you can't just create one of these
// however you like: you have to create it via the addTo method.
}
// Now casting in the handle() method is safe.
}
In this way, the only means of creating the NumberFilter is via the addTo method; and that requires that you're adding it to a TextField.
Casts are a way of you telling the compiler that you know more then it does.
If you know that every time this piece of code gets called it will be from a TextField than it is okay. Otherwise, I would do
try {
TextField textField = (TextField) event.getSource();
//Do Stuff
}
catch(ClassCastException e) {
//handle the error
}
or if you want a little more type safety
if(event.getSource() instanceof TextField) {
TextField textField = (TextField) event.getSource();
}
Or better yet
public class MyTextField extends TextField implements EventHandler<KeyEvent> {
}
then place use this instead of TextField and add your method, then it's type safe.

Using Enabled Property on a JButton in netbeans

I am trying to setup a button similar to the save button with the default CRUD database template (where the button only becomes active if a variable is true). I have looked at the code for the save button and worked out that i need:
A variable to link it with (saveNeeded in their case)
An action to run
I have recreated both of these on another button but it never seams to get enabled. I have print statements on 2 other buttons i am using to set the variable i have my button linked to to true and false so i can see the value is changing.
Is there some crucial step i am missing? this seems like it should be fairly straight forward.
One other thing, if i manualy change the variable to true in my constructor to true and run the application it enables the button and false disables it so that part is working, just not the change.
Any help would be appreciated as i have spent the last few hours trying and can not figure it out
Thanks
The variable or "property" needs to be watched somehow, perhaps by using a PropertyChangeSupport object and allowing other objects to add a PropertyChangeListener to it, making it a "bound property". There's a special version of this for Swing applications that takes care with the Swing event thread, SwingPropertyChangeSupport, and you may wish to use it.
Edit
You asked
Thanks for the reply, i assume that would be what firePropertyChange("saveNeeded", !saveNeeded, saveNeeded); is doing but waht is this doing? does this just notify the program or do i need to catch an handle this somewhere. This is based off the pre generated code so im not sure if it added something in the background.
The class that holds the watched variable would need a private SwingPropertyChangeSupport field. You would give it a public addPropertyChangeListener method where you'd allow other classes to listen to its bound properties, something like this (if the property were a String):
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.event.SwingPropertyChangeSupport;
public class Foo {
public static final String MY_BOUND_PROPERTY = "My Bound Property";
private SwingPropertyChangeSupport spcSupport = new SwingPropertyChangeSupport(
this);
private String myBoundProperty;
public void addPropertyChangeListener(PropertyChangeListener listener) {
spcSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
spcSupport.removePropertyChangeListener(listener);
}
public String getMyBoundProperty() {
return myBoundProperty;
}
public void setMyBoundProperty(String myBoundProperty) {
Object oldValue = this.myBoundProperty;
Object newValue = myBoundProperty;
this.myBoundProperty = myBoundProperty;
PropertyChangeEvent pcEvent = new PropertyChangeEvent(this,
MY_BOUND_PROPERTY, oldValue, newValue);
spcSupport.firePropertyChange(pcEvent);
}
}
Then any class that would like to listen for changes would simply add a PropertyChangeListener to an object of this class and respond to changes as it saw fit.

GWT SuggestBox: How do I force the SuggestBox to select the first item in the suggestion list?

I have a textbox and one suggestbox. I attach a value change and key up handler to the text box such that whatever the user types (or pastes) into the text box is echo-ed inside the suggestbox. I can get the suggestbox to display the suggestion list by calling showSuggestionList on each value change and key up event.
Now, how do I get the suggestbox to automatically choose the first item in the suggestion list?
One of the methods I tried is to programatically simulate key presses, i.e
suggestBox.setFocus(true);
NativeEvent enterEvent = Document.get().createKeyPressEvent(false, false, false, false, KeyCodes.KEY_ENTER);
DomEvent.fireNativeEvent(enterEvent, suggestBox);
textBox.setFocus(true);
This doesn't work at all. The enter key isn't simulated. Another possible solution is to extend SuggestionBox.SuggestionDisplay, but I'm not too sure how to that. Any pointers appreciated.
Update: I'm still working on this and trying various methods.
Here, I tried to implement my own SuggestionDisplay by subclassing DefaultSuggestionDisplay and overriding getCurrentSelection() to make accessible from my class. This doesn't work either. Null is returned.
private class CustomSuggestionDisplay extends DefaultSuggestionDisplay {
#Override
protected Suggestion getCurrentSelection() {
return super.getCurrentSelection();
}
}
suggestBox.setAutoSelectEnabled(true);
textBox.addKeyUpHandler(new KeyUpHandler() {
public void onKeyUp(KeyUpEvent event) {
suggestBox.setValue(textBox.getText(), true);
suggestBox.showSuggestionList();
if (suggestBox.isSuggestionListShowing()) {
String s = ((CustomSuggestionDisplay) suggestBox.getSuggestionDisplay()).getCurrentSelection().getDisplayString();
Window.alert(s);
}
}
});
Here, I tried to attach a value change handler to the SuggestBox, and casting the event type to SuggestOracle.Suggestion. Again, null is returned.
suggestBox.addValueChangeHandler(new ValueChangeHandler<String>() {
public void onValueChange(ValueChangeEvent<String> event) {
String s = ((SuggestOracle.Suggestion) event).getDisplayString();
Window.alert(s);
}
});
Use suggesBox.setAutoSelectEnabled(true)
Here more info about the SuggestBox of GWT:
You could try using addSelectionHandler in conjunction with setAutoSelectEnabled to receive an event whenever a suggestion is selected. You could also have your Oracle send a message when it suggests something, or your Display send a message when it displays a list:
public class AutomaticallySelectingSuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
#Override
protected void showSuggestions(SuggestBox box, Collection<? extends SuggestOracle.Suggestion> suggestions, boolean isDisplayHtml, boolean isAutoSelectEnabled, SuggestBox.SuggestionCallback callback) {
super.showSuggestions(box, suggestions, isDisplayHtml, isAutoSelectEnabled, callback);
fireValueChangeEventWithFirstSuggestion(suggestions);
}
}
This idea feels a little muddled to me, so I hope you can find a solution just using event handlers.

Categories