在ComboBox中为FilteredList设置谓词会影响输入

时间:2016-02-05 07:54:18

标签: java javafx filter combobox predicate

我已实施ComboBox,其列表会根据ComboBox TextField中的输入进行过滤。它可以正常工作,因为你可能期望这种控件的过滤器工作。列表中显示以输入文本开头的每个项目。

我只有一个小问题。如果我从列表中选择一个项目,然后尝试删除文本字段中的最后一个字符,则没有任何反应。如果我从列表中选择一个项目,然后尝试删除除最后一个之外的任何其他字符,则会删除整个字符串。只有这是我在ComboBox中做的第一件事,才会出现这两个问题。如果我先在组合框中写一些东西,或者我第二次选择一个项目,则不会出现任何问题。

对我来说真正奇怪的是,这些问题似乎是由谓词设置引起的(如果我注释掉setPredicate的调用,一切正常)。这很奇怪,因为我认为这应该只影响为谓词设置的列表。它不应该影响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;
                    }
                });
            }
        }
    }
}

编辑:我试图覆盖关键听众,迫切想要解决这个问题:

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();
});

可悲的是,这也没有解决问题。如果我选择&#34;三是数字&#34;项目,然后尝试删除最后一个&#34; e&#34;在&#34; Three&#34;中,这是text属性将在以下之间切换的值:

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

因此它首先删除了正确的字符,但由于某种原因它删除了整个String。如前所述,这只是因为谓词已经设置,而且只有在我第一次选择项目后进行第一次输入时才会发生。

2 个答案:

答案 0 :(得分:2)

了Jonatan,

正如Manuel所说,一个问题是setPredicate()会在你更改组合框模型时触发你的changed()方法两次,但真正的问题是组合框会用适合的任何值覆盖编辑器值。以下是对症状的解释:

  

如果我从列表中选择一个项目,然后尝试删除最后一个项目   文本字段中的字符,没有任何反应。

在这种情况下,删除最后一个char实际上正在发生但是第一次调用setPredicate()匹配一个可能的项目(与删除最后一个char的项目完全相同)并将组合框内容更改为仅一个项目。这会导致调用,其中组合框使用当前的combobox.getValue()字符串恢复编辑器值,从而产生没有任何反应的错觉。它还会导致第二次调用您的changed()方法,但此时编辑器文本已经更改。

  

为什么这只会在第一次发生,但之后再也不会发生?

好问题!这只发生一次,因为您正在修改组合框的整个基础模型一次(如前所述,触发对changed()方法的第二次调用)。

所以在上一个场景发生后,如果你点击下拉按钮(右箭头),你会看到你只剩下一个项目,如果你再次尝试删除一个角色,你仍然会有相同的项目,也就是说,模型(组合框的内容)没有改变,因为setPredicate()仍将匹配相同的内容,因此不会在TextInputControl类中引起markInvalid()调用,因为内容实际上没有改变,这意味着没有恢复再次显示项目字符串(如果要查看文本字段实际还原的位置,请第一次使用JavaFX源查看ComboBoxPopupControl.updateDisplayNode()方法)。

  

如果我从列表中选择一个项目,然后尝试删除任何其他项目   字符比最后一个,整个字符串被删除。

在你的第二个场景中,没有任何东西与第一个setPredicate()调用相匹配(没有匹配你的startsWith条件的项目),这会删​​除组合框中删除当前选择和编辑器字符串的所有项目。

提示:尝试并自己理解这一点,在切换的()方法内部切换一个断点,看看它输入的次数和原因(如果你想要遵循ComboBox及其组件行为,则需要JavaFX源代码)

<强>解决方案: 如果你想继续使用你的ChangeListener,你可以通过在过滤后在编辑器中恢复文本来攻击你的主要问题(这是在setPredicate调用之后被替换的编辑器内容):

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
    }
}

我以前在使用KeyEvent处理程序之前已经完成了这个(为了避免在changed()事件中多次调用我的代码),但是你总是可以使用java.util.concurrent中的Semaphore或你喜欢的类如果您觉得自己开始需要,可以避免任何不必要的重新进入您的方法。现在,getEditor()。setText()将始终尾部恢复正确的值,即使相同的方法向下冒泡两到三次。

希望这有帮助!

答案 1 :(得分:1)

设置谓词将触发ChangeListener,因为您正在更改ComboBox-Items,因此更改了cb-editor的文本值。删除侦听器并重新添加它将阻止这些意外操作。

我为你的改变添加了三行(...) - 方法。 试试看,如果这是你问题的解决方法。

信息:我只使用了您的第一个代码块

@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
}