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 }