modules/controls/src/main/java/com/sun/javafx/scene/control/skin/Utils.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
   1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  * To change this template, choose Tools | Templates
  28  * and open the template in the editor.
  29  */
  30 
  31 package com.sun.javafx.scene.control.skin;
  32 
  33 import java.text.Bidi;
  34 import java.text.BreakIterator;
  35 import java.util.function.Consumer;
  36 
  37 import static javafx.scene.control.OverrunStyle.*;
  38 import javafx.application.Platform;
  39 import javafx.application.ConditionalFeature;

  40 import javafx.beans.InvalidationListener;
  41 import javafx.beans.Observable;
  42 import javafx.beans.property.Property;
  43 import javafx.beans.value.ObservableValue;
  44 import javafx.collections.ObservableList;
  45 import javafx.geometry.Bounds;
  46 import javafx.geometry.HPos;
  47 import javafx.geometry.Point2D;
  48 import javafx.geometry.VPos;
  49 import javafx.scene.Scene;
  50 import javafx.scene.control.ContextMenu;
  51 import javafx.scene.control.MenuItem;
  52 import javafx.scene.control.OverrunStyle;

  53 import javafx.scene.input.KeyCombination;
  54 import javafx.scene.input.Mnemonic;

  55 import javafx.scene.text.Font;
  56 import javafx.scene.text.Text;
  57 import javafx.scene.text.TextBoundsType;
  58 
  59 import com.sun.javafx.scene.control.behavior.TextBinding;
  60 import com.sun.javafx.scene.text.HitInfo;
  61 import com.sun.javafx.scene.text.TextLayout;
  62 import com.sun.javafx.tk.Toolkit;
  63 import javafx.util.Callback;








  64 
  65 /**
  66  * BE REALLY CAREFUL WITH RESTORING OR RESETTING STATE OF helper NODE AS LEFTOVER
  67  * STATE CAUSES REALLY ODD NASTY BUGS!
  68  *
  69  * We expect all methods to set the Font property of helper but other than that
  70  * any properties set should be restored to defaults.
  71  */
  72 public class Utils {
  73 
  74     static final Text helper = new Text();
  75     static final double DEFAULT_WRAPPING_WIDTH = helper.getWrappingWidth();
  76     static final double DEFAULT_LINE_SPACING = helper.getLineSpacing();
  77     static final String DEFAULT_TEXT = helper.getText();
  78     static final TextBoundsType DEFAULT_BOUNDS_TYPE = helper.getBoundsType();
  79 
  80     /* Using TextLayout directly for simple text measurement.
  81      * Instead of restoring the TextLayout attributes to default values
  82      * (each renders the TextLayout unable to efficiently cache layout data).
  83      * It always sets all the attributes pertinent to calculation being performed.
  84      * Note that lineSpacing and boundsType are important when computing the height
  85      * but irrelevant when computing the width.
  86      *
  87      * Note: This code assumes that TextBoundsType#VISUAL is never used by controls.
  88      * */
  89     static final TextLayout layout = Toolkit.getToolkit().getTextLayoutFactory().createLayout();
  90 
  91     static double getAscent(Font font, TextBoundsType boundsType) {
  92         layout.setContent("", font.impl_getNativeFont());
  93         layout.setWrapWidth(0);
  94         layout.setLineSpacing(0);
  95         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
  96             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
  97         } else {
  98             layout.setBoundsType(0);
  99         }
 100         return -layout.getBounds().getMinY();
 101     }
 102 
 103     static double getLineHeight(Font font, TextBoundsType boundsType) {
 104         layout.setContent("", font.impl_getNativeFont());
 105         layout.setWrapWidth(0);
 106         layout.setLineSpacing(0);
 107         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 108             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 109         } else {
 110             layout.setBoundsType(0);
 111         }
 112 
 113         // RT-37092: Use the line bounds specifically, to include font leading.
 114         return layout.getLines()[0].getBounds().getHeight();
 115     }
 116 
 117     static double computeTextWidth(Font font, String text, double wrappingWidth) {
 118         layout.setContent(text != null ? text : "", font.impl_getNativeFont());
 119         layout.setWrapWidth((float)wrappingWidth);
 120         return layout.getBounds().getWidth();
 121     }
 122 
 123     static double computeTextHeight(Font font, String text, double wrappingWidth, TextBoundsType boundsType) {
 124         return computeTextHeight(font, text, wrappingWidth, 0, boundsType);
 125     }
 126 
 127     @SuppressWarnings("deprecation")
 128     static double computeTextHeight(Font font, String text, double wrappingWidth, double lineSpacing, TextBoundsType boundsType) {
 129         layout.setContent(text != null ? text : "", font.impl_getNativeFont());
 130         layout.setWrapWidth((float)wrappingWidth);
 131         layout.setLineSpacing((float)lineSpacing);
 132         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 133             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 134         } else {
 135             layout.setBoundsType(0);
 136         }
 137         return layout.getBounds().getHeight();
 138     }
 139 
 140     static int computeTruncationIndex(Font font, String text, double width) {
 141         helper.setText(text);
 142         helper.setFont(font);
 143         helper.setWrappingWidth(0);
 144         helper.setLineSpacing(0);
 145         // The -2 is a fudge to make sure the result more often matches
 146         // what we get from using computeTextWidth instead. It's not yet
 147         // clear what causes the small discrepancies.
 148         Bounds bounds = helper.getLayoutBounds();
 149         Point2D endPoint = new Point2D(width - 2, bounds.getMinY() + bounds.getHeight() / 2);
 150         final int index = helper.impl_hitTestChar(endPoint).getCharIndex();
 151         // RESTORE STATE
 152         helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
 153         helper.setLineSpacing(DEFAULT_LINE_SPACING);
 154         helper.setText(DEFAULT_TEXT);
 155         return index;
 156     }
 157 
 158     static String computeClippedText(Font font, String text, double width,
 159                                      OverrunStyle type, String ellipsisString) {
 160         if (font == null) {
 161             throw new IllegalArgumentException("Must specify a font");
 162         }
 163         OverrunStyle style = (type == null || type == CLIP) ? ELLIPSIS : type;
 164         final String ellipsis = (type == CLIP) ? "" : ellipsisString;
 165         // if the text is empty or null or no ellipsis, then it always fits
 166         if (text == null || "".equals(text)) {
 167             return text;
 168         }
 169         // if the string width is < the available width, then it fits and
 170         // doesn't need to be clipped.  We use a double point comparison
 171         // of 0.001 (1/1000th of a pixel) to account for any numerical
 172         // discrepancies introduced when the available width was calculated.
 173         // MenuItemSkinBase.doLayout, for example, does a number of double
 174         // point operations when computing the available width.
 175         final double stringWidth = computeTextWidth(font, text, 0);
 176         if (stringWidth - width < 0.0010F) {
 177             return text;
 178         }


 339                     return text.substring(0, leadingIndex + 1) + ellipsis;
 340                 }
 341                 return text.substring(0, leadingIndex + 1) + ellipsis + text.substring(trailingIndex);
 342             } else {
 343                 boolean leadingIndexIsLastLetterInWord =
 344                     Character.isWhitespace(text.charAt(leadingIndex + 1));
 345                 int index = (leadingWhitespace == -1 || leadingIndexIsLastLetterInWord) ? (leadingIndex + 1) : (leadingWhitespace);
 346                 String leading = text.substring(0, index);
 347                 if (trailingIndex < 0) {
 348                     return leading + ellipsis;
 349                 }
 350                 boolean trailingIndexIsFirstLetterInWord =
 351                     Character.isWhitespace(text.charAt(trailingIndex - 1));
 352                 index = (trailingWhitespace == -1 || trailingIndexIsFirstLetterInWord) ? (trailingIndex) : (trailingWhitespace + 1);
 353                 String trailing = text.substring(index);
 354                 return leading + ellipsis + trailing;
 355             }
 356         }
 357     }
 358 
 359     static String computeClippedWrappedText(Font font, String text, double width,
 360                                             double height, OverrunStyle truncationStyle,
 361                                             String ellipsisString, TextBoundsType boundsType) {
 362         if (font == null) {
 363             throw new IllegalArgumentException("Must specify a font");
 364         }
 365 
 366         String ellipsis = (truncationStyle == CLIP) ? "" : ellipsisString;
 367         int eLen = ellipsis.length();
 368         // Do this before using helper, as it's not reentrant.
 369         double eWidth = computeTextWidth(font, ellipsis, 0);
 370         double eHeight = computeTextHeight(font, ellipsis, 0, boundsType);
 371 
 372         if (width < eWidth || height < eHeight) {
 373             // The ellipsis doesn't fit.
 374             return text; // RT-30868 - return text, not empty string.
 375         }
 376 
 377         helper.setText(text);
 378         helper.setFont(font);
 379         helper.setWrappingWidth((int)Math.ceil(width));


 617             // just start walking forward from index until either i > length or
 618             // the first whitespace is found.
 619             int i = index;
 620             while (++i < text.length()) {
 621                 if (Character.isWhitespace(text.charAt(i))) {
 622                     return i;
 623                 }
 624             }
 625             return text.length();
 626         }
 627     }
 628 
 629     // used for layout to adjust widths to honor the min/max policies consistently
 630     public static double boundedSize(double value, double min, double max) {
 631         // if max < value, return max
 632         // if min > value, return min
 633         // if min > max, return min
 634         return Math.min(Math.max(value, min), Math.max(min,max));
 635     }
 636 
 637     static void addMnemonics(ContextMenu popup, Scene scene) {
 638         addMnemonics(popup, scene, false);
 639     }
 640 
 641     static void addMnemonics(ContextMenu popup, Scene scene, boolean initialState) {
 642 
 643         if (!com.sun.javafx.PlatformUtil.isMac()) {
 644 
 645             ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 646             MenuItem menuitem;
 647 
 648             for (int i = 0 ; i < popup.getItems().size() ; i++) {
 649                 menuitem = popup.getItems().get(i);
 650                 /*
 651                 ** check is there are any mnemonics in this menu
 652                 */
 653                 if (menuitem.isMnemonicParsing()) {
 654 
 655                     TextBinding bindings = new TextBinding(menuitem.getText());
 656                     int mnemonicIndex = bindings.getMnemonicIndex() ;
 657                     if (mnemonicIndex >= 0) {
 658                         KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination();
 659                         Mnemonic myMnemonic = new Mnemonic(cmContent.getLabelAt(i), mnemonicKeyCombo);
 660                         scene.addMnemonic(myMnemonic);
 661                         cmContent.getLabelAt(i).impl_setShowMnemonics(initialState);
 662                     }
 663                 }
 664             }
 665         }
 666     }
 667 
 668 
 669 
 670     static void removeMnemonics(ContextMenu popup, Scene scene) {
 671 
 672         if (!com.sun.javafx.PlatformUtil.isMac()) {
 673 
 674             ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 675             MenuItem menuitem;
 676 
 677             for (int i = 0 ; i < popup.getItems().size() ; i++) {
 678                 menuitem = popup.getItems().get(i);
 679                 /*
 680                 ** check is there are any mnemonics in this menu
 681                 */
 682                 if (menuitem.isMnemonicParsing()) {
 683 
 684                     TextBinding bindings = new TextBinding(menuitem.getText());
 685                     int mnemonicIndex = bindings.getMnemonicIndex() ;
 686                     if (mnemonicIndex >= 0) {
 687                         KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination();
 688 
 689                         ObservableList<Mnemonic> mnemonicsList = scene.getMnemonics().get(mnemonicKeyCombo);
 690                         if (mnemonicsList != null) {
 691                             for (int j = 0 ; j < mnemonicsList.size() ; j++) {
 692                                 if (mnemonicsList.get(j).getNode() == cmContent.getLabelAt(i)) {
 693                                     mnemonicsList.remove(j);
 694                                 }
 695                             }
 696                         }
 697                     }
 698                 }
 699             }
 700         }
 701     }
 702 
 703     static double computeXOffset(double width, double contentWidth, HPos hpos) {
 704         if (hpos == null) {
 705             return 0;
 706         }
 707 
 708         switch(hpos) {
 709             case LEFT:
 710                return 0;
 711             case CENTER:
 712                return (width - contentWidth) / 2;
 713             case RIGHT:
 714                return width - contentWidth;
 715             default:
 716                 return 0;
 717         }
 718     }
 719 
 720     static double computeYOffset(double height, double contentHeight, VPos vpos) {
 721         if (vpos == null) {
 722             return 0;
 723         }
 724 
 725         switch(vpos) {
 726             case TOP:
 727                return 0;
 728             case CENTER:
 729                return (height - contentHeight) / 2;
 730             case BOTTOM:
 731                return height - contentHeight;
 732             default:
 733                 return 0;
 734         }
 735     }
 736 
 737     /*
 738     ** Returns true if the platform is to use Two-Level-Focus.
 739     ** This is in the Util class to ease any changes in
 740     ** the criteria for enabling this feature.
 741     **
 742     ** TwoLevelFocus is needed on platforms that
 743     ** only support 5-button navigation (arrow keys and Select/OK).
 744     **
 745     */
 746     public static boolean isTwoLevelFocus() {
 747         return Platform.isSupported(ConditionalFeature.TWO_LEVEL_FOCUS);
 748     }
 749 
 750 
 751     // Workaround for RT-26961. HitInfo.getInsertionIndex() doesn't skip
 752     // complex character clusters / ligatures.
 753     private static BreakIterator charIterator = null;
 754     public static int getHitInsertionIndex(HitInfo hit, String text) {
 755         int charIndex = hit.getCharIndex();
 756         if (text != null && !hit.isLeading()) {
 757             if (charIterator == null) {
 758                 charIterator = BreakIterator.getCharacterInstance();
 759             }
 760             charIterator.setText(text);
 761             int next = charIterator.following(charIndex);
 762             if (next == BreakIterator.DONE) {
 763                 charIndex = hit.getInsertionIndex();
 764             } else {
 765                 charIndex = next;
 766             }
 767         }
 768         return charIndex;
 769     }
 770 
 771     // useful method for linking things together when before a property is
 772     // necessarily set
 773     public static <T> void executeOnceWhenPropertyIsNonNull(ObservableValue<T> p, Consumer<T> consumer) {
 774         if (p == null) return;
 775 
 776         T value = p.getValue();
 777         if (value != null) {
 778             consumer.accept(value);
 779         } else {
 780             final InvalidationListener listener = new InvalidationListener() {
 781                 @Override public void invalidated(Observable observable) {
 782                     T value = p.getValue();
 783 
 784                     if (value != null) {
 785                         p.removeListener(this);
 786                         consumer.accept(value);
 787                     }
 788                 }
 789             };
 790             p.addListener(listener);











 791         }
 792     }
 793 }
   1 /*
   2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  * To change this template, choose Tools | Templates
  28  * and open the template in the editor.
  29  */
  30 
  31 package com.sun.javafx.scene.control.skin;
  32 
  33 import com.sun.javafx.scene.control.behavior.TextBinding;
  34 import com.sun.javafx.scene.text.TextLayout;
  35 import com.sun.javafx.tk.Toolkit;



  36 import javafx.application.ConditionalFeature;
  37 import javafx.application.Platform;
  38 import javafx.beans.InvalidationListener;
  39 import javafx.beans.Observable;

  40 import javafx.beans.value.ObservableValue;
  41 import javafx.collections.ObservableList;
  42 import javafx.geometry.Bounds;
  43 import javafx.geometry.HPos;
  44 import javafx.geometry.Point2D;
  45 import javafx.geometry.VPos;
  46 import javafx.scene.Scene;
  47 import javafx.scene.control.ContextMenu;
  48 import javafx.scene.control.MenuItem;
  49 import javafx.scene.control.OverrunStyle;
  50 import com.sun.javafx.scene.control.ContextMenuContent;
  51 import javafx.scene.input.KeyCombination;
  52 import javafx.scene.input.Mnemonic;
  53 import javafx.scene.paint.Color;
  54 import javafx.scene.text.Font;
  55 import javafx.scene.text.Text;
  56 import javafx.scene.text.TextBoundsType;
  57 
  58 import java.text.Bidi;
  59 import java.text.BreakIterator;
  60 import java.util.Locale;
  61 import java.util.function.Consumer;
  62 
  63 import static javafx.scene.control.OverrunStyle.CENTER_ELLIPSIS;
  64 import static javafx.scene.control.OverrunStyle.CENTER_WORD_ELLIPSIS;
  65 import static javafx.scene.control.OverrunStyle.CLIP;
  66 import static javafx.scene.control.OverrunStyle.ELLIPSIS;
  67 import static javafx.scene.control.OverrunStyle.LEADING_ELLIPSIS;
  68 import static javafx.scene.control.OverrunStyle.LEADING_WORD_ELLIPSIS;
  69 import static javafx.scene.control.OverrunStyle.WORD_ELLIPSIS;
  70 import static javafx.scene.control.skin.TextFieldSkin.TextPosInfo;
  71 
  72 /**
  73  * BE REALLY CAREFUL WITH RESTORING OR RESETTING STATE OF helper NODE AS LEFTOVER
  74  * STATE CAUSES REALLY ODD NASTY BUGS!
  75  *
  76  * We expect all methods to set the Font property of helper but other than that
  77  * any properties set should be restored to defaults.
  78  */
  79 public class Utils {
  80 
  81     static final Text helper = new Text();
  82     static final double DEFAULT_WRAPPING_WIDTH = helper.getWrappingWidth();
  83     static final double DEFAULT_LINE_SPACING = helper.getLineSpacing();
  84     static final String DEFAULT_TEXT = helper.getText();
  85     static final TextBoundsType DEFAULT_BOUNDS_TYPE = helper.getBoundsType();
  86 
  87     /* Using TextLayout directly for simple text measurement.
  88      * Instead of restoring the TextLayout attributes to default values
  89      * (each renders the TextLayout unable to efficiently cache layout data).
  90      * It always sets all the attributes pertinent to calculation being performed.
  91      * Note that lineSpacing and boundsType are important when computing the height
  92      * but irrelevant when computing the width.
  93      *
  94      * Note: This code assumes that TextBoundsType#VISUAL is never used by controls.
  95      * */
  96     static final TextLayout layout = Toolkit.getToolkit().getTextLayoutFactory().createLayout();
  97 
  98     public static double getAscent(Font font, TextBoundsType boundsType) {
  99         layout.setContent("", font.impl_getNativeFont());
 100         layout.setWrapWidth(0);
 101         layout.setLineSpacing(0);
 102         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 103             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 104         } else {
 105             layout.setBoundsType(0);
 106         }
 107         return -layout.getBounds().getMinY();
 108     }
 109 
 110     public static double getLineHeight(Font font, TextBoundsType boundsType) {
 111         layout.setContent("", font.impl_getNativeFont());
 112         layout.setWrapWidth(0);
 113         layout.setLineSpacing(0);
 114         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 115             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 116         } else {
 117             layout.setBoundsType(0);
 118         }
 119 
 120         // RT-37092: Use the line bounds specifically, to include font leading.
 121         return layout.getLines()[0].getBounds().getHeight();
 122     }
 123 
 124     public static double computeTextWidth(Font font, String text, double wrappingWidth) {
 125         layout.setContent(text != null ? text : "", font.impl_getNativeFont());
 126         layout.setWrapWidth((float)wrappingWidth);
 127         return layout.getBounds().getWidth();
 128     }
 129 
 130     public static double computeTextHeight(Font font, String text, double wrappingWidth, TextBoundsType boundsType) {
 131         return computeTextHeight(font, text, wrappingWidth, 0, boundsType);
 132     }
 133 
 134     @SuppressWarnings("deprecation")
 135     public static double computeTextHeight(Font font, String text, double wrappingWidth, double lineSpacing, TextBoundsType boundsType) {
 136         layout.setContent(text != null ? text : "", font.impl_getNativeFont());
 137         layout.setWrapWidth((float)wrappingWidth);
 138         layout.setLineSpacing((float)lineSpacing);
 139         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 140             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 141         } else {
 142             layout.setBoundsType(0);
 143         }
 144         return layout.getBounds().getHeight();
 145     }
 146 
 147     public static int computeTruncationIndex(Font font, String text, double width) {
 148         helper.setText(text);
 149         helper.setFont(font);
 150         helper.setWrappingWidth(0);
 151         helper.setLineSpacing(0);
 152         // The -2 is a fudge to make sure the result more often matches
 153         // what we get from using computeTextWidth instead. It's not yet
 154         // clear what causes the small discrepancies.
 155         Bounds bounds = helper.getLayoutBounds();
 156         Point2D endPoint = new Point2D(width - 2, bounds.getMinY() + bounds.getHeight() / 2);
 157         final int index = helper.impl_hitTestChar(endPoint).getCharIndex();
 158         // RESTORE STATE
 159         helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
 160         helper.setLineSpacing(DEFAULT_LINE_SPACING);
 161         helper.setText(DEFAULT_TEXT);
 162         return index;
 163     }
 164 
 165     public static String computeClippedText(Font font, String text, double width,
 166                                      OverrunStyle type, String ellipsisString) {
 167         if (font == null) {
 168             throw new IllegalArgumentException("Must specify a font");
 169         }
 170         OverrunStyle style = (type == null || type == CLIP) ? ELLIPSIS : type;
 171         final String ellipsis = (type == CLIP) ? "" : ellipsisString;
 172         // if the text is empty or null or no ellipsis, then it always fits
 173         if (text == null || "".equals(text)) {
 174             return text;
 175         }
 176         // if the string width is < the available width, then it fits and
 177         // doesn't need to be clipped.  We use a double point comparison
 178         // of 0.001 (1/1000th of a pixel) to account for any numerical
 179         // discrepancies introduced when the available width was calculated.
 180         // MenuItemSkinBase.doLayout, for example, does a number of double
 181         // point operations when computing the available width.
 182         final double stringWidth = computeTextWidth(font, text, 0);
 183         if (stringWidth - width < 0.0010F) {
 184             return text;
 185         }


 346                     return text.substring(0, leadingIndex + 1) + ellipsis;
 347                 }
 348                 return text.substring(0, leadingIndex + 1) + ellipsis + text.substring(trailingIndex);
 349             } else {
 350                 boolean leadingIndexIsLastLetterInWord =
 351                     Character.isWhitespace(text.charAt(leadingIndex + 1));
 352                 int index = (leadingWhitespace == -1 || leadingIndexIsLastLetterInWord) ? (leadingIndex + 1) : (leadingWhitespace);
 353                 String leading = text.substring(0, index);
 354                 if (trailingIndex < 0) {
 355                     return leading + ellipsis;
 356                 }
 357                 boolean trailingIndexIsFirstLetterInWord =
 358                     Character.isWhitespace(text.charAt(trailingIndex - 1));
 359                 index = (trailingWhitespace == -1 || trailingIndexIsFirstLetterInWord) ? (trailingIndex) : (trailingWhitespace + 1);
 360                 String trailing = text.substring(index);
 361                 return leading + ellipsis + trailing;
 362             }
 363         }
 364     }
 365 
 366     public static String computeClippedWrappedText(Font font, String text, double width,
 367                                             double height, OverrunStyle truncationStyle,
 368                                             String ellipsisString, TextBoundsType boundsType) {
 369         if (font == null) {
 370             throw new IllegalArgumentException("Must specify a font");
 371         }
 372 
 373         String ellipsis = (truncationStyle == CLIP) ? "" : ellipsisString;
 374         int eLen = ellipsis.length();
 375         // Do this before using helper, as it's not reentrant.
 376         double eWidth = computeTextWidth(font, ellipsis, 0);
 377         double eHeight = computeTextHeight(font, ellipsis, 0, boundsType);
 378 
 379         if (width < eWidth || height < eHeight) {
 380             // The ellipsis doesn't fit.
 381             return text; // RT-30868 - return text, not empty string.
 382         }
 383 
 384         helper.setText(text);
 385         helper.setFont(font);
 386         helper.setWrappingWidth((int)Math.ceil(width));


 624             // just start walking forward from index until either i > length or
 625             // the first whitespace is found.
 626             int i = index;
 627             while (++i < text.length()) {
 628                 if (Character.isWhitespace(text.charAt(i))) {
 629                     return i;
 630                 }
 631             }
 632             return text.length();
 633         }
 634     }
 635 
 636     // used for layout to adjust widths to honor the min/max policies consistently
 637     public static double boundedSize(double value, double min, double max) {
 638         // if max < value, return max
 639         // if min > value, return min
 640         // if min > max, return min
 641         return Math.min(Math.max(value, min), Math.max(min,max));
 642     }
 643 
 644     public static void addMnemonics(ContextMenu popup, Scene scene) {
 645         addMnemonics(popup, scene, false);
 646     }
 647 
 648     public static void addMnemonics(ContextMenu popup, Scene scene, boolean initialState) {
 649 
 650         if (!com.sun.javafx.PlatformUtil.isMac()) {
 651 
 652             ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 653             MenuItem menuitem;
 654 
 655             for (int i = 0 ; i < popup.getItems().size() ; i++) {
 656                 menuitem = popup.getItems().get(i);
 657                 /*
 658                 ** check is there are any mnemonics in this menu
 659                 */
 660                 if (menuitem.isMnemonicParsing()) {
 661 
 662                     TextBinding bindings = new TextBinding(menuitem.getText());
 663                     int mnemonicIndex = bindings.getMnemonicIndex() ;
 664                     if (mnemonicIndex >= 0) {
 665                         KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination();
 666                         Mnemonic myMnemonic = new Mnemonic(cmContent.getLabelAt(i), mnemonicKeyCombo);
 667                         scene.addMnemonic(myMnemonic);
 668                         cmContent.getLabelAt(i).impl_setShowMnemonics(initialState);
 669                     }
 670                 }
 671             }
 672         }
 673     }
 674 
 675 
 676 
 677     public static void removeMnemonics(ContextMenu popup, Scene scene) {
 678 
 679         if (!com.sun.javafx.PlatformUtil.isMac()) {
 680 
 681             ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 682             MenuItem menuitem;
 683 
 684             for (int i = 0 ; i < popup.getItems().size() ; i++) {
 685                 menuitem = popup.getItems().get(i);
 686                 /*
 687                 ** check is there are any mnemonics in this menu
 688                 */
 689                 if (menuitem.isMnemonicParsing()) {
 690 
 691                     TextBinding bindings = new TextBinding(menuitem.getText());
 692                     int mnemonicIndex = bindings.getMnemonicIndex() ;
 693                     if (mnemonicIndex >= 0) {
 694                         KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination();
 695 
 696                         ObservableList<Mnemonic> mnemonicsList = scene.getMnemonics().get(mnemonicKeyCombo);
 697                         if (mnemonicsList != null) {
 698                             for (int j = 0 ; j < mnemonicsList.size() ; j++) {
 699                                 if (mnemonicsList.get(j).getNode() == cmContent.getLabelAt(i)) {
 700                                     mnemonicsList.remove(j);
 701                                 }
 702                             }
 703                         }
 704                     }
 705                 }
 706             }
 707         }
 708     }
 709 
 710     public static double computeXOffset(double width, double contentWidth, HPos hpos) {
 711         if (hpos == null) {
 712             return 0;
 713         }
 714 
 715         switch(hpos) {
 716             case LEFT:
 717                return 0;
 718             case CENTER:
 719                return (width - contentWidth) / 2;
 720             case RIGHT:
 721                return width - contentWidth;
 722             default:
 723                 return 0;
 724         }
 725     }
 726 
 727     public static double computeYOffset(double height, double contentHeight, VPos vpos) {
 728         if (vpos == null) {
 729             return 0;
 730         }
 731 
 732         switch(vpos) {
 733             case TOP:
 734                return 0;
 735             case CENTER:
 736                return (height - contentHeight) / 2;
 737             case BOTTOM:
 738                return height - contentHeight;
 739             default:
 740                 return 0;
 741         }
 742     }
 743 
 744     /*
 745     ** Returns true if the platform is to use Two-Level-Focus.
 746     ** This is in the Util class to ease any changes in
 747     ** the criteria for enabling this feature.
 748     **
 749     ** TwoLevelFocus is needed on platforms that
 750     ** only support 5-button navigation (arrow keys and Select/OK).
 751     **
 752     */
 753     public static boolean isTwoLevelFocus() {
 754         return Platform.isSupported(ConditionalFeature.TWO_LEVEL_FOCUS);
 755     }
 756 
 757 
 758     // Workaround for RT-26961. HitInfo.getInsertionIndex() doesn't skip
 759     // complex character clusters / ligatures.
 760     private static BreakIterator charIterator = null;
 761     public static int getHitInsertionIndex(TextPosInfo hit, String text) {
 762         int charIndex = hit.getCharIndex();
 763         if (text != null && !hit.isLeading()) {
 764             if (charIterator == null) {
 765                 charIterator = BreakIterator.getCharacterInstance();
 766             }
 767             charIterator.setText(text);
 768             int next = charIterator.following(charIndex);
 769             if (next == BreakIterator.DONE) {
 770                 charIndex = hit.getInsertionIndex();
 771             } else {
 772                 charIndex = next;
 773             }
 774         }
 775         return charIndex;
 776     }
 777 
 778     // useful method for linking things together when before a property is
 779     // necessarily set
 780     public static <T> void executeOnceWhenPropertyIsNonNull(ObservableValue<T> p, Consumer<T> consumer) {
 781         if (p == null) return;
 782 
 783         T value = p.getValue();
 784         if (value != null) {
 785             consumer.accept(value);
 786         } else {
 787             final InvalidationListener listener = new InvalidationListener() {
 788                 @Override public void invalidated(Observable observable) {
 789                     T value = p.getValue();
 790 
 791                     if (value != null) {
 792                         p.removeListener(this);
 793                         consumer.accept(value);
 794                     }
 795                 }
 796             };
 797             p.addListener(listener);
 798         }
 799     }
 800 
 801     public static String formatHexString(Color c) {
 802         if (c != null) {
 803             return String.format((Locale) null, "#%02x%02x%02x",
 804                     Math.round(c.getRed() * 255),
 805                     Math.round(c.getGreen() * 255),
 806                     Math.round(c.getBlue() * 255));
 807         } else {
 808             return null;
 809         }
 810     }
 811 }