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.
1 Answers
Answers 1
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.
0 comments:
Post a Comment