Thursday, January 25, 2018

Stop ListView selected item from changing until button pressed

Leave a Comment

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.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment