Reputation: 13859
So I am having a problem in an app I am trying to create. I have created a sample app to demonstrate the problem. In the app, I am trying to stop the selected item in the TableView
from changing if enter is not pressed on a TextField
. In my implementation, I am getting a StackOverFlow
error. I understand why I am getting the error. I am basically creating an infinite loop, but I can't think of another way to approach this problem.
If you remove this line of code:
if(!validateTextFields())
{
tvPerson.getSelectionModel().select(oldPerson);
return;
}
The app works like it's designed if you select a table row and then edit the text in the TextField
and press enter on the TextField
. Though, if you select a table row, edit the TextField
and don't press enter, the user can select a new table row without updating the table row he/she was trying to edit. So my questions, how do I stop the user from changing the selectedItem
if he/she hasn't confirmed the TextField
edit by pressing enter.
Controller
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
/**
*
* @author Sedrick
*/
public class FXMLDocumentController implements Initializable {
@FXML TextField tfFirstName, tfLastName;
@FXML TableView<Person> tvPerson;
@FXML TableColumn<Person, String> tcFirstName, tcLastName;
final String firstNames = "Darryl \n" +
"Enriqueta \n" +
"Katherine \n" +
"Harley \n" +
"Arlean \n" +
"Jacquelynn \n" +
"Yuko \n" +
"Dion \n" +
"Vivan \n" +
"Carly \n" +
"Eldon \n" +
"Joe \n" +
"Klara \n" +
"Shona \n" +
"Delores \n" +
"Sabra \n" +
"Vi \n" +
"Gearldine \n" +
"Laine \n" +
"Lila ";
final String lastNames = "Ollie \n" +
"Donnette \n" +
"Audra \n" +
"Angelica \n" +
"Janna \n" +
"Lekisha \n" +
"Michael \n" +
"Tomi \n" +
"Cheryl \n" +
"Roni \n" +
"Aurelio \n" +
"Mayola \n" +
"Kelsie \n" +
"Britteny \n" +
"Dannielle \n" +
"Kym \n" +
"Scotty \n" +
"Deloris \n" +
"Lavenia \n" +
"Sun \n";
@Override
public void initialize(URL url, ResourceBundle rb) {
// TODO
tcFirstName.setCellValueFactory(new PropertyValueFactory("firstName"));
tcLastName.setCellValueFactory(new PropertyValueFactory("lastName"));
tvPerson.setItems(FXCollections.observableArrayList(getPersons()));
tvPerson.getSelectionModel().selectedItemProperty().addListener((obs, oldPerson, newPerson)->{
if(!validateTextFields())
{
tvPerson.getSelectionModel().select(oldPerson);
return;
}
if(newPerson != null)
{
tfFirstName.setText(newPerson.getFirstName());
tfLastName.setText(newPerson.getLastName());
}
});
tfFirstName.setOnKeyReleased(keyEvent ->{
Person tempPerson = tvPerson.getSelectionModel().getSelectedItem();
if(!tfFirstName.getText().trim().equals(tempPerson.getFirstName().trim()))
{
tfFirstName.setStyle("-fx-control-inner-background: red;");
}
});
tfFirstName.setOnAction(actionEvent ->{
Person tempPerson = tvPerson.getSelectionModel().getSelectedItem();
tempPerson.setFirstName(tfFirstName.getText().trim());
tfFirstName.setStyle(null);
});
tfLastName.setOnKeyReleased(keyEvent ->{
Person tempPerson = tvPerson.getSelectionModel().getSelectedItem();
if(tfLastName.getText().trim().equals(tempPerson.getLastName().trim()))
{
tfLastName.setStyle("-fx-control-inner-background: red;");
}
});
tfLastName.setOnAction(actionEvent ->{
Person tempPerson = tvPerson.getSelectionModel().getSelectedItem();
tempPerson.setLastName(tfLastName.getText().trim());
tfLastName.setStyle(null);
});
}
private boolean validateTextFields()
{
if(!tfFirstName.getStyle().isEmpty()){return false;}
if(!tfLastName.getStyle().isEmpty()){return false;}
return true;
}
List<Person> getPersons()
{
List<Person> tempPerson = new ArrayList();
List<String> tempFirstName = Arrays.asList(firstNames.split("\n"));
List<String> tempLastName = Arrays.asList(lastNames.split("\n"));
for(int i = 0; i < tempFirstName.size(); i++)
{
tempPerson.add(new Person(tempFirstName.get(i).trim(), tempLastName.get(i).trim()));
}
return tempPerson;
}
}
FXML
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<AnchorPane id="AnchorPane" prefHeight="575.0" prefWidth="836.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="javafxapplication17.FXMLDocumentController">
<children>
<VBox layoutX="7.0" prefHeight="200.0" prefWidth="100.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<VBox prefHeight="200.0" prefWidth="100.0" spacing="5.0" VBox.vgrow="ALWAYS">
<children>
<HBox spacing="5.0">
<children>
<Label prefHeight="31.0" prefWidth="72.0" text="First Name" />
<TextField fx:id="tfFirstName" />
</children>
</HBox>
<HBox spacing="5.0">
<children>
<Label prefHeight="31.0" prefWidth="72.0" text="Last Name" />
<TextField fx:id="tfLastName" />
</children>
</HBox>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</VBox>
<TableView fx:id="tvPerson" prefHeight="200.0" prefWidth="200.0">
<columns>
<TableColumn fx:id="tcFirstName" prefWidth="108.0" text="First Name" />
<TableColumn fx:id="tcLastName" prefWidth="110.0" text="Last Name" />
</columns>
<VBox.margin>
<Insets />
</VBox.margin>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</padding>
</TableView>
</children>
</VBox>
</children>
</AnchorPane>
Person Class
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
*
* @author Sedrick
*/
public class Person {
StringProperty firstName = new SimpleStringProperty();
StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName)
{
this.firstName.set(firstName);
this.lastName.set(lastName);
}
public StringProperty firstNameProperty()
{
return firstName;
}
public String getFirstName()
{
return firstName.get();
}
public void setFirstName(String firstName)
{
this.firstName.set(firstName);
}
public StringProperty lastNameProperty()
{
return lastName;
}
public String getLastName()
{
return lastName.get();
}
public void setLastName(String firstName)
{
this.lastName.set(firstName);
}
}
Exception
Exception in thread "JavaFX Application Thread" java.lang.StackOverflowError
at javafx.collections.ListChangeBuilder.findSubChange(ListChangeBuilder.java:62)
at javafx.collections.ListChangeBuilder.insertAdd(ListChangeBuilder.java:127)
at javafx.collections.ListChangeBuilder.nextAdd(ListChangeBuilder.java:254)
at javafx.collections.ObservableListBase.nextAdd(ObservableListBase.java:179)
at javafx.collections.transformation.SortedList.setAllToMapping(SortedList.java:354)
at javafx.collections.transformation.SortedList.addRemove(SortedList.java:397)
at javafx.collections.transformation.SortedList.sourceChanged(SortedList.java:108)
at javafx.collections.transformation.TransformationList.lambda$getListener$23(TransformationList.java:106)
at javafx.collections.WeakListChangeListener.onChanged(WeakListChangeListener.java:88)
at com.sun.javafx.collections.ListListenerHelper$SingleChange.fireValueChangedEvent(ListListenerHelper.java:164)
at com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
at javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
at javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
at javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
at javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
at javafx.collections.ModifiableObservableListBase.add(ModifiableObservableListBase.java:155)
at java.util.AbstractList.add(AbstractList.java:108)
at com.sun.javafx.scene.control.SelectedCellsMap.add(SelectedCellsMap.java:118)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.select(TableView.java:2456)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.select(TableView.java:2427)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.select(TableView.java:2485)
at javafxapplication17.FXMLDocumentController.lambda$initialize$0(FXMLDocumentController.java:83)
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.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
at javafx.scene.control.MultipleSelectionModelBase.lambda$new$34(MultipleSelectionModelBase.java:67)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
at javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:72)
at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)
at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:113)
at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:147)
at javafx.scene.control.SelectionModel.setSelectedIndex(SelectionModel.java:68)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.updateSelectedIndex(TableView.java:2945)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.select(TableView.java:2458)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.select(TableView.java:2427)
at javafx.scene.control.TableView$TableViewArrayListSelectionModel.select(TableView.java:2485)
at javafxapplication17.FXMLDocumentController.lambda$initialize$0(FXMLDocumentController.java:83)
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.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
at javafx.scene.control.MultipleSelectionModelBase.lambda$new$34(MultipleSelectionModelBase.java:67)
at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
The exception is really long so I on posted a short version.
Upvotes: 2
Views: 1084
Reputation: 51525
Basically, the "correct" approach would be a custom selectionModel that has a property to be disabled and does nothing in that state. Unfortunately, selectionModels are not designed for being extended/replaced by custom classes. Plus the common ancestor for selection in tabular controls is MultipleSelectionModelBase which is both entirely hidden and extremely buggy. So the chances to make a custom model behave are ... not very good.
Nevertheless, it might be possible (and with investing enough enery and resources might even work reliably ;): implement a custom TableViewSelectionModel that delegates to the default implementation TableViewBitSelectionModel (the one it grabs from the TableView), keeps itself in synch with that and installs itself to the table.
Something like:
public static class VetoableSelection<T> extends TableViewSelectionModel<T> {
private boolean disabled;
private TableViewSelectionModel<T> delegate;
public VetoableSelection(TableView<T> table) {
super(table);
delegate = table.getSelectionModel();
table.setSelectionModel(this);
new VetoableFocusModel<>(table);
delegate.selectedIndexProperty().addListener(c -> indexInvalidated());
}
/**
* keep selectedIndex in sync
*/
private void indexInvalidated() {
setSelectedIndex(delegate.getSelectedIndex());
}
/**
* Does nothing if disabled.
*/
public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
public boolean isDisabled() {
return disabled;
}
/**
* Override all state changing methods to delegate
* if not disabled, do nothing if disabled.
* Here: row selection.
*/
@Override
public void clearAndSelect(int row) {
if (isDisabled()) return;
delegate.clearAndSelect(row);
}
@Override
public void select(int row) {
if (isDisabled()) return;
delegate.select(row);
}
/**
* Here: methods with columns
*/
@Override
public void clearAndSelect(int row, TableColumn<T, ?> column) {
if (isDisabled()) return;
delegate.clearAndSelect(row, column);
}
@Override
public void select(int row, TableColumn<T, ?> column) {
if (isDisabled()) return;
delegate.select(row, column);
}
...
A crude check with your example seems to be working, kind of: it does not allow the selection to change if the modified textFields are not committed. There are problems in not showing the cells in selected state and with dynamic add/remove persons and ... probably with a whole bunch of other contexts.
Upvotes: 4