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