2

In a quite complex javafx application I faced a possible rendering bug. The last two days I could track it down to the following simple application. The following SSCCE demonstrates that in certain circumstances some javafx components are not correctly rendered. As a result, ComboBox and ListView do not show changed content:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TabPane.TabClosingPolicy;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class ComboManagedBug extends Application {
  public static void main(String[] args) {
    launch(args);
  }

  public void start(Stage primaryStage) {
    // add combo box
    ComboBox<String> combo = new ComboBox<>();
    combo.setPromptText("Choose a value...");
    combo.getItems().setAll("1", "2", "3");

    // add list view
    ListView<Label> list = new ListView<>();

    // add "add" button
    Button add = new Button("Add");
    add.setOnAction(e -> list.getItems().add(
      new Label(combo.getSelectionModel().getSelectedItem())));

    // add tab pane
    Tab tab1 = new Tab("First", new VBox(combo, add, list));
    Tab tab2 = new Tab("Second");
    TabPane tabs = new TabPane(tab1, tab2);
    tabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE); // important!

    // add "next" and "cancel" buttons at bottom
    Button next = new Button("Next");
    Button cancel = new Button("Cancel (Triggers refresh)");
    HBox buttons = new HBox(next, cancel);

    // install tab listener
    tabs.getSelectionModel().selectedItemProperty().addListener((a, b, c) -> {
      // intention is to show next button only on first tab
      boolean firstTab = c == tab1;
      next.setVisible(firstTab);
      next.setManaged(firstTab); // important!
    });

    // show
    VBox root = new VBox(new VBox(tabs, buttons)); // important!
    Scene scene = new Scene(root, 400, 400);
    primaryStage.setTitle("ComboBox/ListView Rendering Bug Demo");
    primaryStage.setScene(scene);
    primaryStage.show();
  }
}

Steps to reproduce:

  1. Launch program
  2. Choose combo value -> ComboBox shows chosen value -> OK
  3. Click "Add" -> value gets added to the ListView and is shown -> OK
  4. Activate second tab
  5. Activate first tab
  6. Choose a combo value being different than the previous one -> value in ComboBox does not change -> BUG
  7. Click "Add" -> ListView content does not change -> BUG
  8. Click "Cancel" at the bottom -> although there was no listener added to this button, both the ComboBox and the ListView show the correct values now. So, both containers seem to contain the correct values but they won't be rendered correctly until some UI refreshing is triggered.

Note that there are three important reqiurements to reproduce this bug (marked with "important"):

  1. Toggle managed state of the "Next" button when toggling the tabs to hide this button (usual way to hide a node along with visible state of the node, see JavaFX - setVisible doesn't "hide" the element)
  2. TabClosingPolicy.UNAVAILABLE (a very common case)
  3. a VBox in a VBox (which can easily happen in real life when nesting different javafx component nodes).

Is this bug already known, and does anybody know a workaround for this? I tried Platform.runLater(cancel.fire()) and similar things but without success.

Thank you for any hints, Peter.

Btw., apart from this, our company uses javafx for some years now. In our experience, it is very reliable and programming javafx is fun. I hope there is a simple solution for our problem :)

user27772
  • 522
  • 1
  • 4
  • 18
  • What happens if you would call `#requestLayout()` on the Nodes in question everytime you select an item in the ComboBox? Since clicking on the cancel Button wil not do something value related other than trigger a layout call due to its state changing, I suspect requesting a layout pass fill sufice? – n247s Sep 21 '19 at 22:22
  • What JRE and what OS are you using? – mentallurg Sep 21 '19 at 22:39
  • What version of JavaFX are you using? – Avi Sep 21 '19 at 22:46
  • @n247s: I tried `requestLayout()` a) after `setManaged()` in the existing tab change listener as well as `combo.getSelectionModel().selectedItemProperty().addListener((a, b, c) -> combo.requestLayout());`. It makes no difference, the bug remains. @mentallurg: I tried OpenJDK JRE 11.0.2 as well as OpenJDK JRE 13 on Windows 7. @Avi: I tried both JavaFX 11.0.2 as well as JavaFX 13. No difference, the bug remains. Any more ideas are appreciated. – user27772 Sep 21 '19 at 23:18
  • 1
    don't add nodes (Labels in your case) as items to the list – kleopatra Sep 22 '19 at 03:59
  • And what happens if you put the layout request in `Platform.runLater()`? – n247s Sep 22 '19 at 06:06
  • 2
    If you want to have a label in your `ListView` use a `CellFactory`. `ListView` should never use a node and should always use some normal Java type(non-primitive) or a class/object type created to model data. Examples `ListView`, `ListView`, `ListView`. Never `ListView`. – SedJ601 Sep 22 '19 at 06:18
  • Thanks for your hints, but none of them helped so far. @kleopatra: I changed to ListView but the problem remains (we actually use ListView extends Node> alot and didn't have any problems so far). @n247s: I added `add.setOnAction(e -> {list.getItems().add(combo.getSelectionModel().getSelectedItem()); Platform.runLater(() -> {tabs.requestLayout(); combo.requestLayout(); list.requestLayout();});});` without any difference. @Sedrick: I know about CellFactory (we use it for large lists) but even with ListView the problem remains (please try it yourself). Any more ideas? – user27772 Sep 22 '19 at 08:19
  • @Sedrick (and @kleopatra): I actually wonder what's the matter with not adding Nodes to ListView (and ComboBox). Is there any doc explaining why this shouldn't be done? We didn't have any problems doing this in the last years and for me it is quite handy because we can e. g. style items (of small lists) without the extra need a CellFactory. Thank you for a short explaination or link. – user27772 Sep 22 '19 at 08:38
  • https://stackoverflow.com/questions/20995444/what-are-virtualized-controls-mentioned-in-javafx-documentation – SedJ601 Sep 22 '19 at 13:22
  • I was not able to duplicate this in Java 8. I was able to duplicate it in Java 12.02. – SedJ601 Sep 22 '19 at 13:39
  • 1
    @Sedrick: Thanks for the link. I totally agree that for large lists cell factories make much sense. Further, thanks to confirming that Java 12 also shows the error. I can confirm as well that with my old Java 8 installation the code works as expected. So the bug was likely introduced with JavaFX 9,10 or 11. I made some more tests: Setting `TabPane.setClosable(false)` brings the same effect as `TabClosingPolicy.UNAVAILABLE`. The only workaround so far ist to hide the tab close button via `setStyle()`, then the code works fine as well. If time allows I'll create a bug report. – user27772 Sep 22 '19 at 20:31
  • One last trick I used for the javafx8 TableView resize issue, is resizing the first non-autosizing parent (in most cases the scene). I would increment/decrement the size with a verry small number (e.g. 0.00001), enough to cause listeners to update but not that big it would make a visual impact. For some reason that fixed the issue where `requestLayout` couldn't. I would be interested to see if that works with this case as well, as it would propably mean that some unmanaged child isn't updated properly – n247s Sep 23 '19 at 07:57
  • @n247s: Thanks for your hint. I tried to increment/decrement the width of the TabPane as well as its content within a tab selected listener as you suggested, but without success. Anyway, I found a workaround for my problem that I'll present as answer to my original question. – user27772 Sep 23 '19 at 08:27

1 Answers1

1

As discussed in the comments of the original post, the bug does not occur in JavaFX 8 (Oracle) but in later versions (OpenJFX). I found the following workaround:

public static void fixTabRendering(TabPane tabs) {
    if (tabs.getTabClosingPolicy() != TabClosingPolicy.UNAVAILABLE) return;
    tabs.setTabClosingPolicy(TabClosingPolicy.SELECTED_TAB);
    for (Node node : tabs.lookupAll(".tab-close-button")) {
        // hide "close" button to imitate TabClosingPolicy.UNAVAILABLE
        node.setStyle("-fx-background-color:transparent;-fx-shape:null;-fx-pref-width:0.001");
    }
}

This code should be run after showing the stage (otherwise lookupAll() returns null) as well as after adding tabs to the tab pane. The latter could be achieved via a tab listener:

tabs.getTabs().addListener((Change<?> change) ->
    Platform.runLater(() -> fixTabRendering(tabs)));

Platform.runLater() is required because otherwise lookupAll() may not return the nodes of the added tabs.

Maybe this solution can help someone :)

user27772
  • 522
  • 1
  • 4
  • 18