FULL PRODUCT VERSION :
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
ADDITIONAL OS VERSION INFORMATION :
Windows 7 Pro 64-bit
Microsoft Windows [Version 6.1.7601]
A DESCRIPTION OF THE PROBLEM :
Like its said in the title, ComboBox's prefWidth relates on ComboBoxListView's prefWidth, which relates to ComboBox's width. That is hilarious, because when comboBox is resizable width layout, its prefWidth changes to width after selecting any value.
If we put ComboBox that has 100px prefWidth into a layout and setup it to fill whole layout's line, which is 1200px long, then we select a value, the ComboBox's prefWidth becomes 1200px, and causes layout issues. That causes real issues with layouting - jumping of columns, etc.
In moreover ComboBox's initial prefWidth is not measured basing on stylized ListView's items - for instance no padding is applied.
Our workaround fixes both issues.
I have included workaround into sample test application, so you could test it easily.
TextFields does not matter there, just to show that layout has multiple columns.
Please check methods that we've overwritten in our workaround and give users a better fix.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Open attached sample.
2. Watch width and computedPrefWidth of first ComboBox while resizing application's window..
3. Resize window to be wide enough that width>computedPrefWidth.
4. Change first ComboBox's value - select any value.
5. Repeat steps 2-3 for second ComboBox.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
After 2. The computedPrefWidth value does not change, was same as initial, but width corresponds ComboBox's actual width.
After 4. first ComboBox's computedPrefWidth does not change.
After 5. second ComboBox's computedPrefWidth does not change.
ACTUAL -
After 2. The computedPrefWidth value does not change, was same as initial, but width corresponds ComboBox's actual width.
After 4. first ComboBox's computedPrefWidth changes - this is bad
After 5. second ComboBox's computedPrefWidth does not change.
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
package sample;
import java.util.Comparator;
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application
{
public static void main( String[] args )
{
Application.launch( args );
}
@Override
public void start( Stage stage )
{
System.err.println( System.getProperty( "javafx.runtime.version" ) );
Parent content = createScrollPane();
Scene scene = new Scene( content, 800, 600 );
stage.setScene( scene );
stage.show();
}
public Parent createScrollPane()
{
final VBox vBox = new VBox();
final Label description = new Label(
"Please change ComboBox's selected value, when width is greater than prefWidth, and watch prefWidth value." );
description.setWrapText( true );
VBox.setMargin( description, new Insets( 5 ) );
final TitledPane buggy = new TitledPane( "ComboBox original behaviour", createContent( false ) );
buggy.setCollapsible( false );
final TitledPane workaround = new TitledPane( "ComboBox with workaround", createContent( true ) );
workaround.setCollapsible( false );
vBox.getChildren().addAll( description, buggy, workaround );
final ScrollPane content = new ScrollPane( vBox );
content.setFitToHeight( true );
content.setFitToWidth( true );
return content;
}
private Node createContent( boolean isWorkaround )
{
final GridPane pane = new GridPane();
// pane.setGridLinesVisible(true);
pane.setHgap( 4 );
pane.setVgap( 4 );
pane.setPadding( new Insets( 10 ) );
final TextField textField1 = new TextField( "test" );
pane.add( textField1, 0, 0, 2, 1 );
GridPane.setHgrow( textField1, Priority.ALWAYS );
GridPane.setFillWidth( textField1, true );
GridPane.setHalignment( textField1, HPos.CENTER );
final TextField textField2 = new TextField( "test2" );
pane.add( textField2, 2, 0, 1, 1 );
GridPane.setHgrow( textField2, Priority.ALWAYS );
GridPane.setFillWidth( textField2, true );
GridPane.setHalignment( textField2, HPos.CENTER );
final DoubleProperty computedPrefWidthProperty = new SimpleDoubleProperty();
final ComboBox< Object > comboBox = createComboBox( computedPrefWidthProperty, isWorkaround );
pane.add( comboBox, 0, 1, 3, 1 );
pane.add( new Label( "combo's width: " ), 0, 2, 1, 1 );
final TextField widthTextField = new TextField();
widthTextField.setEditable( false );
widthTextField.textProperty().bind( comboBox.widthProperty().asString() );
GridPane.setHgrow( widthTextField, Priority.ALWAYS );
GridPane.setFillWidth( widthTextField, true );
pane.add( widthTextField, 1, 2, 2, 1 );
pane.add( new Label( "combo's computedPrefWidth: " ), 0, 3, 2, 1 );
final TextField prefWidthTextField = new TextField();
prefWidthTextField.setEditable( false );
prefWidthTextField.textProperty().bind( computedPrefWidthProperty.asString() );
pane.add( prefWidthTextField, 1, 3, 2, 1 );
GridPane.setHgrow( prefWidthTextField, Priority.ALWAYS );
GridPane.setFillWidth( prefWidthTextField, true );
final Button addLongestItem = new Button( "Add longest item to combo" );
addLongestItem.setOnAction( event ->
{
final Object longestItem =
comboBox.getItems().stream().max( Comparator.comparingInt( aO -> aO.toString().length() ) )
.get();
final Object newLongestItem = longestItem.toString() + "-even-longer";
comboBox.getItems().add( newLongestItem );
} );
pane.add( addLongestItem, 0, 4, 1, 1 );
return pane;
}
private ComboBox< Object > createComboBox( DoubleProperty computedPrefWidthProperty,
boolean isWorkaroundCombo )
{
final ComboBox< Object > combo;
if( isWorkaroundCombo )
{
combo = new ComboBoxWithWorkaround()
{
@Override
protected double computePrefWidth( final double height )
{
double ret = super.computePrefWidth( height );
computedPrefWidthProperty.setValue( ret );
return ret;
}
};
}
else
{
combo = new ComboBox< Object >()
{
@Override
protected double computePrefWidth( final double height )
{
double ret = super.computePrefWidth( height );
computedPrefWidthProperty.setValue( ret );
return ret;
}
};
}
combo.setItems( FXCollections.observableArrayList( "One", "Two",
"long long long three long long long three long long long three" ) );
combo.setMaxWidth( Double.MAX_VALUE );
combo.getSelectionModel().select( "One" );
combo.setEditable( false );
GridPane.setHgrow( combo, Priority.ALWAYS );
GridPane.setFillWidth( combo, true );
GridPane.setHalignment( combo, HPos.CENTER );
return combo;
}
private static class ComboBoxWithWorkaround extends ComboBox< Object >
{
@Override
protected Skin< ? > createDefaultSkin()
{
@SuppressWarnings( "restriction" )
Skin skin = new ComboBoxListViewSkin< Object >( this )
{
@SuppressWarnings( "restriction" )
protected double computePrefWidth( double height, double topInset, double rightInset,
double bottomInset, double leftInset )
{
double computePrefWidth =
super.computePrefWidth( height, topInset, rightInset, bottomInset, leftInset );
{
try
{
double result =
superComputePrefWidth( height, topInset, rightInset, bottomInset, leftInset );
return result;
}
catch( Exception aEx )
{
// use standard if skin gets errors
}
}
return computePrefWidth;
}
;
protected double superComputePrefWidth( double height, double topInset, double rightInset,
double bottomInset, double leftInset )
{
if( getDisplayNode() == null )
{
updateDisplayArea();
}
final double arrowWidth = snapSize( arrow.prefWidth( -1 ) );
final double arrowButtonWidth = isButtonMode() ?
0 :
arrowButton.snappedLeftInset() + arrowWidth + arrowButton.snappedRightInset();
double displayNodeWidth = 0;
if( getDisplayNode() instanceof ListCell )
{
ListCell< Object > buttonCell = (ListCell)getDisplayNode();
int index = buttonCell.getIndex();
for( int i = 0; i < getItems().size(); i++ )
{
buttonCell.updateIndex( i );
displayNodeWidth = Math.max( displayNodeWidth, buttonCell.prefWidth( height ) );
}
buttonCell.updateIndex( index );
}
displayNodeWidth = Math.max( 50, Math.max( displayNodeWidth,
getDisplayNode() == null ? 0 : getDisplayNode().prefWidth( height ) ) );
final double totalWidth = displayNodeWidth + arrowButtonWidth;
return leftInset + totalWidth + rightInset;
}
private boolean isButtonMode()
{
return getMode() != null && ( (Object)getMode() ).toString().toUpperCase()
.startsWith( "BUTTON" );
}
};
return skin;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Workaround is included into source code.
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
ADDITIONAL OS VERSION INFORMATION :
Windows 7 Pro 64-bit
Microsoft Windows [Version 6.1.7601]
A DESCRIPTION OF THE PROBLEM :
Like its said in the title, ComboBox's prefWidth relates on ComboBoxListView's prefWidth, which relates to ComboBox's width. That is hilarious, because when comboBox is resizable width layout, its prefWidth changes to width after selecting any value.
If we put ComboBox that has 100px prefWidth into a layout and setup it to fill whole layout's line, which is 1200px long, then we select a value, the ComboBox's prefWidth becomes 1200px, and causes layout issues. That causes real issues with layouting - jumping of columns, etc.
In moreover ComboBox's initial prefWidth is not measured basing on stylized ListView's items - for instance no padding is applied.
Our workaround fixes both issues.
I have included workaround into sample test application, so you could test it easily.
TextFields does not matter there, just to show that layout has multiple columns.
Please check methods that we've overwritten in our workaround and give users a better fix.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Open attached sample.
2. Watch width and computedPrefWidth of first ComboBox while resizing application's window..
3. Resize window to be wide enough that width>computedPrefWidth.
4. Change first ComboBox's value - select any value.
5. Repeat steps 2-3 for second ComboBox.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
After 2. The computedPrefWidth value does not change, was same as initial, but width corresponds ComboBox's actual width.
After 4. first ComboBox's computedPrefWidth does not change.
After 5. second ComboBox's computedPrefWidth does not change.
ACTUAL -
After 2. The computedPrefWidth value does not change, was same as initial, but width corresponds ComboBox's actual width.
After 4. first ComboBox's computedPrefWidth changes - this is bad
After 5. second ComboBox's computedPrefWidth does not change.
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
package sample;
import java.util.Comparator;
import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.FXCollections;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxTest extends Application
{
public static void main( String[] args )
{
Application.launch( args );
}
@Override
public void start( Stage stage )
{
System.err.println( System.getProperty( "javafx.runtime.version" ) );
Parent content = createScrollPane();
Scene scene = new Scene( content, 800, 600 );
stage.setScene( scene );
stage.show();
}
public Parent createScrollPane()
{
final VBox vBox = new VBox();
final Label description = new Label(
"Please change ComboBox's selected value, when width is greater than prefWidth, and watch prefWidth value." );
description.setWrapText( true );
VBox.setMargin( description, new Insets( 5 ) );
final TitledPane buggy = new TitledPane( "ComboBox original behaviour", createContent( false ) );
buggy.setCollapsible( false );
final TitledPane workaround = new TitledPane( "ComboBox with workaround", createContent( true ) );
workaround.setCollapsible( false );
vBox.getChildren().addAll( description, buggy, workaround );
final ScrollPane content = new ScrollPane( vBox );
content.setFitToHeight( true );
content.setFitToWidth( true );
return content;
}
private Node createContent( boolean isWorkaround )
{
final GridPane pane = new GridPane();
// pane.setGridLinesVisible(true);
pane.setHgap( 4 );
pane.setVgap( 4 );
pane.setPadding( new Insets( 10 ) );
final TextField textField1 = new TextField( "test" );
pane.add( textField1, 0, 0, 2, 1 );
GridPane.setHgrow( textField1, Priority.ALWAYS );
GridPane.setFillWidth( textField1, true );
GridPane.setHalignment( textField1, HPos.CENTER );
final TextField textField2 = new TextField( "test2" );
pane.add( textField2, 2, 0, 1, 1 );
GridPane.setHgrow( textField2, Priority.ALWAYS );
GridPane.setFillWidth( textField2, true );
GridPane.setHalignment( textField2, HPos.CENTER );
final DoubleProperty computedPrefWidthProperty = new SimpleDoubleProperty();
final ComboBox< Object > comboBox = createComboBox( computedPrefWidthProperty, isWorkaround );
pane.add( comboBox, 0, 1, 3, 1 );
pane.add( new Label( "combo's width: " ), 0, 2, 1, 1 );
final TextField widthTextField = new TextField();
widthTextField.setEditable( false );
widthTextField.textProperty().bind( comboBox.widthProperty().asString() );
GridPane.setHgrow( widthTextField, Priority.ALWAYS );
GridPane.setFillWidth( widthTextField, true );
pane.add( widthTextField, 1, 2, 2, 1 );
pane.add( new Label( "combo's computedPrefWidth: " ), 0, 3, 2, 1 );
final TextField prefWidthTextField = new TextField();
prefWidthTextField.setEditable( false );
prefWidthTextField.textProperty().bind( computedPrefWidthProperty.asString() );
pane.add( prefWidthTextField, 1, 3, 2, 1 );
GridPane.setHgrow( prefWidthTextField, Priority.ALWAYS );
GridPane.setFillWidth( prefWidthTextField, true );
final Button addLongestItem = new Button( "Add longest item to combo" );
addLongestItem.setOnAction( event ->
{
final Object longestItem =
comboBox.getItems().stream().max( Comparator.comparingInt( aO -> aO.toString().length() ) )
.get();
final Object newLongestItem = longestItem.toString() + "-even-longer";
comboBox.getItems().add( newLongestItem );
} );
pane.add( addLongestItem, 0, 4, 1, 1 );
return pane;
}
private ComboBox< Object > createComboBox( DoubleProperty computedPrefWidthProperty,
boolean isWorkaroundCombo )
{
final ComboBox< Object > combo;
if( isWorkaroundCombo )
{
combo = new ComboBoxWithWorkaround()
{
@Override
protected double computePrefWidth( final double height )
{
double ret = super.computePrefWidth( height );
computedPrefWidthProperty.setValue( ret );
return ret;
}
};
}
else
{
combo = new ComboBox< Object >()
{
@Override
protected double computePrefWidth( final double height )
{
double ret = super.computePrefWidth( height );
computedPrefWidthProperty.setValue( ret );
return ret;
}
};
}
combo.setItems( FXCollections.observableArrayList( "One", "Two",
"long long long three long long long three long long long three" ) );
combo.setMaxWidth( Double.MAX_VALUE );
combo.getSelectionModel().select( "One" );
combo.setEditable( false );
GridPane.setHgrow( combo, Priority.ALWAYS );
GridPane.setFillWidth( combo, true );
GridPane.setHalignment( combo, HPos.CENTER );
return combo;
}
private static class ComboBoxWithWorkaround extends ComboBox< Object >
{
@Override
protected Skin< ? > createDefaultSkin()
{
@SuppressWarnings( "restriction" )
Skin skin = new ComboBoxListViewSkin< Object >( this )
{
@SuppressWarnings( "restriction" )
protected double computePrefWidth( double height, double topInset, double rightInset,
double bottomInset, double leftInset )
{
double computePrefWidth =
super.computePrefWidth( height, topInset, rightInset, bottomInset, leftInset );
{
try
{
double result =
superComputePrefWidth( height, topInset, rightInset, bottomInset, leftInset );
return result;
}
catch( Exception aEx )
{
// use standard if skin gets errors
}
}
return computePrefWidth;
}
;
protected double superComputePrefWidth( double height, double topInset, double rightInset,
double bottomInset, double leftInset )
{
if( getDisplayNode() == null )
{
updateDisplayArea();
}
final double arrowWidth = snapSize( arrow.prefWidth( -1 ) );
final double arrowButtonWidth = isButtonMode() ?
0 :
arrowButton.snappedLeftInset() + arrowWidth + arrowButton.snappedRightInset();
double displayNodeWidth = 0;
if( getDisplayNode() instanceof ListCell )
{
ListCell< Object > buttonCell = (ListCell)getDisplayNode();
int index = buttonCell.getIndex();
for( int i = 0; i < getItems().size(); i++ )
{
buttonCell.updateIndex( i );
displayNodeWidth = Math.max( displayNodeWidth, buttonCell.prefWidth( height ) );
}
buttonCell.updateIndex( index );
}
displayNodeWidth = Math.max( 50, Math.max( displayNodeWidth,
getDisplayNode() == null ? 0 : getDisplayNode().prefWidth( height ) ) );
final double totalWidth = displayNodeWidth + arrowButtonWidth;
return leftInset + totalWidth + rightInset;
}
private boolean isButtonMode()
{
return getMode() != null && ( (Object)getMode() ).toString().toUpperCase()
.startsWith( "BUTTON" );
}
};
return skin;
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Workaround is included into source code.