This is a kind of follow-up of seeing thread-rule violations when mixing Swing/FX and bind both parts to the same model.
Meanwhile I experimented a bit: use a custom property whose only task it is take care of accessing/notifying on the EDT/fx-thread, respectively. The idea is that the custom property
- is backed by a property which needs to be accessed on the EDT
- is used on the fx side, that is its fx api is called from the FX-AT
- its task is to invoke/runLater as appropriate
Gets rid of the thread rule violations ... at a price: when typing in the fx textfield, the caret is set to the start of the text, thus prepending each character. Before going on, questions are
- is it possible that a wrapper like the one below can work?
- is it doing something wrong? (Being a bloody newbie to the game, I might be doing something incredibly stupid ;-)
- what's the reason for the caret setting?
The code (can be played with in the SSCCE of the previous question, single change is to uncomment the wrapper creation and use that in place of the direct text binding to the field)
/**
* Wrapper that switches to FX-AT/EDT as appropriate. The assumption is
* that the delegate needs to be accessed on the EDT while this property
* allows client access on the FX-AT.
*
* @author Jeanette Winzenburg, Berlin
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public class PropertyWrapper<T> extends ObjectPropertyBase<T> {
// the delegate we are keeping synched to
private Property<T> delegate;
// the value which is kept in synch (on being notified) with the delegate's value
// JW: does this make sense at all?
private volatile T value;
// keeping a copy of the bean ... ? better not allow accessing at all?
// private Object delegateBean;
private String delegateName;
private ChangeListener<T> changeListener;
public PropertyWrapper(Property<T> delegate) {
this.delegate = delegate;
bindDelegate();
}
/**
* Returns the value which is kept synched to the delegate's value.
*/
@Override
public T get() {
return value;
}
/**
* Implemented to update the delegate on the EDT
*/
@Override
public void set(T value) {
// PENDING: think about uni-directional binding
updateToDelegate(value);
}
/**
* Updates the delegate's value to the given value.
* Guarantees to do the update on the EDT.
*
* @param value
*/
protected void updateToDelegate(final T value) {
if (SwingUtilities.isEventDispatchThread()) {
doUpdateToDelegate(value);
} else {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
doUpdateToDelegate(value);
}
});
}
}
/**
* Updates the delegate's value to the given value
* This methods runs on the thread that it is called from.
*
* @param the value to set.
*
*/
private void doUpdateToDelegate(T value) {
delegate.setValue(value);
}
/**
* Adds a ChangeListener to the delegate and synchs the value
* to the delegate's value.
*
* This is called once from the constructor, assuming that the thread it is
* called on is compatible with the delegates threading rules.
*/
private void bindDelegate() {
if (changeListener != null) throw new IllegalStateException("cannot bind twice");
value = delegate.getValue();
delegateName = delegate.getName();
changeListener = createChangeListener();
delegate.addListener(
changeListener);
}
/**
* Creates and returns the ChangeLister that's registered to the delegate.
* @return
*/
private ChangeListener<T> createChangeListener() {
ChangeListener<T> l = new ChangeListener<T>() {
@Override
public void changed(ObservableValue<? extends T> observable,
T oldValue, T newValue) {
updateFromDelegate(newValue);
}
};
// weakchangelistener doesn't work ... for some reason
// we seem to need a strong reference to the wrapped listener
// return new WeakChangeListener<T>(l);
return l;
}
/**
* Updates the internal value and notifies its listeners. Schedules the
* activity for execution on the fx-thread, if not already called on it.
*
* @param newValue
*/
protected void updateFromDelegate(final T newValue) {
if (Platform.isFxApplicationThread()) {
doUpdateFromDelegate(newValue);
} else {
Platform.runLater(new Runnable() {
@Override
public void run() {
doUpdateFromDelegate(newValue);
}});
}
}
/**
* Updates the internal value and notifies its listeners. It
* runs on the thread it is called from.
*
* @param newValue the new value.
*/
protected void doUpdateFromDelegate(T newValue) {
value = newValue;
fireValueChangedEvent();
}
/**
* Overridden to guarantee calling super on the fx-thread.
*/
@Override
protected void fireValueChangedEvent() {
if (Platform.isFxApplicationThread()) {
superFireChangedEvent();
} else {
Platform.runLater(new Runnable() {
@Override
public void run() {
superFireChangedEvent();
}});
}
}
protected void superFireChangedEvent() {
super.fireValueChangedEvent();
}
/**
* Implemented to return null.<p>
* PENDING: allow access to delegate's bean? It's risky, as this method
* most probably will be called on the fx-thread: even if we keep a copy
* around, clients might poke around the bean without switching to the EDT.
*/
@Override
public Object getBean() {
return null; //delegate != null ? delegate.getBean() : null;
}
@Override
public String getName() {
return delegateName; //delegate != null ? delegate.getName() : null;
}
@SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(PropertyWrapper.class
.getName());
}
A partial answer (why the misbehaviour of the bidi-bound textfield and what to do against it):
Techically, the behaviour is due to internal flags which get confused when the "back-notification" of a bidi-bound property happens outside of the expected code blocks.
isUpdating
to not update the originating property againdoNotAdjustCaret
is used by TextInputControl to mark changes triggered by itself. The control has a custom TextProperty which uses that flag to either set the selection to the start (for external changes) or not (for internal changes)Now the thread-changing property falls outside the first block, triggers a re-set of the textProperty which in turn isn't recognized as self-triggered, thus resetting the selectin/caret. A work-around is to update the value and "back-fire" directly: