1 /*
   2  * Copyright (c) 2011, 2017, 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.NodeHelper;
  34 import com.sun.javafx.scene.control.behavior.TextBinding;
  35 import com.sun.javafx.scene.text.TextLayout;
  36 import com.sun.javafx.tk.Toolkit;
  37 import javafx.application.ConditionalFeature;
  38 import javafx.application.Platform;
  39 import javafx.beans.InvalidationListener;
  40 import javafx.beans.Observable;
  41 import javafx.beans.value.ObservableValue;
  42 import javafx.collections.ObservableList;
  43 import javafx.geometry.Bounds;
  44 import javafx.geometry.HPos;
  45 import javafx.geometry.Point2D;
  46 import javafx.geometry.VPos;
  47 import javafx.scene.Scene;
  48 import javafx.scene.control.ContextMenu;
  49 import javafx.scene.control.MenuItem;
  50 import javafx.scene.control.OverrunStyle;
  51 import com.sun.javafx.scene.control.ContextMenuContent;
  52 import com.sun.javafx.scene.text.FontHelper;
  53 import java.net.URL;
  54 import javafx.scene.input.KeyCombination;
  55 import javafx.scene.input.Mnemonic;
  56 import javafx.scene.paint.Color;
  57 import javafx.scene.text.Font;
  58 import javafx.scene.text.Text;
  59 import javafx.scene.text.TextBoundsType;
  60 import javafx.scene.text.HitInfo;
  61 
  62 import java.text.Bidi;
  63 import java.util.Locale;
  64 import java.util.function.Consumer;
  65 
  66 import static javafx.scene.control.OverrunStyle.CENTER_ELLIPSIS;
  67 import static javafx.scene.control.OverrunStyle.CENTER_WORD_ELLIPSIS;
  68 import static javafx.scene.control.OverrunStyle.CLIP;
  69 import static javafx.scene.control.OverrunStyle.ELLIPSIS;
  70 import static javafx.scene.control.OverrunStyle.LEADING_ELLIPSIS;
  71 import static javafx.scene.control.OverrunStyle.LEADING_WORD_ELLIPSIS;
  72 import static javafx.scene.control.OverrunStyle.WORD_ELLIPSIS;
  73 
  74 /**
  75  * BE REALLY CAREFUL WITH RESTORING OR RESETTING STATE OF helper NODE AS LEFTOVER
  76  * STATE CAUSES REALLY ODD NASTY BUGS!
  77  *
  78  * We expect all methods to set the Font property of helper but other than that
  79  * any properties set should be restored to defaults.
  80  */
  81 public class Utils {
  82 
  83     static final Text helper = new Text();
  84     static final double DEFAULT_WRAPPING_WIDTH = helper.getWrappingWidth();
  85     static final double DEFAULT_LINE_SPACING = helper.getLineSpacing();
  86     static final String DEFAULT_TEXT = helper.getText();
  87     static final TextBoundsType DEFAULT_BOUNDS_TYPE = helper.getBoundsType();
  88 
  89     /* Using TextLayout directly for simple text measurement.
  90      * Instead of restoring the TextLayout attributes to default values
  91      * (each renders the TextLayout unable to efficiently cache layout data).
  92      * It always sets all the attributes pertinent to calculation being performed.
  93      * Note that lineSpacing and boundsType are important when computing the height
  94      * but irrelevant when computing the width.
  95      *
  96      * Note: This code assumes that TextBoundsType#VISUAL is never used by controls.
  97      * */
  98     static final TextLayout layout = Toolkit.getToolkit().getTextLayoutFactory().createLayout();
  99 
 100     public static double getAscent(Font font, TextBoundsType boundsType) {
 101         layout.setContent("", FontHelper.getNativeFont(font));
 102         layout.setWrapWidth(0);
 103         layout.setLineSpacing(0);
 104         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 105             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 106         } else {
 107             layout.setBoundsType(0);
 108         }
 109         return -layout.getBounds().getMinY();
 110     }
 111 
 112     public static double getLineHeight(Font font, TextBoundsType boundsType) {
 113         layout.setContent("", FontHelper.getNativeFont(font));
 114         layout.setWrapWidth(0);
 115         layout.setLineSpacing(0);
 116         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 117             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 118         } else {
 119             layout.setBoundsType(0);
 120         }
 121 
 122         // RT-37092: Use the line bounds specifically, to include font leading.
 123         return layout.getLines()[0].getBounds().getHeight();
 124     }
 125 
 126     public static double computeTextWidth(Font font, String text, double wrappingWidth) {
 127         layout.setContent(text != null ? text : "", FontHelper.getNativeFont(font));
 128         layout.setWrapWidth((float)wrappingWidth);
 129         return layout.getBounds().getWidth();
 130     }
 131 
 132     public static double computeTextHeight(Font font, String text, double wrappingWidth, TextBoundsType boundsType) {
 133         return computeTextHeight(font, text, wrappingWidth, 0, boundsType);
 134     }
 135 
 136     @SuppressWarnings("deprecation")
 137     public static double computeTextHeight(Font font, String text, double wrappingWidth, double lineSpacing, TextBoundsType boundsType) {
 138         layout.setContent(text != null ? text : "", FontHelper.getNativeFont(font));
 139         layout.setWrapWidth((float)wrappingWidth);
 140         layout.setLineSpacing((float)lineSpacing);
 141         if (boundsType == TextBoundsType.LOGICAL_VERTICAL_CENTER) {
 142             layout.setBoundsType(TextLayout.BOUNDS_CENTER);
 143         } else {
 144             layout.setBoundsType(0);
 145         }
 146         return layout.getBounds().getHeight();
 147     }
 148 
 149     public static Point2D computeMnemonicPosition(Font font, String text, int mnemonicIndex, double wrappingWidth,
 150                                                   double lineSpacing) {
 151         // Input validation
 152         if ((font == null) || (text == null) ||
 153             (mnemonicIndex < 0) || (mnemonicIndex > text.length())) {
 154             return  null;
 155         }
 156 
 157         // Layout the text with given font, wrapping width and line spacing
 158         layout.setContent(text, FontHelper.getNativeFont(font));
 159         layout.setWrapWidth((float)wrappingWidth);
 160         layout.setLineSpacing((float)lineSpacing);
 161 
 162         // The text could be spread over multiple lines
 163         // We need to find out on which line the mnemonic character lies
 164         int start = 0;
 165         int i = 0;
 166         int totalLines = layout.getLines().length;
 167         while (i < totalLines) {
 168             int lineLength =  layout.getLines()[i].getLength();
 169 
 170             if ((mnemonicIndex >= start) &&
 171                 (mnemonicIndex < (start + lineLength))) {
 172                 // mnemonic lies on line 'i'
 173                 break;
 174             }
 175 
 176             start = start + lineLength;
 177             i++;
 178         }
 179 
 180         // Find x and y offsets of mnemonic character position
 181         // in line numbered 'i'
 182         double lineHeight = layout.getBounds().getHeight() / totalLines;
 183         double x = Utils.computeTextWidth(font, text.substring(start, mnemonicIndex), 0 );
 184 
 185         double y = (lineHeight * (i+1));
 186         // Adjust y offset for linespacing except for the last line.
 187         if ((i+1) != totalLines) {
 188             y = y - (lineSpacing / 2);
 189         }
 190 
 191         return new Point2D(x, y);
 192     }
 193 
 194     public static int computeTruncationIndex(Font font, String text, double width) {
 195         helper.setText(text);
 196         helper.setFont(font);
 197         helper.setWrappingWidth(0);
 198         helper.setLineSpacing(0);
 199         // The -2 is a fudge to make sure the result more often matches
 200         // what we get from using computeTextWidth instead. It's not yet
 201         // clear what causes the small discrepancies.
 202         Bounds bounds = helper.getLayoutBounds();
 203         Point2D endPoint = new Point2D(width - 2, bounds.getMinY() + bounds.getHeight() / 2);
 204         final int index = helper.hitTest(endPoint).getCharIndex();
 205         // RESTORE STATE
 206         helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
 207         helper.setLineSpacing(DEFAULT_LINE_SPACING);
 208         helper.setText(DEFAULT_TEXT);
 209         return index;
 210     }
 211 
 212     public static String computeClippedText(Font font, String text, double width,
 213                                      OverrunStyle type, String ellipsisString) {
 214         if (font == null) {
 215             throw new IllegalArgumentException("Must specify a font");
 216         }
 217         OverrunStyle style = (type == null || type == CLIP) ? ELLIPSIS : type;
 218         final String ellipsis = (type == CLIP) ? "" : ellipsisString;
 219         // if the text is empty or null or no ellipsis, then it always fits
 220         if (text == null || "".equals(text)) {
 221             return text;
 222         }
 223         // if the string width is < the available width, then it fits and
 224         // doesn't need to be clipped.  We use a double point comparison
 225         // of 0.001 (1/1000th of a pixel) to account for any numerical
 226         // discrepancies introduced when the available width was calculated.
 227         // MenuItemSkinBase.doLayout, for example, does a number of double
 228         // point operations when computing the available width.
 229         final double stringWidth = computeTextWidth(font, text, 0);
 230         if (stringWidth - width < 0.0010F) {
 231             return text;
 232         }
 233         // the width used by the ellipsis string
 234         final double ellipsisWidth = computeTextWidth(font, ellipsis, 0);
 235         // the available maximum width to fit chars into. This is essentially
 236         // the width minus the space required for the ellipsis string
 237         final double availableWidth = width - ellipsisWidth;
 238 
 239         if (width < ellipsisWidth) {
 240             // The ellipsis doesn't fit.
 241             return "";
 242         }
 243 
 244         // if we got here, then we must clip the text with an ellipsis.
 245         // this can be pretty expensive depending on whether "complex" text
 246         // layout needs to be taken into account. So each ellipsis option has
 247         // to take into account two code paths: the easy way and the correct
 248         // way. This is flagged by the "complexLayout" boolean
 249         // TODO make sure this function call takes into account ligatures, kerning,
 250         // and such as that will change the layout characteristics of the text
 251         // and will require a full complex layout
 252         // TODO since we don't have all the stuff available in FX to determine
 253         // complex text, I'm going to for now assume complex text is always false.
 254         final boolean complexLayout = false;
 255         //requiresComplexLayout(font, text);
 256 
 257         // generally all we want to do is count characters and add their widths.
 258         // For ellipsis that breaks on words, we do NOT want to include any
 259         // hanging whitespace.
 260         if (style == ELLIPSIS ||
 261             style == WORD_ELLIPSIS ||
 262             style == LEADING_ELLIPSIS ||
 263             style == LEADING_WORD_ELLIPSIS) {
 264 
 265             final boolean wordTrim =
 266                 (style == WORD_ELLIPSIS || style == LEADING_WORD_ELLIPSIS);
 267             String substring;
 268             if (complexLayout) {
 269             //            AttributedString a = new AttributedString(text);
 270             //            LineBreakMeasurer m = new LineBreakMeasurer(a.getIterator(), frc);
 271             //            substring = text.substring(0, m.nextOffset((double)availableWidth));
 272             } else {
 273                 // RT-23458: Use a faster algorithm for the most common case
 274                 // where truncation happens at the end, i.e. for ELLIPSIS and
 275                 // CLIP, but not for other cases such as WORD_ELLIPSIS, etc.
 276                 if (style == ELLIPSIS && !new Bidi(text, Bidi.DIRECTION_LEFT_TO_RIGHT).isMixed()) {
 277                     int hit = computeTruncationIndex(font, text, width - ellipsisWidth);
 278                     if (hit < 0 || hit >= text.length()) {
 279                         return text;
 280                     } else {
 281                         return text.substring(0, hit) + ellipsis;
 282                     }
 283                 }
 284 
 285                 // simply total up the widths of all chars to determine how many
 286                 // will fit in the available space. Remember the last whitespace
 287                 // encountered so that if we're breaking on words we can trim
 288                 // and omit it.
 289                 double total = 0.0F;
 290                 int whitespaceIndex = -1;
 291                 // at the termination of the loop, index will be one past the
 292                 // end of the substring
 293                 int index = 0;
 294                 int start = (style == LEADING_ELLIPSIS || style == LEADING_WORD_ELLIPSIS) ? (text.length() - 1) : (0);
 295                 int end = (start == 0) ? (text.length() - 1) : 0;
 296                 int stepValue = (start == 0) ? 1 : -1;
 297                 boolean done = (start == 0) ? start > end : start < end;
 298                 for (int i = start; !done ; i += stepValue) {
 299                     index = i;
 300                     char c = text.charAt(index);
 301                     total = computeTextWidth(font,
 302                                              (start == 0) ? text.substring(0, i + 1)
 303                                                           : text.substring(i, start + 1),
 304                                              0);
 305                     if (Character.isWhitespace(c)) {
 306                         whitespaceIndex = index;
 307                     }
 308                     if (total > availableWidth) {
 309                         break;
 310                     }
 311                     done = start == 0? i >= end : i <= end;
 312                 }
 313                 final boolean fullTrim = !wordTrim || whitespaceIndex == -1;
 314                 substring = (start == 0) ?
 315                     (text.substring(0, fullTrim ? index : whitespaceIndex)) :
 316                         (text.substring((fullTrim ? index : whitespaceIndex) + 1));
 317                 assert(!text.equals(substring));
 318             }
 319             if (style == ELLIPSIS || style == WORD_ELLIPSIS) {
 320                  return substring + ellipsis;
 321             } else {
 322                 //style is LEADING_ELLIPSIS or LEADING_WORD_ELLIPSIS
 323                 return ellipsis + substring;
 324             }
 325         } else {
 326             // these two indexes are INCLUSIVE not exclusive
 327             int leadingIndex = 0;
 328             int trailingIndex = 0;
 329             int leadingWhitespace = -1;
 330             int trailingWhitespace = -1;
 331             // The complex case is going to be killer. What I have to do is
 332             // read all the chars from the left up to the leadingIndex,
 333             // and all the chars from the right up to the trailingIndex,
 334             // and sum those together to get my total. That is, I cannot have
 335             // a running total but must retotal the cummulative chars each time
 336             if (complexLayout) {
 337             } else /*            double leadingTotal = 0;
 338                double trailingTotal = 0;
 339                for (int i=0; i<text.length(); i++) {
 340                double total = computeStringWidth(metrics, text.substring(0, i));
 341                if (total + trailingTotal > availableWidth) break;
 342                leadingIndex = i;
 343                leadingTotal = total;
 344                if (Character.isWhitespace(text.charAt(i))) leadingWhitespace = leadingIndex;
 345 
 346                int index = text.length() - (i + 1);
 347                total = computeStringWidth(metrics, text.substring(index - 1));
 348                if (total + leadingTotal > availableWidth) break;
 349                trailingIndex = index;
 350                trailingTotal = total;
 351                if (Character.isWhitespace(text.charAt(index))) trailingWhitespace = trailingIndex;
 352                }*/
 353             {
 354                 // either CENTER_ELLIPSIS or CENTER_WORD_ELLIPSIS
 355                 // for this case I read one char on the left, then one on the end
 356                 // then second on the left, then second from the end, etc until
 357                 // I have used up all the availableWidth. At that point, I trim
 358                 // the string twice: once from the start to firstIndex, and
 359                 // once from secondIndex to the end. I then insert the ellipsis
 360                 // between the two.
 361                 leadingIndex = -1;
 362                 trailingIndex = -1;
 363                 double total = 0.0F;
 364                 for (int i = 0; i <= text.length() - 1; i++) {
 365                     char c = text.charAt(i);
 366                     //total += metrics.charWidth(c);
 367                     total += computeTextWidth(font, "" + c, 0);
 368                     if (total > availableWidth) {
 369                         break;
 370                     }
 371                     leadingIndex = i;
 372                     if (Character.isWhitespace(c)) {
 373                         leadingWhitespace = leadingIndex;
 374                     }
 375                     int index = text.length() - 1 - i;
 376                     c = text.charAt(index);
 377                     //total += metrics.charWidth(c);
 378                     total += computeTextWidth(font, "" + c, 0);
 379                     if (total > availableWidth) {
 380                         break;
 381                     }
 382                     trailingIndex = index;
 383                     if (Character.isWhitespace(c)) {
 384                         trailingWhitespace = trailingIndex;
 385                     }
 386                 }
 387             }
 388             if (leadingIndex < 0) {
 389                 return ellipsis;
 390             }
 391             if (style == CENTER_ELLIPSIS) {
 392                 if (trailingIndex < 0) {
 393                     return text.substring(0, leadingIndex + 1) + ellipsis;
 394                 }
 395                 return text.substring(0, leadingIndex + 1) + ellipsis + text.substring(trailingIndex);
 396             } else {
 397                 boolean leadingIndexIsLastLetterInWord =
 398                     Character.isWhitespace(text.charAt(leadingIndex + 1));
 399                 int index = (leadingWhitespace == -1 || leadingIndexIsLastLetterInWord) ? (leadingIndex + 1) : (leadingWhitespace);
 400                 String leading = text.substring(0, index);
 401                 if (trailingIndex < 0) {
 402                     return leading + ellipsis;
 403                 }
 404                 boolean trailingIndexIsFirstLetterInWord =
 405                     Character.isWhitespace(text.charAt(trailingIndex - 1));
 406                 index = (trailingWhitespace == -1 || trailingIndexIsFirstLetterInWord) ? (trailingIndex) : (trailingWhitespace + 1);
 407                 String trailing = text.substring(index);
 408                 return leading + ellipsis + trailing;
 409             }
 410         }
 411     }
 412 
 413     public static String computeClippedWrappedText(Font font, String text, double width,
 414                                             double height, OverrunStyle truncationStyle,
 415                                             String ellipsisString, TextBoundsType boundsType) {
 416         if (font == null) {
 417             throw new IllegalArgumentException("Must specify a font");
 418         }
 419 
 420         String ellipsis = (truncationStyle == CLIP) ? "" : ellipsisString;
 421         int eLen = ellipsis.length();
 422         // Do this before using helper, as it's not reentrant.
 423         double eWidth = computeTextWidth(font, ellipsis, 0);
 424         double eHeight = computeTextHeight(font, ellipsis, 0, boundsType);
 425 
 426         if (width < eWidth || height < eHeight) {
 427             // The ellipsis doesn't fit.
 428             return text; // RT-30868 - return text, not empty string.
 429         }
 430 
 431         helper.setText(text);
 432         helper.setFont(font);
 433         helper.setWrappingWidth((int)Math.ceil(width));
 434         helper.setBoundsType(boundsType);
 435         helper.setLineSpacing(0);
 436 
 437         boolean leading =  (truncationStyle == LEADING_ELLIPSIS ||
 438                             truncationStyle == LEADING_WORD_ELLIPSIS);
 439         boolean center =   (truncationStyle == CENTER_ELLIPSIS ||
 440                             truncationStyle == CENTER_WORD_ELLIPSIS);
 441         boolean trailing = !(leading || center);
 442         boolean wordTrim = (truncationStyle == WORD_ELLIPSIS ||
 443                             truncationStyle == LEADING_WORD_ELLIPSIS ||
 444                             truncationStyle == CENTER_WORD_ELLIPSIS);
 445 
 446         String result = text;
 447         int len = (result != null) ? result.length() : 0;
 448         int centerLen = -1;
 449 
 450         Point2D centerPoint = null;
 451         if (center) {
 452             // Find index of character in the middle of the visual text area
 453             centerPoint = new Point2D((width - eWidth) / 2, height / 2 - helper.getBaselineOffset());
 454         }
 455 
 456         // Find index of character at the bottom left of the text area.
 457         // This should be the first character of a line that would be clipped.
 458         Point2D endPoint = new Point2D(0, height - helper.getBaselineOffset());
 459 
 460         int hit = helper.hitTest(endPoint).getCharIndex();
 461         if (hit >= len) {
 462             helper.setBoundsType(TextBoundsType.LOGICAL); // restore
 463             return text;
 464         }
 465         if (center) {
 466             hit = helper.hitTest(centerPoint).getCharIndex();
 467         }
 468 
 469         if (hit > 0 && hit < len) {
 470             // Step one, make a truncation estimate.
 471 
 472             if (center || trailing) {
 473                 int ind = hit;
 474                 if (center) {
 475                     // This is for the first part, i.e. beginning of text up to ellipsis.
 476                     if (wordTrim) {
 477                         int brInd = lastBreakCharIndex(text, ind + 1);
 478                         if (brInd >= 0) {
 479                             ind = brInd + 1;
 480                         } else {
 481                             brInd = firstBreakCharIndex(text, ind);
 482                             if (brInd >= 0) {
 483                                 ind = brInd + 1;
 484                             }
 485                         }
 486                     }
 487                     centerLen = ind + eLen;
 488                 } // else: text node wraps at words, so wordTrim is not needed here.
 489                 result = result.substring(0, ind) + ellipsis;
 490             }
 491 
 492             if (leading || center) {
 493                 // The hit is an index counted from the beginning, but we need
 494                 // the opposite, i.e. an index counted from the end.  However,
 495                 // the Text node does not support wrapped line layout in the
 496                 // reverse direction, starting at the bottom right corner.
 497 
 498                 // We'll simulate by assuming the index will be a similar
 499                 // number, then back up 10 characters just to add some slop.
 500                 // For example, the ending lines might pack tighter than the
 501                 // beginning lines, and therefore fit a higher number of
 502                 // characters.
 503                 int ind = Math.max(0, len - hit - 10);
 504                 if (ind > 0 && wordTrim) {
 505                     int brInd = lastBreakCharIndex(text, ind + 1);
 506                     if (brInd >= 0) {
 507                         ind = brInd + 1;
 508                     } else {
 509                         brInd = firstBreakCharIndex(text, ind);
 510                         if (brInd >= 0) {
 511                             ind = brInd + 1;
 512                         }
 513                     }
 514                 }
 515                 if (center) {
 516                     // This is for the second part, i.e. from ellipsis to end of text.
 517                     result = result + text.substring(ind);
 518                 } else {
 519                     result = ellipsis + text.substring(ind);
 520                 }
 521             }
 522 
 523             // Step two, check if text still overflows after we added the ellipsis.
 524             // If so, remove one char or word at a time.
 525             while (true) {
 526                 helper.setText(result);
 527                 int hit2 = helper.hitTest(endPoint).getCharIndex();
 528                 if (center && hit2 < centerLen) {
 529                     // No room for text after ellipsis. Maybe there is a newline
 530                     // here, and the next line falls outside the view.
 531                     if (hit2 > 0 && result.charAt(hit2-1) == '\n') {
 532                         hit2--;
 533                     }
 534                     result = text.substring(0, hit2) + ellipsis;
 535                     break;
 536                 } else if (hit2 > 0 && hit2 < result.length()) {
 537                     if (leading) {
 538                         int ind = eLen + 1; // Past ellipsis and first char.
 539                         if (wordTrim) {
 540                             int brInd = firstBreakCharIndex(result, ind);
 541                             if (brInd >= 0) {
 542                                 ind = brInd + 1;
 543                             }
 544                         }
 545                         result = ellipsis + result.substring(ind);
 546                     } else if (center) {
 547                         int ind = centerLen + 1; // Past ellipsis and first char.
 548                         if (wordTrim) {
 549                             int brInd = firstBreakCharIndex(result, ind);
 550                             if (brInd >= 0) {
 551                                 ind = brInd + 1;
 552                             }
 553                         }
 554                         result = result.substring(0, centerLen) + result.substring(ind);
 555                     } else {
 556                         int ind = result.length() - eLen - 1; // Before last char and ellipsis.
 557                         if (wordTrim) {
 558                             int brInd = lastBreakCharIndex(result, ind);
 559                             if (brInd >= 0) {
 560                                 ind = brInd;
 561                             }
 562                         }
 563                         result = result.substring(0, ind) + ellipsis;
 564                     }
 565                 } else {
 566                     break;
 567                 }
 568             }
 569         }
 570         // RESTORE STATE
 571         helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
 572         helper.setLineSpacing(DEFAULT_LINE_SPACING);
 573         helper.setText(DEFAULT_TEXT);
 574         helper.setBoundsType(DEFAULT_BOUNDS_TYPE);
 575         return result;
 576     }
 577 
 578 
 579     private static int firstBreakCharIndex(String str, int start) {
 580         char[] chars = str.toCharArray();
 581         for (int i = start; i < chars.length; i++) {
 582             if (isPreferredBreakCharacter(chars[i])) {
 583                 return i;
 584             }
 585         }
 586         return -1;
 587     }
 588 
 589     private static int lastBreakCharIndex(String str, int start) {
 590         char[] chars = str.toCharArray();
 591         for (int i = start; i >= 0; i--) {
 592             if (isPreferredBreakCharacter(chars[i])) {
 593                 return i;
 594             }
 595         }
 596         return -1;
 597     }
 598 
 599     /* Recognizes white space and latin punctuation as preferred
 600      * line break positions. Could do a bit better with using more
 601      * of the properties from the Character class.
 602      */
 603     private static boolean isPreferredBreakCharacter(char ch) {
 604         if (Character.isWhitespace(ch)) {
 605             return true;
 606         } else {
 607             switch (ch) {
 608             case ';' :
 609             case ':' :
 610             case '.' :
 611                 return true;
 612             default: return false;
 613             }
 614         }
 615     }
 616 
 617     private static boolean requiresComplexLayout(Font font, String string) {
 618         /*        Map attrs = font.getAttributes();
 619            if (contains(attrs, KERNING, KERNING_ON) ||
 620            contains(attrs, LIGATURES, LIGATURES_ON) ||
 621            (attrs.containsKey(TRACKING) && attrs.get(TRACKING) != null)) {
 622            return true;
 623            }
 624            return isComplexLayout(string.toCharArray(), 0, string.length());
 625          */
 626         return false;
 627     }
 628 
 629     static int computeStartOfWord(Font font, String text, int index) {
 630         if ("".equals(text) || index < 0) return 0;
 631         if (text.length() <= index) return text.length();
 632         // if the given index is not in a word (but in whitespace), then
 633         // simply return the index
 634         if (Character.isWhitespace(text.charAt(index))) {
 635             return index;
 636         }
 637         boolean complexLayout = requiresComplexLayout(font, text);
 638         if (complexLayout) {
 639             // TODO needs implementation
 640             return 0;
 641         } else {
 642             // just start walking backwards from index until either i<0 or
 643             // the first whitespace is found.
 644             int i = index;
 645             while (--i >= 0) {
 646                 if (Character.isWhitespace(text.charAt(i))) {
 647                     return i + 1;
 648                 }
 649             }
 650             return 0;
 651         }
 652     }
 653 
 654     static int computeEndOfWord(Font font, String text, int index) {
 655         if (text.equals("") || index < 0) {
 656             return 0;
 657         }
 658         if (text.length() <= index) {
 659             return text.length();
 660         }
 661         // if the given index is not in a word (but in whitespace), then
 662         // simply return the index
 663         if (Character.isWhitespace(text.charAt(index))) {
 664             return index;
 665         }
 666         boolean complexLayout = requiresComplexLayout(font, text);
 667         if (complexLayout) {
 668             // TODO needs implementation
 669             return text.length();
 670         } else {
 671             // just start walking forward from index until either i > length or
 672             // the first whitespace is found.
 673             int i = index;
 674             while (++i < text.length()) {
 675                 if (Character.isWhitespace(text.charAt(i))) {
 676                     return i;
 677                 }
 678             }
 679             return text.length();
 680         }
 681     }
 682 
 683     // used for layout to adjust widths to honor the min/max policies consistently
 684     public static double boundedSize(double value, double min, double max) {
 685         // if max < value, return max
 686         // if min > value, return min
 687         // if min > max, return min
 688         return Math.min(Math.max(value, min), Math.max(min,max));
 689     }
 690 
 691     public static void addMnemonics(ContextMenu popup, Scene scene) {
 692         addMnemonics(popup, scene, false);
 693     }
 694 
 695     public static void addMnemonics(ContextMenu popup, Scene scene, boolean initialState) {
 696 
 697         if (!com.sun.javafx.PlatformUtil.isMac()) {
 698 
 699             ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 700             MenuItem menuitem;
 701 
 702             for (int i = 0 ; i < popup.getItems().size() ; i++) {
 703                 menuitem = popup.getItems().get(i);
 704                 /*
 705                 ** check is there are any mnemonics in this menu
 706                 */
 707                 if (menuitem.isMnemonicParsing()) {
 708 
 709                     TextBinding bindings = new TextBinding(menuitem.getText());
 710                     int mnemonicIndex = bindings.getMnemonicIndex() ;
 711                     if (mnemonicIndex >= 0) {
 712                         KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination();
 713                         Mnemonic myMnemonic = new Mnemonic(cmContent.getLabelAt(i), mnemonicKeyCombo);
 714                         scene.addMnemonic(myMnemonic);
 715                         NodeHelper.setShowMnemonics(cmContent.getLabelAt(i), initialState);
 716                     }
 717                 }
 718             }
 719         }
 720     }
 721 
 722 
 723 
 724     public static void removeMnemonics(ContextMenu popup, Scene scene) {
 725 
 726         if (!com.sun.javafx.PlatformUtil.isMac()) {
 727 
 728             ContextMenuContent cmContent = (ContextMenuContent)popup.getSkin().getNode();
 729             MenuItem menuitem;
 730 
 731             for (int i = 0 ; i < popup.getItems().size() ; i++) {
 732                 menuitem = popup.getItems().get(i);
 733                 /*
 734                 ** check is there are any mnemonics in this menu
 735                 */
 736                 if (menuitem.isMnemonicParsing()) {
 737 
 738                     TextBinding bindings = new TextBinding(menuitem.getText());
 739                     int mnemonicIndex = bindings.getMnemonicIndex() ;
 740                     if (mnemonicIndex >= 0) {
 741                         KeyCombination mnemonicKeyCombo = bindings.getMnemonicKeyCombination();
 742 
 743                         ObservableList<Mnemonic> mnemonicsList = scene.getMnemonics().get(mnemonicKeyCombo);
 744                         if (mnemonicsList != null) {
 745                             for (int j = 0 ; j < mnemonicsList.size() ; j++) {
 746                                 if (mnemonicsList.get(j).getNode() == cmContent.getLabelAt(i)) {
 747                                     mnemonicsList.remove(j);
 748                                 }
 749                             }
 750                         }
 751                     }
 752                 }
 753             }
 754         }
 755     }
 756 
 757     public static double computeXOffset(double width, double contentWidth, HPos hpos) {
 758         if (hpos == null) {
 759             return 0;
 760         }
 761 
 762         switch(hpos) {
 763             case LEFT:
 764                return 0;
 765             case CENTER:
 766                return (width - contentWidth) / 2;
 767             case RIGHT:
 768                return width - contentWidth;
 769             default:
 770                 return 0;
 771         }
 772     }
 773 
 774     public static double computeYOffset(double height, double contentHeight, VPos vpos) {
 775         if (vpos == null) {
 776             return 0;
 777         }
 778 
 779         switch(vpos) {
 780             case TOP:
 781                return 0;
 782             case CENTER:
 783                return (height - contentHeight) / 2;
 784             case BOTTOM:
 785                return height - contentHeight;
 786             default:
 787                 return 0;
 788         }
 789     }
 790 
 791     /*
 792     ** Returns true if the platform is to use Two-Level-Focus.
 793     ** This is in the Util class to ease any changes in
 794     ** the criteria for enabling this feature.
 795     **
 796     ** TwoLevelFocus is needed on platforms that
 797     ** only support 5-button navigation (arrow keys and Select/OK).
 798     **
 799     */
 800     public static boolean isTwoLevelFocus() {
 801         return Platform.isSupported(ConditionalFeature.TWO_LEVEL_FOCUS);
 802     }
 803 
 804 
 805     // useful method for linking things together when before a property is
 806     // necessarily set
 807     public static <T> void executeOnceWhenPropertyIsNonNull(ObservableValue<T> p, Consumer<T> consumer) {
 808         if (p == null) return;
 809 
 810         T value = p.getValue();
 811         if (value != null) {
 812             consumer.accept(value);
 813         } else {
 814             final InvalidationListener listener = new InvalidationListener() {
 815                 @Override public void invalidated(Observable observable) {
 816                     T value = p.getValue();
 817 
 818                     if (value != null) {
 819                         p.removeListener(this);
 820                         consumer.accept(value);
 821                     }
 822                 }
 823             };
 824             p.addListener(listener);
 825         }
 826     }
 827 
 828     public static String formatHexString(Color c) {
 829         if (c != null) {
 830             return String.format((Locale) null, "#%02x%02x%02x",
 831                     Math.round(c.getRed() * 255),
 832                     Math.round(c.getGreen() * 255),
 833                     Math.round(c.getBlue() * 255));
 834         } else {
 835             return null;
 836         }
 837     }
 838 
 839     public static URL getResource(String str) {
 840         return Utils.class.getResource(str);
 841     }
 842 
 843 }