Run the following, and resize the window horizontally. The listview, its contents and the label on the right are unstable and take on different positions.
package hs.mediasystem;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TitledBlockSample extends Application {
private final ObjectProperty<Media> data = new SimpleObjectProperty<>();
public ObjectProperty<Media> dataProperty() { return data; }
protected final ObjectBinding<ObservableList<Casting>> castings = Bindings.select(dataProperty(), "castings");
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) throws Exception {
VBox vbox = new VBox();
HBox hbox = new HBox();
ListView<Media> listView = new ListView<>();
listView.getItems().add(new Media("Some title"));
for(int i = 0; i < 1000; i++) {
Media media = new Media("Title #" + i);
ObservableList<Casting> list = FXCollections.observableArrayList();
list.add(new Casting() {{
characterName.set("Bambi");
}});
media.castings.set(list);
listView.getItems().add(media);
}
data.bind(listView.getFocusModel().focusedItemProperty());
CastingsRow castingsRow = createCastingsRow();
vbox.getChildren().add(new Label("Detailed Information About the Selected Item:"));
vbox.getChildren().add(createTitledBlock("Title", castingsRow, castingsRow.empty.not()));
hbox.getChildren().addAll(vbox, listView);
stage.setScene(new Scene(hbox));
// stage.getScene().getStylesheets().add("default.css");
// stage.getScene().getStylesheets().add("select-media/detail-pane.css");
// stage.getScene().getStylesheets().add("select-media/media-detail-pane.css");
stage.setWidth(1024);
stage.setHeight(600);
stage.show();
}
protected Pane createTitledBlock(final String title, final Node content, final BooleanExpression visibleCondition) {
return new VBox() {{
setFillWidth(true);
getChildren().add(new Label(title) {{
getStyleClass().add("header");
}});
getChildren().add(content);
if(visibleCondition != null) {
managedProperty().bind(visibleCondition);
visibleProperty().bind(visibleCondition);
}
setStyle("-fx-border-color: red; -fx-border-width: 2px");
}};
}
protected CastingsRow createCastingsRow() {
CastingsRow castingsRow = new CastingsRow();
castingsRow.castings.bind(castings);
return castingsRow;
}
public class CastingsRow extends TilePane {
public final ObjectProperty<ObservableList<Casting>> castings = new SimpleObjectProperty<>();
public final BooleanProperty empty = new SimpleBooleanProperty(true);
public CastingsRow() {
getStyleClass().add("castings-row");
widthProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
createCastingChildren(castings.get());
}
});
castings.addListener(new ChangeListener<ObservableList<Casting>>() {
private final ListChangeListener<Casting> listChangeListener = new ListChangeListener<Casting>() {
@Override
public void onChanged(ListChangeListener.Change<? extends Casting> c) {
createCastingChildren(c.getList());
}
};
private final WeakListChangeListener<Casting> weakListChangeListener = new WeakListChangeListener<>(listChangeListener);
@Override
public void changed(ObservableValue<? extends ObservableList<Casting>> observable, ObservableList<Casting> old, ObservableList<Casting> current) {
if(old != null) {
old.removeListener(weakListChangeListener);
}
createCastingChildren(current);
if(current != null) {
current.addListener(weakListChangeListener);
}
}
});
}
private final WeakBinder binder = new WeakBinder();
private void createCastingChildren(ObservableList<? extends Casting> castings) {
/*
* Cleanup old children (setting the image to null reduces the chance that an unneeded image is loaded in the background):
*/
for(Node node : getChildren()) {
// ((CastingImage)node).image.set(null);
}
getChildren().clear();
empty.set(true);
/*
* Create new children:
*/
double castingSize = 100 + getHgap();
if(castings != null) {
// double space = getWidth() - castingSize;
for(final Casting casting : castings) {
empty.set(false);
// if(space < 0) {
// break;
// }
CastingImage castingImage = new CastingImage();
castingImage.setStyle("-fx-border-color: black; -fx-border-width: 4px;");
// binder.bind(castingImage.title, casting.person.get().name);
// binder.bind(castingImage.image, casting.person.get().photo);
binder.bind(castingImage.characterName, casting.characterName);
getChildren().add(castingImage);
// space -= castingSize;
}
}
}
}
public class Media {
public final StringProperty title = new SimpleStringProperty();
public final ObjectProperty<ObservableList<Casting>> castings = new SimpleObjectProperty<>();
public final ObjectProperty<ObservableList<Casting>> castingsProperty() { return castings; }
public Media(String title) {
this.title.set(title);
}
}
public class CastingImage extends Button {
// public final ObjectProperty<ImageHandle> image = new SimpleObjectProperty<>();
public final StringProperty title = new SimpleStringProperty();
public final StringProperty characterName = new SimpleStringProperty();
private final StringBinding formattedCharacterName = new StringBinding() {
{
bind(characterName);
}
@Override
protected String computeValue() {
return characterName.get() == null ? null : "as " + formatCharacterName(characterName.get(), 40);
}
};
public CastingImage() {
getStyleClass().add("cast-item");
VBox vbox = new VBox();
Button imageView = new Button("?");
Label label = new Label();
// AsyncImageProperty photo = new AsyncImageProperty();
// photo.imageHandleProperty().bind(image);
imageView.getStyleClass().add("cast-photo");
// imageView.imageProperty().bind(photo);
// imageView.setSmooth(true);
// imageView.setPreserveRatio(true);
imageView.setMinHeight(122);
imageView.setAlignment(Pos.CENTER);
label.getStyleClass().add("cast-name");
label.textProperty().bind(title);
label.setMinWidth(100);
label.setMaxWidth(100);
vbox.getChildren().addAll(imageView, label);
Label characterNameLabel = new Label();
characterNameLabel.getStyleClass().add("cast-character-name");
characterNameLabel.textProperty().bind(formattedCharacterName);
characterNameLabel.setMinWidth(100);
characterNameLabel.setMaxWidth(100);
vbox.getChildren().add(characterNameLabel);
BooleanBinding visibleBinding = characterName.isNotNull().and(characterName.isNotEqualTo(""));
characterNameLabel.managedProperty().bind(visibleBinding);
characterNameLabel.visibleProperty().bind(visibleBinding);
setGraphic(vbox);
}
private String formatCharacterName(String rawCharacterName, int cutOffLength) {
String characterName = rawCharacterName.replaceAll(" / ", "|");
int more = 0;
for(;;) {
int index = characterName.lastIndexOf('|');
if(index == -1) {
return characterName;
}
if(index > cutOffLength) { // too long, cut it off
characterName = characterName.substring(0, index).trim();
more++;
}
else {
if(more == 0) {
characterName = characterName.substring(0, index).trim() + " & " + characterName.substring(index + 1).trim();
}
else {
characterName = characterName.substring(0, index).trim() + " (" + (more + 1) + "\u00a0more)";
}
return characterName.replaceAll(" *\\| *", ", ");
}
}
}
}
public class WeakBinder {
private final List<Object> hardRefs = new ArrayList<>();
private final Map<ObservableValue<?>, WeakInvalidationListener> listeners = new HashMap<>();
public void unbindAll() {
for(ObservableValue<?> observableValue : listeners.keySet()) {
observableValue.removeListener(listeners.get(observableValue));
}
hardRefs.clear();
listeners.clear();
}
public <T> void bind(final Property<T> property, final ObservableValue<? extends T> dest) {
InvalidationListener invalidationListener = new InvalidationListener() {
@Override
public void invalidated(Observable observable) {
property.setValue(dest.getValue());
}
};
WeakInvalidationListener weakInvalidationListener = new WeakInvalidationListener(invalidationListener);
listeners.put(dest, weakInvalidationListener);
dest.addListener(weakInvalidationListener);
property.setValue(dest.getValue());
hardRefs.add(invalidationListener);
}
}
public class Casting {
public final StringProperty role = new SimpleStringProperty();
public final StringProperty characterName = new SimpleStringProperty();
public final IntegerProperty index = new SimpleIntegerProperty();
// public final ObjectProperty<Person> person = object("person");
// public final ObjectProperty<Media> media = object("media");
}
}