-
Bug
-
Resolution: Fixed
-
P3
-
jfx11, 8, 9, 10
-
x86_64
-
generic
ADDITIONAL SYSTEM INFORMATION :
Windows 7 Professional 64-bit: Microsoft Windows [Version 6.1.7601]
java 10.0.2 2018-07-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.2+13)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)
A DESCRIPTION OF THE PROBLEM :
Setting default button, ButtonSkin#defaultButtonRunnable to be added to Scene#getAccellerators() on new KeyCombination( KeyCode.ENTER ) in ButtonSkin#setDefaultButton(boolean).
Acceleration is never removed from Scene's accelerator map, only replaced by a new one - that causes memory leak.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Run attached test case application.
2. Press "Add tab".
3. Focus text field in the center, press ENTER on keyboard".
4. Remove that tab by clicking "X" in tab pane's header.
5. Add breakpoint to ButtonSkin#defaultButtonRunnable first line and press ENTER on keyboard or search for ButtonSkin in memory dump.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
After 3. Default action should be run - alert dialog will be shown.
After 5. ButtonSkin#defaultButtonRunnable is removed from the memory and breakpoint is not activated after pressing ENTER.
ACTUAL -
After 3. Default action should be run - alert dialog will be shown.
After 5. ButtonSkin#defaultButtonRunnable is held in memory, because of scene's acceleration and breakpoint is activated after pressing ENTER, but default action is not fired.
---------- BEGIN SOURCE ----------
package com.example;
import java.util.Objects;
import java.util.function.Consumer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Main extends Application
{
private int counter = 0;
@Override
public void start( final Stage primaryStage )
{
Scene scene = new Scene( createContent(), 800, 600 );
primaryStage.setScene( scene );
primaryStage.show();
}
private Parent createContent()
{
final BorderPane container = new BorderPane();
container.setPadding( new Insets( 5 ) );
final TabPane tabs = new TabPane();
container.setCenter( tabs );
final HBox aHBox = new HBox();
final Button addButton = new Button( "Add tab" );
addButton.setOnAction( event -> addTab( tabs ) );
final Button addWorkaroundButton = new Button( "Add tab with workaround" );
addWorkaroundButton.setOnAction( event -> addWorkaroundTab( tabs ) );
final Button gcButton = new Button( "Run GC" );
gcButton.setOnAction( event -> System.gc() );
aHBox.getChildren().addAll( addButton, addWorkaroundButton, gcButton );
container.setBottom( aHBox );
BorderPane.setAlignment( aHBox, Pos.CENTER );
return container;
}
private void addWorkaroundTab( final TabPane aTabs )
{
final String name = "Tab " + ++counter;
final Tab aTab = new Tab( name );
final DefaultButtonStateHelper defaultButtonStateHelper = new DefaultButtonStateHelper();
aTab.setContent( createTabContent( name, button -> {
defaultButtonStateHelper.applyDefaultButtonWorkaround( button, true );
} ) );
aTab.setOnClosed( event -> {
defaultButtonStateHelper.cleanup();
} );
aTabs.getTabs().add( aTab );
}
private void addTab( final TabPane aTabs )
{
final String name = "Tab " + ++counter;
final Tab aTab = new Tab( name );
aTab.setContent( createTabContent( name, button -> button.setDefaultButton( true ) ) );
aTabs.getTabs().add( aTab );
}
private Node createTabContent( final String aName, final Consumer< Button > aDefaultStateSetter )
{
final BorderPane ret = new BorderPane( );
final TextField aDummy = new TextField( "Read-only Dummy" );
aDummy.setEditable( false );
ret.setCenter( aDummy );
final Button dummyButton = new Button( "DummyButton" );
final Button defaultButton = new Button( "defaultButton" );
aDefaultStateSetter.accept( defaultButton );
defaultButton.setOnAction( event -> {
final Alert aAlert =
new Alert( Alert.AlertType.INFORMATION, "Default button was run for " + aName,
ButtonType.CLOSE );
aAlert.showAndWait();
} );
final ButtonBar aButtonBar = new ButtonBar();
aButtonBar.getButtons().addAll( dummyButton, defaultButton );
ret.setBottom( aButtonBar );
return ret;
}
public static void main(String[] args) {
System.err.println( System.getProperty( "javafx.runtime.version" ) );
Application.launch( args );
}
public static class DefaultButtonStateHelper
{
private static final KeyCodeCombination DEFAULT_BUTTON_KEY_COMBINATION =
new KeyCodeCombination( KeyCode.ENTER );
private ChangeListener< Scene > defaultButtonSceneListener;
private Button lastDefaultButton;
private static void removeDefaultbuttonAcceleratorFromScene( final Scene aScene )
{
if( aScene == null )
{
return;
}
aScene.getAccelerators().remove( DEFAULT_BUTTON_KEY_COMBINATION );
}
public void applyDefaultButtonWorkaround( final Button aButton, final boolean isDefaultButton )
{
Objects.requireNonNull( aButton );
if( isDefaultButton )
{
if( aButton != lastDefaultButton )
{
cleanup();
lastDefaultButton = aButton;
}
if( defaultButtonSceneListener == null )
{
defaultButtonSceneListener = ( observable, oldValue, newValue ) -> {
aButton.setDefaultButton( newValue != null );
if( newValue == null )
{
removeDefaultbuttonAcceleratorFromScene( oldValue );
}
};
aButton.sceneProperty().addListener( defaultButtonSceneListener );
}
}
else
{
if( aButton != lastDefaultButton )
{
throw new IllegalStateException(
"Last default button which has been registered here is different than given button." );
}
cleanup();
}
}
public void cleanup()
{
if( lastDefaultButton != null && defaultButtonSceneListener != null )
{
lastDefaultButton.sceneProperty().removeListener( defaultButtonSceneListener );
}
defaultButtonSceneListener = null;
lastDefaultButton = null;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
added to executable test case, but instead of "Add tab", press "Add tab with workaround"
FREQUENCY : always
Windows 7 Professional 64-bit: Microsoft Windows [Version 6.1.7601]
java 10.0.2 2018-07-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.2+13)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)
A DESCRIPTION OF THE PROBLEM :
Setting default button, ButtonSkin#defaultButtonRunnable to be added to Scene#getAccellerators() on new KeyCombination( KeyCode.ENTER ) in ButtonSkin#setDefaultButton(boolean).
Acceleration is never removed from Scene's accelerator map, only replaced by a new one - that causes memory leak.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Run attached test case application.
2. Press "Add tab".
3. Focus text field in the center, press ENTER on keyboard".
4. Remove that tab by clicking "X" in tab pane's header.
5. Add breakpoint to ButtonSkin#defaultButtonRunnable first line and press ENTER on keyboard or search for ButtonSkin in memory dump.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
After 3. Default action should be run - alert dialog will be shown.
After 5. ButtonSkin#defaultButtonRunnable is removed from the memory and breakpoint is not activated after pressing ENTER.
ACTUAL -
After 3. Default action should be run - alert dialog will be shown.
After 5. ButtonSkin#defaultButtonRunnable is held in memory, because of scene's acceleration and breakpoint is activated after pressing ENTER, but default action is not fired.
---------- BEGIN SOURCE ----------
package com.example;
import java.util.Objects;
import java.util.function.Consumer;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class Main extends Application
{
private int counter = 0;
@Override
public void start( final Stage primaryStage )
{
Scene scene = new Scene( createContent(), 800, 600 );
primaryStage.setScene( scene );
primaryStage.show();
}
private Parent createContent()
{
final BorderPane container = new BorderPane();
container.setPadding( new Insets( 5 ) );
final TabPane tabs = new TabPane();
container.setCenter( tabs );
final HBox aHBox = new HBox();
final Button addButton = new Button( "Add tab" );
addButton.setOnAction( event -> addTab( tabs ) );
final Button addWorkaroundButton = new Button( "Add tab with workaround" );
addWorkaroundButton.setOnAction( event -> addWorkaroundTab( tabs ) );
final Button gcButton = new Button( "Run GC" );
gcButton.setOnAction( event -> System.gc() );
aHBox.getChildren().addAll( addButton, addWorkaroundButton, gcButton );
container.setBottom( aHBox );
BorderPane.setAlignment( aHBox, Pos.CENTER );
return container;
}
private void addWorkaroundTab( final TabPane aTabs )
{
final String name = "Tab " + ++counter;
final Tab aTab = new Tab( name );
final DefaultButtonStateHelper defaultButtonStateHelper = new DefaultButtonStateHelper();
aTab.setContent( createTabContent( name, button -> {
defaultButtonStateHelper.applyDefaultButtonWorkaround( button, true );
} ) );
aTab.setOnClosed( event -> {
defaultButtonStateHelper.cleanup();
} );
aTabs.getTabs().add( aTab );
}
private void addTab( final TabPane aTabs )
{
final String name = "Tab " + ++counter;
final Tab aTab = new Tab( name );
aTab.setContent( createTabContent( name, button -> button.setDefaultButton( true ) ) );
aTabs.getTabs().add( aTab );
}
private Node createTabContent( final String aName, final Consumer< Button > aDefaultStateSetter )
{
final BorderPane ret = new BorderPane( );
final TextField aDummy = new TextField( "Read-only Dummy" );
aDummy.setEditable( false );
ret.setCenter( aDummy );
final Button dummyButton = new Button( "DummyButton" );
final Button defaultButton = new Button( "defaultButton" );
aDefaultStateSetter.accept( defaultButton );
defaultButton.setOnAction( event -> {
final Alert aAlert =
new Alert( Alert.AlertType.INFORMATION, "Default button was run for " + aName,
ButtonType.CLOSE );
aAlert.showAndWait();
} );
final ButtonBar aButtonBar = new ButtonBar();
aButtonBar.getButtons().addAll( dummyButton, defaultButton );
ret.setBottom( aButtonBar );
return ret;
}
public static void main(String[] args) {
System.err.println( System.getProperty( "javafx.runtime.version" ) );
Application.launch( args );
}
public static class DefaultButtonStateHelper
{
private static final KeyCodeCombination DEFAULT_BUTTON_KEY_COMBINATION =
new KeyCodeCombination( KeyCode.ENTER );
private ChangeListener< Scene > defaultButtonSceneListener;
private Button lastDefaultButton;
private static void removeDefaultbuttonAcceleratorFromScene( final Scene aScene )
{
if( aScene == null )
{
return;
}
aScene.getAccelerators().remove( DEFAULT_BUTTON_KEY_COMBINATION );
}
public void applyDefaultButtonWorkaround( final Button aButton, final boolean isDefaultButton )
{
Objects.requireNonNull( aButton );
if( isDefaultButton )
{
if( aButton != lastDefaultButton )
{
cleanup();
lastDefaultButton = aButton;
}
if( defaultButtonSceneListener == null )
{
defaultButtonSceneListener = ( observable, oldValue, newValue ) -> {
aButton.setDefaultButton( newValue != null );
if( newValue == null )
{
removeDefaultbuttonAcceleratorFromScene( oldValue );
}
};
aButton.sceneProperty().addListener( defaultButtonSceneListener );
}
}
else
{
if( aButton != lastDefaultButton )
{
throw new IllegalStateException(
"Last default button which has been registered here is different than given button." );
}
cleanup();
}
}
public void cleanup()
{
if( lastDefaultButton != null && defaultButtonSceneListener != null )
{
lastDefaultButton.sceneProperty().removeListener( defaultButtonSceneListener );
}
defaultButtonSceneListener = null;
lastDefaultButton = null;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
added to executable test case, but instead of "Add tab", press "Add tab with workaround"
FREQUENCY : always
- relates to
-
JDK-8236840 Memory leak when switching ButtonSkin
-
- Resolved
-