diff --git a/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/skin/Utils.java b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/skin/Utils.java index b7b43dbace8..cf84677486a 100644 --- a/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/skin/Utils.java +++ b/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/skin/Utils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -79,11 +79,12 @@ */ public class Utils { - static final Text helper = new Text(); - static final double DEFAULT_WRAPPING_WIDTH = helper.getWrappingWidth(); - static final double DEFAULT_LINE_SPACING = helper.getLineSpacing(); - static final String DEFAULT_TEXT = helper.getText(); - static final TextBoundsType DEFAULT_BOUNDS_TYPE = helper.getBoundsType(); + private static final Text textInstance = new Text(); + private static final double DEFAULT_WRAPPING_WIDTH = textInstance.getWrappingWidth(); + private static final double DEFAULT_LINE_SPACING = textInstance.getLineSpacing(); + private static final String DEFAULT_TEXT = textInstance.getText(); + private static final TextBoundsType DEFAULT_BOUNDS_TYPE = textInstance.getBoundsType(); + private static final AtomicBoolean helperGuard = new AtomicBoolean(false); /* Using TextLayout directly for simple text measurement. * Instead of restoring the TextLayout attributes to default values @@ -94,38 +95,82 @@ public class Utils { * * Note: This code assumes that TextBoundsType#VISUAL is never used by controls. * */ - static final TextLayout layout = Toolkit.getToolkit().getTextLayoutFactory().createLayout(); + private static final TextLayout layoutInstance = Toolkit.getToolkit().getTextLayoutFactory().createLayout(); + private static final AtomicBoolean layoutGuard = new AtomicBoolean(false); - public static double getAscent(Font font, TextBoundsType boundsType) { - layout.setContent("", FontHelper.getNativeFont(font)); - layout.setWrapWidth(0); - layout.setLineSpacing(0); - if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) { - layout.setBoundsType(TextLayout.BOUNDS_CENTER); + private static Text helper() { + if (helperGuard.compareAndSet(false, true)) { + return textInstance; } else { - layout.setBoundsType(0); + return new Text(); } - return -layout.getBounds().getMinY(); } - public static double getLineHeight(Font font, TextBoundsType boundsType) { - layout.setContent("", FontHelper.getNativeFont(font)); - layout.setWrapWidth(0); - layout.setLineSpacing(0); - if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) { - layout.setBoundsType(TextLayout.BOUNDS_CENTER); + private static void release(Text t) { + if (t == textInstance) { + helperGuard.set(false); + } + } + + private static TextLayout layout() { + if (layoutGuard.compareAndSet(false, true)) { + return layoutInstance; } else { - layout.setBoundsType(0); + return Toolkit.getToolkit().getTextLayoutFactory().createLayout(); } + } - // JDK-8093957: Use the line bounds specifically, to include font leading. - return layout.getLines()[0].getBounds().getHeight(); + private static void release(TextLayout t) { + if (t == layoutInstance) { + layoutGuard.set(false); + } + } + + public static double getAscent(Font font, TextBoundsType boundsType) { + TextLayout layout = layout(); + try { + layout.setContent("", FontHelper.getNativeFont(font)); + layout.setWrapWidth(0); + layout.setLineSpacing(0); + if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) { + layout.setBoundsType(TextLayout.BOUNDS_CENTER); + } else { + layout.setBoundsType(0); + } + return -layout.getBounds().getMinY(); + } finally { + release(layout); + } + } + + public static double getLineHeight(Font font, TextBoundsType boundsType) { + TextLayout layout = layout(); + try { + layout.setContent("", FontHelper.getNativeFont(font)); + layout.setWrapWidth(0); + layout.setLineSpacing(0); + if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) { + layout.setBoundsType(TextLayout.BOUNDS_CENTER); + } else { + layout.setBoundsType(0); + } + + // JDK-8093957: Use the line bounds specifically, to include font leading. + return layout.getLines()[0].getBounds().getHeight(); + } finally { + release(layout); + } } public static double computeTextWidth(Font font, String text, double wrappingWidth) { - layout.setContent(text != null ? text : "", FontHelper.getNativeFont(font)); - layout.setWrapWidth((float)wrappingWidth); - return layout.getBounds().getWidth(); + TextLayout layout = layout(); + try { + layout.setContent(text != null ? text : "", FontHelper.getNativeFont(font)); + layout.setWrapWidth((float)wrappingWidth); + return layout.getBounds().getWidth(); + } finally { + release(layout); + } } public static double computeTextHeight(Font font, String text, double wrappingWidth, TextBoundsType boundsType) { @@ -133,15 +178,20 @@ public static double computeTextHeight(Font font, String text, double wrappingWi } public static double computeTextHeight(Font font, String text, double wrappingWidth, double lineSpacing, TextBoundsType boundsType) { - layout.setContent(text != null ? text : "", FontHelper.getNativeFont(font)); - layout.setWrapWidth((float)wrappingWidth); - layout.setLineSpacing((float)lineSpacing); - if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) { - layout.setBoundsType(TextLayout.BOUNDS_CENTER); - } else { - layout.setBoundsType(0); + TextLayout layout = layout(); + try { + layout.setContent(text != null ? text : "", FontHelper.getNativeFont(font)); + layout.setWrapWidth((float)wrappingWidth); + layout.setLineSpacing((float)lineSpacing); + if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) { + layout.setBoundsType(TextLayout.BOUNDS_CENTER); + } else { + layout.setBoundsType(0); + } + return layout.getBounds().getHeight(); + } finally { + release(layout); } - return layout.getBounds().getHeight(); } public static Point2D computeMnemonicPosition(Font font, String text, int mnemonicIndex, double wrappingWidth, @@ -152,33 +202,42 @@ public static Point2D computeMnemonicPosition(Font font, String text, int mnemon return null; } - // Layout the text with given font, wrapping width and line spacing - layout.setContent(text, FontHelper.getNativeFont(font)); - layout.setWrapWidth((float)wrappingWidth); - layout.setLineSpacing((float)lineSpacing); - - // The text could be spread over multiple lines - // We need to find out on which line the mnemonic character lies int start = 0; int i = 0; - int totalLines = layout.getLines().length; int lineLength = 0; - while (i < totalLines) { - lineLength = layout.getLines()[i].getLength(); + int totalLines; + double lineHeight; - if ((mnemonicIndex >= start) && - (mnemonicIndex < (start + lineLength))) { - // mnemonic lies on line 'i' - break; + // Layout the text with given font, wrapping width and line spacing + TextLayout layout = layout(); + try { + layout.setContent(text, FontHelper.getNativeFont(font)); + layout.setWrapWidth((float)wrappingWidth); + layout.setLineSpacing((float)lineSpacing); + + // The text could be spread over multiple lines + // We need to find out on which line the mnemonic character lies + totalLines = layout.getLines().length; + while (i < totalLines) { + lineLength = layout.getLines()[i].getLength(); + + if ((mnemonicIndex >= start) && + (mnemonicIndex < (start + lineLength))) { + // mnemonic lies on line 'i' + break; + } + + start += lineLength; + i++; } - start += lineLength; - i++; + // Find x and y offsets of mnemonic character position + // in line numbered 'i' + lineHeight = layout.getBounds().getHeight() / totalLines; + } finally { + release(layout); } - // Find x and y offsets of mnemonic character position - // in line numbered 'i' - double lineHeight = layout.getBounds().getHeight() / totalLines; double x = Utils.computeTextWidth(font, text.substring(start, mnemonicIndex), 0); if (isRTL) { double lineWidth = Utils.computeTextWidth(font, text.substring(start, (start + lineLength - 1)), 0); @@ -195,21 +254,26 @@ public static Point2D computeMnemonicPosition(Font font, String text, int mnemon } public static int computeTruncationIndex(Font font, String text, double width) { - helper.setText(text); - helper.setFont(font); - helper.setWrappingWidth(0); - helper.setLineSpacing(0); - // The -2 is a fudge to make sure the result more often matches - // what we get from using computeTextWidth instead. It's not yet - // clear what causes the small discrepancies. - Bounds bounds = helper.getLayoutBounds(); - Point2D endPoint = new Point2D(width - 2, bounds.getMinY() + bounds.getHeight() / 2); - final int index = helper.hitTest(endPoint).getCharIndex(); - // RESTORE STATE - helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH); - helper.setLineSpacing(DEFAULT_LINE_SPACING); - helper.setText(DEFAULT_TEXT); - return index; + Text helper = helper(); + try { + helper.setText(text); + helper.setFont(font); + helper.setWrappingWidth(0); + helper.setLineSpacing(0); + // The -2 is a fudge to make sure the result more often matches + // what we get from using computeTextWidth instead. It's not yet + // clear what causes the small discrepancies. + Bounds bounds = helper.getLayoutBounds(); + Point2D endPoint = new Point2D(width - 2, bounds.getMinY() + bounds.getHeight() / 2); + final int index = helper.hitTest(endPoint).getCharIndex(); + // RESTORE STATE + helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH); + helper.setLineSpacing(DEFAULT_LINE_SPACING); + helper.setText(DEFAULT_TEXT); + return index; + } finally { + release(helper); + } } /** @@ -495,53 +559,84 @@ public static String computeClippedWrappedText( return text; // JDK-8092895 (JDK-8092895) - return text, not empty string. } - helper.setText(text); - helper.setFont(font); - helper.setWrappingWidth((int)Math.ceil(width)); - helper.setBoundsType(boundsType); - helper.setLineSpacing(lineSpacing); - - boolean leading = (truncationStyle == LEADING_ELLIPSIS || - truncationStyle == LEADING_WORD_ELLIPSIS); - boolean center = (truncationStyle == CENTER_ELLIPSIS || - truncationStyle == CENTER_WORD_ELLIPSIS); - boolean trailing = !(leading || center); - boolean wordTrim = (truncationStyle == WORD_ELLIPSIS || - truncationStyle == LEADING_WORD_ELLIPSIS || - truncationStyle == CENTER_WORD_ELLIPSIS); + Text helper = helper(); + try { + helper.setText(text); + helper.setFont(font); + helper.setWrappingWidth((int)Math.ceil(width)); + helper.setBoundsType(boundsType); + helper.setLineSpacing(lineSpacing); + + boolean leading = (truncationStyle == LEADING_ELLIPSIS || + truncationStyle == LEADING_WORD_ELLIPSIS); + boolean center = (truncationStyle == CENTER_ELLIPSIS || + truncationStyle == CENTER_WORD_ELLIPSIS); + boolean trailing = !(leading || center); + boolean wordTrim = (truncationStyle == WORD_ELLIPSIS || + truncationStyle == LEADING_WORD_ELLIPSIS || + truncationStyle == CENTER_WORD_ELLIPSIS); + + String result = text; + boolean truncated = false; + int len = (result != null) ? result.length() : 0; + int centerLen = -1; + + Point2D centerPoint = null; + if (center) { + // Find index of character in the middle of the visual text area + centerPoint = new Point2D((width - eWidth) / 2, height / 2 - helper.getBaselineOffset()); + } - String result = text; - boolean truncated = false; - int len = (result != null) ? result.length() : 0; - int centerLen = -1; + // Find index of character at the bottom left of the text area. + // This should be the first character of a line that would be clipped. + Point2D endPoint = new Point2D(0, height - helper.getBaselineOffset()); - Point2D centerPoint = null; - if (center) { - // Find index of character in the middle of the visual text area - centerPoint = new Point2D((width - eWidth) / 2, height / 2 - helper.getBaselineOffset()); - } - - // Find index of character at the bottom left of the text area. - // This should be the first character of a line that would be clipped. - Point2D endPoint = new Point2D(0, height - helper.getBaselineOffset()); + int hit = helper.hitTest(endPoint).getCharIndex(); + if (hit >= len) { + helper.setBoundsType(TextBoundsType.LOGICAL); // restore + return text; + } + if (center) { + hit = helper.hitTest(centerPoint).getCharIndex(); + } - int hit = helper.hitTest(endPoint).getCharIndex(); - if (hit >= len) { - helper.setBoundsType(TextBoundsType.LOGICAL); // restore - return text; - } - if (center) { - hit = helper.hitTest(centerPoint).getCharIndex(); - } + if (hit > 0 && hit < len) { + // Step one, make a truncation estimate. - if (hit > 0 && hit < len) { - // Step one, make a truncation estimate. + if (center || trailing) { + int ind = hit; + if (center) { + // This is for the first part, i.e. beginning of text up to ellipsis. + if (wordTrim) { + int brInd = lastBreakCharIndex(text, ind + 1); + if (brInd >= 0) { + ind = brInd + 1; + } else { + brInd = firstBreakCharIndex(text, ind); + if (brInd >= 0) { + ind = brInd + 1; + } + } + } + centerLen = ind + eLen; + } // else: text node wraps at words, so wordTrim is not needed here. + result = result.substring(0, ind) + ellipsis; + truncated = true; + } - if (center || trailing) { - int ind = hit; - if (center) { - // This is for the first part, i.e. beginning of text up to ellipsis. - if (wordTrim) { + if (leading || center) { + // The hit is an index counted from the beginning, but we need + // the opposite, i.e. an index counted from the end. However, + // the Text node does not support wrapped line layout in the + // reverse direction, starting at the bottom right corner. + + // We'll simulate by assuming the index will be a similar + // number, then back up 10 characters just to add some slop. + // For example, the ending lines might pack tighter than the + // beginning lines, and therefore fit a higher number of + // characters. + int ind = Math.max(0, len - hit - 10); + if (ind > 0 && wordTrim) { int brInd = lastBreakCharIndex(text, ind + 1); if (brInd >= 0) { ind = brInd + 1; @@ -552,105 +647,78 @@ public static String computeClippedWrappedText( } } } - centerLen = ind + eLen; - } // else: text node wraps at words, so wordTrim is not needed here. - result = result.substring(0, ind) + ellipsis; - truncated = true; - } - - if (leading || center) { - // The hit is an index counted from the beginning, but we need - // the opposite, i.e. an index counted from the end. However, - // the Text node does not support wrapped line layout in the - // reverse direction, starting at the bottom right corner. - - // We'll simulate by assuming the index will be a similar - // number, then back up 10 characters just to add some slop. - // For example, the ending lines might pack tighter than the - // beginning lines, and therefore fit a higher number of - // characters. - int ind = Math.max(0, len - hit - 10); - if (ind > 0 && wordTrim) { - int brInd = lastBreakCharIndex(text, ind + 1); - if (brInd >= 0) { - ind = brInd + 1; + if (center) { + // This is for the second part, i.e. from ellipsis to end of text. + result = result + text.substring(ind); } else { - brInd = firstBreakCharIndex(text, ind); - if (brInd >= 0) { - ind = brInd + 1; - } + result = ellipsis + text.substring(ind); + truncated = true; } } - if (center) { - // This is for the second part, i.e. from ellipsis to end of text. - result = result + text.substring(ind); - } else { - result = ellipsis + text.substring(ind); - truncated = true; - } - } - // Step two, check if text still overflows after we added the ellipsis. - // If so, remove one char or word at a time. - while (true) { - helper.setText(result); - int hit2 = helper.hitTest(endPoint).getCharIndex(); - if (center && hit2 < centerLen) { - // No room for text after ellipsis. Maybe there is a newline - // here, and the next line falls outside the view. - if (hit2 > 0 && result.charAt(hit2-1) == '\n') { - hit2--; - } - // should have used StringBuilder - result = text.substring(0, hit2) + ellipsis; - truncated = true; - break; - } else if (hit2 > 0 && hit2 < result.length()) { - if (leading) { - int ind = eLen + 1; // Past ellipsis and first char. - if (wordTrim) { - int brInd = firstBreakCharIndex(result, ind); - if (brInd >= 0) { - ind = brInd + 1; - } + // Step two, check if text still overflows after we added the ellipsis. + // If so, remove one char or word at a time. + while (true) { + helper.setText(result); + int hit2 = helper.hitTest(endPoint).getCharIndex(); + if (center && hit2 < centerLen) { + // No room for text after ellipsis. Maybe there is a newline + // here, and the next line falls outside the view. + if (hit2 > 0 && result.charAt(hit2-1) == '\n') { + hit2--; } - result = ellipsis + result.substring(ind); + // should have used StringBuilder + result = text.substring(0, hit2) + ellipsis; truncated = true; - } else if (center) { - int ind = centerLen + 1; // Past ellipsis and first char. - if (wordTrim) { - int brInd = firstBreakCharIndex(result, ind); - if (brInd >= 0) { - ind = brInd + 1; + break; + } else if (hit2 > 0 && hit2 < result.length()) { + if (leading) { + int ind = eLen + 1; // Past ellipsis and first char. + if (wordTrim) { + int brInd = firstBreakCharIndex(result, ind); + if (brInd >= 0) { + ind = brInd + 1; + } } - } - result = result.substring(0, centerLen) + result.substring(ind); - } else { - int ind = result.length() - eLen - 1; // Before last char and ellipsis. - if (wordTrim) { - int brInd = lastBreakCharIndex(result, ind); - if (brInd >= 0) { - ind = brInd; + result = ellipsis + result.substring(ind); + truncated = true; + } else if (center) { + int ind = centerLen + 1; // Past ellipsis and first char. + if (wordTrim) { + int brInd = firstBreakCharIndex(result, ind); + if (brInd >= 0) { + ind = brInd + 1; + } } + result = result.substring(0, centerLen) + result.substring(ind); + } else { + int ind = result.length() - eLen - 1; // Before last char and ellipsis. + if (wordTrim) { + int brInd = lastBreakCharIndex(result, ind); + if (brInd >= 0) { + ind = brInd; + } + } + result = result.substring(0, ind) + ellipsis; + truncated = true; } - result = result.substring(0, ind) + ellipsis; - truncated = true; + } else { + break; } - } else { - break; } } + // RESTORE STATE + helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH); + helper.setLineSpacing(DEFAULT_LINE_SPACING); + helper.setText(DEFAULT_TEXT); + helper.setBoundsType(DEFAULT_BOUNDS_TYPE); + textTruncated.set(truncated); + return result; + } finally { + release(helper); } - // RESTORE STATE - helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH); - helper.setLineSpacing(DEFAULT_LINE_SPACING); - helper.setText(DEFAULT_TEXT); - helper.setBoundsType(DEFAULT_BOUNDS_TYPE); - textTruncated.set(truncated); - return result; } - private static int firstBreakCharIndex(String str, int start) { char[] chars = str.toCharArray(); for (int i = start; i < chars.length; i++) { @@ -921,5 +989,4 @@ public static String formatHexString(Color c) { public static URL getResource(String str) { return Utils.class.getResource(str); } - } diff --git a/tests/system/src/test/java/test/robot/javafx/scene/NodeInitializationStressTest.java b/tests/system/src/test/java/test/robot/javafx/scene/NodeInitializationStressTest.java index 4dcfc707909..0aa36f7404e 100644 --- a/tests/system/src/test/java/test/robot/javafx/scene/NodeInitializationStressTest.java +++ b/tests/system/src/test/java/test/robot/javafx/scene/NodeInitializationStressTest.java @@ -215,7 +215,6 @@ public void bubbleChart() { }); } - @Disabled("JDK-8347392") // FIX @Test public void button() { assumeFalse(SKIP_TEST); @@ -227,7 +226,7 @@ public void button() { accessControl(c); c.setAlignment(Pos.CENTER); c.setText(nextString()); - c.setDefaultButton(true); + c.setDefaultButton(nextBoolean()); }); } @@ -246,7 +245,6 @@ public void canvas() { }); } - @Disabled("JDK-8347392") // FIX @Test public void checkBox() { assumeFalse(SKIP_TEST); @@ -255,13 +253,12 @@ public void checkBox() { c.setSkin(new CheckBoxSkin(c)); return c; }, (c) -> { - c.setAllowIndeterminate(true); - c.setSelected(true); + c.setAllowIndeterminate(nextBoolean()); + c.setSelected(nextBoolean()); accessControl(c); }); } - @Disabled("JDK-8347392") // FIX @Test public void choiceBox() { assumeFalse(SKIP_TEST); @@ -270,8 +267,8 @@ public void choiceBox() { c.setSkin(new ChoiceBoxSkin(c)); return c; }, (c) -> { - c.getItems().setAll("ChoiceBox", "1", "2"); - c.getSelectionModel().select(0); + c.getItems().setAll("ChoiceBox", "1", "2", "3"); + c.getSelectionModel().select(nextInt(4)); accessControl(c); }); } @@ -329,7 +326,6 @@ public void datePicker() { }); } - @Disabled("JDK-8347392") // FIX @Test public void hyperlink() { assumeFalse(SKIP_TEST); @@ -338,12 +334,11 @@ public void hyperlink() { c.setSkin(new HyperlinkSkin(c)); return c; }, (c) -> { - c.setVisited(true); + c.setVisited(nextBoolean()); accessControl(c); }); } - @Disabled("JDK-8347392") // FIX @Test public void label() { assumeFalse(SKIP_TEST); @@ -447,7 +442,6 @@ public void pieChart() { }); } - @Disabled("JDK-8347392") // FIX @Test public void radioButton() { assumeFalse(SKIP_TEST); @@ -592,7 +586,6 @@ public void text() { }); } - @Disabled("JDK-8347392") // FIX @Test public void textArea() { assumeFalse(SKIP_TEST); @@ -602,6 +595,7 @@ public void textArea() { return c; }, (c) -> { accessTextInputControl(c); + c.setWrapText(nextBoolean()); }); } @@ -632,7 +626,7 @@ public void textFlow() { }); } - @Disabled("JDK-8347392") // FIX + @Disabled("JDK-8349255") // FIX @Test public void titledPane() { assumeFalse(SKIP_TEST); @@ -648,7 +642,6 @@ public void titledPane() { }); } - @Disabled("JDK-8347392") // FIX @Test public void toggleButton() { assumeFalse(SKIP_TEST);