/*
 * Copyright (c) 2011, 2019, 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
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.sun.javafx.scene.control.skin;

import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.control.behavior.TextBinding;
import com.sun.javafx.scene.text.TextLayout;
import com.sun.javafx.tk.Toolkit;
import javafx.application.ConditionalFeature;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.OverrunStyle;
import com.sun.javafx.scene.control.ContextMenuContent;
import com.sun.javafx.scene.text.FontHelper;
import java.net.URL;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.Mnemonic;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextBoundsType;
import javafx.scene.text.HitInfo;

import java.text.Bidi;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;

import static javafx.scene.control.OverrunStyle.CENTER_ELLIPSIS;
import static javafx.scene.control.OverrunStyle.CENTER_WORD_ELLIPSIS;
import static javafx.scene.control.OverrunStyle.CLIP;
import static javafx.scene.control.OverrunStyle.ELLIPSIS;
import static javafx.scene.control.OverrunStyle.LEADING_ELLIPSIS;
import static javafx.scene.control.OverrunStyle.LEADING_WORD_ELLIPSIS;
import static javafx.scene.control.OverrunStyle.WORD_ELLIPSIS;

BE REALLY CAREFUL WITH RESTORING OR RESETTING STATE OF helper NODE AS LEFTOVER STATE CAUSES REALLY ODD NASTY BUGS! We expect all methods to set the Font property of helper but other than that any properties set should be restored to defaults.
/** * BE REALLY CAREFUL WITH RESTORING OR RESETTING STATE OF helper NODE AS LEFTOVER * STATE CAUSES REALLY ODD NASTY BUGS! * * We expect all methods to set the Font property of helper but other than that * any properties set should be restored to defaults. */
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(); /* Using TextLayout directly for simple text measurement. * Instead of restoring the TextLayout attributes to default values * (each renders the TextLayout unable to efficiently cache layout data). * It always sets all the attributes pertinent to calculation being performed. * Note that lineSpacing and boundsType are important when computing the height * but irrelevant when computing the width. * * Note: This code assumes that TextBoundsType#VISUAL is never used by controls. * */ static final TextLayout layout = Toolkit.getToolkit().getTextLayoutFactory().createLayout(); 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); } else { layout.setBoundsType(0); } 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); } else { layout.setBoundsType(0); } // RT-37092: Use the line bounds specifically, to include font leading. return layout.getLines()[0].getBounds().getHeight(); } 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(); } public static double computeTextHeight(Font font, String text, double wrappingWidth, TextBoundsType boundsType) { return computeTextHeight(font, text, wrappingWidth, 0, boundsType); } @SuppressWarnings("deprecation") 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); } return layout.getBounds().getHeight(); } public static Point2D computeMnemonicPosition(Font font, String text, int mnemonicIndex, double wrappingWidth, double lineSpacing) { // Input validation if ((font == null) || (text == null) || (mnemonicIndex < 0) || (mnemonicIndex > text.length())) { 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; while (i < totalLines) { int lineLength = layout.getLines()[i].getLength(); if ((mnemonicIndex >= start) && (mnemonicIndex < (start + lineLength))) { // mnemonic lies on line 'i' break; } start += lineLength; i++; } // 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); double y = (lineHeight * (i+1)); // Adjust y offset for linespacing except for the last line. if ((i+1) != totalLines) { y -= (lineSpacing / 2); } return new Point2D(x, y); } 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; } public static String computeClippedText(Font font, String text, double width, OverrunStyle type, String ellipsisString) { if (font == null) { throw new IllegalArgumentException("Must specify a font"); } OverrunStyle style = (type == null || type == CLIP) ? ELLIPSIS : type; final String ellipsis = (type == CLIP) ? "" : ellipsisString; // if the text is empty or null or no ellipsis, then it always fits if (text == null || "".equals(text)) { return text; } // if the string width is < the available width, then it fits and // doesn't need to be clipped. We use a double point comparison // of 0.001 (1/1000th of a pixel) to account for any numerical // discrepancies introduced when the available width was calculated. // MenuItemSkinBase.doLayout, for example, does a number of double // point operations when computing the available width. final double stringWidth = computeTextWidth(font, text, 0); if (stringWidth - width < 0.0010F) { return text; } // the width used by the ellipsis string final double ellipsisWidth = computeTextWidth(font, ellipsis, 0); // the available maximum width to fit chars into. This is essentially // the width minus the space required for the ellipsis string final double availableWidth = width - ellipsisWidth; if (width < ellipsisWidth) { // The ellipsis doesn't fit. return ""; } // if we got here, then we must clip the text with an ellipsis. // this can be pretty expensive depending on whether "complex" text // layout needs to be taken into account. So each ellipsis option has // to take into account two code paths: the easy way and the correct // way. This is flagged by the "complexLayout" boolean // TODO make sure this function call takes into account ligatures, kerning, // and such as that will change the layout characteristics of the text // and will require a full complex layout // TODO since we don't have all the stuff available in FX to determine // complex text, I'm going to for now assume complex text is always false. final boolean complexLayout = false; //requiresComplexLayout(font, text); // generally all we want to do is count characters and add their widths. // For ellipsis that breaks on words, we do NOT want to include any // hanging whitespace. if (style == ELLIPSIS || style == WORD_ELLIPSIS || style == LEADING_ELLIPSIS || style == LEADING_WORD_ELLIPSIS) { final boolean wordTrim = (style == WORD_ELLIPSIS || style == LEADING_WORD_ELLIPSIS); String substring; if (complexLayout) { // AttributedString a = new AttributedString(text); // LineBreakMeasurer m = new LineBreakMeasurer(a.getIterator(), frc); // substring = text.substring(0, m.nextOffset((double)availableWidth)); } else { // RT-23458: Use a faster algorithm for the most common case // where truncation happens at the end, i.e. for ELLIPSIS and // CLIP, but not for other cases such as WORD_ELLIPSIS, etc. if (style == ELLIPSIS && !new Bidi(text, Bidi.DIRECTION_LEFT_TO_RIGHT).isMixed()) { int hit = computeTruncationIndex(font, text, width - ellipsisWidth); if (hit < 0 || hit >= text.length()) { return text; } else { return text.substring(0, hit) + ellipsis; } } // simply total up the widths of all chars to determine how many // will fit in the available space. Remember the last whitespace // encountered so that if we're breaking on words we can trim // and omit it. double total = 0.0F; int whitespaceIndex = -1; // at the termination of the loop, index will be one past the // end of the substring int index = 0; int start = (style == LEADING_ELLIPSIS || style == LEADING_WORD_ELLIPSIS) ? (text.length() - 1) : (0); int end = (start == 0) ? (text.length() - 1) : 0; int stepValue = (start == 0) ? 1 : -1; boolean done = (start == 0) ? start > end : start < end; for (int i = start; !done ; i += stepValue) { index = i; char c = text.charAt(index); total = computeTextWidth(font, (start == 0) ? text.substring(0, i + 1) : text.substring(i, start + 1), 0); if (Character.isWhitespace(c)) { whitespaceIndex = index; } if (total > availableWidth) { break; } done = start == 0? i >= end : i <= end; } final boolean fullTrim = !wordTrim || whitespaceIndex == -1; substring = (start == 0) ? (text.substring(0, fullTrim ? index : whitespaceIndex)) : (text.substring((fullTrim ? index : whitespaceIndex) + 1)); assert(!text.equals(substring)); } if (style == ELLIPSIS || style == WORD_ELLIPSIS) { return substring + ellipsis; } else { //style is LEADING_ELLIPSIS or LEADING_WORD_ELLIPSIS return ellipsis + substring; } } else { // these two indexes are INCLUSIVE not exclusive int leadingIndex = 0; int trailingIndex = 0; int leadingWhitespace = -1; int trailingWhitespace = -1; // The complex case is going to be killer. What I have to do is // read all the chars from the left up to the leadingIndex, // and all the chars from the right up to the trailingIndex, // and sum those together to get my total. That is, I cannot have // a running total but must retotal the cummulative chars each time if (complexLayout) { } else /* double leadingTotal = 0; double trailingTotal = 0; for (int i=0; i<text.length(); i++) { double total = computeStringWidth(metrics, text.substring(0, i)); if (total + trailingTotal > availableWidth) break; leadingIndex = i; leadingTotal = total; if (Character.isWhitespace(text.charAt(i))) leadingWhitespace = leadingIndex; int index = text.length() - (i + 1); total = computeStringWidth(metrics, text.substring(index - 1)); if (total + leadingTotal > availableWidth) break; trailingIndex = index; trailingTotal = total; if (Character.isWhitespace(text.charAt(index))) trailingWhitespace = trailingIndex; }*/ { // either CENTER_ELLIPSIS or CENTER_WORD_ELLIPSIS // for this case I read one char on the left, then one on the end // then second on the left, then second from the end, etc until // I have used up all the availableWidth. At that point, I trim // the string twice: once from the start to firstIndex, and // once from secondIndex to the end. I then insert the ellipsis // between the two. leadingIndex = -1; trailingIndex = -1; double total = 0.0F; for (int i = 0; i <= text.length() - 1; i++) { char c = text.charAt(i); //total += metrics.charWidth(c); total += computeTextWidth(font, "" + c, 0); if (total > availableWidth) { break; } leadingIndex = i; if (Character.isWhitespace(c)) { leadingWhitespace = leadingIndex; } int index = text.length() - 1 - i; c = text.charAt(index); //total += metrics.charWidth(c); total += computeTextWidth(font, "" + c, 0); if (total > availableWidth) { break; } trailingIndex = index; if (Character.isWhitespace(c)) { trailingWhitespace = trailingIndex; } } } if (leadingIndex < 0) { return ellipsis; } if (style == CENTER_ELLIPSIS) { if (trailingIndex < 0) { return text.substring(0, leadingIndex + 1) + ellipsis; } return text.substring(0, leadingIndex + 1) + ellipsis + text.substring(trailingIndex); } else { boolean leadingIndexIsLastLetterInWord = Character.isWhitespace(text.charAt(leadingIndex + 1)); int index = (leadingWhitespace == -1 || leadingIndexIsLastLetterInWord) ? (leadingIndex + 1) : (leadingWhitespace); String leading = text.substring(0, index); if (trailingIndex < 0) { return leading + ellipsis; } boolean trailingIndexIsFirstLetterInWord = Character.isWhitespace(text.charAt(trailingIndex - 1)); index = (trailingWhitespace == -1 || trailingIndexIsFirstLetterInWord) ? (trailingIndex) : (trailingWhitespace + 1); String trailing = text.substring(index); return leading + ellipsis + trailing; } } } public static String computeClippedWrappedText(Font font, String text, double width, double height, OverrunStyle truncationStyle, String ellipsisString, TextBoundsType boundsType) { if (font == null) { throw new IllegalArgumentException("Must specify a font"); } String ellipsis = (truncationStyle == CLIP) ? "" : ellipsisString; int eLen = ellipsis.length(); // Do this before using helper, as it's not reentrant. double eWidth = computeTextWidth(font, ellipsis, 0); double eHeight = computeTextHeight(font, ellipsis, 0, boundsType); if (width < eWidth || height < eHeight) { // The ellipsis doesn't fit. return text; // RT-30868 - return text, not empty string. } helper.setText(text); helper.setFont(font); helper.setWrappingWidth((int)Math.ceil(width)); helper.setBoundsType(boundsType); helper.setLineSpacing(0); 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; 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()); } // 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(); } 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; } 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; } else { brInd = firstBreakCharIndex(text, ind); 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 { result = ellipsis + text.substring(ind); } } // 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 = text.substring(0, hit2) + ellipsis; 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 = ellipsis + result.substring(ind); } 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; } } else { break; } } } // RESTORE STATE helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH); helper.setLineSpacing(DEFAULT_LINE_SPACING); helper.setText(DEFAULT_TEXT); helper.setBoundsType(DEFAULT_BOUNDS_TYPE); return result; } private static int firstBreakCharIndex(String str, int start) { char[] chars = str.toCharArray(); for (int i = start; i < chars.length; i++) { if (isPreferredBreakCharacter(chars[i])) { return i; } } return -1; } private static int lastBreakCharIndex(String str, int start) { char[] chars = str.toCharArray(); for (int i = start; i >= 0; i--) { if (isPreferredBreakCharacter(chars[i])) { return i; } } return -1; } /* Recognizes white space and latin punctuation as preferred * line break positions. Could do a bit better with using more * of the properties from the Character class. */ private static boolean isPreferredBreakCharacter(char ch) { if (Character.isWhitespace(ch)) { return true; } else { switch (ch) { case ';' : case ':' : case '.' : return true; default: return false; } } } private static boolean requiresComplexLayout(Font font, String string) { /* Map attrs = font.getAttributes(); if (contains(attrs, KERNING, KERNING_ON) || contains(attrs, LIGATURES, LIGATURES_ON) || (attrs.containsKey(TRACKING) && attrs.get(TRACKING) != null)) { return true; } return isComplexLayout(string.toCharArray(), 0, string.length()); */ return false; } static int computeStartOfWord(Font font, String text, int index) { if ("".equals(text) || index < 0) return 0; if (text.length() <= index) return text.length(); // if the given index is not in a word (but in whitespace), then // simply return the index if (Character.isWhitespace(text.charAt(index))) { return index; } boolean complexLayout = requiresComplexLayout(font, text); if (complexLayout) { // TODO needs implementation return 0; } else { // just start walking backwards from index until either i<0 or // the first whitespace is found. int i = index; while (--i >= 0) { if (Character.isWhitespace(text.charAt(i))) { return i + 1; } } return 0; } } static int computeEndOfWord(Font font, String text, int index) { if (text.equals("") || index < 0) { return 0; } if (text.length() <= index) { return text.length(); } // if the given index is not in a word (but in whitespace), then // simply return the index if (Character.isWhitespace(text.charAt(index))) { return index; } boolean complexLayout = requiresComplexLayout(font, text); if (complexLayout) { // TODO needs implementation return text.length(); } else { // just start walking forward from index until either i > length or // the first whitespace is found. int i = index; while (++i < text.length()) { if (Character.isWhitespace(text.charAt(i))) { return i; } } return text.length(); } } // used for layout to adjust widths to honor the min/max policies consistently public static double boundedSize(double value, double min, double max) { // if max < value, return max // if min > value, return min // if min > max, return min return Math.min(Math.max(value, min), Math.max(min,max)); } public static void addMnemonics(ContextMenu popup, Scene scene) { addMnemonics(popup, scene, false); } public static void addMnemonics(ContextMenu popup, Scene scene, boolean initialState) { addMnemonics(popup, scene, initialState, null); } public static void addMnemonics(ContextMenu popup, Scene scene, boolean initialState, List<Mnemonic> into) { if (!com.sun.javafx.PlatformUtil.isMac()) { ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode(); MenuItem menuitem; for (int i = 0 ; i < popup.getItems().size() ; i++) { menuitem = popup.getItems().get(i); /* ** check is there are any mnemonics in this menu */ if (menuitem.isMnemonicParsing()) { TextBinding bindings = new TextBinding(menuitem.getText()); int mnemonicIndex = bindings.getMnemonicIndex() ; if (mnemonicIndex >= 0) { KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination(); Mnemonic myMnemonic = new Mnemonic(cmContent.getLabelAt(i), mnemonicKeyCombo); scene.addMnemonic(myMnemonic); NodeHelper.setShowMnemonics(cmContent.getLabelAt(i), initialState); if (into != null) { into.add(myMnemonic); } } } } } } public static void removeMnemonics(ContextMenu popup, Scene scene) { if (!com.sun.javafx.PlatformUtil.isMac()) { ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode(); MenuItem menuitem; for (int i = 0 ; i < popup.getItems().size() ; i++) { menuitem = popup.getItems().get(i); /* ** check is there are any mnemonics in this menu */ if (menuitem.isMnemonicParsing()) { TextBinding bindings = new TextBinding(menuitem.getText()); int mnemonicIndex = bindings.getMnemonicIndex() ; if (mnemonicIndex >= 0) { KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination(); ObservableList<Mnemonic> mnemonicsList = scene.getMnemonics().get(mnemonicKeyCombo); if (mnemonicsList != null) { for (int j = 0 ; j < mnemonicsList.size() ; j++) { if (mnemonicsList.get(j).getNode() == cmContent.getLabelAt(i)) { mnemonicsList.remove(j); } } } } } } } } public static double computeXOffset(double width, double contentWidth, HPos hpos) { if (hpos == null) { return 0; } switch(hpos) { case LEFT: return 0; case CENTER: return (width - contentWidth) / 2; case RIGHT: return width - contentWidth; default: return 0; } } public static double computeYOffset(double height, double contentHeight, VPos vpos) { if (vpos == null) { return 0; } switch(vpos) { case TOP: return 0; case CENTER: return (height - contentHeight) / 2; case BOTTOM: return height - contentHeight; default: return 0; } } /* ** Returns true if the platform is to use Two-Level-Focus. ** This is in the Util class to ease any changes in ** the criteria for enabling this feature. ** ** TwoLevelFocus is needed on platforms that ** only support 5-button navigation (arrow keys and Select/OK). ** */ public static boolean isTwoLevelFocus() { return Platform.isSupported(ConditionalFeature.TWO_LEVEL_FOCUS); } // useful method for linking things together when before a property is // necessarily set public static <T> void executeOnceWhenPropertyIsNonNull(ObservableValue<T> p, Consumer<T> consumer) { if (p == null) return; T value = p.getValue(); if (value != null) { consumer.accept(value); } else { final InvalidationListener listener = new InvalidationListener() { @Override public void invalidated(Observable observable) { T value = p.getValue(); if (value != null) { p.removeListener(this); consumer.accept(value); } } }; p.addListener(listener); } } public static String formatHexString(Color c) { if (c != null) { return String.format((Locale) null, "#%02x%02x%02x", Math.round(c.getRed() * 255), Math.round(c.getGreen() * 255), Math.round(c.getBlue() * 255)); } else { return null; } } public static URL getResource(String str) { return Utils.class.getResource(str); } }