A DESCRIPTION OF THE PROBLEM :
Consider a ButtonBar in which one Button's text is wider than the others', and when the ButtonBar has the widest computed pref width in its Parent.
Problem 1: The initial call to ButtonBar.computePrefWidth(double) happens before the call to ButtonBarSkin.resizeButtons(). Thus, the Buttons compute their initial (non-uniform) pref widths. If the ButtonBar is widest child in its Parent, then the Parent is laid out too narrow to accommodate the true (eventual) pref widths of the Buttons. Then, the call to ButtonBarSkin.resizeButtons() happens, and the Buttons get their pref widths set to the pref width of the Button with the widest text. Then, there is not enough width in the Parent, and thus one or more Buttons' text gets truncated.
Problem 2: The ButtonBarSkin pref width computation ignores insets: SkinBase.computePrefWidth(double,double,double,double,double) ignores the insets values, instead using only ButtonBar's child HBox's minX and prefWidth. Thus, if the developer has set any padding in the ButtonBar, then ButtonBarSkin computes a pref width that is too narrow.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the executable test case application. Observe that the Stage initializes at the initial pref width of the VBox, which is the pref width of its widest child, which later truncates the longest Button text after the ButtonBarSkin sets the uniform pref button widths.
Observe that the pref widths of the ButtonBars are equal, despite the padding in the otherwise duplicate second ButtonBar. Stretch the width of the Stage until it reaches the pref width of the ButtonBars. Observe that the ButtonBar with padding still truncates the longest text. The ButtonBar without padding does not truncate the longest text, because it computes a correct pref width.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
ButtonBar Button text should not be truncated if the ButtonBar is laid out at its initial preferred width.
ButtonBar Button text should not be truncated if the ButtonBar has padding and the ButtonBar is laid out at its final preferred width.
ACTUAL -
ButtonBar Button text is truncated.
---------- BEGIN SOURCE ----------
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* An example of truncated text in Buttons in ButtonBars.
**/
public class ButtonBarTruncatedTextExample extends Application {
public static void main( final String[] args ) {
launch( args );
}
final Label buttonBarWithoutPaddingLabel = new Label();
final ButtonBar buttonBarWithoutPadding = createButtonBar();
final Label buttonBarWithPaddingLabel = new Label();
final ButtonBar buttonBarWithPadding = createButtonBar();
@Override
public void start( final Stage primaryStage ) throws Exception {
buttonBarWithoutPadding.needsLayoutProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
buttonBarWithoutPadding.widthProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
buttonBarWithPadding.setStyle( "-fx-padding: 0 1em 0 1em;" );
buttonBarWithPadding.needsLayoutProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
buttonBarWithPadding.widthProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
primaryStage.setScene( new Scene( new VBox(
new Label( "ButtonBar without padding:" ),
buttonBarWithoutPaddingLabel,
buttonBarWithoutPadding,
new Label( "ButtonBar with padding:" ),
buttonBarWithPaddingLabel,
buttonBarWithPadding ) ) );
primaryStage.show();
}
private void updateLabels() {
buttonBarWithoutPaddingLabel.setText( "prefWidth(-1): " + buttonBarWithoutPadding.prefWidth( -1 ) +
", width: " + buttonBarWithoutPadding.getWidth() );
buttonBarWithPaddingLabel.setText( "prefWidth(-1): " + buttonBarWithPadding.prefWidth( -1 ) +
", width: " + buttonBarWithPadding.getWidth() );
}
private ButtonBar createButtonBar() {
final ButtonBar buttonBar = new ButtonBar();
final Button yesButton = new Button( "Yes" );
ButtonBar.setButtonData( yesButton, ButtonBar.ButtonData.YES );
final Button noButton = new Button( "No" );
ButtonBar.setButtonData( noButton, ButtonBar.ButtonData.NO );
final Button cancelButton = new Button( "Exit application" );
ButtonBar.setButtonData( cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE );
buttonBar.getButtons().addAll( yesButton, noButton, cancelButton );
return buttonBar;
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Set this derived class of ButtonBarSkin as the Skin class for ButtonBar.
It can't work around the problem by calling resizeButtons() earlier because that is, unfortunately, a private method even though the default Skins were made public.
So, instead, it sets the button widths to non-uniform.
Also, it overrides computePrefWidth(...) to include the insets in the computation.
import javafx.collections.ListChangeListener;
import javafx.scene.Node;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.skin.ButtonBarSkin;
import javafx.scene.layout.HBox;
/**
* Works around a Button text truncation problem in {@link ButtonBarSkin}.
*
* <P></P>
* The initial call to {@link ButtonBarSkin#computePrefWidth(double, double, double, double, double)}
* happens before any call to {@link ButtonBarSkin#resizeButtons()}.
* If one Button's text is wider than the others, and wider than its OS-dependent min width,
* and the {@link ButtonBar} is the widest child of its Parent,
* then the Parent and thus the enclosing Stage will get sized too narrow to accommodate the preferred width
* of the ButtonBar after the later call to {@code resizeButtons()} sets the Buttons to uniform preferred width.
* This causes truncation of the wider text.
* <P></P>
* This class sets all of the Buttons to "not uniformly sized" so that the initial computed pref width
* is sufficient to display the text in all of the Buttons.</LI>
* <P></P>
* <P>This class also implements {@link #computePrefWidth(double, double, double, double, double)} to work around the
* superclass implementation's omission of the insets (if any) from its computation.</P>
*
* Filed a JavaFX bug: TODO
**/
public class ButtonBarPreventTruncatedTextSkin extends ButtonBarSkin {
public ButtonBarPreventTruncatedTextSkin( final ButtonBar control ) {
super( control );
// final ChangeListener<Parent> dialogMinWidthSetter = ( __, ___, parent ) -> {
// if( parent instanceof DialogPane ) {
// control.setMinWidth( Region.USE_PREF_SIZE );
// }
// };
// control.parentProperty().addListener( dialogMinWidthSetter );
// dialogMinWidthSetter.changed( null, null, control.getParent() ); // Initial update.
final ListChangeListener<? super Node> buttonsChangeListener = ___ ->
control.getButtons().forEach( child -> {
ButtonBar.setButtonUniformSize( child, false ); // Non-uniform size for all.
} );
control.getButtons().addListener( buttonsChangeListener );
buttonsChangeListener.onChanged( null ); // Initial update.
}
/**
* Workaround for superclass implementation's omission of the insets.
*/
@Override
protected double computePrefWidth( final double height,
final double topInset,
final double rightInset,
final double bottomInset,
final double leftInset ) {
// JavaFX's superclass method ignores the insets.
// If we have the single "container" child that we expect, then use a computation that includes the insets:
if( getChildren().size() == 1 && getChildren().get( 0 ) instanceof HBox &&
getChildren().get( 0 ).getStyleClass().contains( "container" ) ) {
return leftInset + getChildren().get( 0 ).prefWidth( height ) + rightInset;
} else {
// We must be using a different version of the superclass with a significant code change. Defer to it.
return super.computePrefWidth( height, topInset, rightInset, bottomInset, leftInset );
}
}
}
FREQUENCY : always
Consider a ButtonBar in which one Button's text is wider than the others', and when the ButtonBar has the widest computed pref width in its Parent.
Problem 1: The initial call to ButtonBar.computePrefWidth(double) happens before the call to ButtonBarSkin.resizeButtons(). Thus, the Buttons compute their initial (non-uniform) pref widths. If the ButtonBar is widest child in its Parent, then the Parent is laid out too narrow to accommodate the true (eventual) pref widths of the Buttons. Then, the call to ButtonBarSkin.resizeButtons() happens, and the Buttons get their pref widths set to the pref width of the Button with the widest text. Then, there is not enough width in the Parent, and thus one or more Buttons' text gets truncated.
Problem 2: The ButtonBarSkin pref width computation ignores insets: SkinBase.computePrefWidth(double,double,double,double,double) ignores the insets values, instead using only ButtonBar's child HBox's minX and prefWidth. Thus, if the developer has set any padding in the ButtonBar, then ButtonBarSkin computes a pref width that is too narrow.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Run the executable test case application. Observe that the Stage initializes at the initial pref width of the VBox, which is the pref width of its widest child, which later truncates the longest Button text after the ButtonBarSkin sets the uniform pref button widths.
Observe that the pref widths of the ButtonBars are equal, despite the padding in the otherwise duplicate second ButtonBar. Stretch the width of the Stage until it reaches the pref width of the ButtonBars. Observe that the ButtonBar with padding still truncates the longest text. The ButtonBar without padding does not truncate the longest text, because it computes a correct pref width.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
ButtonBar Button text should not be truncated if the ButtonBar is laid out at its initial preferred width.
ButtonBar Button text should not be truncated if the ButtonBar has padding and the ButtonBar is laid out at its final preferred width.
ACTUAL -
ButtonBar Button text is truncated.
---------- BEGIN SOURCE ----------
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* An example of truncated text in Buttons in ButtonBars.
**/
public class ButtonBarTruncatedTextExample extends Application {
public static void main( final String[] args ) {
launch( args );
}
final Label buttonBarWithoutPaddingLabel = new Label();
final ButtonBar buttonBarWithoutPadding = createButtonBar();
final Label buttonBarWithPaddingLabel = new Label();
final ButtonBar buttonBarWithPadding = createButtonBar();
@Override
public void start( final Stage primaryStage ) throws Exception {
buttonBarWithoutPadding.needsLayoutProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
buttonBarWithoutPadding.widthProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
buttonBarWithPadding.setStyle( "-fx-padding: 0 1em 0 1em;" );
buttonBarWithPadding.needsLayoutProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
buttonBarWithPadding.widthProperty().addListener( ( __, ___, ____ ) -> {
updateLabels();
} );
primaryStage.setScene( new Scene( new VBox(
new Label( "ButtonBar without padding:" ),
buttonBarWithoutPaddingLabel,
buttonBarWithoutPadding,
new Label( "ButtonBar with padding:" ),
buttonBarWithPaddingLabel,
buttonBarWithPadding ) ) );
primaryStage.show();
}
private void updateLabels() {
buttonBarWithoutPaddingLabel.setText( "prefWidth(-1): " + buttonBarWithoutPadding.prefWidth( -1 ) +
", width: " + buttonBarWithoutPadding.getWidth() );
buttonBarWithPaddingLabel.setText( "prefWidth(-1): " + buttonBarWithPadding.prefWidth( -1 ) +
", width: " + buttonBarWithPadding.getWidth() );
}
private ButtonBar createButtonBar() {
final ButtonBar buttonBar = new ButtonBar();
final Button yesButton = new Button( "Yes" );
ButtonBar.setButtonData( yesButton, ButtonBar.ButtonData.YES );
final Button noButton = new Button( "No" );
ButtonBar.setButtonData( noButton, ButtonBar.ButtonData.NO );
final Button cancelButton = new Button( "Exit application" );
ButtonBar.setButtonData( cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE );
buttonBar.getButtons().addAll( yesButton, noButton, cancelButton );
return buttonBar;
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Set this derived class of ButtonBarSkin as the Skin class for ButtonBar.
It can't work around the problem by calling resizeButtons() earlier because that is, unfortunately, a private method even though the default Skins were made public.
So, instead, it sets the button widths to non-uniform.
Also, it overrides computePrefWidth(...) to include the insets in the computation.
import javafx.collections.ListChangeListener;
import javafx.scene.Node;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.skin.ButtonBarSkin;
import javafx.scene.layout.HBox;
/**
* Works around a Button text truncation problem in {@link ButtonBarSkin}.
*
* <P></P>
* The initial call to {@link ButtonBarSkin#computePrefWidth(double, double, double, double, double)}
* happens before any call to {@link ButtonBarSkin#resizeButtons()}.
* If one Button's text is wider than the others, and wider than its OS-dependent min width,
* and the {@link ButtonBar} is the widest child of its Parent,
* then the Parent and thus the enclosing Stage will get sized too narrow to accommodate the preferred width
* of the ButtonBar after the later call to {@code resizeButtons()} sets the Buttons to uniform preferred width.
* This causes truncation of the wider text.
* <P></P>
* This class sets all of the Buttons to "not uniformly sized" so that the initial computed pref width
* is sufficient to display the text in all of the Buttons.</LI>
* <P></P>
* <P>This class also implements {@link #computePrefWidth(double, double, double, double, double)} to work around the
* superclass implementation's omission of the insets (if any) from its computation.</P>
*
* Filed a JavaFX bug: TODO
**/
public class ButtonBarPreventTruncatedTextSkin extends ButtonBarSkin {
public ButtonBarPreventTruncatedTextSkin( final ButtonBar control ) {
super( control );
// final ChangeListener<Parent> dialogMinWidthSetter = ( __, ___, parent ) -> {
// if( parent instanceof DialogPane ) {
// control.setMinWidth( Region.USE_PREF_SIZE );
// }
// };
// control.parentProperty().addListener( dialogMinWidthSetter );
// dialogMinWidthSetter.changed( null, null, control.getParent() ); // Initial update.
final ListChangeListener<? super Node> buttonsChangeListener = ___ ->
control.getButtons().forEach( child -> {
ButtonBar.setButtonUniformSize( child, false ); // Non-uniform size for all.
} );
control.getButtons().addListener( buttonsChangeListener );
buttonsChangeListener.onChanged( null ); // Initial update.
}
/**
* Workaround for superclass implementation's omission of the insets.
*/
@Override
protected double computePrefWidth( final double height,
final double topInset,
final double rightInset,
final double bottomInset,
final double leftInset ) {
// JavaFX's superclass method ignores the insets.
// If we have the single "container" child that we expect, then use a computation that includes the insets:
if( getChildren().size() == 1 && getChildren().get( 0 ) instanceof HBox &&
getChildren().get( 0 ).getStyleClass().contains( "container" ) ) {
return leftInset + getChildren().get( 0 ).prefWidth( height ) + rightInset;
} else {
// We must be using a different version of the superclass with a significant code change. Defer to it.
return super.computePrefWidth( height, topInset, rightInset, bottomInset, leftInset );
}
}
}
FREQUENCY : always