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