I'm trying to set every column minimum width to 100px. I prefer to use tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
Explanation
If you set TableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY) all columns will be equally resized until the TableViews maximum width is reached.
Explanation from
At the moment the width of each column is determined by the width of the TableViewdevided by the number of columns.
I tried to set every column the minimum width but that didn't work. I've also saw that people just created their own callback for the setColumnResizePolicy but I couldn't implement my idea of what should happen.
MCVE
public class MCVE extends Application {
private Scene mainScene;
private Stage mainStage;
private Label filename;
private VBox mainBox;
private TableView<String> tableView;
private Button open;
private Button save;
private Button neu;
private Button settings;
private Button table;
private Button row;
private Button column;
private Button date;
public static void main(String[] args) {
Application.launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
initButton();
initTable();
mainStage = new Stage();
filename = new Label("Nr. 100 - Test Data (Applikation: TEST)");
mainScene = new Scene(mainVBox(), 1200, 600);
tableView.prefWidthProperty().bind(mainBox.widthProperty());
tableView.prefHeightProperty().bind(mainBox.heightProperty());
mainStage.setScene(mainScene);
mainStage.show();
}
private GridPane mainGrid() {
GridPane gridPane = new GridPane();
gridPane.setVgap(10);
gridPane.setHgap(10);
gridPane.add(filename, 0, 0);
gridPane.add(buttonBox(), 0, 1);
gridPane.add(tableView, 0, 2);
return gridPane;
}
private VBox buttonBox() {
VBox buttonBox = new VBox();
HBox firstRowBox = new HBox();
HBox secRowBox = new HBox();
firstRowBox.getChildren().addAll(open, save, neu, settings);
firstRowBox.setSpacing(5);
secRowBox.getChildren().addAll(table, row, column, date);
secRowBox.setSpacing(5);
buttonBox.getChildren().addAll(firstRowBox, secRowBox);
buttonBox.setSpacing(5);
buttonBox.prefWidthProperty().bind(mainBox.widthProperty());
return buttonBox;
}
private VBox mainVBox() {
mainBox = new VBox();
mainBox.prefWidthProperty().bind(mainStage.widthProperty().multiply(0.8));
mainBox.setPadding(new Insets(10, 10, 10 ,10));
mainBox.getChildren().add(mainGrid());
return mainBox;
}
private void initButton() {
open = new Button("Open");
open.setPrefWidth(100);
save = new Button("Save");
save.setPrefWidth(100);
neu = new Button("New");
neu.setPrefWidth(100);
settings = new Button("Settings");
settings.setPrefWidth(100);
table = new Button("Table");
table.setPrefWidth(100);
row = new Button("Row");
row.setPrefWidth(100);
column = new Button("Column");
column.setPrefWidth(100);
date = new Button("Date");
date.setPrefWidth(100);
}
private TableView initTable() {
tableView = new TableView<>();
// Create column UserName (Data type of String).
TableColumn<String, String> userNameCol //
= new TableColumn<>("User Name");
// Create column Email (Data type of String).
TableColumn<String, String> emailCol//
= new TableColumn<>("Email");
// Create 2 sub column for FullName.
TableColumn<String, String> firstNameCol //
= new TableColumn<>("First Name");
TableColumn<String, String> lastNameCol //
= new TableColumn<>("Last Name");
// Active Column
TableColumn<String, Boolean> activeCol//
= new TableColumn<>("Active");
tableView.getColumns().addAll(userNameCol, emailCol, firstNameCol, lastNameCol, activeCol);
for (int i = 0; tableView.getColumns().size() < i; i++){
tableView.getColumns().get(i).setMinWidth(100);
}
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
return tableView;
}
}
In the end I expect that after I load the file, the width of the column should still be the TableView width devided by the number of columns expect the width would be < 100px. In this case every width should be 100px and a scrollpane appears (ignores the scrollbar here).
Thank you for your help!
Your constraint of min width is never set...
Just replace this:
for (int i = 0; tableView.getColumns().size() < i; i++){
to this:
for (int i = 0; i < tableView.getColumns().size(); i++) {
Or even better use forEach:
tableView.getColumns().forEach(column -> column.setMinWidth(100));
Related
I been trying to create a desktop app with javafx and have been hardstuck trying to figure out why the GridPane and HBox in the VBox have been refusing to expand past a certain point in the vertical direction. I have set their .vgrow properties to Priority.ALWAYS and they still seem to not want to expand when I resize the window.
The code is shown below:
package display;
import java.io.File;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.event.Event;
import javafx.event.EventHandler;
public class OxygenDisplay
{
private String openFileName;
private String fileContents;
private String activeVaultPath;
private final Stage primaryStage;
private final Scene primaryScene;
private final MenuBar topMenu;
private final GridPane middleStruct;
private final HBox bottomOptions;
public OxygenDisplay(String fileName, String fileContents, String path)
{
this.openFileName = fileName;
this.fileContents = fileContents;
this.activeVaultPath = path;
// Create node structure for menu bar
this.topMenu = this.generateTopMenuBar();
// Create node structure for vault display and text editor
this.middleStruct = this.generateMiddleStructure();
// Create node structure for lower options bar
this.bottomOptions = this.generateBottomOptions();
// Post view to user
VBox root = new VBox();
for (Node child: root.getChildren())
{
VBox.setVgrow(child, Priority.ALWAYS);
}
root.getChildren().addAll(
this.topMenu,
this.middleStruct,
this.bottomOptions
);
this.primaryScene = new Scene(root, 400, 300);
this.primaryStage = new Stage();
// Init all listeners
this.initListeners();
this.primaryStage.setTitle("Oxygen");
this.primaryStage.setScene(primaryScene);
this.primaryStage.show();
}
private void initListeners()
{
// Add window height and width listeners
this.primaryStage.widthProperty().addListener((obsv, oldv, newv) ->
{
// this.mdEditor.prefWidth(newv.doubleValue());
// this.mdEditor.maxWidth(newv.doubleValue());
});
this.primaryStage.heightProperty().addListener((obsv, oldv, newv) ->
{
// this.mdEditor.prefHeight(newv.doubleValue());
// this.mdEditor.maxHeight(newv.doubleValue());
});
}
public String getOpenFileName()
{
return this.openFileName;
}
public void setOpenFileName(String file)
{
this.openFileName = file;
}
public String getFileContents()
{
return this.fileContents;
}
public void setFileContents(String data)
{
this.fileContents = data;
}
private MenuBar generateTopMenuBar()
{
MenuBar topBar = new MenuBar();
Menu file = new Menu("File");
Menu edit = new Menu("Edit");
Menu view = new Menu("View");
Menu help = new Menu("Help");
SeparatorMenuItem fileSeparator = new SeparatorMenuItem();
SeparatorMenuItem editSeparator = new SeparatorMenuItem();
// TODO: Add event handlers for these options
MenuItem open = new MenuItem("Open");
MenuItem newNote = new MenuItem("New Note");
MenuItem newVault = new MenuItem("New Vault");
MenuItem save = new MenuItem("Save");
MenuItem saveAs = new MenuItem("Save as");
file.getItems().addAll(
open,
newNote,
newVault,
fileSeparator,
save,
saveAs
);
// TODO: Add event handlers for these options
MenuItem copy = new MenuItem("Copy");
MenuItem paste = new MenuItem("Paste");
MenuItem cut = new MenuItem("Cut");
MenuItem find = new MenuItem("Find");
edit.getItems().addAll(
cut,
copy,
paste,
editSeparator,
find
);
// TODO: Add event handlers for these options
MenuItem appearance = new MenuItem("Appearance");
view.getItems().addAll(
appearance
);
// TODO: Add event handlers for these options
MenuItem about = new MenuItem("About");
help.getItems().addAll(
about
);
topBar.getMenus().addAll(
file,
edit,
view,
help
);
return topBar;
}
private void generateFileStructure(File node, TreeItem<String> root)
{
// TODO: Order the tree structure so that files are always before
// directories. Currently it is based on file names.
if (node.isDirectory())
{
TreeItem<String> item = new TreeItem<>(node.getName());
root.getChildren().add(item);
for (File f: node.listFiles())
{
TreeItem<String> temp = new TreeItem<>();
item.getChildren().add(temp);
// When parent is expanded continue the recursion
item.addEventHandler(TreeItem.branchExpandedEvent(),
new EventHandler() {
#Override
public void handle(Event event)
{
generateFileStructure(f, item);
item.getChildren().remove(temp);
item.removeEventHandler(TreeItem.branchExpandedEvent(),
this);
}
});
}
} else
{
root.getChildren().add(new TreeItem<>(node.getName()));
}
}
private TreeView<String> generateVaultStructure()
{
File fileInputDirLocation = new File(this.activeVaultPath);
File[] fileList = fileInputDirLocation.listFiles();
TreeView<String> vaultStruct = new TreeView<>();
TreeItem<String> root =
new TreeItem<>(fileInputDirLocation.getAbsoluteFile().getName());
// create tree
assert fileList != null;
for (File file: fileList)
{
this.generateFileStructure(file, root);
}
vaultStruct.setRoot(root);
return vaultStruct;
}
private GridPane generateMiddleStructure()
{
GridPane gridPane = new GridPane();
TreeView<String> vaultStruct = this.generateVaultStructure();
TextArea mdEditor = new TextArea();
mdEditor.setText(this.fileContents);
gridPane.add(vaultStruct, 0, 0);
gridPane.add(mdEditor, 1, 0);
// For resizing purposes
GridPane.setHgrow(vaultStruct, Priority.NEVER);
GridPane.setHgrow(mdEditor, Priority.ALWAYS);
GridPane.setVgrow(vaultStruct, Priority.ALWAYS);
GridPane.setVgrow(mdEditor, Priority.ALWAYS);
return gridPane;
}
private HBox generateBottomOptions()
{
HBox bottomOptions = new HBox();
Label openFileDisplay = new Label("");
openFileDisplay.textProperty().bind(
new SimpleStringProperty(this.openFileName)
);
bottomOptions.getChildren().addAll(
openFileDisplay
);
// For resizing purposes
HBox.setHgrow(bottomOptions, Priority.ALWAYS);
return bottomOptions;
}
}
Example image:
The red area is what I need to remove
Any help is appreciated in finding out why this is occuring? Let me know if anyone wants to run the above code as it requires a seperate class that extends Application for javafx.
The fix was the order of the for loop which gave all children process Priority.ALWAYS:
Old:
// Post view to user
VBox root = new VBox();
for (Node child: root.getChildren())
{
VBox.setVgrow(child, Priority.ALWAYS);
}
root.getChildren().addAll(
this.topMenu,
this.middleStruct,
this.bottomOptions
);
New:
// Post view to user
VBox root = new VBox();
root.setMaxHeight(Double.MAX_VALUE);
root.getChildren().addAll(
this.topMenu,
this.middleStruct,
this.bottomOptions
);
for (Node child: root.getChildren())
{
VBox.setVgrow(child, Priority.ALWAYS);
}
Although now the GridPane and Label/HBox expand together due to both of the panes having Priority.ALWAYS. Hence, to overcome this is to give GridPane maximum priority.
New Version 2:
// Post view to user
VBox root = new VBox();
root.setMaxHeight(Double.MAX_VALUE);
root.getChildren().addAll(
this.topMenu,
this.middleStruct,
this.bottomOptions
);
VBox.setVgrow(this.middleStruct, Priority.ALWAYS);
VBox.setVgrow(this.bottomOptions, Priority.NEVER);
Cheers.
This is my main class. I'm trying to get the string that is being selected from the combobox and "get the wall color" from that string. The problem is that when I run the application, after hitting the playbutton I get an error saying that in line 75 - if (stringWallColor.equals("Default - Black")), stringWallColor is null.
Does that I mean that it uses the public static String stringWallColor which is null as default? And how can I fix this?
public static String stringWallColor;
public static void main(String[] args) {
launch(args);
System.out.println("Done!");
}
public void start(Stage primaryStage) throws Exception {
Main.primaryStage = primaryStage;
MenuBar MENU = new MenuBar();
MenuGenerator.menuCreator(MENU);
Button playButton = new Button("Play Game");
ComboBox<String> wallColorCombo = new ComboBox<>();
wallColorCombo.setPromptText("Choose wall color");
wallColorCombo.getItems().addAll(
"Default - Black",
"Dark Green",
"Dark Red",
"Dark Gray",
"Saddle Brown",
"Midnight Blue",
"Dark Magenta",
"Crimson",
"Navy");
stringWallColor = wallColorCombo.getSelectionModel().getSelectedItem();
gameGrid = new GridPane();
GridPane root = new GridPane();
root.add(MENU,0,0);
root.add(gameGrid, 0, 1);
gameScene = new Scene(root,600,625);
GridPane startGrid = new GridPane();
startGrid.setHgap(20);
startGrid.setVgap(20);
playButton.setLineSpacing(10);
startGrid.add(playButton,8,12);
startGrid.add(wallColorCombo,7,12);
GridPane root0= new GridPane();
root0.add(startGrid,0,1);
Scene startScene = new Scene(root0, 400, 400);
primaryStage.setScene(startScene);
primaryStage.setTitle(GameEngine.GAME_NAME);
primaryStage.show();
System.out.println("Default save file not loaded yet");
playButton.setOnAction(e -> {
MenuGenerator.loadDefaultSaveFile(primaryStage);
System.out.println("Default save file loaded");
primaryStage.setScene(gameScene); });
}
public static Color getWallColor() {
Color wallColor = Color.BLACK;
if (stringWallColor.equals("Default - Black"))
wallColor = Color.BLACK;
if (stringWallColor.equals("Dark Green"))
wallColor = Color.DARKGREEN;
if (stringWallColor.equals("Dark Red"))
wallColor = Color.DARKRED;
if (stringWallColor.equals("Dark Gray"))
wallColor = Color.DARKGRAY;
if (stringWallColor.equals("Saddle Brown"))
wallColor = Color.SADDLEBROWN;
if (stringWallColor.equals("Midnight Blue"))
wallColor = Color.MIDNIGHTBLUE;
if (stringWallColor.equals("Dark Magenta"))
wallColor = Color.DARKMAGENTA;
if (stringWallColor.equals("Crimson"))
wallColor = Color.CRIMSON;
if (stringWallColor.equals("Navy"))
wallColor = Color.NAVY;
return wallColor;
}
}
Solved by setting stringWallColor = wallColorCombo.getSelectionModel().getSelectedItem(); as an action for the playbutton.
I have a GUI that takes the input of an image name, and once the refresh button is pressed, its displayed in a fixed position. I want to be able to input a character 'A-G' which will change the X position of the image, and a character '0-6' that will change the Y position of the image. The name of the images are just "A1", "A2"..."A5". So, if the user inputs "A1B3", it will display the image A1 in X-Position 'B' and Y-Position '3'. So B could be 200, and 3 could be 300, which makes the (X,Y) coordinates (200,300).
This is my code that gets the users input for the image.
private void getImage(){
Image img = new Image("comp1110/ass2/gui/assets/" + textField.getText() + ".png", 100, 100, false, false);
ImageView image = new ImageView();
image.setImage(img);
image.setX(100);
image.setY(100);
pane.getChildren().add(image);
}
I think you should do something along the lines of this yes there is some cosmetic issues that you will need to fix. But its only to give you an idea of what to do. It uses a gridpane so you don't have to worry about getting exact coordinates I choose a vbox so I didn't have to worry about layout you can keep the Pane that you have it shouldn't make a difference.
public class Main extends Application {
private GridPane gridPane;
private TextField imageTextField = new TextField();
private HashMap<String,String> hashMap = new HashMap<>();
#Override
public void start(Stage primaryStage) {
fillHashMapValues();
gridPane = new GridPane();
gridPane.setGridLinesVisible(true);
for (int i = 0; i < 7; i++) {
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setPercentHeight(14);
gridPane.getRowConstraints().add(rowConstraints);
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(14);
gridPane.getColumnConstraints().add(columnConstraints);
gridPane.addColumn(i);
gridPane.addRow(i);
}
imageTextField.setPromptText("Enter Image Letters?");
TextField textField = new TextField();
textField.setPromptText("Enter Coordinates");
Button button = new Button("Go!");
button.setOnAction(event -> {
addToGridPane(textField.getText());
});
VBox vBox = new VBox();
vBox.setPrefSize(300, 300);
vBox.setAlignment(Pos.TOP_CENTER);
vBox.getChildren().addAll(imageTextField, textField, button, gridPane);
primaryStage.setScene(new Scene(vBox));
primaryStage.show();
button.requestFocus();//This is only so you can see the prompt text its irrelevant
}
private void fillHashMapValues(){
hashMap.put("A", "1");
hashMap.put("B", "2");
hashMap.put("C", "3");
hashMap.put("D", "4");
hashMap.put("E", "5");
hashMap.put("F", "6");
hashMap.put("G", "7");
}
private void addToGridPane(String string){
char[] chars = string.toCharArray();
if(chars.length==2){//Do more data validation here
if(hashMap.containsKey(String.valueOf(chars[0]))) {
int xValue = Integer.parseInt(hashMap.get(String.valueOf(chars[0])));
int yValue = Integer.parseInt(String.valueOf(chars[1]));
ImageView image = getImage();
gridPane.add(image, xValue, yValue);
}
}
}
private ImageView getImage(){
Image image = new Image("comp1110/ass2/gui/assets/" + imageTextField.getText() + ".png", 100, 100, false, false);
ImageView imageView = new ImageView();
imageView.setImage(image);
//imageView.setX(100);
//imageView.setY(100);
//pane.getChildren().add(image);
return imageView;
}
public static void main(String[] args) { launch(args); }
}
I'm trying to make TreeView with CheckBoxTreeItems. When I collapse/expand a CheckBoxTreeItems the image I set up does not display correctly. I googled but I couldn't find correct answer. On Stack Overflow, I found a similar problem, but I didn't get a valid answer.
E.g
JavaFX CheckBoxTreeItem graphic disappear when siblings collapse
JavaFX CheckBoxTreeItem: Graphic disappears if graph is extended
Any ideas?
public class ClientApplication extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
ImageView folderIcon = new ImageView();
Image folderImage = new Image("image/folder.png");
folderIcon.setImage(folderImage);
folderIcon.setFitWidth(16);
folderIcon.setFitHeight(16);
CheckBoxTreeItem<String> rootItem = new CheckBoxTreeItem<String>("folder", folderIcon);
rootItem.setExpanded(true);
for (int i = 0; i < 4; i++) {
CheckBoxTreeItem<String> checkBoxTreeItem = new CheckBoxTreeItem<String>("Sample" + (i + 1), folderIcon);
rootItem.getChildren().add(checkBoxTreeItem);
}
final TreeView<String> tree = new TreeView<String>(rootItem);
tree.setCellFactory(CheckBoxTreeCell.<String>forTreeView());
tree.setRoot(rootItem);
tree.setShowRoot(true);
StackPane root = new StackPane();
root.getChildren().add(tree);
stage.setScene(new Scene(root, 300, 250));
stage.show();
}
}
enter image description here
I tried to use the ideas provided by #Jai,But when I click the expand/collapse icon, there is still a problem.Attachment is a screenshot.Thanks in advance.
enter image description here
ImageView is a JavaFX control. This means that each instance represents a unique control you see on your screen. You should never use the same instance for multiple locations in your GUI.
On the other hand, Image represents an image (i.e. an array of pixel data), so it is reusable.
This should work:
#Override
public void start(final Stage stage) {
final Image folderImage = new Image("image/folder.png");
CheckBoxTreeItem<String> rootItem = new CheckBoxTreeItem<String>("folder", createImageView(folderImage));
rootItem.setExpanded(true);
for (int i = 0; i < 4; i++) {
CheckBoxTreeItem<String> checkBoxTreeItem = new CheckBoxTreeItem<String>("Sample" + (i + 1), createImageView(folderImage));
rootItem.getChildren().add(checkBoxTreeItem);
}
final TreeView<String> tree = new TreeView<String>(rootItem);
tree.setCellFactory(CheckBoxTreeCell.<String>forTreeView());
tree.setRoot(rootItem);
tree.setShowRoot(true);
StackPane root = new StackPane();
root.getChildren().add(tree);
stage.setScene(new Scene(root, 300, 250));
stage.show();
}
private ImageView createImageView(Image folderImage) {
ImageView folderIcon = new ImageView();
folderIcon.setImage(folderImage);
folderIcon.setFitWidth(16);
folderIcon.setFitHeight(16);
return folderIcon;
}
I'm making a chat application using JavaFX for the GUI. I display the chat content in a ListView, but I have one big problem - it's very very slow. When I add new items to the list and especially when I scroll the list up/down. I think maybe it has something to do with the fact that the list refreshes itsellf every time a new item is added (each cell in the list!) and also refreshes every time I scroll up/down.
Does someone know what can I do to solve this problem? TNX
I override ListCell's updateItem:
chatListView.setCellFactory(new Callback<ListView<UserInfo>, ListCell<UserInfo>>() {
#Override
public ListCell<UserInfo> call(ListView<UserInfo> p) {
ListCell<UserInfo> cell = new ListCell<UserInfo>() {
#Override
protected void updateItem(UserInfo item, boolean bln) {
super.updateItem(item, bln);
if (item != null) {
BorderPane borderPane = new BorderPane();
ImageView profileImage = new ImageView(new Image(item.getImageURL()));
profileImage.setFitHeight(32);
profileImage.setFitWidth(32);
Rectangle clip = new Rectangle(
profileImage.getFitWidth(), profileImage.getFitHeight()
);
clip.setArcWidth(30);
clip.setArcHeight(30);
profileImage.setClip(clip);
SnapshotParameters parameters = new SnapshotParameters();
parameters.setFill(Color.TRANSPARENT);
WritableImage image = profileImage.snapshot(parameters, null);
profileImage.setClip(null);
profileImage.setImage(image);
ImageView arrowImage = new ImageView(new Image("arrow1.png"));
ImageView arrowImage2 = new ImageView(new Image("arrow1.png"));
Label nameLabel = new Label(item.getUserName());
nameLabel.setStyle(" -fx-text-alignment: center; -fx-padding: 2;");
HBox hbox = null;
Label textLabel = new Label();
String messageText = splitTolines(item.getMessage());
textLabel.setText(messageText);
textLabel.setStyle("-fx-background-color: #a1f2cd; "
+ "-fx-padding: 10;\n"
+ "-fx-spacing: 5;");
hbox = new HBox(arrowImage, textLabel);
VBox vbox = new VBox(profileImage, nameLabel);
BorderPane.setMargin(vbox, new Insets(0, 10, 10, 10));
BorderPane.setMargin(hbox, new Insets(10, 0, 0, 0));
//Time
Date dNow = new Date();
SimpleDateFormat ft = new SimpleDateFormat("hh:mm a");
Label timeLabel = new Label(ft.format(dNow));
timeLabel.setStyle("-fx-font: 8px Tahoma; -fx-width: 100%");
HBox hbox2 = new HBox(arrowImage2, timeLabel);
arrowImage2.setVisible(false);
VBox vbox2 = new VBox(hbox, hbox2);
borderPane.setCenter(vbox2);
borderPane.setLeft(vbox);
setGraphic(borderPane);
}
}
};
return cell;
}
});
Never ever add (big) GUI Elements in updateItem() without checking if it is not already there.
updateItem() is called everytime for EVERY SINGLE ROW when you scroll, resize or change gui in any other way.
You should alway reset the graphic to null if you do not have an item or the second boolean of updateItem(item, empty) is false, because the second boolean is the EMPTY flag.
I recommend to you that you use a VBox instead of a ListView.
You must not build new instances of your components everytime the view gets updated.
Instanciate them one time initialy, then you reuse and change their attributes.
I just noticed that too. It's too slow even for a list containing only 5-10 items (with scaled images and text). Since I need no selection feature, I also rewrote the code to use VBox instead and the slowness is immediately gone!
To emulate the setItems, I have a helper function which you may find handy:
public static <S, T> void mapByValue(
ObservableList<S> sourceList,
ObservableList<T> targetList,
Function<S, T> mapper)
{
Objects.requireNonNull(sourceList);
Objects.requireNonNull(targetList);
Objects.requireNonNull(mapper);
targetList.clear();
Map<S, T> sourceToTargetMap = new HashMap<>();
// Populate targetList by sourceList and mapper
for (S s : sourceList)
{
T t = mapper.apply(s);
targetList.add(t);
sourceToTargetMap.put(s, t);
}
// Listen to changes in sourceList and update targetList accordingly
ListChangeListener<S> sourceListener = new ListChangeListener<S>()
{
#Override
public void onChanged(ListChangeListener.Change<? extends S> c)
{
while (c.next())
{
if (c.wasPermutated())
{
for (int i = c.getFrom(); i < c.getTo(); i++)
{
int j = c.getPermutation(i);
S s = sourceList.get(j);
T t = sourceToTargetMap.get2(s);
targetList.set(i, t);
}
}
else
{
for (S s : c.getRemoved())
{
T t = sourceToTargetMap.get2(s);
targetList.remove2(t);
sourceToTargetMap.remove2(s);
}
int i = c.getFrom();
for (S s : c.getAddedSubList())
{
T t = mapper.apply(s);
targetList.add(i, t);
sourceToTargetMap.put(s, t);
i += 1;
}
}
}
}
};
sourceList.addListener(new WeakListChangeListener<>(sourceListener));
// Store the listener in targetList to prevent GC
// The listener should be active as long as targetList exists
targetList.addListener((InvalidationListener) iv ->
{
Object[] refs = { sourceListener, };
Objects.requireNonNull(refs);
});
}
It can then be used like:
ObservableList<Bookmark> bookmarkList;
VBox bookmarkListVBox;
mapByValue(bookmarkList, bookmarkListVBox.getChildren(), bmk -> new Label(bmk.getName());
To automatically update the list (VBox's children) from observable list.
PS: other functions such as grouping are here => ObservableListHelper