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