/* * Copyright 2013 Digital Rapids Corporation. */ package vboxpopupwidth; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javafx.application.Application; import javafx.collections.ObservableList; import javafx.event.Event; import javafx.event.EventHandler; import javafx.geometry.Bounds; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.IndexRange; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import javafx.stage.Popup; import javafx.stage.Stage; import javafx.stage.Window; /** * * @author scott.palmer */ public class VBoxPopupWidth extends Application { private ListView choiceList; private List allAvailableChoices; private ObservableList choices = javafx.collections.FXCollections.observableArrayList(); private volatile Popup popup; private Label filterLabel; private String filterText = ""; private TextField tf; @Override public void start(Stage primaryStage) { allAvailableChoices = Arrays.asList("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"); tf = new TextField(); tf.setPrefWidth(400); tf.setPromptText("Type $ to insert from a list"); tf.addEventFilter(KeyEvent.KEY_PRESSED, keyPressed); tf.addEventFilter(KeyEvent.KEY_TYPED, keyTyped); filterLabel = new Label(filterText); filterLabel.setStyle("-fx-text-fill: white; -fx-background-color: grey; "); filterLabel.setMaxWidth(Double.MAX_VALUE); filterLabel.visibleProperty().bind(filterLabel.textProperty().isNotEqualTo("")); filterLabel.managedProperty().bind(filterLabel.visibleProperty()); BorderPane root = new BorderPane(); root.setCenter(new Label( "Type in the field below.\n" + "Typing a $ will bring up a popup.\n" + "continuing to type will filter the\n" + "available options in the list.\n" + "Soemtimes the VBox width shrinks to\n" + "the size of the label showing the\n" + "current filter instead of allowing\n" + "for the size of the list.\n\n" + "Try typing '$', 't' then backspace.\n\n" + "It all works fine in JavaFX 2.2")); root.setBottom(tf); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Popup Layout Width Problem"); primaryStage.setScene(scene); primaryStage.show(); } private void doPickFromList() { filterLabel.setText(filterText); applyFilterToAvailableChoices(); Popup choicePopup = new Popup(); choiceList = new ListView<>(choices); VBox vbox = new VBox(); vbox.getChildren().addAll(filterLabel, choiceList); choicePopup.getContent().add(vbox); choicePopup.setAutoHide(true); choicePopup.setHideOnEscape(false); choicePopup.setOnAutoHide(new EventHandler() { @Override public void handle(Event t) { popup = null; } }); choiceList.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { @Override public void handle(MouseEvent t) { // if primary button double-click with no modifiers if (t.getClickCount() == 2 && t.isStillSincePress() && t.getButton() == MouseButton.PRIMARY && !t.isAltDown() && !t.isControlDown() && !t.isShiftDown() && !t.isMetaDown() && choiceList.getSelectionModel().getSelectedItem() != null) { // These extra checks are so we don't respond to a double click // on the scrollbar or inc/dec arrows. // TODO: something less fragile (need a heavier solution like custom ListCell) if (t.getTarget() instanceof Node) { Node n = (Node) t.getTarget(); ObservableList styles = n.getStyleClass(); if (styles.contains("list-cell") || styles.contains("text")) { completeChoice(); t.consume(); } } } } }); double screenX = 0; double screenY = 0; Scene scene = tf.getScene(); Window w = scene.getWindow(); if (w != null) { screenX = w.getX(); screenY = w.getY(); } screenX += scene.getX(); screenY += scene.getY(); Bounds bounds = tf.localToScene(tf.getBoundsInLocal()); screenX += bounds.getMinX(); screenY += bounds.getMinY() + bounds.getHeight(); choicePopup.show(tf, screenX, screenY); popup = choicePopup; } private EventHandler keyPressed = new EventHandler() { @Override public void handle(KeyEvent t) { if (popup != null) { dealWithChoiceSelection(t); return; } // This one doesn't get called (with my keyboard that has $ on shift+4) if (t.getCode() == KeyCode.DOLLAR) { t.consume(); } else if ("$".equals(t.getText())) { // must use getText for KEY_PRESSED events. (getCharacter is always CHAR_UNDEFINED) //System.err.println("Consuming the $ pressed event"); t.consume(); } } }; private EventHandler keyTyped = new EventHandler() { @Override public void handle(KeyEvent t) { if (popup != null) { if (t.getCharacter().length() > 0) { char c = t.getCharacter().charAt(0); if (c == 27) { // Escape cancelChoiceInsertion(); } else { if (c == 32 || c == 13 || c == 10) { // line feed?? It happens in JFXPanel in a JOptionPane! completeChoice(); } else if (c == 8) { // backspace filterText = filterText.substring(0, Math.max(0, filterText.length() - 1)); filterLabel.setText(filterText); applyFilterToAvailableChoices(); } else if (Character.isLetterOrDigit(c) || c == '_') { String oldFilterText = filterText; filterText += c; boolean filterOk = applyFilterToAvailableChoices(); if (!filterOk) { // display beep? filterText = oldFilterText; } filterLabel.setText(filterText); } } } t.consume(); return; } // Must used getCharacter for KEY_TYPED events // (getText() is always empty, getCode() is always UNDEFINED) if ("$".equals(t.getCharacter())) { // insert a variable at caret position doPickFromList(); t.consume(); } } }; private boolean applyFilterToAvailableChoices() { List choicesThatMatch = new ArrayList<>(); String filter = filterText.toLowerCase(); for (String x : allAvailableChoices) { if (x.toLowerCase().startsWith(filter)) { choicesThatMatch.add(x); } } if (!choicesThatMatch.isEmpty()) { choices.setAll(choicesThatMatch); return true; } return false; } private void dealWithChoiceSelection(KeyEvent t) { if (t.getCode() == KeyCode.ESCAPE) { cancelChoiceInsertion(); } if (t.getCode() == KeyCode.ENTER) { completeChoice(); } t.consume(); } private void cancelChoiceInsertion() { if (popup != null) { popup.hide(); } popup = null; filterText = ""; } private void completeChoice() { popup.hide(); String var = choiceList.getSelectionModel().getSelectedItem(); if (var == null && choices.size() == 1) { var = choices.get(0); } popup = null; if (var != null) { insertChoice(var); } filterText = ""; } // called when a $ is typed private void insertChoice(String pick) { IndexRange selection = tf.getSelection(); String text = tf.getText(); if (text == null) { text = ""; } // if there is a selection - replace it int selStart = selection.getStart(); int selEnd = selection.getEnd(); // Selection start & end follow the caret postion when there is no // selection. We rely on this below. // pos will become selStart + length of inserted stuff String preText = text.substring(0, selStart); String postText = text.substring(selEnd, text.length()); String insertedText = pick; String newText = preText + insertedText + postText; int pos = preText.length() + insertedText.length(); tf.setText(newText); tf.positionCaret(pos); // This clears the selection as well } public static void main(String[] args) { launch(args); } }