TextField onEdit listener - java

I am trying to use TextField in javafx.
The scenario: I have list view populated with specific objects and edit button to edit the object associated with list cell of list view.
When I click on edit button it redirects me to a pane with editing feature where I can edit the name of that object and save it using a save button.
So I have to put validation on save button to make it enable and disable.
If I edit the name in text field then it should enable the save button otherwise it should remains disabled.
I have tried using different methods on text fields as below.
textField.textPorperty.addListener(listener -> {
//Logic to enable disable save button
});
As I am using list view, this listener gives me old value as previously edited object which does not satisfy my condition.
I can not use
textField.focusedProperty().addListener((observableValue, oldValue, newValue) -> {});
as It does not give me expected behavior.
Can anyone help me to solve this issue?

You need to implement additional logic that decides whether or not a change to the textProperty should change the enablement state of the button. This requires:
a reference to the initial value (on setting the text to the input, f.i. on changes to selection in the list)
a boolean property that keeps the enablement state (below it's called buffering)
a listener to the textField that updates the enablement state as needed
Below is a very simplified example - just to get you started - that extracts those basics into a dedicated class named BufferedTextInput. Buffering is changed internally on:
set to false if the "subject" value is set or a change is committed/discarded
set to true once on being notified on the first change of the textField
More complex logic (like not buffering on detecting a change back to the original value) can be implemented as needed.
/**
* Bind disable property of commit/cancel button to actual change.
* http://stackoverflow.com/q/29935643/203657
*/
public class ManualBufferingDemo extends Application {
private Parent getContent() {
ObservableList<Person> persons = FXCollections.observableList(Person.persons(),
person -> new Observable[] {person.lastNameProperty()});
ListView<Person> listView = new ListView<>(persons);
TextField lastName = new TextField();
Consumer<String> committer = text -> System.out.println("committing: " + text);
BufferedTextInput buffer = new BufferedTextInput(lastName, committer);
Button save = new Button("Save");
save.setOnAction(e -> {
buffer.commit();
});
save.disableProperty().bind(Bindings.not(buffer.bufferingProperty()));
Button cancel = new Button("Cancel");
cancel.setOnAction(e -> {
buffer.flush();
});
listView.getSelectionModel().selectedItemProperty().addListener((source, old, current) -> {
buffer.setSubject(current.lastNameProperty());
});
cancel.disableProperty().bind(Bindings.not(buffer.bufferingProperty()));
VBox content = new VBox(listView, lastName, save, cancel);
return content;
}
public static class BufferedTextInput {
private ReadOnlyBooleanWrapper buffering;
private StringProperty value;
private TextField input;
private Consumer<String> committer;
public BufferedTextInput(TextField input, Consumer<String> committer) {
buffering = new ReadOnlyBooleanWrapper(this, "buffering", false);
value = new SimpleStringProperty(this, "");
this.input = input;
this.committer = committer;
input.textProperty().addListener((source, old, current) -> {
updateState(old, current);
});
input.setOnAction(e -> commit());
}
private void updateState(String old, String current) {
if (isBuffering()) return;
if (value.get().equals(current)) return;
setBuffering(true);
}
public void setSubject(StringProperty value) {
this.value = value;
input.setText(value.get());
setBuffering(false);
}
public void commit() {
committer.accept(input.getText());
this.value.set(input.getText());
setBuffering(false);
}
public void flush() {
input.setText(value.get());
setBuffering(false);
}
public boolean isBuffering() {
return buffering.get();
}
public ReadOnlyBooleanProperty bufferingProperty() {
return buffering.getReadOnlyProperty();
}
private void setBuffering(boolean buffer) {
buffering.set(buffer);
}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
For production use, such direct coupling between view and model (f.i. when needing the buffering for a complete form) isn't good enough, further separation might be needed. See BufferedObjectProperty and its usage in a FX adaption of the infamous AlbumManager example (very crude)

Related

javafx - .IndexOutOfBoundsException for table view

I would to ask why does IndexOutOfBoundsException appear when I try to delete the first row from the table view of supplement which is index 0. I am using a button to delete the row
Update: update the code to have a minimal reproducible example
SupplementTest.java
public class SupplementTest extends Application {
WindowController windowGUI = new WindowController();
Stage stageGUI;
Scene sceneGUI;
#Override
public void start(Stage primaryStage) throws IOException {
FXMLLoader assignment2 = new FXMLLoader(getClass().getResource("SupplementFXML.fxml"));
Parent fxmlFile = assignment2.load();
try {
stageGUI = primaryStage;
windowGUI.initialize();
sceneGUI = new Scene(fxmlFile, 250, 350);
stageGUI.setScene(sceneGUI);
stageGUI.setTitle("Supplement");
stageGUI.show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
WindowController.java
public class WindowController {
Stage newWindow = new Stage();
boolean deleteSupplement;
#FXML
private GridPane primaryGrid = new GridPane();
#FXML
private Label supplementLabel = new Label();
#FXML
private Button deleteBtn = new Button(), addBtn = new Button();
public TableView<Supplement> supplementView = new TableView<>();
int suppIndex;
ArrayList<Supplement> supplementList = new ArrayList<>();
// initialize Method
public void initialize() {
newWindow.initModality(Modality.APPLICATION_MODAL);
newWindow.setOnCloseRequest(e -> e.consume());
initializeWindow();
updateSupplementList();
}
public void initializeWindow() {
deleteSupplement = false;
TableColumn<Supplement, String> suppNameColumn = new TableColumn<>("Name");
suppNameColumn.setCellValueFactory(new PropertyValueFactory<>("supplementName"));
TableColumn<Supplement, Double> suppCostColumn = new TableColumn<>("Weekly Cost");
suppCostColumn.setCellValueFactory(new PropertyValueFactory<>("weeklyCost"));
supplementView.getColumns().addAll(suppNameColumn, suppCostColumn);
supplementView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
suppIndex = supplementView.getSelectionModel().getSelectedIndex();
addBtn.setOnAction(e -> {
supplementList.add(new Supplement("Test1", 10));
supplementList.add(new Supplement("Test2", 20));
supplementList.add(new Supplement("Test3", 15));
updateSupplementList();
});
// remove button
deleteBtn.setOnAction(e -> {
deleteSupplement = true;
deleteSupplement();
});
}
public void updateSupplementList() {
supplementView.getItems().clear();
if (supplementList.size() > 0) {
for(int i = 0; i < supplementList.size(); i++) {
Supplement supplement = new Supplement(supplementList.get(i).getSupplementName(),
supplementList.get(i).getWeeklyCost());
supplementView.getItems().add(supplement);
}
}
}
public void deleteSupplement() {
try {
ObservableList<Supplement> supplementSelected, allSupplement;
allSupplement = supplementView.getItems();
supplementSelected = supplementView.getSelectionModel().getSelectedItems();
supplementSelected.forEach(allSupplement::remove);
supplementList.remove(suppIndex);
} catch(Exception ex) {
ex.printStackTrace();
}
}
}
Supplement.java
public class Supplement implements Serializable {
private String supplementName;
private double weeklyCost;
public Supplement() {
this.supplementName = "";
this.weeklyCost = 0.00;
}
public Supplement(String suppName, double weeklyCost) {
this.supplementName = suppName;
this.weeklyCost = weeklyCost;
}
public String getSupplementName() {
return supplementName;
}
public double getWeeklyCost() {
return weeklyCost;
}
public void setSupplementName(String supplementName) {
this.supplementName = supplementName;
}
public void setWeeklyCost(double weeklyCost) {
this.weeklyCost = weeklyCost;
}
}
How do I fix it so that when I delete any index in the table view the IndexOutOfBoundsException does not appear?
It's difficult to know for certain what is causing the exception, because your code is both incomplete (so no-one here can copy, paste, and run it to reproduce the error), and very confusing (it is full of seemingly-unnecessary code). However:
You seem to be doing two different things to delete the selected item(s) from the table:
supplementSelected = supplementView.getSelectionModel().getSelectedItems();
supplementSelected.forEach(allSupplement::remove);
which is an attempt to delete all selected items (though I don't believe it will work if more than one item is selected)
and
supplementList.remove(suppIndex);
which will delete the selected item, as defined by the selected index property in the selection model. (It is the currently selected item in a single selection model, or the last selected item in a multiple selection model, or -1 if nothing is selected.)
The latter will not work, because you only ever set suppIndex in your initialization code:
public void initializeWindow() {
// ...
suppIndex = supplementView.getSelectionModel().getSelectedIndex();
// ...
}
Of course, when this code is executed, the user has not had a chance to selected anything (the table isn't even displayed at this point), so nothing is selected, and so suppIndex is assigned -1. Since you never change it, it is always -1, and so when you call
supplementList.remove(suppIndex);
you get the obvious exception.
If you are only supporting single selection, and want to delete the currently selected item (or the last selected item in multiple selection), just get the selection at the time. You probably still want to check something is selected:
public void deleteSupplement() {
int selectedIndex = supplementView.getSelectionModel().getSelectedIndex();
if (selectedIndex >= 0) {
supplementView.getItems().remove(selectedIndex);
}
}
A slight variation on this, which I think is preferable, is to work with the actual object instead of its index:
public void deleteSupplement() {
Supplement selection = supplementView.getSelectionModel().getSelectedItem();
if (selection != null) {
supplementView.getItems().remove(selection);
}
}
Now, of course (in a theme that is common to a lot of your code), you can remove suppIndex entirely; it is completely redundant.
If you want to support multiple selection, and delete all selected items, then the code you currently have for that will cause an issue if more than one item is selected. The problem is that if a selected item is removed from the table's items list, it will also be removed from the selection model's selected items list. Thus, the selected items list (supplementSelected) in your code changes while you are iterating over it with forEach(...), which will throw a ConcurrentModificationException.
To avoid this, you should copy the list of selected items into another list, and remove those items:
public void deleteSupplement() {
List<Supplement> selectedItems
= new ArrayList<>(supplementView.getSelectionModel().getSelectedItems());
supplementView.getItems().removeAll(selectedItems);
}
Of course, this code also works with single selection (when the list is always either length 0 or length 1).
To address a couple of other issues: there is really no point in keeping a separate list of Supplement items. The table already keeps that list, and you can reference it at any time with supplementView.getItems(). (If you wanted to reference the list elsewhere, e.g. in a model in a MVC design, you should make sure that there is just a second reference to the existing list; don't create a new list.)
In particular, you should not rebuild the table entirely from scratch every time you add a new item to the list. Get rid of the redundant supplementList entirely from your code. Get rid of updateSupplementList() entirely; it is firstly doing way too much work, and secondly (and more importantly) will replace all the existing items just because you add a new one. This will lost important information (for example it will reset the selection).
To add new items, all you need is
addBtn.setOnAction(e -> {
supplementView.getItems().add(new Supplement("Test1", 10));
supplementView.getItems().add(new Supplement("Test2", 20));
supplementView.getItems().add(new Supplement("Test3", 15));
});
There are various other parts of your code that don't make any sense, such as:
The deleteSupplement variable. This seems to have no purpose.
The try-catch in the deleteSupplement method. The only exceptions that can be thrown here are unchecked exceptions caused by programming logic errors (such as the one you see). There is no point in catching those; you need to fix the errors so the exceptions are not thrown.
The #FXML annotations. You should never initialize fields that are annotated #FXML. This annotation means that the FXMLLoader will initialize these fields. In this case (as far as I can tell) these are not even associated with an FXML file at all, so the annotation should be removed.

StyleableProperty: how to change value programatically at runtime?

My use-case:
a custom property on a control that should be configurable via css
the property must be changeable at runtime
for a given instance of the control, the programmatic change must not be reverted on re-applying the css
A custom StyleableProperty looks like a perfect match to implement the requirement. Below is an example that implements (taken without change from the class javadoc of StyleablePropertyFactory).
All is fine except for the last requirement: on applyCss, the default value from the stylesheet is reapplied. To reproduce:
run the example, note that the initial "selected" state (the checkbox' selected is bound it) of the MyButton is true
click the custom button, note that the "selected" doesn't change to false (though the actionHandler changes it)
click on the second ("toggle") button, note that the selected state of the custom button changes to false
hover the mouse over the custom button, note that the selected state falls back to true
The reason for falling back to true (the value set via style), can be traced to applyCss which happens on state changes ... which is understandable and might be the correct thingy-to-do most of the times, but not in my context.
So the questions:
am I on the right track with using StyleableProperty?
if so, how to tweak such that it's not re-apply after a manual change has happened?
if not, what else to do?
or maybe asking the wrong questions altogether: maybe properties which are settable via css are not meant to be (permanently) changed by code?
The example:
public class StyleableButtonDriver extends Application {
/**
* example code from class doc of StyleablePropertyFactory.
*/
private static class MyButton extends Button {
private static final StyleablePropertyFactory<MyButton> FACTORY
= new StyleablePropertyFactory<>(Button.getClassCssMetaData());
MyButton(String labelText) {
super(labelText);
getStyleClass().add("my-button");
setStyle("-my-selected: true");
}
// Typical JavaFX property implementation
public ObservableValue<Boolean> selectedProperty() { return (ObservableValue<Boolean>)selected; }
public final boolean isSelected() { return selected.getValue(); }
public final void setSelected(boolean isSelected) { selected.setValue(isSelected); }
// StyleableProperty implementation reduced to one line
private final StyleableProperty<Boolean> selected =
FACTORY.createStyleableBooleanProperty(
this, "selected", "-my-selected", s -> s.selected);
#Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return FACTORY.getCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return FACTORY.getCssMetaData();
}
}
private Parent createContent() {
MyButton button = new MyButton("styleable button");
button.setOnAction(e -> {
// does not work: reset on applyCss
boolean isSelected = button.isSelected();
button.setSelected(!isSelected);
});
CheckBox box = new CheckBox("button selected");
box.selectedProperty().bind(button.selectedProperty());
Button toggle = new Button("toggle button");
toggle.setOnAction(e -> {
boolean isSelected = button.isSelected();
button.setSelected(!isSelected);
});
BorderPane content = new BorderPane(button);
content.setBottom(new HBox(10, box, toggle));
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 300, 200));
//same behavior as setting the style directly
// URL uri = getClass().getResource("xstyleable.css");
// stage.getScene().getStylesheets().add(uri.toExternalForm());
// not useful: would have to override all
// Application.setUserAgentStylesheet(uri.toExternalForm());
stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(StyleableButtonDriver.class.getName());
}
You are on the right track, but since you need to override the default priority of the style origins (user agent stylesheet < programmatically assigned < css stylesheet < Node.style property), you cannot use SyleablePropertyFactory for creating this property. You need to create a CssMetaData object that indicates a property as non-setable, if the property was programatically assigned.
private static class MyButton extends Button {
private static final List<CssMetaData<? extends Styleable, ?>> CLASS_CSS_METADATA;
private static final CssMetaData<MyButton, Boolean> SELECTED;
static {
SELECTED = new CssMetaData<MyButton, Boolean>("-my-selected", StyleConverter.getBooleanConverter()) {
#Override
public boolean isSettable(MyButton styleable) {
// not setable, if bound or set by user
return styleable.selected.getStyleOrigin() != StyleOrigin.USER && !styleable.selected.isBound();
}
#Override
public StyleableProperty<Boolean> getStyleableProperty(MyButton styleable) {
return styleable.selected;
}
};
// copy list of button css metadata to list and add new metadata object
List<CssMetaData<? extends Styleable, ?>> buttonData = Button.getClassCssMetaData();
List<CssMetaData<? extends Styleable, ?>> mybuttonData = new ArrayList<>(buttonData.size()+1);
mybuttonData.addAll(buttonData);
mybuttonData.add(SELECTED);
CLASS_CSS_METADATA = Collections.unmodifiableList(mybuttonData);
}
MyButton(String labelText) {
super(labelText);
getStyleClass().add("my-button");
setStyle("-my-selected: true");
}
// Typical JavaFX property implementation
public ObservableValue<Boolean> selectedProperty() { return selected; }
public final boolean isSelected() { return selected.get(); }
public final void setSelected(boolean isSelected) { selected.set(isSelected); }
// StyleableProperty implementation reduced to one line
private final SimpleStyleableBooleanProperty selected = new SimpleStyleableBooleanProperty(SELECTED, this, "selected");
#Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return CLASS_CSS_METADATA;
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return CLASS_CSS_METADATA;
}
}

Switch add-on with push, vaadin

I have a simple example of the add-on switch with vaadin, what I want is to keep the state of the switch even when I update the UI, that is, I support multiple tabs, but I can not do it, this push example is very similar to What I want to do but with a textField.
https://github.com/vaadin-marcus/push-example/blob/master/src/main/java/com/vaadin/training/ScrumBoardLayout.java
https://github.com/rucko24/MVP/blob/testingSwitchPushTemu/src/main/java/com/Core/vaadin/pushServer/ejemploPushMarkus/ScrumBoard.java
To my example I add a bulb so that when another accesses the application can see the current state of the bulb. My example in github is this with only 3 classes
https://github.com/rucko24/MVP/tree/testingSwitchPushTemu/src/main/java/com/Core/vaadin/pushServer/ejemploPushMarkus
This is the swithc listener that changes my bulb, but when I get the boolean value (true, or false), I still do not understand the right way to push the other switch
switchTemu.addValueChangeListener(new Property.ValueChangeListener() {
private static final long serialVersionUID = 1L;
#Override
public void valueChange(Property.ValueChangeEvent event) {
boolean estado = (boolean) event.getProperty().getValue();
ScrumBoard.addSwitch(estado);
switchTemu.removeValueChangeListener(this);
if(estado == Boolean.TRUE) {
bombilla.setIcon(bombillaON);
}else {
bombilla.setIcon(bombillaOFF);
}
switchTemu.addValueChangeListener(this);
}
});
Update
In my example github achievement, change the state of all switches to all UI, but I still do not know how to get the state of the switches
I made a couple of changes to your sources (still basic, but it gets you started):
only 1 common shared state
switch value change listeners now just trigger a state changed event
state changed listeners now update the UI elements when triggered
upon registration, a state changed listeners is informed (triggered) about the current state
The main idea is to have just a single shared state and any change is communicated to all the listeners (including the one where the change originated).
Below you can find the code: (P.S. I did not recompile my widgetset so the nice switch icon falls back to the default check box style)
1) SwitchState - represents the state of the switch shared between all the app instances
public enum SwitchState {
ON(true, new ThemeResource("img/on.png")), OFF(false, new ThemeResource("img/off.png"));
private final boolean value;
private final ThemeResource icon;
SwitchState(boolean value, ThemeResource icon) {
this.value = value;
this.icon = icon;
}
public boolean getValue() {
return value;
}
public ThemeResource getIcon() {
return icon;
}
public static SwitchState from(boolean value) {
return value ? ON : OFF;
}
}
2) ScrumBoard common state and listeners manager
public class ScrumBoard {
// list of listeners
private static List<SwitchChangeListener> LISTENERS = new ArrayList<>();
// initial state
private static SwitchState STATE = SwitchState.OFF;
// state change listener contract
public interface SwitchChangeListener {
void handleStateChange(SwitchState state);
}
// handle a a state change request
public static synchronized void updateState(boolean value) {
STATE = SwitchState.from(value);
fireChangeEvent(STATE);
}
// register a new state listener
public static synchronized void addSwitchChangeListener(SwitchChangeListener listener) {
System.out.println("Added listener for " + listener);
LISTENERS.add(listener);
// when a new listener is registered, also inform it of the current state
listener.handleStateChange(STATE);
}
// remove a state listener
public static synchronized void removeSwitchListener(SwitchChangeListener listener) {
LISTENERS.remove(listener);
}
// fire a change event to all registered listeners
private static void fireChangeEvent(SwitchState state) {
for (SwitchChangeListener listener : LISTENERS) {
listener.handleStateChange(state);
}
}
}
3) ScrumBoardLayout - UI layout and components
public class ScrumBoardLayout extends VerticalLayout implements ScrumBoard.SwitchChangeListener {
private Label icon = new Label();
private Switch mySwitch = new Switch();
public ScrumBoardLayout() {
setMargin(true);
setSpacing(true);
addHeader();
// listen for state changes
ScrumBoard.addSwitchChangeListener(this);
}
private void addHeader() {
mySwitch.setImmediate(true);
icon.setSizeUndefined();
// notify of state change
mySwitch.addValueChangeListener((Property.ValueChangeListener) event -> ScrumBoard.updateState((Boolean) event.getProperty().getValue()));
VerticalLayout layout = new VerticalLayout();
layout.setHeight("78%");
layout.addComponents(icon, mySwitch);
layout.setComponentAlignment(icon, Alignment.BOTTOM_CENTER);
layout.setComponentAlignment(mySwitch, Alignment.BOTTOM_CENTER);
layout.setExpandRatio(mySwitch, 1);
addComponents(layout);
}
#Override
public void handleStateChange(SwitchState state) {
// update UI on state change
UI.getCurrent().access(() -> {
mySwitch.setValue(state.getValue());
icon.setIcon(state.getIcon());
Notification.show(state.name(), Type.ASSISTIVE_NOTIFICATION);
});
}
#Override
public void detach() {
super.detach();
ScrumBoard.removeSwitchListener(this);
}
}
4) Result
I could see that with the ThemeResource () class, changing the bulb to its ON / OFF effect is strange, but I solve it as follows
.bombillo-on {
#include valo-animate-in-fade($duration: 1s);
width: 181px;
height: 216px;
background: url(img/on.png) no-repeat;
}
.bombillo-off {
#include valo-animate-in-fade($duration: 1s);
width: 181px;
height: 216px;
background: url(img/off.png) no-repeat;
}
public enum Sstate {
ON(true,"bombillo-on"),
OFF(false,"bombillo-off");
private boolean value;
private String style;
Sstate(boolean value, String style) {
this.value = value;
this.style = style;
}
public boolean getValue() { return value;}
public String getStyle() { return style;}
public static Sstate from(boolean value) { return value ? ON:OFF;}
}
And the handleChangeEvent It stays like this
#Override
public void handleChangeEvent(Sstate state) {
ui.access(() -> {
bombilla.setStyleName(state.getStyle());
s.setValue(state.getValue());
System.out.println(state+" values "+s);
});
}
UPDATE:
I notice an issue, that when I add a new view, or change using the buttonMenuToggle, it loses the synchronization, and update the bulb quite strange, clear with the themeResource does not happen that.
Solution:
to avoid UiDetachedException when using the Navigator try this, It works very well
#Override
public void handleChangeEvent(Sstate state) {
if(!ui.isAttached()) {
BroadcastesSwitch.removeListener(this);
return;
}
ui.access(() -> {
bombilla.setStyleName(state.getStyle());
s.setValue(state.getValue());
System.out.println(state+" values "+s);
});
}

Creating a custom tree with javafx

Basically, I wanted to know if I could create a tree and custom it on javaFX...
I tried to do it, but couldn't do anything so far with this code...
public class Main{
......
public Main() throws Exception{
......
// TreeView created
TreeView tv = (TreeView) fxmlLoader.getNamespace().get("treeview");
TreeItem<String> rootItem = new TreeItem<String>("liss");
rootItem.setExpanded(true);
tv.setRoot(rootItem);
/*for (int i = 1; i < 6; i++) {
TreeItem<String> item = new TreeItem<String> ("Message" + i);
rootItem.getChildren().add(item);
}
TreeItem<String> item = new TreeItem<String> ("MessageWoot");
rootItem.getChildren().add(item);
*/
//tv.setEditable(true);
tv.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>() {
#Override
public TreeCell<String> call(TreeView<String> arg0) {
// custom tree cell that defines a context menu for the root tree item
return new MyTreeCell();
}
});
stage.show();
}
//
private static class MyTreeCell extends TextFieldTreeCell<String> {
private ContextMenu addMenu = new ContextMenu();
public boolean clickedFirstTime = false;
public MyTreeCell() {
// instantiate the root context menu
MenuItem addMenuItem = new MenuItem("Expand");
addMenu.getItems().add(addMenuItem);
addMenuItem.setOnAction(new EventHandler() {
public void handle(Event t) {
TreeItem n0 =
new TreeItem<String>("'program'");
TreeItem n1 =
new TreeItem<String>("<identifier>");
TreeItem n2 =
new TreeItem<String>("body");
getTreeItem().getChildren().add(n0);
getTreeItem().getChildren().add(n1);
getTreeItem().getChildren().add(n2);
}
});
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// if the item is not empty and is a root...
//if (!empty && getTreeItem().getParent() == null && this.clickedFirstTime) {
System.out.println("UPDATEITEM -> clickedFirstTime : "+this.clickedFirstTime);
if (!this.clickedFirstTime) {
System.out.println("WOOT");
setContextMenu(addMenu);
this.clickedFirstTime = true;
}
}
}
And I'm questioning myself if this is the right "technology" which will solve what I'm trying to do...
What's my objective in this?
Firstly, I'm looking to add or delete a treeItem. I must say that a certain treeItem may be added only once or any N times, like a restriction (for example: treeItem < 6 for a certain level scope and a certain path of the root of tree view).
Secondly, make some treeItem editable and others not editable! When it is Editable, you may pop up something for the user in order to insert some input for example!
Is it possible ?
I saw the tutorial from https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm#BABJGGGF but I'm really confused with this tutorial ... I don't really understand the cell factory mechanism... The fact that he does apply to TreeView when i want only a certain TreeItem... Or how could I control that effect/behaviour ?
I mean, I'm really really lost with TreeView. Probably, TreeView isn't what I'm looking for ...
P.S.: I know that I cannot apply any visual effect or add menus to a tree items and that i use a cell factory mechanism to overcome this obstacle. Just I don't understand the idea and how could I do it !
Sure this is the right "technology", if you want to use JavaFX. You should probably use a more complex type parameter for TreeItem however. You can use your a custom TreeCell to allow the desired user interaction.
This example allows adding children and removing nodes via context menu (unless the content is "nocontext") as well as editing the content (as long as the content is not "noedit"); on the root node the delete option is disabled:
tv.setEditable(true);
tv.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>() {
private final MyContextMenu contextMenu = new MyContextMenu();
private final StringConverter converter = new DefaultStringConverter();
#Override
public TreeCell<String> call(TreeView<String> param) {
return new CustomTreeCell(contextMenu, converter);
}
});
public class CustomTreeCell extends TextFieldTreeCell<String> {
private final MyContextMenu contextMenu;
public CustomTreeCell(MyContextMenu contextMenu, StringConverter<String> converter) {
super(converter);
if (contextMenu == null) {
throw new NullPointerException();
}
this.contextMenu = contextMenu;
this.setOnContextMenuRequested(evt -> {
prepareContextMenu(getTreeItem());
evt.consume();
});
}
private void prepareContextMenu(TreeItem<String> item) {
MenuItem delete = contextMenu.getDelete();
boolean root = item.getParent() == null;
if (!root) {
delete.setOnAction(evt -> {
item.getParent().getChildren().remove(item);
contextMenu.freeActionListeners();
});
}
delete.setDisable(root);
contextMenu.getAdd().setOnAction(evt -> {
item.getChildren().add(new TreeItem<>("new item"));
contextMenu.freeActionListeners();
});
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContextMenu("nocontext".equals(item) ? null : contextMenu.getContextMenu());
setEditable(!"noedit".equals(item));
}
}
}
public class MyContextMenu {
private final ContextMenu contextMenu;
private final MenuItem add;
private final MenuItem delete;
public MyContextMenu() {
this.add = new MenuItem("add child");
this.delete = new MenuItem("delete");
this.contextMenu = new ContextMenu(add, delete);
}
public ContextMenu getContextMenu() {
return contextMenu;
}
public MenuItem getAdd() {
return add;
}
public MenuItem getDelete() {
return delete;
}
/**
* This method prevents memory leak by setting all actionListeners to null.
*/
public void freeActionListeners() {
this.add.setOnAction(null);
this.delete.setOnAction(null);
}
}
Of course more complex checks can be done in the updateItem and prepareContextMenu and different user interactions can be supported (TextFieldTreeCell may not be the appropriate superclass for you; You could use a "normal" TreeCell and show a different stage/dialog to edit the item when the user selects a MenuItem in the context menu).
Some clarification about cell factories
Cell factories are used to create the cells in a class that displays data (e.g. TableColumn, TreeView, ListView). When such a class needs to display content, it uses it's cell factory to create the Cells that it uses to display the data. The content displayed in such a cell may be changed (see updateItem method).
Example
(I'm not 100% sure this is exactly the way it's done, but it should be sufficiently close)
A TreeView is created to display a expanded root node with 2 non expanded children.
The TreeView determines that it needs to display 3 items for the root node and it's 2 children. The TreeView therefore uses it's cell factory to creates 3 cells and adds them to it's layout and assigns the displayed items.
Now the user expands the first child, which has 2 children of it's own. The TreeView determines that it needs 2 more cells to display the items. The new cells are added at the end of the layout for efficiency and the items of the cells are updated:
The cell that previously contained the last child is updated and now contains the first child of the first item.
The 2 newly added cells are updated to contain the second child of the first child and the second child of the root respecitvely.
So I decided to eliminate TreeView (because the documentation is so trash...) and, instead, I decided to implement a Webview !
Why ?
Like that, I could create an HTML document and use jstree (jquery plugin - https://www.jstree.com ) in there. It is a plugin which will create the treeview basically.
And the documentation is ten time better than treeview oracle documentation unfortunately.
Also, the possibility in creating/editing the tree with jstree is better.
Which concludes me that it was the best solution that I could figure out for me.
Also, whoever who will read me, I did a bridge with the webview and my javafx application ! It is a connection between my HTML document and the java application (Read more here - https://blogs.oracle.com/javafx/entry/communicating_between_javascript_and_javafx).
Hope It will help more people.

JavaBeanProperty: jdk7 vs jdk 8 - WeakOnTheFeet vs StrongInTheArm

A long standing issue (some call it - arguably - feature :) is the weakness of all listeners installed by all fx-bindings. As a consequence, we can't build "chains" of properties without keeping a strong reference to each link of the chain.
A particular type of such a chain link is a JavaBeanProperty: its purpose is to adapt a javabean property to a fx-property. Typically, nobody is interested in the adapter as such, so its usage would do something like
private Parent createContentBean() {
...
// local ref only
Property property = createJavaBeanProperty();
Bindings.bindBidirectional(label.textProperty(), property, NumberFormat.getInstance());
.. wondering why the label isn't updated. Changing property to a strong reference will work as expected (leaving me puzzeld as to who is responsible to feed the dummy, but that's another question):
Property property;
private Parent createContentBean() {
...
// instantiate the field
property = createJavaBeanProperty();
Bindings.bindBidirectional(label.textProperty(), property, NumberFormat.getInstance());
Long intro, but nearly there: jdk8 somehow changed the implementation so that the first approach is now working, there's no longer any need to keep a strong reference to a JavaBeanProperty. On the other hand, custom implementations of "chain links" still need a strong reference.
Questions:
is the change of behaviour intentional and if so, why?
how is it achieved? The code looks very similar ... and I would love to try something similar in custom adapters
A complete example to play with:
public class BeanAdapterExample extends Application {
private Counter counter;
public BeanAdapterExample() {
this.counter = new Counter();
}
Property property;
private Parent createContentBean() {
VBox content = new VBox();
Label label = new Label();
// strong ref
property = createJavaBeanProperty();
// local property
Property property = createJavaBeanProperty();
Bindings.bindBidirectional(label.textProperty(), property, NumberFormat.getInstance());
Slider slider = new Slider();
slider.valueProperty().bindBidirectional(property);
Button button = new Button("increase");
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent paramT) {
counter.increase();
}
});
content.getChildren().add(label);
content.getChildren().add(slider);
content.getChildren().add(button);
return content;
}
protected JavaBeanDoubleProperty createJavaBeanProperty(){
try {
return JavaBeanDoublePropertyBuilder.create()
.bean(counter).name("count").build();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
#Override
public void start(Stage stage) throws Exception {
Scene scene = new Scene(createContentBean());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
public static class Counter {
private double count;
public Counter() {
this(0);
}
public Counter(double count) {
this.count = count;
}
/**
* Increases the counter by 1.
*/
public void increase() {
setCount(getCount()+ 1.);
}
/**
* #return the count
*/
public double getCount() {
return count;
}
/**
* #param count the count to set
*/
public void setCount(double count) {
double old = getCount();
this.count = count;
firePropertyChange("count", old, getCount());
}
PropertyChangeSupport support = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener l) {
support.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
support.removePropertyChangeListener(l);
}
protected void firePropertyChange(String name, Object oldValue,
Object newValue) {
support.firePropertyChange(name, oldValue, newValue);
}
}
}
BTW: added the Swing tag because adapting core beans will be a frequent task in migration
Reminds me on an issue I've stumbled across last year - a binding does not create a strong reference so the property will be garbage collected if the property is a method local field.
Gropingly trying to answer part of my own answer:
tentative guess: it's not intentional. Looks like now the JavaBeanProperty is never garbage collected, which couldn't have been the requirement.
the only difference I could find is a phantomReference (Cleaner in the snippet) to the property, created in its constructor: that seems to keep it strong enough to never (?) be released. If I mimic that in custom properties, they "work" in a chain but are not garbage collected as well. Not an option, IMO.
The jdk8 constructor of the property:
JavaBeanDoubleProperty(PropertyDescriptor descriptor, Object bean) {
this.descriptor = descriptor;
this.listener = descriptor.new Listener<Number>(bean, this);
descriptor.addListener(listener);
Cleaner.create(this, new Runnable() {
#Override
public void run() {
JavaBeanDoubleProperty.this.descriptor.removeListener(listener);
}
});
}
The other way round: if I add such a reference to an arbitrary custom property, then it's stuck in memory just the same way as the javabeanProperty:
protected SimpleDoubleProperty createPhantomedProperty(final boolean phantomed) {
SimpleDoubleProperty adapter = new SimpleDoubleProperty(){
{
// prevents the property from being garbage collected
// must be done here in the constructor
// otherwise reclaimed immediately
if (phantomed) {
Cleaner.create(this, new Runnable() {
#Override
public void run() {
// empty, could do what here?
LOG.info("runnable in cleaner");
}
});
}
}
};
return adapter;
}
To reproduce the non-collection, add the code snippet below to my example code in the question, run in jdk7/8 and monitor with your favourite tool (used VisualVM): while running, click the "create" to create 100k of free-flying JavaBeanProperties. In jdk7, they never even show up in the memory sampler. In jdk8, they are created (sloooowly! so you might reduce the number) and build up. Forced garbage collection has no effect, even after nulling the underlying bean they are bound to.
Button create100K = new Button("create 100k properties");
create100K.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent paramT) {
Property propertyFX;
/// can't measure any effect
for (int i = 0; i < 100000; i++) {
propertyFX = createCountProperty();
}
LOG.info("created 100k adapters");
}
});
Button releaseCounter = new Button("release counter");
releaseCounter.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent paramT) {
counter = null;
}
});
Just FYI: created an issue for the potential memory leak - which is already marked as fixed, that was quick! Unfortunately, the fix-version is 8u20, not sure what to do until then. The only thingy coming to my mind is to c&p all JavaBeanXXProperty/Builders and add the fix. At the price of heavy warnings and unavailability in security-restricted environments. Also, we are back to the jdk7 behaviour (would have been too lucky, eating the cake and still have it :-)

Categories

Resources