JDK-8117829 : Jittery Stage Resizing
  • Type: Bug
  • Component: javafx
  • Sub-Component: scenegraph
  • Affected Version: 8
  • Priority: P4
  • Status: Resolved
  • Resolution: Duplicate
  • Submitted: 2013-06-05
  • Updated: 2015-06-17
  • Resolved: 2013-06-10
Related Reports
Duplicate :  
Description
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");
  }

}

Comments
This is most probably a duplicate of RT-30750.
06-06-2013