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