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         }
 179         // the width used by the ellipsis string
 180         final double ellipsisWidth = computeTextWidth(font, ellipsis, 0);
 181         // the available maximum width to fit chars into. This is essentially
 182         // the width minus the space required for the ellipsis string
 183         final double availableWidth = width - ellipsisWidth;
 184 
 185         if (width < ellipsisWidth) {
 186             // The ellipsis doesn't fit.
 187             return "";
 188         }
 189 
 190         // if we got here, then we must clip the text with an ellipsis.
 191         // this can be pretty expensive depending on whether "complex" text
 192         // layout needs to be taken into account. So each ellipsis option has
 193         // to take into account two code paths: the easy way and the correct
 194         // way. This is flagged by the "complexLayout" boolean
 195         // TODO make sure this function call takes into account ligatures, kerning,
 196         // and such as that will change the layout characteristics of the text
 197         // and will require a full complex layout
 198         // TODO since we don't have all the stuff available in FX to determine
 199         // complex text, I'm going to for now assume complex text is always false.
 200         final boolean complexLayout = false;
 201         //requiresComplexLayout(font, text);
 202 
 203         // generally all we want to do is count characters and add their widths.
 204         // For ellipsis that breaks on words, we do NOT want to include any
 205         // hanging whitespace.
 206         if (style == ELLIPSIS ||
 207             style == WORD_ELLIPSIS ||
 208             style == LEADING_ELLIPSIS ||
 209             style == LEADING_WORD_ELLIPSIS) {
 210 
 211             final boolean wordTrim =
 212                 (style == WORD_ELLIPSIS || style == LEADING_WORD_ELLIPSIS);
 213             String substring;
 214             if (complexLayout) {
 215             //            AttributedString a = new AttributedString(text);
 216             //            LineBreakMeasurer m = new LineBreakMeasurer(a.getIterator(), frc);
 217             //            substring = text.substring(0, m.nextOffset((double)availableWidth));
 218             } else {
 219                 // RT-23458: Use a faster algorithm for the most common case
 220                 // where truncation happens at the end, i.e. for ELLIPSIS and
 221                 // CLIP, but not for other cases such as WORD_ELLIPSIS, etc.
 222                 if (style == ELLIPSIS && !new Bidi(text, Bidi.DIRECTION_LEFT_TO_RIGHT).isMixed()) {
 223                     int hit = computeTruncationIndex(font, text, width - ellipsisWidth);
 224                     if (hit < 0 || hit >= text.length()) {
 225                         return text;
 226                     } else {
 227                         return text.substring(0, hit) + ellipsis;
 228                     }
 229                 }
 230 
 231                 // simply total up the widths of all chars to determine how many
 232                 // will fit in the available space. Remember the last whitespace
 233                 // encountered so that if we're breaking on words we can trim
 234                 // and omit it.
 235                 double total = 0.0F;
 236                 int whitespaceIndex = -1;
 237                 // at the termination of the loop, index will be one past the
 238                 // end of the substring
 239                 int index = 0;
 240                 int start = (style == LEADING_ELLIPSIS || style == LEADING_WORD_ELLIPSIS) ? (text.length() - 1) : (0);
 241                 int end = (start == 0) ? (text.length() - 1) : 0;
 242                 int stepValue = (start == 0) ? 1 : -1;
 243                 boolean done = (start == 0) ? start > end : start < end;
 244                 for (int i = start; !done ; i += stepValue) {
 245                     index = i;
 246                     char c = text.charAt(index);
 247                     total = computeTextWidth(font,
 248                                              (start == 0) ? text.substring(0, i + 1)
 249                                                           : text.substring(i, start + 1),
 250                                              0);
 251                     if (Character.isWhitespace(c)) {
 252                         whitespaceIndex = index;
 253                     }
 254                     if (total > availableWidth) {
 255                         break;
 256                     }
 257                     done = start == 0? i >= end : i <= end;
 258                 }
 259                 final boolean fullTrim = !wordTrim || whitespaceIndex == -1;
 260                 substring = (start == 0) ?
 261                     (text.substring(0, fullTrim ? index : whitespaceIndex)) :
 262                         (text.substring((fullTrim ? index : whitespaceIndex) + 1));
 263                 assert(!text.equals(substring));
 264             }
 265             if (style == ELLIPSIS || style == WORD_ELLIPSIS) {
 266                  return substring + ellipsis;
 267             } else {
 268                 //style is LEADING_ELLIPSIS or LEADING_WORD_ELLIPSIS
 269                 return ellipsis + substring;
 270             }
 271         } else {
 272             // these two indexes are INCLUSIVE not exclusive
 273             int leadingIndex = 0;
 274             int trailingIndex = 0;
 275             int leadingWhitespace = -1;
 276             int trailingWhitespace = -1;
 277             // The complex case is going to be killer. What I have to do is
 278             // read all the chars from the left up to the leadingIndex,
 279             // and all the chars from the right up to the trailingIndex,
 280             // and sum those together to get my total. That is, I cannot have
 281             // a running total but must retotal the cummulative chars each time
 282             if (complexLayout) {
 283             } else /*            double leadingTotal = 0;
 284                double trailingTotal = 0;
 285                for (int i=0; i<text.length(); i++) {
 286                double total = computeStringWidth(metrics, text.substring(0, i));
 287                if (total + trailingTotal > availableWidth) break;
 288                leadingIndex = i;
 289                leadingTotal = total;
 290                if (Character.isWhitespace(text.charAt(i))) leadingWhitespace = leadingIndex;
 291 
 292                int index = text.length() - (i + 1);
 293                total = computeStringWidth(metrics, text.substring(index - 1));
 294                if (total + leadingTotal > availableWidth) break;
 295                trailingIndex = index;
 296                trailingTotal = total;
 297                if (Character.isWhitespace(text.charAt(index))) trailingWhitespace = trailingIndex;
 298                }*/
 299             {
 300                 // either CENTER_ELLIPSIS or CENTER_WORD_ELLIPSIS
 301                 // for this case I read one char on the left, then one on the end
 302                 // then second on the left, then second from the end, etc until
 303                 // I have used up all the availableWidth. At that point, I trim
 304                 // the string twice: once from the start to firstIndex, and
 305                 // once from secondIndex to the end. I then insert the ellipsis
 306                 // between the two.
 307                 leadingIndex = -1;
 308                 trailingIndex = -1;
 309                 double total = 0.0F;
 310                 for (int i = 0; i <= text.length() - 1; i++) {
 311                     char c = text.charAt(i);
 312                     //total += metrics.charWidth(c);
 313                     total += computeTextWidth(font, "" + c, 0);
 314                     if (total > availableWidth) {
 315                         break;
 316                     }
 317                     leadingIndex = i;
 318                     if (Character.isWhitespace(c)) {
 319                         leadingWhitespace = leadingIndex;
 320                     }
 321                     int index = text.length() - 1 - i;
 322                     c = text.charAt(index);
 323                     //total += metrics.charWidth(c);
 324                     total += computeTextWidth(font, "" + c, 0);
 325                     if (total > availableWidth) {
 326                         break;
 327                     }
 328                     trailingIndex = index;
 329                     if (Character.isWhitespace(c)) {
 330                         trailingWhitespace = trailingIndex;
 331                     }
 332                 }
 333             }
 334             if (leadingIndex < 0) {
 335                 return ellipsis;
 336             }
 337             if (style == CENTER_ELLIPSIS) {
 338                 if (trailingIndex < 0) {
 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));
 380         helper.setBoundsType(boundsType);
 381         helper.setLineSpacing(0);
 382 
 383         boolean leading =  (truncationStyle == LEADING_ELLIPSIS ||
 384                             truncationStyle == LEADING_WORD_ELLIPSIS);
 385         boolean center =   (truncationStyle == CENTER_ELLIPSIS ||
 386                             truncationStyle == CENTER_WORD_ELLIPSIS);
 387         boolean trailing = !(leading || center);
 388         boolean wordTrim = (truncationStyle == WORD_ELLIPSIS ||
 389                             truncationStyle == LEADING_WORD_ELLIPSIS ||
 390                             truncationStyle == CENTER_WORD_ELLIPSIS);
 391 
 392         String result = text;
 393         int len = (result != null) ? result.length() : 0;
 394         int centerLen = -1;
 395 
 396         Point2D centerPoint = null;
 397         if (center) {
 398             // Find index of character in the middle of the visual text area
 399             centerPoint = new Point2D((width - eWidth) / 2, height / 2 - helper.getBaselineOffset());
 400         }
 401 
 402         // Find index of character at the bottom left of the text area.
 403         // This should be the first character of a line that would be clipped.
 404         Point2D endPoint = new Point2D(0, height - helper.getBaselineOffset());
 405 
 406         int hit = helper.impl_hitTestChar(endPoint).getCharIndex();
 407         if (hit >= len) {
 408             helper.setBoundsType(TextBoundsType.LOGICAL); // restore
 409             return text;
 410         }
 411         if (center) {
 412             hit = helper.impl_hitTestChar(centerPoint).getCharIndex();
 413         }
 414 
 415         if (hit > 0 && hit < len) {
 416             // Step one, make a truncation estimate.
 417 
 418             if (center || trailing) {
 419                 int ind = hit;
 420                 if (center) {
 421                     // This is for the first part, i.e. beginning of text up to ellipsis.
 422                     if (wordTrim) {
 423                         int brInd = lastBreakCharIndex(text, ind + 1);
 424                         if (brInd >= 0) {
 425                             ind = brInd + 1;
 426                         } else {
 427                             brInd = firstBreakCharIndex(text, ind);
 428                             if (brInd >= 0) {
 429                                 ind = brInd + 1;
 430                             }
 431                         }
 432                     }
 433                     centerLen = ind + eLen;
 434                 } // else: text node wraps at words, so wordTrim is not needed here.
 435                 result = result.substring(0, ind) + ellipsis;
 436             }
 437 
 438             if (leading || center) {
 439                 // The hit is an index counted from the beginning, but we need
 440                 // the opposite, i.e. an index counted from the end.  However,
 441                 // the Text node does not support wrapped line layout in the
 442                 // reverse direction, starting at the bottom right corner.
 443 
 444                 // We'll simulate by assuming the index will be a similar
 445                 // number, then back up 10 characters just to add some slop.
 446                 // For example, the ending lines might pack tighter than the
 447                 // beginning lines, and therefore fit a higher number of
 448                 // characters.
 449                 int ind = Math.max(0, len - hit - 10);
 450                 if (ind > 0 && wordTrim) {
 451                     int brInd = lastBreakCharIndex(text, ind + 1);
 452                     if (brInd >= 0) {
 453                         ind = brInd + 1;
 454                     } else {
 455                         brInd = firstBreakCharIndex(text, ind);
 456                         if (brInd >= 0) {
 457                             ind = brInd + 1;
 458                         }
 459                     }
 460                 }
 461                 if (center) {
 462                     // This is for the second part, i.e. from ellipsis to end of text.
 463                     result = result + text.substring(ind);
 464                 } else {
 465                     result = ellipsis + text.substring(ind);
 466                 }
 467             }
 468 
 469             // Step two, check if text still overflows after we added the ellipsis.
 470             // If so, remove one char or word at a time.
 471             while (true) {
 472                 helper.setText(result);
 473                 int hit2 = helper.impl_hitTestChar(endPoint).getCharIndex();
 474                 if (center && hit2 < centerLen) {
 475                     // No room for text after ellipsis. Maybe there is a newline
 476                     // here, and the next line falls outside the view.
 477                     if (hit2 > 0 && result.charAt(hit2-1) == '\n') {
 478                         hit2--;
 479                     }
 480                     result = text.substring(0, hit2) + ellipsis;
 481                     break;
 482                 } else if (hit2 > 0 && hit2 < result.length()) {
 483                     if (leading) {
 484                         int ind = eLen + 1; // Past ellipsis and first char.
 485                         if (wordTrim) {
 486                             int brInd = firstBreakCharIndex(result, ind);
 487                             if (brInd >= 0) {
 488                                 ind = brInd + 1;
 489                             }
 490                         }
 491                         result = ellipsis + result.substring(ind);
 492                     } else if (center) {
 493                         int ind = centerLen + 1; // Past ellipsis and first char.
 494                         if (wordTrim) {
 495                             int brInd = firstBreakCharIndex(result, ind);
 496                             if (brInd >= 0) {
 497                                 ind = brInd + 1;
 498                             }
 499                         }
 500                         result = result.substring(0, centerLen) + result.substring(ind);
 501                     } else {
 502                         int ind = result.length() - eLen - 1; // Before last char and ellipsis.
 503                         if (wordTrim) {
 504                             int brInd = lastBreakCharIndex(result, ind);
 505                             if (brInd >= 0) {
 506                                 ind = brInd;
 507                             }
 508                         }
 509                         result = result.substring(0, ind) + ellipsis;
 510                     }
 511                 } else {
 512                     break;
 513                 }
 514             }
 515         }
 516         // RESTORE STATE
 517         helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
 518         helper.setLineSpacing(DEFAULT_LINE_SPACING);
 519         helper.setText(DEFAULT_TEXT);
 520         helper.setBoundsType(DEFAULT_BOUNDS_TYPE);
 521         return result;
 522     }
 523 
 524 
 525     private static int firstBreakCharIndex(String str, int start) {
 526         char[] chars = str.toCharArray();
 527         for (int i = start; i < chars.length; i++) {
 528             if (isPreferredBreakCharacter(chars[i])) {
 529                 return i;
 530             }
 531         }
 532         return -1;
 533     }
 534 
 535     private static int lastBreakCharIndex(String str, int start) {
 536         char[] chars = str.toCharArray();
 537         for (int i = start; i >= 0; i--) {
 538             if (isPreferredBreakCharacter(chars[i])) {
 539                 return i;
 540             }
 541         }
 542         return -1;
 543     }
 544 
 545     /* Recognizes white space and latin punctuation as preferred
 546      * line break positions. Could do a bit better with using more
 547      * of the properties from the Character class.
 548      */
 549     private static boolean isPreferredBreakCharacter(char ch) {
 550         if (Character.isWhitespace(ch)) {
 551             return true;
 552         } else {
 553             switch (ch) {
 554             case ';' :
 555             case ':' :
 556             case '.' :
 557                 return true;
 558             default: return false;
 559             }
 560         }
 561     }
 562 
 563     private static boolean requiresComplexLayout(Font font, String string) {
 564         /*        Map attrs = font.getAttributes();
 565            if (contains(attrs, KERNING, KERNING_ON) ||
 566            contains(attrs, LIGATURES, LIGATURES_ON) ||
 567            (attrs.containsKey(TRACKING) && attrs.get(TRACKING) != null)) {
 568            return true;
 569            }
 570            return isComplexLayout(string.toCharArray(), 0, string.length());
 571          */
 572         return false;
 573     }
 574 
 575     static int computeStartOfWord(Font font, String text, int index) {
 576         if ("".equals(text) || index < 0) return 0;
 577         if (text.length() <= index) return text.length();
 578         // if the given index is not in a word (but in whitespace), then
 579         // simply return the index
 580         if (Character.isWhitespace(text.charAt(index))) {
 581             return index;
 582         }
 583         boolean complexLayout = requiresComplexLayout(font, text);
 584         if (complexLayout) {
 585             // TODO needs implementation
 586             return 0;
 587         } else {
 588             // just start walking backwards from index until either i<0 or
 589             // the first whitespace is found.
 590             int i = index;
 591             while (--i >= 0) {
 592                 if (Character.isWhitespace(text.charAt(i))) {
 593                     return i + 1;
 594                 }
 595             }
 596             return 0;
 597         }
 598     }
 599 
 600     static int computeEndOfWord(Font font, String text, int index) {
 601         if (text.equals("") || index < 0) {
 602             return 0;
 603         }
 604         if (text.length() <= index) {
 605             return text.length();
 606         }
 607         // if the given index is not in a word (but in whitespace), then
 608         // simply return the index
 609         if (Character.isWhitespace(text.charAt(index))) {
 610             return index;
 611         }
 612         boolean complexLayout = requiresComplexLayout(font, text);
 613         if (complexLayout) {
 614             // TODO needs implementation
 615             return text.length();
 616         } else {
 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 }