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
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.
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
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 }
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.
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 };
|
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
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.
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
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 }
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.
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 };
|