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