How to reset ComboBox and display PromptText? - java

Note: I am expanding on duplicate question here because it does not include a MCVE. The few other similar questions I've found also do not include working answers.
I am unable to find a way to have a ComboBox display the PromptText after clearing the selection.
Here is the MCVE:
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
final VBox root = new VBox(10);
root.setAlignment(Pos.TOP_CENTER);
root.setPadding(new Insets(10));
final ComboBox<String> cboSelection = new ComboBox<>();
final Button btnClear = new Button("Clear");
// Set ComboBox selections
final ObservableList<String> subjectsList = FXCollections.observableArrayList();
subjectsList.addAll("Software", "Math", "Physics");
// Setup the Subject selection
cboSelection.setPromptText("Select Subject");
cboSelection.setItems(subjectsList);
// Set action for "Clear" button
btnClear.setOnAction(e -> {
cboSelection.setValue(null);
});
root.getChildren().addAll(cboSelection, btnClear);
primaryStage.setTitle("ComboBox Demo");
primaryStage.setScene(new Scene(root, 200, 100));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Clicking the "Clear" button will set the selected value to null and clear the selection of the ComboBox, but the prompt text does not show again. This does not seem like the normal expected behavior.
I have tried clearSelection() as well as setPromptText() within the button's onAction and nothing seems to work to get the prompt text back.

According to the documentation, the prompt text should not actually be displayed at all here:
Prompt text is not displayed in all circumstances, it is dependent upon the subclasses of ComboBoxBase to clarify when promptText will be shown. For example, in most cases prompt text will never be shown when a combo box is non-editable (that is, prompt text is only shown when user input is allowed via text input).
If you want to see some prompt text when the selection is null (and you do not have an editable combo box), use a custom buttonCell on the combo box:
cboSelection.setPromptText("Select Subject");
cboSelection.setButtonCell(new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty) ;
if (empty || item == null) {
setText("Select Subject");
} else {
setText(item);
}
}
});
Note that it seems you also need to set the prompt text, as in the code in the question, in order to get the text to appear initially. I assume this is because of the same bug (I'm guessing that the library code is incorrectly setting the text of the button cell to the prompt text initially; if the prompt text is not set, the text gets set to null, apparently after the button cell's update method is invoked).
And you can obviously make this reusable by creating a named subclass of ListCell:
public class PromptButtonCell<T> extends ListCell<T> {
private final StringProperty promptText = new SimpleStringProperty();
public PromptButtonCell(String promptText) {
this.promptText.addListener((obs, oldText, newText) -> {
if (isEmpty() || getItem() == null) {
setText(newText);
}
});
setPromptText(promptText);
}
public StringProperty promptTextProperty() {
return promptText ;
}
public final String getPromptText() {
return promptTextProperty().get();
}
public final void setPromptText(String promptText) {
promptTextProperty().set(promptText);
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(getPromptText());
} else {
setText(item);
}
}
}
and then just
cboSelection.setButtonCell("Select Subject");
cboSelection.setButtonCell(new PromptButtonCell<>("Select Subject"));

Related

Adding a Button to a column in TableView JavaFX

So, I am trying to add a button to a column using Table View in JavaFX. I have successfully created a single button for one column; using the same code to add another button on another column with a small change of variables is resulting me in one error which I am unable to fix. The error is that it is not allowing me to use the word super. Below is the code in which I am having the error on;
TableColumn<UserDetails, UserDetails> addColumn = column("Add", ReadOnlyObjectWrapper<UserDetails>::new, 50);
addColumn.setCellFactory(col -> {
Button addButton = new Button("Add");
TableCell<UserDetails, UserDetails> addCell = new TableCell<UserDetails, UserDetails>() {
public void addItems(UserDetails userDetails, boolean empty) {
super.addItems(userDetails, empty); //This line is the error (super)
if (empty) {
setGraphic(null);
} else {
setGraphic(addButton);
}
}
};
addButton.setOnAction(event -> add(addCell.getItem(), primaryStage));
return addCell;
});
what am I doing wrong?
As you can see in the TableCell javadoc there is no addItems method in TableCell. You probably wanted to use the updateItem method:
#Override
protected void updateItem(UserDetails userDetails, boolean empty) {
super.updateItem(userDetails, empty);
...

JavaFX MenuItem does not react on MouseEvent.CLICKED

I am writting a little desktop application with a TreeView according to the Oracle-Example from here: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm.
From a MenuItem action of a ContextMenu, I would like to fire an event which shall create a new TreeItem below the item where I opened the ContextMenu from.
For MenuItem, it is possible to use the setOnAction(EventHandler<ActionEvent> event) method, but I only want to fire the action from a left mouse-click.
First, it is not possible to add an EventHandler to a MenuItem although it provides the method addEventHandler(EventType type, EventHandler<EventType> handler) with the event-type MouseEvent.ANY (or anything else). The handle-method of the event-handler is not called.
Second, i can use a workarround by adding a Label to a MenuItem by menuItem.setGraphic(label) and add an EventHandler to the label. This one works although MouseEvent.MOUSE_CLICKED is not called by an EventHandler's handle-method on a Label.
Is this "normal" behaviour? I understand that a label does not react on a click-event, but I do not understand why it is not possible to register a separate EventHandler or EventFilter on a MenuItem.
ContextMenu uses a MenuItemContainer, which is a
Container responsible for laying out a single row in the menu - in other
words, this contains and lays out a single MenuItem, regardless of it's
specific subtype.
Fur this purpose it seems to create new Nodes representing the MenuItem. So any EventHandlers added to the MenuItem will not be called.
To make it work as you intended, you can use a CustomMenuItem and add the according EventHandler to its content:
public class ContextMenuCell extends TreeCell<String> {
private ContextMenu menu;
public ContextMenuCell() {
Label lbl = new Label("Add item");
MenuItem menuItem = new CustomMenuItem(lbl);
lbl.setOnMouseClicked(evt -> {
if (evt.getButton() != MouseButton.PRIMARY) {
return;
}
TreeItem treeItem =
new TreeItem<String>("New item");
if (getTreeItem().isLeaf()) {
getTreeItem().getParent().getChildren().add(getIndex(), treeItem);
} else {
getTreeItem().getChildren().add(0, treeItem);
}
});
menu = new ContextMenu(menuItem);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
setGraphic(getTreeItem().getGraphic());
setContextMenu(menu);
}
}
}
Menu and MenuItem are not Nodes, so they will not handle mouse clicks since they are not displayed on the screen. A workaround is to set a graphics object (Node) to the MenuItem and add the listener to this Node. Works also for other menus like CheckMenuItem etc.:
public class RunJavaFX extends Application {
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
//the label will be our graphics object (Node)
Label l = new Label("Your Menu Text");
l.setTextFill(Color.BLACK); //set black since default CSS Style sets it to background color of the Menu
//add either over addEventFilter or addEventHandler
l.addEventFilter(MouseEvent.MOUSE_PRESSED, ev -> {
if (ev.getButton() == MouseButton.SECONDARY) {
System.out.println("RightClick: " + ev.getSource() + System.nanoTime());
} else {
System.out.println("Not Right Click: " + ev.getSource() + System.nanoTime());
}
ev.consume(); //optional
});
//create the MenuItem with an empty text and set the label l as graphics object
MenuItem mI = new MenuItem("", l);
//create the dummy menu and MenuBar for the example
Menu m = new Menu("Menu");
m.getItems().add(mI);
MenuBar mB = new MenuBar(m);
//create the dummy scene for the example
Scene scene = new Scene(mB);
primaryStage.setScene(scene);
primaryStage.show();
}
}

JavaFX setting Button style on Mouse Click

I've got a problem with a Java project I'm working on: I'm creating a grid of buttons via code in javafx on a pane. The buttons are all types of a subclass of the javafx Button class that i wrote.
Here's the header of the class:
private final String BASIC_STYLE = "-fx-font: 6 arial;";
private final String CLICKED_STYLE = "-fx-background-color: #0f0";
private int row;
private int col;
private String category;
private boolean selected = false;
Within the constructor i do the follwing:
this.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
toggleSelected();
}
});
Here's the toggleSelected() Method:
public void toggleSelected() {
this.selected = !selected;
this.setStyle(selected ? this.BASIC_STYLE : this.BASIC_STYLE+this.CLICKED_STYLE);
}
It's basically supposed to swap the style everytime you click the button. When i click the button, the button first gets selected by the OS (the border is becoming blue) and only after i click a second time on the exact same button it'll become green (the style that i'm giving it via setStyle).
However, the selected property becomes true on the first click and false on the second click, which means i click once on the button and it gets a blue border and selected = true, if i click on it a second time it becomes green and selected = false and if i click on it a third time it becomes normal again but selected will be true again.
I find it to be really strange that the first click on a button changes the "selected" variable correctly but not the style. Why is this happening and how can i avoid that i've to select the button first before i can click it?
You initialize
selected = false ;
and
setStyle(BASIC_STYLE);
But your event handler enforces the rule
selected == true -> setStyle(BASIC_STYLE);
selected == false -> setStyle(CLICKED_STYLE);
So your initial state is inconsistent with the state your handler enforces.
From the initial state, the first time you click, selected is set to true which causes setStyle(BASIC_STYLE) (which is the value it already has, so nothing changes). From then on, everything will switch as required.
You either need to change the initial state, or switch the logic of the setStyle(...) call in the handler.
public class ButtonEnterAction extends Button {
boolean selected = true;
public ButtonEnterAction(String connect) {
setText(connect);
action();
}
public ButtonEnterAction() {
action();
}
private void action() {
EventHandler<KeyEvent> enterEvent = (KeyEvent event) -> {
if (event.getCode() == KeyCode.ENTER) {
fire();
}
};
addEventFilter(KeyEvent.KEY_PRESSED, enterEvent);
// setOnMouseEntered(new EventHandler<MouseEvent>() {
// #Override
// public void handle(MouseEvent me) {
// SepiaTone st = new SepiaTone();
// setEffect(st);
// }
// });
// setOnMouseExited(new EventHandler<MouseEvent>() {
// #Override
// public void handle(MouseEvent me) {
// setEffect(null);
// }
// });
}
#Override
public void fire() {
super.fire(); //To change body of generated methods, choose Tools | Templates.
if (selected) {
SepiaTone st = new SepiaTone();
setEffect(st);
} else {
setEffect(null);
}
selected = !selected;
}
}
Create the Instant Class in ButtonEnterAction is like.
ButtonEnterAction bea = new ButtonEnterAction("TestButton");
bea.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("hello");
}
});

How to automatically wrap JavaFX 2 ListView?

How can I make an auto wrap ListView (multiline when the text is too long) in JavaFX 2? I know that if I put a \n to the string, it will be multiline, but the content is too dynamic.
Or is there a good way to put \n to the String after every xyz pixel length?
You can put a TextArea in the ListCell.graphicProperty(). This is usually used to set an icon in a list cell but can just as easy to set to any Node subclass.
Here is the exact code how I did it finally.
ListView<String> messages = new ListView<>();
messages.relocate(10, 210);
messages.setPrefSize(this.getPrefWidth() - 20, this.getPrefHeight() - 250);
messages.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> list) {
final ListCell cell = new ListCell() {
private Text text;
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (!isEmpty()) {
text = new Text(item.toString());
text.setWrappingWidth(messages.getPrefWidth());
setGraphic(text);
}
}
};
return cell;
}
});
There is no need to create additional controls such as TextArea or Text. It is enough to just setWrapText(true) of the ListCell and setPrefWidth(50.0) or so. It will be automatically rewrapped on resize.
Here is my working code in Kotlin:
datasets.setCellFactory()
{
object : ListCell<Dataset>()
{
init
{
isWrapText = true
prefWidth = 50.0
}
override fun updateItem(item: Dataset?, empty: Boolean)
{
super.updateItem(item, empty)
text = item?.toString()
}
}
}
By the way, this is how I made it to correctly wrap CamelCase words:
replace(Regex("(?<=\\p{javaLowerCase})(?=\\p{javaUpperCase})|(?<=[_.])"), "\u2028")
Here \u2028 is the Unicode soft line break character, which javaFX respects.

JavaFX 2 setting the caret position in the table view edit TextField after requesting focus

In JavaFX 2 I created a custom TableCell which overrides the startEdit() method to request the focus. So as soon as I invoke the edit command on the cell, the editing text field which appears gets focused.
The next step would be to set the caret position to the end of the text field. But for unknown reasons it doesn't seem to work. It is always placed before the 1st char.
Here is what I tried so far:
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
textField.end();
setGraphic(textField);
((TextField)getGraphic()).end();
textField.end();
Platform.runLater(
new Runnable() {
#Override
public void run() {
getGraphic().requestFocus();
}
});
}
}
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.end();
Platform.runLater(
new Runnable() {
#Override
public void run() {
getGraphic().requestFocus();
textField.end();
((TextField)getGraphic()).end();
}
});
}
}
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
textField.end();
setGraphic(textField);
((TextField)getGraphic()).end();
getGraphic().requestFocus();
((TextField)getGraphic()).end();
textField.end();
}
}
The logical approach would be to request focus on the text field, and then move the caret, but it doesn't seem to work for whichever reason.
Maybe someone can enlighten me?
I had the same problem, and after trying lots of different things, I finally found the solution.
Don't ask me why, but the problem seems to be caused by creating a new TextField for each edit. If you reuse the existing textfield, it works!
So try this:
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
if (textField == null)
createTextField();
else
textField.setText(getString());
setText(null);
setGraphic(textField);
Platform.runLater(
new Runnable() {
#Override
public void run() {
textField.requestFocus();
textField.deselect();
textField.end();
}
});
}
}
Neither of the above answers properly address the problem of cursor positioning in the custom table cell's TextField. If you have found that the above works for you then imho it is more a matter of luck, subject to the race condition of the control having been laid out prior to the cursor being positioned.
You need to modify the GUI component at the correct time in the spirit of the JavaFX framework. i.e. in the controls layoutChildren method. e.g. you need to override the layoutChildren method of the custom TableCell:
TextField textField = new TextField() {
private boolean first = true;
#Override protected void layoutChildren() {
super.layoutChildren();
// Set cursor caret at end of text (and clear highlighting)
if (first) {
this.end();
first = false;
}
}
};
I also note that Java 1.8.0_241 also contains this problem in the TextFieldTableCell implementation. Worse TextField is completely private to the TextFieldTableCell implementation, so in order to work around that I chose to copy the source of javax.scene.table.cell.TextFieldTableCell and javax.scene.table.cell.CellUtils. TextField is instantiated in CellUtils, so you can fix the cursor positioning there. e.g.
static <T> TextField createTextField(final Cell<T> cell, final StringConverter<T> converter) {
final TextField textField = new TextField(getItemText(cell, converter)) {
private boolean first = true;
#Override protected void layoutChildren() {
super.layoutChildren();
// Set cursor caret at end of text (and clear highlighting)
if (first) {
this.end();
first = false;
}
};
...
...
}

Categories

Resources