Setting a predicate for FilteredList in a ComboBox affects input

I have implemented a ComboBox where its list is filtered using input in a ComboBox TextField . It works as you would expect a filter for such a control to work. Each item in the list that starts with text input is displayed in the list.

I have only one small problem. If I select an item from the list and then try to delete the last character in the text box, nothing will happen. If I select an item from the list and then try to delete any other character than the last, the entire line will be deleted. Both of these problems only occur if this is the first thing I do in ComboBox . If I first write something in the combo box or if I select an item a second time, none of the problems described arise.

What is really strange to me is that these problems are apparently caused by setting the predicate (if I comment on the setPredicate call, everything works fine). This is strange since I think that this should only affect the list on which the predicate is given. It should not affect the rest of the ComboBox .

 import javafx.application.Application; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.scene.Scene; import javafx.scene.control.ComboBox; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.StringConverter; public class TestInputFilter extends Application { public void start(Stage stage) { VBox root = new VBox(); ComboBox<ComboBoxItem> cb = new ComboBox<ComboBoxItem>(); cb.setEditable(true); cb.setConverter(new StringConverter<ComboBoxItem>() { @Override // To convert the ComboBoxItem to a String we just call its // toString() method. public String toString(ComboBoxItem object) { return object == null ? null : object.toString(); } @Override // To convert the String to a ComboBoxItem we loop through all of // the items in the combobox dropdown and select anyone that starts // with the String. If we don't find a match we create our own // ComboBoxItem. public ComboBoxItem fromString(String string) { return cb.getItems().stream().filter(item -> item.getText().startsWith(string)).findFirst() .orElse(new ComboBoxItem(string)); } }); ObservableList<ComboBoxItem> options = FXCollections.observableArrayList(new ComboBoxItem("One is a number"), new ComboBoxItem("Two is a number"), new ComboBoxItem("Three is a number"), new ComboBoxItem("Four is a number"), new ComboBoxItem("Five is a number"), new ComboBoxItem("Six is a number"), new ComboBoxItem("Seven is a number")); FilteredList<ComboBoxItem> filteredOptions = new FilteredList<ComboBoxItem>(options, p -> true); cb.setItems(filteredOptions); InputFilter inputFilter = new InputFilter(cb, filteredOptions); cb.getEditor().textProperty().addListener(inputFilter); root.getChildren().add(cb); stage.setScene(new Scene(root)); stage.show(); } public static void main(String[] args) { launch(); } class ComboBoxItem { private String text; public ComboBoxItem(String text) { this.text = text; } public String getText() { return text; } @Override public String toString() { return text; } } class InputFilter implements ChangeListener<String> { private ComboBox<ComboBoxItem> box; private FilteredList<ComboBoxItem> items; public InputFilter(ComboBox<ComboBoxItem> box, FilteredList<ComboBoxItem> items) { this.box = box; this.items = items; } @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { String value = newValue; // If any item is selected we get the first word of that item. String selected = box.getSelectionModel().getSelectedItem() != null ? box.getSelectionModel().getSelectedItem().getText() : null; // If an item is selected and the value of in the editor is the same // as the selected item we don't filter the list. if (selected != null && value.equals(selected)) { items.setPredicate(item -> { return true; }); } else { items.setPredicate(item -> { if (item.getText().toUpperCase().startsWith(value.toUpperCase())) { return true; } else { return false; } }); } } } } 

Edit: I tried to override the key listeners in a desperate attempt to solve the problem:

 cb.getEditor().addEventFilter(KeyEvent.KEY_PRESSED, e -> { TextField editor = cb.getEditor(); int caretPos = cb.getEditor().getCaretPosition(); StringBuilder text = new StringBuilder(cb.getEditor().getText()); // If BACKSPACE is pressed we remove the character at the index // before the caret position. if (e.getCode().equals(KeyCode.BACK_SPACE)) { // BACKSPACE should only remove a character if the caret // position isn't zero. if (caretPos > 0) { text.deleteCharAt(--caretPos); } e.consume(); } // If DELETE is pressed we remove the character at the caret // position. else if (e.getCode().equals(KeyCode.DELETE)) { // DELETE should only remove a character if the caret isn't // positioned after that last character in the text. if (caretPos < text.length()) { text.deleteCharAt(caretPos); } } // If LEFT key is pressed we move the caret one step to the left. else if (e.getCode().equals(KeyCode.LEFT)) { caretPos--; } // If RIGHT key is pressed we move the caret one step to the right. else if (e.getCode().equals(KeyCode.RIGHT)) { caretPos++; } // Otherwise we just add the key text to the text. // TODO We are currently not handling UP/DOWN keys (should move // caret to the end/beginning of the text). // TODO We are currently not handling keys that doesn't represent // any symbol, like ALT. Since they don't have a text, they will // just move the caret one step to the right. In this case, that // caret should just hold its current position. else { text.insert(caretPos++, e.getText()); e.consume(); } final int finalPos = caretPos; // We set the editor text to the new text and finally we move the // caret to its new position. editor.setText(text.toString()); Platform.runLater(() -> editor.positionCaret(finalPos)); }); // We just consume KEY_RELEASED and KEY_TYPED since we don't want to // have duplicated input. cb.getEditor().addEventFilter(KeyEvent.KEY_RELEASED, e -> { e.consume(); }); cb.getEditor().addEventFilter(KeyEvent.KEY_TYPED, e -> { e.consume(); }); 

Unfortunately, this also does not fix the problem. If, for example, I select β€œThree is a number,” and then try to delete the last β€œe” in β€œThree,” these are the values ​​that the text property will switch between:

 TextProperty: Three is a number TextPropery: Thre is a number TextPropery: 

Therefore, it first deletes the correct character, but then for some reason deletes the integer String . As mentioned earlier, this only happens because the predicate has been set, and this only happens when I make the first input after I first select the item.

+5
source share
2 answers

Jonathan

As Manuel said, one of the problems is that setPredicate () will run your changed () method twice since you are changing the combobox model, however the real problem is that combobox overwrites the editor values ​​with any values. Here is an explanation of your symptoms:

If I select an item from the list and then try to delete the last character in the text box, nothing happens.

In this case, the removal of the last char actually occurs, however, the first call to setPredicate () corresponds to one possible element (exactly the same element that you deleted with the last char)), and changes the contents of the list to only one element. This causes a call when combobox restores the value of the editor with the current line combobox.getValue (), creating the illusion that nothing is happening. It also calls the second call to your changed () method, but at this point the editor text has already been changed.

Why does this happen only for the first time, but then never again?

Good question! This happens only once because you change the entire combobox base model once (which, as explained earlier, calls the second call to the changed () method).

So, after the previous scenario happens, if you click the drop-down button (right arrow), you will see that you have only one element left, and if you try to delete one symbol again, you will still have the same element, model (combobox content) has not changed because setPredicate () will still match the same content, so it does not call markInvalid () in the TextInputControl class, because the content has not actually changed, which means not restoring item string again (If you want to see where ekstovoe field actually restored for the first time, see. ComboBoxPopupControl.updateDisplayNode () method JavaFX sources).

If I select an item from the list and then try to delete others than the last, the entire row is deleted.

In your second scenario, nothing matches the first call to setPredicate () (NO elements according to the startWith condition), which removes all int elements in which you delete your current selection and editor line.

TIP. Try it and do it for yourself, switch the breakpoint inside the changed () method to find out how many times it is entered and why (the JavaFX source is needed if you want to monitor ComboBox and its component behavior)

Solution: If you want to continue to use your ChangeListener, you can simply attack the main problem (which is the replacement of the editor after calling setPredicate) by restoring the text in the editor after filtering:

 class InputFilter implements ChangeListener<String> { private ComboBox<ComboBoxItem> box; private FilteredList<ComboBoxItem> items; public InputFilter(ComboBox<ComboBoxItem> box, FilteredList<ComboBoxItem> items) { this.box = box; this.items = items; } @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { String value = newValue; // If any item is selected we get the first word of that item. String selected = box.getSelectionModel().getSelectedItem() != null ? box.getSelectionModel().getSelectedItem().getText() : null; // If an item is selected and the value of in the editor is the same // as the selected item we don't filter the list. if (selected != null && value.equals(selected)) { items.setPredicate(item -> { return true; }); } else { // This will most likely change the box editor contents items.setPredicate(item -> { if (item.getText().toUpperCase().startsWith(value.toUpperCase())) { return true; } else { return false; } }); // Restore the original search text since it was changed box.getEditor().setText(value); } //box.show(); // <-- Uncomment this line for a neat look } } 

Personally, I have done this before with KeyEvent handlers in the past (to avoid multiple calls to my code in the changed () event), however you can always use Semaphore or your favorite class from java.util.concurrent to avoid unwanted re-entry into your method if you feel that you will come in handy. Right now, getEditor (). SetText () will always return the correct value, even if the same method bubbles two or three times.

Hope this helps!

+2
source

Setting the predicate will call your ChangeListener because you are changing the ComboBox-Items and therefore the cb-editor text value. Removing the listener and re-adding will prevent these unexpected actions.

I added three lines to your change (...) - method. Try if this is the fix for your problem.

Info: I used only the first block of code

 @Override public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) { String value = newValue; // If any item is selected we get the first word of that item. String selected = box.getSelectionModel().getSelectedItem() != null ? box.getSelectionModel().getSelectedItem().getText() : null; box.getEditor().textProperty().removeListener(this); // new line #1 // If an item is selected and the value of in the editor is the same // as the selected item we don't filter the list. if (selected != null && value.equals(selected)) { items.setPredicate(item -> { return true; }); } else { items.setPredicate(item -> { if (item.getText().toUpperCase().startsWith(value.toUpperCase())) { return true; } else { return false; } }); box.getEditor().setText(newValue); // new line #2 } box.getEditor().textProperty().addListener(this); // new line #3 } 
+1
source

Source: https://habr.com/ru/post/1242352/


All Articles