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