SwingNode rendering artifacts after size changed while hierarchically invisible

XMLWordPrintable

    • Type: Bug
    • Resolution: Unresolved
    • Priority: P3
    • tbd
    • Affects Version/s: jfx11, 8u471, jfx17, jfx21, jfx25, jfx26
    • Component/s: javafx
    • x86_64
    • windows

      ADDITIONAL SYSTEM INFORMATION :
      Windows 11, JDK Eclipse Temurin 25.0.1, JavaFX 19.0.2.1, 21.0.9, and 25.0.1

      A DESCRIPTION OF THE PROBLEM :
      A SwingNode frequently has rendering artifacts in its content JComponent after it is resized while the SwingNode is hierarchically invisible (for example, an ancestor's isVisible() is false) and then the SwingNode becomes hierarchically visible.

      A good example of this situation is when the SwingNode is in a deselected Tab of a TabPane while the TabPane is resized.

      Code can clear the artifacts by calling SwingNode.content.revalidate() and SwingNode.content.repaint(). Resizing the SwingNode while hierarchically visible also seems to reliably clear the artifacts.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Run the test case reproducer application SwingNodeRenderingBugReproducer.
      Resize the window rapidly and select different tabs a few times as described in the application's instruction Label.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      After resizing, SwingNodes should correctly render their content JComponents regardless of whether their Tabs were selected during resizing.
      ACTUAL -
      After a few attempts, you should observe that a newly selected Tab's JComponent content has obvious rendering artifacts.
      For example, sometimes the entire content area is black or blank.
      Sometimes, the rendered component exhibits visual artifacts where rectangular portions of the content appear to have been pseudo-randomly rendered in scattered incorrect locations within the SwingNode. These displaced fragments of the component's pixels are often inconsistent and appear as though different regions of the rendered content have been misaligned or misplaced.

      ---------- BEGIN SOURCE ----------
      package com.example.swingnodebugreproducer;

      import java.awt.*;
      import javafx.application.Application;
      import javafx.beans.InvalidationListener;
      import javafx.beans.property.ReadOnlyBooleanProperty;
      import javafx.beans.property.ReadOnlyBooleanWrapper;
      import javafx.embed.swing.SwingNode;
      import javafx.geometry.Pos;
      import javafx.scene.Node;
      import javafx.scene.Scene;
      import javafx.scene.control.Button;
      import javafx.scene.control.CheckBox;
      import javafx.scene.control.Label;
      import javafx.scene.control.Tab;
      import javafx.scene.control.TabPane;
      import javafx.scene.layout.BorderPane;
      import javafx.scene.layout.VBox;
      import javafx.stage.Stage;
      import javax.swing.*;
      import javax.swing.table.DefaultTableModel;

      /**
       * Reproduces SwingNode rendering artifacts with JavaFX 19.0.2.1, 21.0.9, and 25.0.1.
       */
      public class SwingNodeRenderingBugReproducer extends Application {

          @Override
          public void start(final Stage primaryStage) {
              final TabPane tabPane = new TabPane();

              for (int i = 1; i <= 4; i++) {
                  final Tab tab = createTabWithTable("Tab " + i);
                  tabPane.getTabs().add(tab);
              }

              final BorderPane root = new BorderPane(tabPane);
              root.setStyle("-fx-padding: 1em;");
              final Label label = new Label("""
                      After this window opens, resize it a few times and select a few different tabs.
                      Rapid, significant size changes seem to help reproduce the bug.
                      
                      After a few tries, one or more of the unselected tabs will have JComponent rendering artifacts,
                      and you will see the problem after you select the tab.
                      
                      Press the button below to clear the rendering artifacts once.
                      Resizing the window also seems to reliably clear the rendering artifacts.
                      
                      The bug seems to happen when the SwingNode size changes while it is hierarchically invisible,
                      and then transitions to hierarchically visible.
                      For example, when TabPaneSkin's TabContentRegion sets itself invisible when deselected, visible when selected.
                      
                      Select the checkbox below to clear rendering artifacts every time each SwingNode
                      becomes hierarchically visible.
                      Doing that seems to reliably compensate for the rendering bug.
                      """
              );
              label.setStyle("-fx-padding: 1em;");

              final Button button = new Button(
                      "Revalidate and repaint selected tab's SwingNode content");
              button.setOnAction(e -> {
                  final Tab selectedTab =
                          tabPane.getSelectionModel().getSelectedItem();
                  if (selectedTab != null) {
                      final SwingNode swingNode = (SwingNode) selectedTab.getContent();
                      final JComponent content = swingNode.getContent();
                      content.revalidate();
                      content.repaint();
                  }
              });

              final CheckBox checkBox = new CheckBox(
                      "Apply hierarchical SwingNode visibility workaround.\n" +
                              "While checked, the workaround seems to reliably remove rendering artifacts.");
              checkBox.selectedProperty().addListener(
                      observable -> {
                          if (checkBox.isSelected()) {
                              // To prevent confusion, clear rendering artifacts on the selected Tab now.
                              button.fire();
                          }
                      }
              );

              final VBox vBox = new VBox(label, button, checkBox);
              vBox.setStyle("-fx-spacing: 1em;");
              vBox.setAlignment(Pos.CENTER);
              root.setBottom(vBox);
              final Scene scene = new Scene(root, 800, 600);

              primaryStage.setTitle("SwingNode Rendering Bug Reproducer");
              primaryStage.setScene(scene);
              primaryStage.show();

              tabPane.getTabs().forEach(tab -> {
                  final SwingNode swingNode = (SwingNode) tab.getContent();

                  // Apply the HierarchicalVisibilityTracker.
                  HierarchicalVisibilityTracker tracker =
                          new HierarchicalVisibilityTracker(swingNode);

                  tracker.hierarchicallyVisibleProperty().addListener(
                          (obs, wasVisible, isNowHierarchicallyVisible) -> {
                              // If the SwingNode just became hierarchically visible and the
                              // checkbox is selected, apply the workaround to clear rendering artifacts.
                              if (isNowHierarchicallyVisible && checkBox.isSelected()) {
                                  SwingUtilities.invokeLater(() -> {
                                      JComponent content = swingNode.getContent();
                                      if (content != null) {
                                          // Clear rendering artifacts.
                                          content.revalidate();
                                          content.repaint();
                                      }
                                  });
                              }
                          });
              });
          }

          private Tab createTabWithTable(final String title) {
              final SwingNode swingNode = new SwingNode();
              final JPanel panel = new JPanel(new BorderLayout());
              final JTable table = createTable();

              final JScrollPane scrollPane = new JScrollPane(table);
              panel.add(scrollPane, BorderLayout.CENTER);
              swingNode.setContent(panel);

              final Tab tab = new Tab(title);
              tab.setContent(swingNode);
              return tab;
          }

          private JTable createTable() {
              final String[] columnNames =
                      {"Column 1", "Column 2", "Column 3", "Column 4"};
              final Object[][] emptyData = new Object[30][4];

              final DefaultTableModel tableModel =
                      new DefaultTableModel(emptyData, columnNames);
              final JTable table = new JTable(tableModel);
              for (int i = 0; i < 30; i++) {
                  table.setValueAt("Row " + (i + 1) + ", Col 1", i, 0);
                  table.setValueAt("Row " + (i + 1) + ", Col 2", i, 1);
                  table.setValueAt("Row " + (i + 1) + ", Col 3", i, 2);
                  table.setValueAt("Row " + (i + 1) + ", Col 4", i, 3);
              }
              return table;
          }

          private static class HierarchicalVisibilityTracker {

              private final ReadOnlyBooleanWrapper hierarchicallyVisible =
                      new ReadOnlyBooleanWrapper(false);

              private final Node nodeToTrack;

              public HierarchicalVisibilityTracker(Node nodeToTrack) {
                  this.nodeToTrack = nodeToTrack;

                  // Add listeners to track visibility of the node and its *current* ancestors.
                  // A production quality implementation would dynamically add and remove listeners
                  // as nodeToTrack's ancestry changes, and conditionally on whether nodeToTrack
                  // is contained in a Scene. But, in this bug reproducer we won't change
                  // nodeToTrack's ancestry or its Scene, so this simpler implementation suffices.
                  InvalidationListener visibilityListener =
                          obs -> updateHierarchicallyVisibleProperty();
                  Node node = nodeToTrack;
                  while (node != null) {
                      node.visibleProperty().addListener(visibilityListener);
                      node = node.getParent();
                  }

                  // Initial update of hierarchical visibility.
                  updateHierarchicallyVisibleProperty();
              }

              public ReadOnlyBooleanProperty hierarchicallyVisibleProperty() {
                  return hierarchicallyVisible.getReadOnlyProperty();
              }

              private void updateHierarchicallyVisibleProperty() {
                  Node node = nodeToTrack;
                  boolean visible = true;

                  // Traverse through nodeToTrack and its ancestors to check visibility
                  while (node != null) {
                      if (!node.isVisible()) {
                          visible = false;
                          break;
                      }
                      node = node.getParent();
                  }

                  hierarchicallyVisible.set(visible);
              }
          }

          static void main(String[] args) {
              Application.launch(SwingNodeRenderingBugReproducer.class, args);
          }
      }

      ---------- END SOURCE ----------

        1. Screenshot.png
          56 kB
          Praveen Narayanaswamy
        2. SwingNodeRenderingBugReproducer.java
          8 kB
          Praveen Narayanaswamy

            Assignee:
            Prasanta Sadhukhan
            Reporter:
            Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: