I want to have an editable ComboBox that contains items of some type (e.g. integers), where I can add and delete items, allow the user to edit existing items, and allow for duplicate items.
The problem is that whenever the user edits an existing item, and changes its value to a value of an item already present in the list, the editor (textfield) causes the selection model to select the item already present in the list instead of modifying the edited item.
I tried circumventing this by creating a wrapper class that contains the item and has an unique index. However, this causes problems in StringConverter.fromString because I have to create a new wrapper every time it converts.
An easy solution I think would be to stop the editor from searching through the items whenever an edit is made, so that the selection model does not change.
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.Arrays;
public class ComboBoxTest extends Application {
private final ComboBox<Integer> comboBox = new ComboBox<>();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final Group root = new Group();
root.getChildren().add(comboBox);
final Scene scene = new Scene(root, 200, 200);
primaryStage.setScene(scene);
primaryStage.show();
comboBox.getItems().addAll(Arrays.asList(1, 2, 3, 4));
comboBox.setConverter(
new StringConverter<Integer>() {
#Override
public String toString(Integer integer) {
return integer == null ? "" : String.valueOf(integer);
}
#Override
public Integer fromString(String s) {
return Integer.parseInt(s);
}
});
comboBox.setPromptText("select value");
comboBox.setEditable(true);
}
}
Related
I want to ask if it is possible to make a chip in JFXChipView editable once it has been set.
You can create your own JFXChip and implement a behavior to enable editing. First, you need to have an editable label. I looked up online and I found this post: JavaFX custom control - editable label. Then, you can extend JFXChip to use that EditableLabel:
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXChip;
import com.jfoenix.controls.JFXChipView;
import com.jfoenix.svg.SVGGlyph;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.scene.layout.HBox;
public class EditableChip<T> extends JFXChip<Property<T>> {
protected final HBox root;
public EditableChip(JFXChipView<Property<T>> view, Property<T> item) {
super(view, item);
JFXButton closeButton = new JFXButton(null, new SVGGlyph());
closeButton.getStyleClass().add("close-button");
closeButton.setOnAction(event -> {
view.getChips().remove(item);
event.consume();
});
// Create the label with an initial value from the item
String initialValue = view.getConverter().toString(item);
EditableLabel label = new EditableLabel(initialValue);
label.setMaxWidth(100);
// Bind the item to the text in the label
item.bind(Bindings.createObjectBinding(() -> view.getConverter().fromString(label.getText()).getValue(), label.textProperty()));
root = new HBox(label, closeButton);
getChildren().setAll(root);
}
}
Note: I am using Property<T> instead of using the desired class T because JFXChipView stores the item the first time you add it. And in that case, you're going to get the values as you entered them the first time when calling JFXChipView#getChips().
Sample application:
import com.jfoenix.controls.JFXChipView;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class EditableChipViewApp extends Application {
#Override
public void start(Stage primaryStage) {
JFXChipView<Property<String>> chipView = new JFXChipView<>();
chipView.setChipFactory(EditableChip::new);
chipView.setConverter(new StringConverter<Property<String>>() {
#Override
public String toString(Property<String> object) {
return object == null ? null : object.getValue();
}
#Override
public Property<String> fromString(String string) {
return new SimpleStringProperty(string);
}
});
VBox container = new VBox(chipView);
Scene scene = new Scene(container, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Result:
This is how you get the actual values of the chips:
List<String> chipsValues = chipView.getChips().stream().map(Property::getValue).collect(Collectors.toList());
i'm working on a project and i'd like to find a way to change the background color of some elements in a listView. i've find a way to add css style class to the listView in general but not to specific elements .
Also , i've heard about cell factory but I dont know if cell factory can adapt during the programme or just set up things at the begging
(i have a listView of an object that I call player , and I want that , when the player in the listView get enough points , his name becomes red)
is there a way to do something like this ?
ListView<Players> listview = ...;
for(Player p : listView){
p.addListener(//change color to red)
}
Thanks
I would use ObservableList and addListener to the given List.
Sample code:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application {
private StackPane main;
private ListView<Player> players;
#Override
public void start(Stage stage) {
main = new StackPane();
var scene = new Scene(main, 640, 480);
players = new ListView<Player>();
ObservableList<Player> playerObjs = FXCollections.observableArrayList (
new Player("A", 50),
new Player("B", 30),
new Player("C", 60),
new Player("D", 5),
new Player("E", 0)
);
players.setItems(playerObjs);
playerObjs.addListener(new ListChangeListener<Player>() {
#Override
public void onChanged(Change<? extends Player> change) {
updateView();
}
});
main.getChildren().add(players);
stage.setScene(scene);
stage.show();
}
public void updateView() {
for(int i = 0; i < players.getItems().size(); i++) {
if(players.getItems().get(i).getHp() < 10) {
players.getItems().get(i).setBackground(...);
}
}
}
public static void main(String[] args) {
launch();
}
}
Now, everytime the list changes, it calls updateView(), which if some condition holds, will set the given item Background to some value.
Let me know if that helped.
I'm trying to write a program similar to the contacts app on an android phone using javafx. In the fxml file I have a VBox which contains three textfields, the first two fields are for first name and last name, and the third one is for a number.
Now what I want the program to do is when the textfield for number is filled with even a single character, another textfield to be automatically added to the VBox. (for another number).
and I want the same thing to happen for the next field. and any other field that follows, so it has a recursive form.
Now the only method I know that might accomplish this, is using a listener, but I have no idea how to create such a recursive listener. and The listener to the old field would have to be removed once it has accomplished its job, so it wouldn't continuously create new fields when typing something in the old field. but you can't remove a listener while you're inside it.
Is there a way to do this?
A lambda expression can't refer to itself, but an anonymous inner class can, so if you implement your listener as an anonymous inner class, you can achieve what you're looking to do:
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class DynamicTextFields extends Application {
private TextField lastTextField ;
#Override
public void start(Stage primaryStage) {
lastTextField = new TextField();
VBox vbox = new VBox(5, lastTextField);
ChangeListener<String> textFieldListener = new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> obs, String oldValue, String newValue) {
lastTextField.textProperty().removeListener(this);
lastTextField = new TextField();
lastTextField.textProperty().addListener(this);
vbox.getChildren().add(lastTextField);
}
};
lastTextField.textProperty().addListener(textFieldListener);
Scene scene = new Scene(new ScrollPane(vbox), 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Register a ChangeListener to the text property of the TextFields that adds/removes the TextField based on the index every time the text changes from empty to non-empty or the other way round.
public void addTextField(Pane parent) {
TextField textField = new TextField();
textField.textProperty().addListener((o, oldValue, newValue) -> {
boolean wasEmpty = oldValue.isEmpty();
boolean isEmpty = newValue.isEmpty();
if (wasEmpty != isEmpty) {
if (wasEmpty) {
// append textfield if last becomes non-empty
if (parent.getChildren().get(parent.getChildren().size() - 1) == textField) {
addTextField(parent);
}
} else {
int tfIndex = parent.getChildren().indexOf(textField);
if (tfIndex < parent.getChildren().size() - 1) {
// remove textfield if this is not the last one
parent.getChildren().remove(tfIndex);
parent.getChildren().get(tfIndex).requestFocus();
}
}
}
});
parent.getChildren().add(textField);
}
VBox root = new VBox();
addTextField(root);
I am using ControlsFX's CustomTextField. When I click on one of the autocomplete options, I need to clear the TextField and create a Tag so I can add it to a FlowPane. How do I set up an OnClick or OnSelectionChange listener or override the current OnClick?
I took a look at the CustomTextField documentation and I can't find a clear way of doing what you want. So I will guess you have to implement it yourself or to find a workaround. In case you decide to choose the second choice here is something that I believe works very well :
import java.util.ArrayList;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TestApplication extends Application {
private boolean addedBySelection = false;
private ArrayList<String> tagList = new ArrayList<>();
private FlowPane tagPane;
#Override
public void start(Stage primaryStage) throws Exception {
VBox mainPane = new VBox(10);
mainPane.setStyle("-fx-background-color : white");
mainPane.setPadding(new Insets(15));
mainPane.setAlignment(Pos.CENTER);
tagPane = new FlowPane(15, 10);
tagPane.setPrefHeight(50);
CustomTextField field = new CustomTextField() {
#Override
public void paste() {
super.paste();
addedBySelection = false;
}
};
field.setOnKeyPressed(e -> {
addedBySelection = false;
});
field.setOnKeyReleased(e -> {
addedBySelection = true;
});
field.textProperty().addListener(e -> {
if (addedBySelection) {
System.out.println("Text Changed from the suggession list ");
addTag(field.getText());
addedBySelection = false;
field.clear();
addedBySelection = true;
} else {
System.out.println("User Input (Mouse paste, or typing) ");
}
});
TextFields.bindAutoCompletion(field, new String[] { "Java", "C++", "C#", "Python", "Haskell" });
mainPane.getChildren().addAll(field, tagPane);
Scene scene = new Scene(mainPane, 200, 100);
primaryStage.setScene(scene);
primaryStage.show();
}
private void addTag(String tag) {
if (!tagList.contains(tag)) {
tagList.add(tag);
Label tagLabel = new Label(tag);
tagLabel.setStyle("-fx-background-color : #E1ECF4; -fx-text-fill : #6A739D;");
tagPane.getChildren().add(tagLabel);
}
}
public static void main(String[] args) {
launch(args);
}
}
I tried to keep it a simple as it could. The code above is doing exactly what you are after. The logic is to set a listener on the textProperty ( cause we can't set one on selection with the mouse from the autocomplete list ) and somehow find out if the user actually triggers the event using the autocomplete list or not. Thus I have a flag looking for user inputs (ex. key press) and 'releasing' the flag each time the keys are released. We have to catch the paste action as well in order to avoid mistakes if the user pastes a text on the field. One more last thing is the way we clear the field. We have to set our flag to false because the field.clear() will trigger an event as well and we don't want to fall into an event loop.
Note : With the current workaround, you will see that you are able to make a selection from the autocomplete list by pressing the enter key as well.
I have a JavaFX TableView with single cell selection enabled. When a user selects a cell the selection highlight will flicker when new data is added to the table
A small example that demonstrates the problem:
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class SelectionBug extends Application
{
public static void main(
String[] args)
{
Application.launch(args);
}
#Override
public void start(
Stage primaryStage) throws Exception
{
final ObservableList<DummyData> list = FXCollections.observableArrayList();
final TableView<DummyData> tableView = new TableView<>(list);
tableView.getColumns().add(createColumn(item -> item.getColumn1()));
tableView.getColumns().add(createColumn(item -> item.getColumn2()));
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
tableView.getSelectionModel().setCellSelectionEnabled(true);
final Thread thread = new Thread(() ->
{
while (true)
{
Platform.runLater(() -> list.add(new DummyData()));
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
//do nothing
}
}
});
thread.setDaemon(true);
thread.start();
final BorderPane root = new BorderPane();
root.setCenter(tableView);
primaryStage.setScene(new Scene(root, 500, 500));
primaryStage.show();
}
private TableColumn<DummyData, String> createColumn(
final Callback<DummyData, String> dataGetter)
{
final TableColumn<DummyData, String> column = new TableColumn<>();
column.setCellValueFactory(cellData -> new ReadOnlyStringWrapper(dataGetter.call(cellData.getValue())));
return column;
}
private static class DummyData
{
private final String mColumn1;
private final String mColumn2;
public DummyData()
{
final Random ramdom = new Random();
mColumn1 = Integer.toString(ramdom.nextInt(1000));
mColumn2 = Integer.toString(ramdom.nextInt(1000));
}
public String getColumn1()
{
return mColumn1;
}
public String getColumn2()
{
return mColumn2;
}
}
}
If you run that and select a cell, you'll see the flickering.
My digging so far suggests it's to do with cell recycling in the table view: I changed the Cell Factory to assign and log out a unique ID and the cell's item for each cell object and found that the ID <-> item relationship is not constant; each cell object gets moved around the tableview showing different data with every update to the data model. This means that the selected property is modified on every update, causing the pseudoClassState to change. I suspect it's a subtle timing issue with when the cell is taken out of the tableview and when the cell's selected property is changed
Has anyone else seen this problem, and does anyone have any kind of workaround?
Probably a bit late for you, but I had a similar problem and managed to solve it by wrapping the TableView in an extra AnchorPane.