diff --git a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/LabeledSkinBase.java b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/LabeledSkinBase.java index f2cc4861509..18a7585cd0f 100644 --- a/modules/javafx.controls/src/main/java/javafx/scene/control/skin/LabeledSkinBase.java +++ b/modules/javafx.controls/src/main/java/javafx/scene/control/skin/LabeledSkinBase.java @@ -548,11 +548,12 @@ protected void layoutLabelInArea(double x, double y, double w, double h, Pos ali graphicHeight = graphic.getLayoutBounds().getHeight(); } + updateDisplayedText(w, h); + if (ignoreText) { textWidth = textHeight = 0; text.setText(""); } else { - updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed textWidth = snapSizeX(Math.min(text.getLayoutBounds().getWidth(), wrapWidth)); textHeight = snapSizeY(Math.min(text.getLayoutBounds().getHeight(), wrapHeight)); } @@ -599,13 +600,19 @@ protected void layoutLabelInArea(double x, double y, double w, double h, Pos ali Point2D mnemonicPos = null; double mnemonicWidth = 0.0; double mnemonicHeight = 0.0; - if (containsMnemonic) { - final Font font = text.getFont(); - String preSt = mnemonicInfo.getText(); - boolean isRTL = (labeledNode.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT); - mnemonicPos = Utils.computeMnemonicPosition(font, preSt, mnemonicInfo.getMnemonicIndex(), this.wrapWidth, labeled.getLineSpacing(), isRTL); - mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(mnemonicInfo.getMnemonicIndex(), mnemonicInfo.getMnemonicIndex() + 1), 0); - mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType()); + if (containsMnemonic && mnemonicInfo != null) { + int mnemonicIndex = mnemonicInfo.getMnemonicIndex(); + String cleanText = mnemonicInfo.getText(); + // The mnemonic KeyCode can also be defined in the following form: Exit_(q) + // In this case, no mnemonic character is available and the mnemonicIndex value is equal to the text.length(). + // This means we cannot substring the character from the text. + if (mnemonicIndex >= 0 && mnemonicInfo.getExtendedMnemonicText() == null) { + final Font font = text.getFont(); + boolean isRTL = (labeledNode.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT); + mnemonicPos = Utils.computeMnemonicPosition(font, cleanText, mnemonicIndex, this.wrapWidth, labeled.getLineSpacing(), isRTL); + mnemonicWidth = Utils.computeTextWidth(font, cleanText.substring(mnemonicIndex, mnemonicIndex + 1), 0); + mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType()); + } } @@ -1021,16 +1028,19 @@ && getSkinnable().isMnemonicParsing()) { } if (containsMnemonic) { - if (mnemonic_underscore == null) { - mnemonic_underscore = new Line(); - mnemonic_underscore.setStartX(0.0f); - mnemonic_underscore.setStartY(0.0f); - mnemonic_underscore.setEndY(0.0f); - mnemonic_underscore.getStyleClass().clear(); - mnemonic_underscore.getStyleClass().setAll("mnemonic-underline"); - } - if (!getChildren().contains(mnemonic_underscore)) { - getChildren().add(mnemonic_underscore); + // If only the graphic is visible, we do not need mnemonic_underscore node + if (labeled.getContentDisplay() != ContentDisplay.GRAPHIC_ONLY) { + if (mnemonic_underscore == null) { + mnemonic_underscore = new Line(); + mnemonic_underscore.setStartX(0.0f); + mnemonic_underscore.setStartY(0.0f); + mnemonic_underscore.setEndY(0.0f); + mnemonic_underscore.getStyleClass().clear(); + mnemonic_underscore.getStyleClass().setAll("mnemonic-underline"); + } + if (!getChildren().contains(mnemonic_underscore)) { + getChildren().add(mnemonic_underscore); + } } } else if (mnemonic_underscore != null && getChildren().contains(mnemonic_underscore)) { Platform.runLater(() -> { diff --git a/modules/javafx.controls/src/test/java/test/javafx/scene/control/skin/LabelSkinTest.java b/modules/javafx.controls/src/test/java/test/javafx/scene/control/skin/LabelSkinTest.java index 0aff216b3ab..3cc3dddbb52 100644 --- a/modules/javafx.controls/src/test/java/test/javafx/scene/control/skin/LabelSkinTest.java +++ b/modules/javafx.controls/src/test/java/test/javafx/scene/control/skin/LabelSkinTest.java @@ -28,10 +28,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.sun.javafx.PlatformUtil; +import com.sun.javafx.scene.control.behavior.MnemonicInfo; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.ObservableMap; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; @@ -41,13 +50,17 @@ import javafx.scene.control.skin.LabelSkinBaseShim; import javafx.scene.control.skin.LabeledSkinBaseShim; import javafx.scene.effect.BlendMode; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.Mnemonic; import javafx.scene.layout.HBox; import javafx.scene.paint.Color; +import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.stage.Stage; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import com.sun.javafx.scene.control.skin.Utils; @@ -63,6 +76,7 @@ public class LabelSkinTest { private Label label; private LabelSkinMock skin; private Text text; + private StageLoader stageLoader; @BeforeEach public void setup() { @@ -77,6 +91,13 @@ public void setup() { text = (Text) SkinBaseShim.getChildren(skin).get(0); } + @AfterEach + public void teardown() { + if (stageLoader != null) { + stageLoader.dispose(); + } + } + /**************************************************************************** * * * Tests for change notification * @@ -2092,6 +2113,116 @@ public void testEscapedMnemonicSymbolIsNotProcessedWhenParsingIsDisabled() { assertEquals("foo __bar", LabelSkinBaseShim.getText(label).getText()); } + @Test + public void testMnemonics_setMnemonicParsing() { + label.setMnemonicParsing(true); + checkMnemonics(); + + label.setMnemonicParsing(false); + checkMnemonics(); + + label.setMnemonicParsing(true); + checkMnemonics(); + } + + @Test + public void testMnemonics_setContentDisplay() { + label.setMnemonicParsing(true); + + ContentDisplay originalContentDisplay = label.getContentDisplay(); + checkMnemonics(); + + label.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + checkMnemonics(); + + label.setContentDisplay(ContentDisplay.TEXT_ONLY); + checkMnemonics(); + + label.setContentDisplay(originalContentDisplay); + checkMnemonics(); + } + + private void checkMnemonics() { + Toolkit tk = Toolkit.getToolkit(); + StageLoader sl = createStageLoader(label); + tk.firePulse(); + + ObservableMap> mnemonics = sl.getStage().getScene().getMnemonics(); + + Function mnemonicRegistrationChecker = character -> + mnemonics.getOrDefault( + new MnemonicInfo.MnemonicKeyCombination(character), FXCollections.emptyObservableList()) + .stream().filter(m -> m.getNode() == label).toList().size() == 1; + + Supplier mnemonicNodePresenceChecker = () -> label.getChildrenUnmodifiable().stream().anyMatch(p -> p instanceof Line); + + Function mnemonicErrorMessageSupplier = expected -> + (expected + ? "Mnemonic registration expected but not found: " + : "No mnemonic expected but was: ") + + mnemonics; + + // Mac does not support mnemonics + boolean expectedMnemonics = PlatformUtil.isMac() ? false : label.isMnemonicParsing(); + boolean expectedMnemonicsNode = expectedMnemonics && label.getContentDisplay() != ContentDisplay.GRAPHIC_ONLY; + + // -------------------------------------------------------------------- + + // no text + label.setText(null); + tk.firePulse(); + assertTrue(mnemonics.values().stream().allMatch(List::isEmpty), mnemonicErrorMessageSupplier.apply(false)); + assertFalse(mnemonicNodePresenceChecker.get()); + + // initial mnemonic + label.setText("foo_bar"); + tk.firePulse(); + assertEquals(expectedMnemonics, mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(expectedMnemonics)); + assertEquals(expectedMnemonicsNode, mnemonicNodePresenceChecker.get()); + + // mnemonic -> text + label.setText("xxx"); + tk.firePulse(); + assertFalse(mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(false)); + assertFalse(mnemonicNodePresenceChecker.get()); + + // text -> mnemonic + label.setText("foo_bar"); + tk.firePulse(); + assertEquals(expectedMnemonics, mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(expectedMnemonics)); + assertEquals(expectedMnemonicsNode, mnemonicNodePresenceChecker.get()); + + // mnemonic -> empty + label.setText(""); + tk.firePulse(); + assertFalse(mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(false)); + assertFalse(mnemonicNodePresenceChecker.get()); + + // empty -> mnemonic + label.setText("foo_bar"); + tk.firePulse(); + assertEquals(expectedMnemonics, mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(expectedMnemonics)); + assertEquals(expectedMnemonicsNode, mnemonicNodePresenceChecker.get()); + + // mnemonic -> null + label.setText(null); + tk.firePulse(); + assertFalse(mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(false)); + assertFalse(mnemonicNodePresenceChecker.get()); + + // null -> mnemonic + label.setText("foo_bar"); + tk.firePulse(); + assertEquals(expectedMnemonics, mnemonicRegistrationChecker.apply("b"), mnemonicErrorMessageSupplier.apply(expectedMnemonics)); + assertEquals(expectedMnemonicsNode, mnemonicNodePresenceChecker.get()); + + // extended mnemonic + label.setText("test_(t)"); + tk.firePulse(); + assertEquals(expectedMnemonics, mnemonicRegistrationChecker.apply("t"), mnemonicErrorMessageSupplier.apply(expectedMnemonics)); + assertEquals(expectedMnemonicsNode, mnemonicNodePresenceChecker.get()); + } + /********************************************************************* * * Tests for bug reports * @@ -2126,6 +2257,13 @@ public void testBaselineAlignmentWhenTextIsSetAndGraphicIsSet() { sl.dispose(); } + private StageLoader createStageLoader(Node node) { + if (stageLoader == null) { + stageLoader = new StageLoader(node); + } + return stageLoader; + } + /******************************************************************************** * *