--- old/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/LabeledSkinBase.java 2015-09-03 15:19:45.118779500 -0700 +++ /dev/null 2015-09-03 15:19:46.000000000 -0700 @@ -1,1158 +0,0 @@ -/* - * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.sun.javafx.scene.control.skin; - -import javafx.application.Platform; -import javafx.beans.InvalidationListener; -import javafx.geometry.HPos; -import javafx.geometry.NodeOrientation; -import javafx.geometry.Orientation; -import javafx.geometry.Pos; -import javafx.geometry.VPos; -import javafx.scene.AccessibleAttribute; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.control.Labeled; -import javafx.scene.control.OverrunStyle; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.Mnemonic; -import javafx.scene.shape.Line; -import javafx.scene.shape.Rectangle; -import javafx.scene.text.Font; - -import com.sun.javafx.scene.control.behavior.BehaviorBase; -import com.sun.javafx.scene.control.behavior.TextBinding; - -import static javafx.scene.control.ContentDisplay.*; -import static javafx.scene.control.OverrunStyle.*; -import javafx.scene.image.ImageView; - -public abstract class LabeledSkinBase> extends BehaviorSkinBase { - - /** - * The Text node used to display the text. This is package only - * for the sake of testing! - */ - LabeledText text; - - /** - * Indicates that the text content is invalid and needs to be updated. - * This is package private only for the sake of testing. - */ - boolean invalidText = true; - - /** - * A reference to the last-known graphic on the Labeled. This reference - * is kept so that we can remove listeners from the old graphic later - */ - Node graphic; - - /** - * The cached full width of the non-truncated text. We only want to - * recompute this if the text has itself changed, or if the font has changed. - * This is package private ONLY FOR THE SAKE OF TESTING - */ - double textWidth = Double.NEGATIVE_INFINITY; - - /** - * The cached width of the ellipsis string. This will be recomputed - * if the font or the ellipsisString property have changed. - * This is package private ONLY FOR THE SAKE OF TESTING - */ - double ellipsisWidth = Double.NEGATIVE_INFINITY; - - /** - * A listener which is applied to the graphic whenever the graphic is set - * and is visible within the labeled. For example, if there is a graphic - * defined on the Labeled but the ContentDisplay is set to TEXT_ONLY, then - * we will not bother installing this listener on the graphic. In all - * other cases, if the graphic is defined, it will have this listener - * added to it, which ensures that if the graphic's layout bounds change, - * we end up performing a layout and potentially update the visible text. - * - * This is package private ONLY FOR THE SAKE OF TESTING - */ - final InvalidationListener graphicPropertyChangedListener = valueModel -> { - invalidText = true; - getSkinnable().requestLayout(); - }; - - private Rectangle textClip; - private double wrapWidth; - private double wrapHeight; - - - /** - * Constructor for LabeledSkinBase. The Labeled must be specified, and cannot be null. - * At the conclusion of the constructor call, the skin will be marked as - * needsLayout, and will be fully configured based on the current state of - * the labeled. Any subsequent changes to the Labeled will be handled via - * listeners and applied appropriately. - * - * @param labeled - */ - public LabeledSkinBase(final C labeled, final B behavior) { - super(labeled, behavior); - - // Configure the Text node with all of the attributes from the - // Labeled which apply to it. - text = new LabeledText(labeled); - - updateChildren(); - - // Labels do not block the mouse by default, unlike most other UI Controls. - //consumeMouseEvents(false); - - // Register listeners - registerChangeListener(labeled.ellipsisStringProperty(), "ELLIPSIS_STRING"); - registerChangeListener(labeled.widthProperty(), "WIDTH"); - registerChangeListener(labeled.heightProperty(), "HEIGHT"); - registerChangeListener(labeled.textFillProperty(), "TEXT_FILL"); - registerChangeListener(labeled.fontProperty(), "FONT"); - registerChangeListener(labeled.graphicProperty(), "GRAPHIC"); - registerChangeListener(labeled.contentDisplayProperty(), "CONTENT_DISPLAY"); - registerChangeListener(labeled.labelPaddingProperty(), "LABEL_PADDING"); - registerChangeListener(labeled.graphicTextGapProperty(), "GRAPHIC_TEXT_GAP"); - registerChangeListener(labeled.alignmentProperty(), "ALIGNMENT"); - registerChangeListener(labeled.mnemonicParsingProperty(), "MNEMONIC_PARSING"); - registerChangeListener(labeled.textProperty(), "TEXT"); - registerChangeListener(labeled.textAlignmentProperty(), "TEXT_ALIGNMENT"); - registerChangeListener(labeled.textOverrunProperty(), "TEXT_OVERRUN"); - registerChangeListener(labeled.wrapTextProperty(), "WRAP_TEXT"); - registerChangeListener(labeled.underlineProperty(), "UNDERLINE"); - registerChangeListener(labeled.lineSpacingProperty(), "LINE_SPACING"); - registerChangeListener(labeled.sceneProperty(), "SCENE"); - } - - /*************************************************************************** - * * - * Control State Changes * - * * - **************************************************************************/ - - @Override protected void handleControlPropertyChanged(String p) { - super.handleControlPropertyChanged(p); - /* - * There are basically 2 things to worry about in each of these handlers - * 1) Update the Text node - * 2) Have the text metrics changed? - * - * If the metrics have changed, we need to request a layout and invalidate - * the text so that we recompute the display text on next read. - */ - if ("WIDTH".equals(p)) { - updateWrappingWidth(); - invalidText = true; - // No requestLayout() because Control will force a layout - } else if ("HEIGHT".equals(p)) { - invalidText = true; - // No requestLayout() because Control will force a layout - } else if ("FONT".equals(p)) { - textMetricsChanged(); - invalidateWidths(); - ellipsisWidth = Double.NEGATIVE_INFINITY; - } else if ("GRAPHIC".equals(p)) { - updateChildren(); - textMetricsChanged(); - } else if ("CONTENT_DISPLAY".equals(p)) { - updateChildren(); - textMetricsChanged(); - } else if ("LABEL_PADDING".equals(p)) { - textMetricsChanged(); - } else if ("GRAPHIC_TEXT_GAP".equals(p)) { - textMetricsChanged(); - } else if ("ALIGNMENT".equals(p)) { - // Doesn't involve text metrics because if the text is too long, then - // it will already have fit all available width and a change to hpos - // has no effect. Or it is too short (i.e. it all fits) and we don't - // have to worry about truncation. So just call request layout. - // Doesn't involve text metrics because if the text is too long, then - // it will already have fit all available height and a change to vpos - // has no effect. Or it is too short (i.e. it all fits) and we don't - // have to worry about truncation. So just call request layout. - getSkinnable().requestLayout(); - } else if ("MNEMONIC_PARSING".equals(p)) { - containsMnemonic = false; - textMetricsChanged(); - } else if ("TEXT".equals(p)) { - updateChildren(); - textMetricsChanged(); - invalidateWidths(); - } else if ("TEXT_ALIGNMENT".equals(p)) { - // NO-OP - } else if ("TEXT_OVERRUN".equals(p)) { - textMetricsChanged(); - } else if ("ELLIPSIS_STRING".equals(p)) { - textMetricsChanged(); - invalidateWidths(); - ellipsisWidth = Double.NEGATIVE_INFINITY; - } else if ("WRAP_TEXT".equals(p)) { - updateWrappingWidth(); - textMetricsChanged(); - } else if ("UNDERLINE".equals(p)) { - textMetricsChanged(); - } else if ("LINE_SPACING".equals(p)) { - textMetricsChanged(); - } else if ("SCENE".equals(p)) { - sceneChanged(); - } - } - - protected double topLabelPadding() { // TODOJASPER remove these if you can - return snapSize(getSkinnable().getLabelPadding().getTop()); - } - - protected double bottomLabelPadding() { - return snapSize(getSkinnable().getLabelPadding().getBottom()); - } - - protected double leftLabelPadding() { - return snapSize(getSkinnable().getLabelPadding().getLeft()); - } - - protected double rightLabelPadding() { - return snapSize(getSkinnable().getLabelPadding().getRight()); - } - - - /** - * Called whenever some state has changed that affects the text metrics. - * Changes here will involve invalidating the display text so the next - * call to updateDisplayedText computes a new value, and call requestLayout. - */ - private void textMetricsChanged() { - invalidText = true; - getSkinnable().requestLayout(); - } - - /* - ** The Label is a mnemonic, and it's target node - ** has changed, but it's label hasn't so just - ** swap them over, and tidy up. - */ - protected void mnemonicTargetChanged() { - if (containsMnemonic == true) { - /* - ** was there previously a labelFor - */ - removeMnemonic(); - - /* - ** is there a new labelFor - */ - Control control = getSkinnable(); - if (control instanceof Label) { - labeledNode = ((Label)control).getLabelFor(); - addMnemonic(); - } - else { - labeledNode = null; - } - } - } - - private void sceneChanged() { - final Labeled labeled = getSkinnable(); - Scene scene = labeled.getScene(); - - if (scene != null && containsMnemonic) { - addMnemonic(); - } - - } - - /** - * Marks minWidth as being invalid and in need of recomputation. - */ - private void invalidateWidths() { - textWidth = Double.NEGATIVE_INFINITY; - } - - /** - * Updates the content of the underlying Text node. This method should - * only be called when necessary. If the invalidText flag is not set, then - * the method is a no-op. This care is taken because recomputing the - * text to display is an expensive operation. Package private ONLY FOR THE - * SAKE OF TESTING. - */ - void updateDisplayedText() { - updateDisplayedText(-1, -1); - } - - private void updateDisplayedText(double w, double h) { - if (invalidText) { - final Labeled labeled = getSkinnable(); - String s = labeled.getText(); - - int mnemonicIndex = -1; - - /* - ** if there's a valid string then parse it - */ - if (s != null && s.length() > 0) { - bindings = new TextBinding(s); - - if (!com.sun.javafx.PlatformUtil.isMac() && getSkinnable().isMnemonicParsing() == true) { - /* - ** the Labeled has a MnemonicParsing property, - ** if set true, then auto-parsing will check for - ** a mnemonic - */ - if (labeled instanceof Label) { - // buttons etc - labeledNode = ((Label)labeled).getLabelFor(); - } else { - labeledNode = labeled; - } - - if (labeledNode == null) { - labeledNode = labeled; - } - mnemonicIndex = bindings.getMnemonicIndex() ; - } - } - - /* - ** we were previously a mnemonic - */ - if (containsMnemonic) { - /* - ** are we no longer a mnemonic, or have we changed code? - */ - if (mnemonicScene != null) { - if (mnemonicIndex == -1 || - (bindings != null && !bindings.getMnemonicKeyCombination().equals(mnemonicCode))) { - removeMnemonic(); - } - containsMnemonic = false; - } - } - else { - /* - ** this can happen if mnemonic parsing is - ** disabled on a previously valid mnemonic - */ - removeMnemonic(); - } - - /* - ** check we have a labeled - */ - if (s != null && s.length() > 0) { - if (mnemonicIndex >= 0 && containsMnemonic == false) { - containsMnemonic = true; - mnemonicCode = bindings.getMnemonicKeyCombination(); - addMnemonic(); - } - } - - if (containsMnemonic == true) { - s = bindings.getText(); - if (mnemonic_underscore == null) { - mnemonic_underscore = new Line(); - mnemonic_underscore.setStartX(0.0f); - mnemonic_underscore.setStartY(0.0f); - mnemonic_underscore.setEndY(0.0f); - mnemonic_underscore.getStyleClass().clear(); - mnemonic_underscore.getStyleClass().setAll("mnemonic-underline"); - } - if (!getChildren().contains(mnemonic_underscore)) { - getChildren().add(mnemonic_underscore); - } - } else { - /* - ** we don't need a mnemonic.... - */ - if (getSkinnable().isMnemonicParsing() == true && com.sun.javafx.PlatformUtil.isMac() && bindings != null) { - s = bindings.getText(); - } - else { - s = labeled.getText(); - } - if (mnemonic_underscore != null) { - if (getChildren().contains(mnemonic_underscore)) { - Platform.runLater(() -> { - getChildren().remove(mnemonic_underscore); - mnemonic_underscore = null; - }); - } - } - } - - int len = s != null ? s.length() : 0; - boolean multiline = false; - - if (s != null && len > 0) { - int i = s.indexOf('\n'); - if (i > -1 && i < len - 1) { - // Multiline text with embedded newlines - not - // taking into account a potential trailing newline. - multiline = true; - } - } - - String result; - boolean horizontalPosition = - (labeled.getContentDisplay() == ContentDisplay.LEFT || - labeled.getContentDisplay() == ContentDisplay.RIGHT); - - double availableWidth = labeled.getWidth() - snappedLeftInset() - leftLabelPadding() - - snappedRightInset() - rightLabelPadding(); - availableWidth = Math.max(availableWidth, 0); - - if (w == -1) { - w = availableWidth; - } - double minW = Math.min(computeMinLabeledPartWidth(-1, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableWidth); - if (horizontalPosition && !isIgnoreGraphic()) { - double graphicW = (labeled.getGraphic().getLayoutBounds().getWidth() + labeled.getGraphicTextGap()); - w -= graphicW; - minW -= graphicW; - } - wrapWidth = Math.max(minW, w); - - boolean verticalPosition = - (labeled.getContentDisplay() == ContentDisplay.TOP || - labeled.getContentDisplay() == ContentDisplay.BOTTOM); - - double availableHeight = labeled.getHeight() - snappedTopInset() - topLabelPadding() - - snappedBottomInset() - bottomLabelPadding(); - availableHeight = Math.max(availableHeight, 0); - - if (h == -1) { - h = availableHeight; - } - double minH = Math.min(computeMinLabeledPartHeight(wrapWidth, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableHeight); - if (verticalPosition && labeled.getGraphic() != null) { - double graphicH = labeled.getGraphic().getLayoutBounds().getHeight() + labeled.getGraphicTextGap(); - h -= graphicH; - minH -= graphicH; - } - wrapHeight = Math.max(minH, h); - - updateWrappingWidth(); - - Font font = text.getFont(); - OverrunStyle truncationStyle = labeled.getTextOverrun(); - String ellipsisString = labeled.getEllipsisString(); - - if (labeled.isWrapText()) { - result = Utils.computeClippedWrappedText(font, s, wrapWidth, wrapHeight, truncationStyle, ellipsisString, text.getBoundsType()); - } else if (multiline) { - StringBuilder sb = new StringBuilder(); - - String[] splits = s.split("\n"); - for (int i = 0; i < splits.length; i++) { - sb.append(Utils.computeClippedText(font, splits[i], wrapWidth, truncationStyle, ellipsisString)); - if (i < splits.length - 1) { - sb.append('\n'); - } - } - - // TODO: Consider what to do in the case where vertical space is - // limited and the last visible line isn't already truncated - // with a trailing ellipsis. What if the style calls for leading - // or center ellipses? We could possibly add an additional - // trailing ellipsis to the last visible line, like this: - - // +--------------------------------+ - // | This is some long text with multiple lines\n - // | where more than one exceed the|width\n - // | and wrapText is false, and all|lines\n - // +--don't fit.--------------------+ - // - // +--------------------------------+ - // | This is some...multiple lines | - // | where more t...ceed the width | - // | and wrapText...d all lines... | - // +--------------------------------+ - - result = sb.toString(); - } else { - result = Utils.computeClippedText(font, s, wrapWidth, truncationStyle, ellipsisString); - } - - if (result != null && result.endsWith("\n")) { - // Strip ending newline so we don't display another row. - result = result.substring(0, result.length() - 1); - } - - text.setText(result); - updateWrappingWidth(); - invalidText = false; - } - } - - private void addMnemonic() { - if (labeledNode != null) { - mnemonicScene = labeledNode.getScene(); - if (mnemonicScene != null) { - mnemonicScene.addMnemonic(new Mnemonic(labeledNode, mnemonicCode)); - } - } - } - - - private void removeMnemonic() { - if (mnemonicScene != null && labeledNode != null) { - mnemonicScene.removeMnemonic(new Mnemonic(labeledNode, mnemonicCode)); - mnemonicScene = null; - } - } - - /** - * Updates the wrapping width of the text node. Although changing the font - * does affect the metrics used for text layout, this method does not - * call requestLayout or invalidate the text, since it may be called - * from the constructor and such work would be duplicative and wasted. - */ - private void updateWrappingWidth() { - final Labeled labeled = getSkinnable(); - text.setWrappingWidth(0); - if (labeled.isWrapText()) { - // Note that the wrapping width needs to be set to zero before - // getting the text's real preferred width. - double w = Math.min(text.prefWidth(-1), wrapWidth); - text.setWrappingWidth(w); - } - } - - /** - * Updates the children managed by LabeledSkinBase, which can be the Labeled - * graphic and/or a Text node. Only those nodes which actually must - * be used are used. For example, with a ContentDisplay of - * GRAPHIC_ONLY the text node is not added, and with a ContentDisplay - * of TEXT_ONLY, the graphic is not added. - */ - protected void updateChildren() { - final Labeled labeled = getSkinnable(); - // Only in some situations do we want to have the graphicPropertyChangedListener - // installed. Since updateChildren() is not called much, we'll just remove it always - // and reinstall it later if it is necessary to do so. - if (graphic != null) { - graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener); - } - // Now update the graphic (since it may have changed) - graphic = labeled.getGraphic(); - - // RT-19851 Only setMouseTransparent(true) for an ImageView. This allows the button - // to be picked regardless of the changing images on top of it. - if (graphic instanceof ImageView) { - graphic.setMouseTransparent(true); - } - - // Now update the children (and add the graphicPropertyChangedListener as necessary) - if (isIgnoreGraphic()) { - if (labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) { - getChildren().clear(); - } else { - getChildren().setAll(text); - } - } else { - graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener); - if (isIgnoreText()) { - getChildren().setAll(graphic); - } else { - getChildren().setAll(graphic, text); - } - - // RT-37420 - graphic.impl_processCSS(false); - - } - - } - - /** - * Gets whether for various computations we can ignore the presence of the graphic - * (or lack thereof). - * @return - */ - protected boolean isIgnoreGraphic() { - return (graphic == null || - !graphic.isManaged() || - getSkinnable().getContentDisplay() == ContentDisplay.TEXT_ONLY); - } - - /** - * Gets whether for various computations we can ignore the presence of the text. - * @return - */ - protected boolean isIgnoreText() { - final Labeled labeled = getSkinnable(); - final String txt = labeled.getText(); - return (txt == null || - txt.equals("") || - labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY); - } - - /*************************************************************************** - * * - * Skin Layout * - * * - **************************************************************************/ - - /** - * Compute and return the minimum width of this Labeled. The minimum width is - * the smaller of the width of "..." and the width with the actual text. - * In this way, if the text width itself is smaller than the ellipsis then - * we should use that as the min width, otherwise the ellipsis needs to be the - * min width. - *

- * We use the same calculation here regardless of whether we are talking - * about a single or multiline labeled. So a multiline labeled may find that - * the width of the "..." is as small as it will ever get. - */ - @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - return computeMinLabeledPartWidth(height, topInset, rightInset, bottomInset, leftInset); - } - - private double computeMinLabeledPartWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - // First compute the minTextWidth by checking the width of the string - // made by the ellipsis "...", and then by checking the width of the - // string made up by labeled.text. We want the smaller of the two. - final Labeled labeled = getSkinnable(); - final ContentDisplay contentDisplay = labeled.getContentDisplay(); - final double gap = labeled.getGraphicTextGap(); - double minTextWidth = 0; - - final Font font = text.getFont(); - OverrunStyle truncationStyle = labeled.getTextOverrun(); - String ellipsisString = labeled.getEllipsisString(); - final String string = labeled.getText(); - final boolean emptyText = string == null || string.isEmpty(); - - if (!emptyText) { - // We only want to recompute the full text width if the font or text changed - if (truncationStyle == CLIP) { - if (textWidth == Double.NEGATIVE_INFINITY) { - // Show at minimum the first character - textWidth = Utils.computeTextWidth(font, string.substring(0, 1), 0); - } - minTextWidth = textWidth; - } else { - if (textWidth == Double.NEGATIVE_INFINITY) { - textWidth = Utils.computeTextWidth(font, string, 0); - } - // We only want to recompute the ellipsis width if the font has changed - if (ellipsisWidth == Double.NEGATIVE_INFINITY) { - ellipsisWidth = Utils.computeTextWidth(font, ellipsisString, 0); - } - minTextWidth = Math.min(textWidth, ellipsisWidth); - } - } - - // Now inspect the graphic and the hpos to determine the the minWidth - final Node graphic = labeled.getGraphic(); - double width; - if (isIgnoreGraphic()) { - width = minTextWidth; - } else if (isIgnoreText()) { - width = graphic.minWidth(-1); - } else if (contentDisplay == LEFT || contentDisplay == RIGHT){ - width = (minTextWidth + graphic.minWidth(-1) + gap); - } else { - width = Math.max(minTextWidth, graphic.minWidth(-1)); - } - - return width + leftInset + leftLabelPadding() + - rightInset + rightLabelPadding(); - } - - @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return computeMinLabeledPartHeight(width, topInset, rightInset, bottomInset, leftInset); - } - - private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - final Labeled labeled = getSkinnable(); - final Font font = text.getFont(); - - String str = labeled.getText(); - if (str != null && str.length() > 0) { - int newlineIndex = str.indexOf('\n'); - if (newlineIndex >= 0) { - str = str.substring(0, newlineIndex); - } - } - - // TODO figure out how to cache this effectively. - // Base minimum height on one line (ignoring wrapping here). - double s = labeled.getLineSpacing(); - final double textHeight = Utils.computeTextHeight(font, str, 0, s, text.getBoundsType()); - - double h = textHeight; - - // Now we want to add on the graphic if necessary! - if (!isIgnoreGraphic()) { - final Node graphic = labeled.getGraphic(); - if (labeled.getContentDisplay() == ContentDisplay.TOP - || labeled.getContentDisplay() == ContentDisplay.BOTTOM) { - h = graphic.minHeight(width) + labeled.getGraphicTextGap() + textHeight; - } else { - h = Math.max(textHeight, graphic.minHeight(width)); - } - } - - return topInset + h + bottomInset + topLabelPadding() - bottomLabelPadding(); - } - - @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - // Get the preferred width of the text - final Labeled labeled = getSkinnable(); - final Font font = text.getFont(); - final String string = labeled.getText(); - boolean emptyText = string == null || string.isEmpty(); - double widthPadding = leftInset + leftLabelPadding() + - rightInset + rightLabelPadding(); - - double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0); - - // Fix for RT-39889 - double graphicWidth = graphic == null ? 0.0 : - Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1)); - - // Now add on the graphic, gap, and padding as appropriate - final Node graphic = labeled.getGraphic(); - if (isIgnoreGraphic()) { - return textWidth + widthPadding; - } else if (isIgnoreText()) { - return graphicWidth + widthPadding; - } else if (labeled.getContentDisplay() == ContentDisplay.LEFT - || labeled.getContentDisplay() == ContentDisplay.RIGHT) { - return textWidth + labeled.getGraphicTextGap() + graphicWidth + widthPadding; - } else { - return Math.max(textWidth, graphicWidth) + widthPadding; - } - } - - @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - final Labeled labeled = getSkinnable(); - final Font font = text.getFont(); - final ContentDisplay contentDisplay = labeled.getContentDisplay(); - final double gap = labeled.getGraphicTextGap(); - width -= leftInset + leftLabelPadding() + - rightInset + rightLabelPadding(); - - String str = labeled.getText(); - if (str != null && str.endsWith("\n")) { - // Strip ending newline so we don't count another row. - str = str.substring(0, str.length() - 1); - } - - double textWidth = width; - if (!isIgnoreGraphic() && - (contentDisplay == LEFT || contentDisplay == RIGHT)) { - textWidth -= (graphic.prefWidth(-1) + gap); - } - - // TODO figure out how to cache this effectively. - final double textHeight = Utils.computeTextHeight(font, str, - labeled.isWrapText() ? textWidth : 0, - labeled.getLineSpacing(), text.getBoundsType()); - - // Now we want to add on the graphic if necessary! - double h = textHeight; - if (!isIgnoreGraphic()) { - final Node graphic = labeled.getGraphic(); - if (contentDisplay == TOP || contentDisplay == BOTTOM) { - h = graphic.prefHeight(width) + gap + textHeight; - } else { - h = Math.max(textHeight, graphic.prefHeight(width)); - } - } - - return topInset + h + bottomInset + topLabelPadding() + bottomLabelPadding(); - } - - @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - return getSkinnable().prefWidth(height); - } - - @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return getSkinnable().prefHeight(width); - } - - @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { - double textBaselineOffset = text.getBaselineOffset(); - double h = textBaselineOffset; - final Labeled labeled = getSkinnable(); - final Node g = labeled.getGraphic(); - if (!isIgnoreGraphic()) { - ContentDisplay contentDisplay = labeled.getContentDisplay(); - if (contentDisplay == ContentDisplay.TOP) { - h = g.prefHeight(-1) + labeled.getGraphicTextGap() + textBaselineOffset; - } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT) { - h = textBaselineOffset + (g.prefHeight(-1) - text.prefHeight(-1)) / 2; - } - } - - return topInset + topLabelPadding() + h; - } - - public TextBinding bindings; - Line mnemonic_underscore; - - private boolean containsMnemonic = false; - private Scene mnemonicScene = null; - private KeyCombination mnemonicCode; - // needs to be an object, as MenuItem isn't a node - private Node labeledNode = null; - - /** - * The Layout algorithm works like this: - * - * - Get the labeled w/h, graphic w/h, text w/h - * - Compute content w/h based on graphicVPos, graphicHPos, - * graphicTextGap, and graphic w/h and text w/h - * - (Note that the text content has been pre-truncated where - * necessary) - * - compute content x/y based on content w/h and labeled w/h - * and the labeled's hpos and vpos - * - position the graphic and text - */ - @Override protected void layoutChildren(final double x, final double y, - final double w, final double h) { - layoutLabelInArea(x, y, w, h); - } - - /** - * Performs the actual layout of the label content within the area given. - * This method is called by subclasses that override layoutChildren(). - * - * @param x The x position of the label part of the control, inside padding - * - * @param y The y position of the label part of the control, inside padding - * - * @param w The width of the label part of the control, not including padding - * - * @param h The height of the label part of the control, not including padding - */ - protected void layoutLabelInArea(double x, double y, double w, double h) { - layoutLabelInArea(x, y, w, h, null); - } - - /** - * Performs the actual layout of the label content within the area given. - * This method is called by subclasses that override layoutChildren(). - * - * @param x The x position of the label part of the control, inside padding - * - * @param y The y position of the label part of the control, inside padding - * - * @param w The width of the label part of the control, not including padding - * - * @param h The height of the label part of the control, not including padding - * - * @param alignment The alignment of the label part of the control within the given area. If null, then the control's alignment will be used. - */ - protected void layoutLabelInArea(double x, double y, double w, double h, Pos alignment) { - // References to essential labeled state - final Labeled labeled = getSkinnable(); - final ContentDisplay contentDisplay = labeled.getContentDisplay(); - - if (alignment == null) { - alignment = labeled.getAlignment(); - } - - final HPos hpos = alignment == null ? HPos.LEFT : alignment.getHpos(); - final VPos vpos = alignment == null ? VPos.CENTER : alignment.getVpos(); - - // Figure out whether we should ignore the Graphic, and/or - // ignore the Text - final boolean ignoreGraphic = isIgnoreGraphic(); - final boolean ignoreText = isIgnoreText(); - - x += leftLabelPadding(); - y += topLabelPadding(); - w -= leftLabelPadding() + rightLabelPadding(); - h -= topLabelPadding() + bottomLabelPadding(); - - // Compute some standard useful numbers for the graphic, text, and gap - double graphicWidth; - double graphicHeight; - double textWidth; - double textHeight; - - if (ignoreGraphic) { - graphicWidth = graphicHeight = 0; - } else if (ignoreText) { - if (graphic.isResizable()) { - Orientation contentBias = graphic.getContentBias(); - if (contentBias == Orientation.HORIZONTAL) { - graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); - graphicHeight = Utils.boundedSize(h, graphic.minHeight(graphicWidth), graphic.maxHeight(graphicWidth)); - } else if (contentBias == Orientation.VERTICAL) { - graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); - graphicWidth = Utils.boundedSize(w, graphic.minWidth(graphicHeight), graphic.maxWidth(graphicHeight)); - } else { - graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); - graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); - } - graphic.resize(graphicWidth, graphicHeight); - } else { - graphicWidth = graphic.getLayoutBounds().getWidth(); - graphicHeight = graphic.getLayoutBounds().getHeight(); - } - } else { - graphic.autosize(); // We have to do this before getting metrics - graphicWidth = graphic.getLayoutBounds().getWidth(); - graphicHeight = graphic.getLayoutBounds().getHeight(); - } - - if (ignoreText) { - textWidth = textHeight = 0; - text.setText(""); - } else { - updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed - textWidth = snapSize(Math.min(text.getLayoutBounds().getWidth(), wrapWidth)); - textHeight = snapSize(Math.min(text.getLayoutBounds().getHeight(), wrapHeight)); - } - - final double gap = (ignoreText || ignoreGraphic) ? 0 : labeled.getGraphicTextGap(); - - // Figure out the contentWidth and contentHeight. This is the width - // and height of the Labeled and Graphic together, not the available - // content area (which would be a different calculation). - double contentWidth = Math.max(graphicWidth, textWidth); - double contentHeight = Math.max(graphicHeight, textHeight); - if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) { - contentHeight = graphicHeight + gap + textHeight; - } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) { - contentWidth = graphicWidth + gap + textWidth; - } - - // Now we want to compute the x/y location to place the content at. - - // Compute the contentX position based on hpos and the space available - double contentX; - if (hpos == HPos.LEFT) { - contentX = x; - } else if (hpos == HPos.RIGHT) { - contentX = x + (w - contentWidth); - } else { - // TODO Baseline may not be handled correctly - // may have been CENTER or null, treat as center - contentX = (x + ((w - contentWidth) / 2.0)); - } - - // Compute the contentY position based on vpos and the space available - double contentY; - if (vpos == VPos.TOP) { - contentY = y; - } else if (vpos == VPos.BOTTOM) { - contentY = (y + (h - contentHeight)); - } else { - // TODO Baseline may not be handled correctly - // may have been CENTER, BASELINE, or null, treat as center - contentY = (y + ((h - contentHeight) / 2.0)); - } - - double preMnemonicWidth = 0.0; - double mnemonicWidth = 0.0; - double mnemonicHeight = 0.0; - if (containsMnemonic) { - final Font font = text.getFont(); - String preSt = bindings.getText(); - preMnemonicWidth = Utils.computeTextWidth(font, preSt.substring(0,bindings.getMnemonicIndex()) , 0); - mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(bindings.getMnemonicIndex(),bindings.getMnemonicIndex()+1) , 0); - mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType()); - } - - - // Now to position the graphic and text. At this point I know the - // contentX and contentY locations (including the padding and whatnot - // that was defined on the Labeled). I also know the content width and - // height. So now I just need to lay out the graphic and text within - // that content x/y/w/h area. - if ((!ignoreGraphic || !ignoreText) && !text.isManaged()) { - text.setManaged(true); - } - - if (ignoreGraphic && ignoreText) { - // There might be a text node as a child, or a graphic node as - // a child. However we don't have to do anything for the graphic - // node because the only way it can be a child and still have - // ignoreGraphic true is if it is unmanaged. Text however might - // be a child but still not matter, in which case we will just - // stop managing it (although really I wish it just wasn't here - // all all in that case) - if (text.isManaged()) { - text.setManaged(false); - } - text.relocate(snapPosition(contentX), snapPosition(contentY)); - } else if (ignoreGraphic) { - // Since I only have to position the text, it goes at the - // contentX/contentY location. Note that positionNode will - // adjust the text based on the text's minX/minY so no need to - // worry about that here - text.relocate(snapPosition(contentX), snapPosition(contentY)); - if (containsMnemonic) { - mnemonic_underscore.setEndX(mnemonicWidth-2.0); - mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1); - } - - } else if (ignoreText) { - // there isn't text to display, so we need to position it - // such that it doesn't affect the content area (although when - // there is a graphic, the text isn't even in the scene) - text.relocate(snapPosition(contentX), snapPosition(contentY)); - graphic.relocate(snapPosition(contentX), snapPosition(contentY)); - if (containsMnemonic) { - mnemonic_underscore.setEndX(mnemonicWidth); - mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); - mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1); - - } - } else { - // There is both text and a graphic, so I need to position them - // relative to each other - double graphicX = 0; - double graphicY = 0; - double textX = 0; - double textY = 0; - - if (contentDisplay == ContentDisplay.TOP) { - graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); - textX = contentX + ((contentWidth - textWidth) / 2.0); - // The graphic is above the text, so it is positioned at - // graphicY and the text below it. - graphicY = contentY; - textY = graphicY + graphicHeight + gap; - } else if (contentDisplay == ContentDisplay.RIGHT) { - // The graphic is to the right of the text - textX = contentX; - graphicX = textX + textWidth + gap; - graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); - textY = contentY + ((contentHeight - textHeight) / 2.0); - } else if (contentDisplay == ContentDisplay.BOTTOM) { - graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); - textX = contentX + ((contentWidth - textWidth) / 2.0); - // The graphic is below the text - textY = contentY; - graphicY = textY + textHeight + gap; - } else if (contentDisplay == ContentDisplay.LEFT) { - // The graphic is to the left of the text, so the graphicX is - // simply the contentX and the textX is to the right of it. - graphicX = contentX; - textX = graphicX + graphicWidth + gap; - graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); - textY = contentY + ((contentHeight - textHeight) / 2.0); - } else if (contentDisplay == ContentDisplay.CENTER) { - graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); - textX = contentX + ((contentWidth - textWidth) / 2.0); - graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); - textY = contentY + ((contentHeight - textHeight) / 2.0); - } - text.relocate(snapPosition(textX), snapPosition(textY)); - if (containsMnemonic) { - mnemonic_underscore.setEndX(mnemonicWidth); - mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); - mnemonic_underscore.relocate(snapPosition(textX+preMnemonicWidth), snapPosition(textY+mnemonicHeight-1)); - } - graphic.relocate(snapPosition(graphicX), snapPosition(graphicY)); - } - - /** - * check if the label text overflows it's bounds. - * If there's an overflow, and no text clip then - * we'll clip it. - * If there is no overflow, and the label text has a - * clip, then remove it. - */ - if ((text != null) && - ((text.getLayoutBounds().getHeight() > wrapHeight) || - (text.getLayoutBounds().getWidth() > wrapWidth))) { - - if (textClip == null) { - textClip = new Rectangle(); - } - - if (labeled.getEffectiveNodeOrientation() == NodeOrientation.LEFT_TO_RIGHT) { - textClip.setX(text.getLayoutBounds().getMinX()); - } else { - textClip.setX(text.getLayoutBounds().getMaxX() - wrapWidth); - } - textClip.setY(text.getLayoutBounds().getMinY()); - textClip.setWidth(wrapWidth); - textClip.setHeight(wrapHeight); - if (text.getClip() == null) { - text.setClip(textClip); - } - } - else { - /** - * content fits inside bounds, no need - * for a clip - */ - if (text.getClip() != null) { - text.setClip(null); - } - } - } - - @Override - protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { - switch (attribute) { - case TEXT: { - Labeled labeled = getSkinnable(); - String accText = labeled.getAccessibleText(); - if (accText != null && !accText.isEmpty()) return accText; - - /* Use the text in the binding if available to handle mnemonics */ - if (bindings != null) { - String text = bindings.getText(); - if (text != null && !text.isEmpty()) return text; - } - /* Avoid the content in text.getText() as it can contain ellipses - * for clipping - */ - if (labeled != null) { - String text = labeled.getText(); - if (text != null && !text.isEmpty()) return text; - } - /* Use the graphic as last resource. Note that this implementation - * does not attempt to combine the label and graphics if both - * are being displayed - */ - if (graphic != null) { - Object result = graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT); - if (result != null) return result; - } - return null; - } - case MNEMONIC: { - if (bindings != null) { - return bindings.getMnemonic(); - } - return null; - } - default: return super.queryAccessibleAttribute(attribute, parameters); - } - } -} --- /dev/null 2015-09-03 15:19:46.000000000 -0700 +++ new/modules/controls/src/main/java/javafx/scene/control/skin/LabeledSkinBase.java 2015-09-03 15:19:44.318733700 -0700 @@ -0,0 +1,1172 @@ +/* + * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.control.skin; + +import com.sun.javafx.scene.control.LabeledText; +import com.sun.javafx.scene.control.behavior.TextBinding; +import com.sun.javafx.scene.control.skin.Utils; +import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.geometry.HPos; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.AccessibleAttribute; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Accordion; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Labeled; +import javafx.scene.control.OverrunStyle; +import javafx.scene.control.SkinBase; +import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.Mnemonic; +import javafx.scene.shape.Line; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; + +import static javafx.scene.control.ContentDisplay.BOTTOM; +import static javafx.scene.control.ContentDisplay.LEFT; +import static javafx.scene.control.ContentDisplay.RIGHT; +import static javafx.scene.control.ContentDisplay.TOP; +import static javafx.scene.control.OverrunStyle.CLIP; + +/** + * Default skin implementation for controls extends {@link Labeled}. + * + * @see Labeled + * @since 9 + */ +public abstract class LabeledSkinBase extends SkinBase { + + /*************************************************************************** + * * + * Private fields * + * * + **************************************************************************/ + + /** + * The Text node used to display the text. This is package only + * for the sake of testing! + */ + LabeledText text; + + /** + * Indicates that the text content is invalid and needs to be updated. + * This is package private only for the sake of testing. + */ + boolean invalidText = true; + + /** + * A reference to the last-known graphic on the Labeled. This reference + * is kept so that we can remove listeners from the old graphic later + */ + Node graphic; + + /** + * The cached full width of the non-truncated text. We only want to + * recompute this if the text has itself changed, or if the font has changed. + * This is package private ONLY FOR THE SAKE OF TESTING + */ + double textWidth = Double.NEGATIVE_INFINITY; + + /** + * The cached width of the ellipsis string. This will be recomputed + * if the font or the ellipsisString property have changed. + * This is package private ONLY FOR THE SAKE OF TESTING + */ + double ellipsisWidth = Double.NEGATIVE_INFINITY; + + /** + * A listener which is applied to the graphic whenever the graphic is set + * and is visible within the labeled. For example, if there is a graphic + * defined on the Labeled but the ContentDisplay is set to TEXT_ONLY, then + * we will not bother installing this listener on the graphic. In all + * other cases, if the graphic is defined, it will have this listener + * added to it, which ensures that if the graphic's layout bounds change, + * we end up performing a layout and potentially update the visible text. + * + * This is package private ONLY FOR THE SAKE OF TESTING + */ + final InvalidationListener graphicPropertyChangedListener = valueModel -> { + invalidText = true; + getSkinnable().requestLayout(); + }; + + private Rectangle textClip; + private double wrapWidth; + private double wrapHeight; + + private TextBinding bindings; + private Line mnemonic_underscore; + + private boolean containsMnemonic = false; + private Scene mnemonicScene = null; + private KeyCombination mnemonicCode; + // needs to be an object, as MenuItem isn't a node + private Node labeledNode = null; + + + + /*************************************************************************** + * * + * Constructors * + * * + **************************************************************************/ + + /** + * Constructor for LabeledSkinBase. The Labeled must be specified, and cannot be null. + * At the conclusion of the constructor call, the skin will be marked as + * needsLayout, and will be fully configured based on the current state of + * the labeled. Any subsequent changes to the Labeled will be handled via + * listeners and applied appropriately. + * + * @param labeled The labeled that this skin should be installed onto. + */ + public LabeledSkinBase(final C labeled) { + super(labeled); + + // Configure the Text node with all of the attributes from the + // Labeled which apply to it. + text = new LabeledText(labeled); + + updateChildren(); + + // Labels do not block the mouse by default, unlike most other UI Controls. + //consumeMouseEvents(false); + + // Register listeners + /* + * There are basically 2 things to worry about in each of these handlers + * 1) Update the Text node + * 2) Have the text metrics changed? + * + * If the metrics have changed, we need to request a layout and invalidate + * the text so that we recompute the display text on next read. + */ + registerChangeListener(labeled.ellipsisStringProperty(), o -> { + textMetricsChanged(); + invalidateWidths(); + ellipsisWidth = Double.NEGATIVE_INFINITY; + }); + registerChangeListener(labeled.widthProperty(), o -> { + updateWrappingWidth(); + invalidText = true; + // No requestLayout() because Control will force a layout + }); + registerChangeListener(labeled.heightProperty(), o -> { + invalidText = true; + // No requestLayout() because Control will force a layout + }); + registerChangeListener(labeled.fontProperty(), o -> { + textMetricsChanged(); + invalidateWidths(); + ellipsisWidth = Double.NEGATIVE_INFINITY; + }); + registerChangeListener(labeled.graphicProperty(), o -> { + updateChildren(); + textMetricsChanged(); + }); + registerChangeListener(labeled.contentDisplayProperty(), o -> { + updateChildren(); + textMetricsChanged(); + }); + registerChangeListener(labeled.labelPaddingProperty(), o -> textMetricsChanged()); + registerChangeListener(labeled.graphicTextGapProperty(), o -> textMetricsChanged()); + registerChangeListener(labeled.alignmentProperty(), o -> { + // Doesn't involve text metrics because if the text is too long, then + // it will already have fit all available width and a change to hpos + // has no effect. Or it is too short (i.e. it all fits) and we don't + // have to worry about truncation. So just call request layout. + // Doesn't involve text metrics because if the text is too long, then + // it will already have fit all available height and a change to vpos + // has no effect. Or it is too short (i.e. it all fits) and we don't + // have to worry about truncation. So just call request layout. + getSkinnable().requestLayout(); + }); + registerChangeListener(labeled.mnemonicParsingProperty(), o -> { + containsMnemonic = false; + textMetricsChanged(); + }); + registerChangeListener(labeled.textProperty(), o -> { + updateChildren(); + textMetricsChanged(); + invalidateWidths(); + }); + registerChangeListener(labeled.textAlignmentProperty(), o -> { /* NO-OP */ }); + registerChangeListener(labeled.textOverrunProperty(), o -> textMetricsChanged()); + registerChangeListener(labeled.wrapTextProperty(), o -> { + updateWrappingWidth(); + textMetricsChanged(); + }); + registerChangeListener(labeled.underlineProperty(), o -> textMetricsChanged()); + registerChangeListener(labeled.lineSpacingProperty(), o -> textMetricsChanged()); + registerChangeListener(labeled.sceneProperty(), o -> sceneChanged()); + } + + + + /*************************************************************************** + * * + * Public API * + * * + **************************************************************************/ + + /** + * Updates the children managed by LabeledSkinBase, which can be the Labeled + * graphic and/or a Text node. Only those nodes which actually must + * be used are used. For example, with a ContentDisplay of + * GRAPHIC_ONLY the text node is not added, and with a ContentDisplay + * of TEXT_ONLY, the graphic is not added. + */ + protected void updateChildren() { + final Labeled labeled = getSkinnable(); + // Only in some situations do we want to have the graphicPropertyChangedListener + // installed. Since updateChildren() is not called much, we'll just remove it always + // and reinstall it later if it is necessary to do so. + if (graphic != null) { + graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener); + } + // Now update the graphic (since it may have changed) + graphic = labeled.getGraphic(); + + // RT-19851 Only setMouseTransparent(true) for an ImageView. This allows the button + // to be picked regardless of the changing images on top of it. + if (graphic instanceof ImageView) { + graphic.setMouseTransparent(true); + } + + // Now update the children (and add the graphicPropertyChangedListener as necessary) + if (isIgnoreGraphic()) { + if (labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) { + getChildren().clear(); + } else { + getChildren().setAll(text); + } + } else { + graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener); + if (isIgnoreText()) { + getChildren().setAll(graphic); + } else { + getChildren().setAll(graphic, text); + } + + // RT-37420 + graphic.impl_processCSS(false); + + } + + } + + /** + * Compute and return the minimum width of this Labeled. The minimum width is + * the smaller of the width of "..." and the width with the actual text. + * In this way, if the text width itself is smaller than the ellipsis then + * we should use that as the min width, otherwise the ellipsis needs to be the + * min width. + *

+ * We use the same calculation here regardless of whether we are talking + * about a single or multiline labeled. So a multiline labeled may find that + * the width of the "..." is as small as it will ever get. + */ + @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return computeMinLabeledPartWidth(height, topInset, rightInset, bottomInset, leftInset); + } + + /** {@inheritDoc} */ + @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return computeMinLabeledPartHeight(width, topInset, rightInset, bottomInset, leftInset); + } + + /** {@inheritDoc} */ + @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + // Get the preferred width of the text + final Labeled labeled = getSkinnable(); + final Font font = text.getFont(); + final String string = labeled.getText(); + boolean emptyText = string == null || string.isEmpty(); + double widthPadding = leftInset + leftLabelPadding() + + rightInset + rightLabelPadding(); + + double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0); + + // Fix for RT-39889 + double graphicWidth = graphic == null ? 0.0 : + Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1)); + + // Now add on the graphic, gap, and padding as appropriate + final Node graphic = labeled.getGraphic(); + if (isIgnoreGraphic()) { + return textWidth + widthPadding; + } else if (isIgnoreText()) { + return graphicWidth + widthPadding; + } else if (labeled.getContentDisplay() == ContentDisplay.LEFT + || labeled.getContentDisplay() == ContentDisplay.RIGHT) { + return textWidth + labeled.getGraphicTextGap() + graphicWidth + widthPadding; + } else { + return Math.max(textWidth, graphicWidth) + widthPadding; + } + } + + /** {@inheritDoc} */ + @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + final Labeled labeled = getSkinnable(); + final Font font = text.getFont(); + final ContentDisplay contentDisplay = labeled.getContentDisplay(); + final double gap = labeled.getGraphicTextGap(); + width -= leftInset + leftLabelPadding() + + rightInset + rightLabelPadding(); + + String str = labeled.getText(); + if (str != null && str.endsWith("\n")) { + // Strip ending newline so we don't count another row. + str = str.substring(0, str.length() - 1); + } + + double textWidth = width; + if (!isIgnoreGraphic() && + (contentDisplay == LEFT || contentDisplay == RIGHT)) { + textWidth -= (graphic.prefWidth(-1) + gap); + } + + // TODO figure out how to cache this effectively. + final double textHeight = Utils.computeTextHeight(font, str, + labeled.isWrapText() ? textWidth : 0, + labeled.getLineSpacing(), text.getBoundsType()); + + // Now we want to add on the graphic if necessary! + double h = textHeight; + if (!isIgnoreGraphic()) { + final Node graphic = labeled.getGraphic(); + if (contentDisplay == TOP || contentDisplay == BOTTOM) { + h = graphic.prefHeight(width) + gap + textHeight; + } else { + h = Math.max(textHeight, graphic.prefHeight(width)); + } + } + + return topInset + h + bottomInset + topLabelPadding() + bottomLabelPadding(); + } + + /** {@inheritDoc} */ + @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return getSkinnable().prefWidth(height); + } + + /** {@inheritDoc} */ + @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return getSkinnable().prefHeight(width); + } + + /** {@inheritDoc} */ + @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { + double textBaselineOffset = text.getBaselineOffset(); + double h = textBaselineOffset; + final Labeled labeled = getSkinnable(); + final Node g = labeled.getGraphic(); + if (!isIgnoreGraphic()) { + ContentDisplay contentDisplay = labeled.getContentDisplay(); + if (contentDisplay == ContentDisplay.TOP) { + h = g.prefHeight(-1) + labeled.getGraphicTextGap() + textBaselineOffset; + } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT) { + h = textBaselineOffset + (g.prefHeight(-1) - text.prefHeight(-1)) / 2; + } + } + + return topInset + topLabelPadding() + h; + } + + /** + * The Layout algorithm works like this: + * + * - Get the labeled w/h, graphic w/h, text w/h + * - Compute content w/h based on graphicVPos, graphicHPos, + * graphicTextGap, and graphic w/h and text w/h + * - (Note that the text content has been pre-truncated where + * necessary) + * - compute content x/y based on content w/h and labeled w/h + * and the labeled's hpos and vpos + * - position the graphic and text + */ + @Override protected void layoutChildren(final double x, final double y, + final double w, final double h) { + layoutLabelInArea(x, y, w, h); + } + + /** + * Performs the actual layout of the label content within the area given. + * This method is called by subclasses that override layoutChildren(). + * + * @param x The x position of the label part of the control, inside padding + * + * @param y The y position of the label part of the control, inside padding + * + * @param w The width of the label part of the control, not including padding + * + * @param h The height of the label part of the control, not including padding + */ + protected void layoutLabelInArea(double x, double y, double w, double h) { + layoutLabelInArea(x, y, w, h, null); + } + + /** + * Performs the actual layout of the label content within the area given. + * This method is called by subclasses that override layoutChildren(). + * + * @param x The x position of the label part of the control, inside padding + * + * @param y The y position of the label part of the control, inside padding + * + * @param w The width of the label part of the control, not including padding + * + * @param h The height of the label part of the control, not including padding + * + * @param alignment The alignment of the label part of the control within the given area. If null, then the control's alignment will be used. + */ + protected void layoutLabelInArea(double x, double y, double w, double h, Pos alignment) { + // References to essential labeled state + final Labeled labeled = getSkinnable(); + final ContentDisplay contentDisplay = labeled.getContentDisplay(); + + if (alignment == null) { + alignment = labeled.getAlignment(); + } + + final HPos hpos = alignment == null ? HPos.LEFT : alignment.getHpos(); + final VPos vpos = alignment == null ? VPos.CENTER : alignment.getVpos(); + + // Figure out whether we should ignore the Graphic, and/or + // ignore the Text + final boolean ignoreGraphic = isIgnoreGraphic(); + final boolean ignoreText = isIgnoreText(); + + x += leftLabelPadding(); + y += topLabelPadding(); + w -= leftLabelPadding() + rightLabelPadding(); + h -= topLabelPadding() + bottomLabelPadding(); + + // Compute some standard useful numbers for the graphic, text, and gap + double graphicWidth; + double graphicHeight; + double textWidth; + double textHeight; + + if (ignoreGraphic) { + graphicWidth = graphicHeight = 0; + } else if (ignoreText) { + if (graphic.isResizable()) { + Orientation contentBias = graphic.getContentBias(); + if (contentBias == Orientation.HORIZONTAL) { + graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); + graphicHeight = Utils.boundedSize(h, graphic.minHeight(graphicWidth), graphic.maxHeight(graphicWidth)); + } else if (contentBias == Orientation.VERTICAL) { + graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); + graphicWidth = Utils.boundedSize(w, graphic.minWidth(graphicHeight), graphic.maxWidth(graphicHeight)); + } else { + graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); + graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); + } + graphic.resize(graphicWidth, graphicHeight); + } else { + graphicWidth = graphic.getLayoutBounds().getWidth(); + graphicHeight = graphic.getLayoutBounds().getHeight(); + } + } else { + graphic.autosize(); // We have to do this before getting metrics + graphicWidth = graphic.getLayoutBounds().getWidth(); + graphicHeight = graphic.getLayoutBounds().getHeight(); + } + + if (ignoreText) { + textWidth = textHeight = 0; + text.setText(""); + } else { + updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed + textWidth = snapSize(Math.min(text.getLayoutBounds().getWidth(), wrapWidth)); + textHeight = snapSize(Math.min(text.getLayoutBounds().getHeight(), wrapHeight)); + } + + final double gap = (ignoreText || ignoreGraphic) ? 0 : labeled.getGraphicTextGap(); + + // Figure out the contentWidth and contentHeight. This is the width + // and height of the Labeled and Graphic together, not the available + // content area (which would be a different calculation). + double contentWidth = Math.max(graphicWidth, textWidth); + double contentHeight = Math.max(graphicHeight, textHeight); + if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) { + contentHeight = graphicHeight + gap + textHeight; + } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) { + contentWidth = graphicWidth + gap + textWidth; + } + + // Now we want to compute the x/y location to place the content at. + + // Compute the contentX position based on hpos and the space available + double contentX; + if (hpos == HPos.LEFT) { + contentX = x; + } else if (hpos == HPos.RIGHT) { + contentX = x + (w - contentWidth); + } else { + // TODO Baseline may not be handled correctly + // may have been CENTER or null, treat as center + contentX = (x + ((w - contentWidth) / 2.0)); + } + + // Compute the contentY position based on vpos and the space available + double contentY; + if (vpos == VPos.TOP) { + contentY = y; + } else if (vpos == VPos.BOTTOM) { + contentY = (y + (h - contentHeight)); + } else { + // TODO Baseline may not be handled correctly + // may have been CENTER, BASELINE, or null, treat as center + contentY = (y + ((h - contentHeight) / 2.0)); + } + + double preMnemonicWidth = 0.0; + double mnemonicWidth = 0.0; + double mnemonicHeight = 0.0; + if (containsMnemonic) { + final Font font = text.getFont(); + String preSt = bindings.getText(); + preMnemonicWidth = Utils.computeTextWidth(font, preSt.substring(0, bindings.getMnemonicIndex()), 0); + mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(bindings.getMnemonicIndex(), bindings.getMnemonicIndex() + 1), 0); + mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType()); + } + + + // Now to position the graphic and text. At this point I know the + // contentX and contentY locations (including the padding and whatnot + // that was defined on the Labeled). I also know the content width and + // height. So now I just need to lay out the graphic and text within + // that content x/y/w/h area. + if ((!ignoreGraphic || !ignoreText) && !text.isManaged()) { + text.setManaged(true); + } + + if (ignoreGraphic && ignoreText) { + // There might be a text node as a child, or a graphic node as + // a child. However we don't have to do anything for the graphic + // node because the only way it can be a child and still have + // ignoreGraphic true is if it is unmanaged. Text however might + // be a child but still not matter, in which case we will just + // stop managing it (although really I wish it just wasn't here + // all all in that case) + if (text.isManaged()) { + text.setManaged(false); + } + text.relocate(snapPosition(contentX), snapPosition(contentY)); + } else if (ignoreGraphic) { + // Since I only have to position the text, it goes at the + // contentX/contentY location. Note that positionNode will + // adjust the text based on the text's minX/minY so no need to + // worry about that here + text.relocate(snapPosition(contentX), snapPosition(contentY)); + if (containsMnemonic) { + mnemonic_underscore.setEndX(mnemonicWidth-2.0); + mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1); + } + + } else if (ignoreText) { + // there isn't text to display, so we need to position it + // such that it doesn't affect the content area (although when + // there is a graphic, the text isn't even in the scene) + text.relocate(snapPosition(contentX), snapPosition(contentY)); + graphic.relocate(snapPosition(contentX), snapPosition(contentY)); + if (containsMnemonic) { + mnemonic_underscore.setEndX(mnemonicWidth); + mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); + mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1); + + } + } else { + // There is both text and a graphic, so I need to position them + // relative to each other + double graphicX = 0; + double graphicY = 0; + double textX = 0; + double textY = 0; + + if (contentDisplay == ContentDisplay.TOP) { + graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); + textX = contentX + ((contentWidth - textWidth) / 2.0); + // The graphic is above the text, so it is positioned at + // graphicY and the text below it. + graphicY = contentY; + textY = graphicY + graphicHeight + gap; + } else if (contentDisplay == ContentDisplay.RIGHT) { + // The graphic is to the right of the text + textX = contentX; + graphicX = textX + textWidth + gap; + graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); + textY = contentY + ((contentHeight - textHeight) / 2.0); + } else if (contentDisplay == ContentDisplay.BOTTOM) { + graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); + textX = contentX + ((contentWidth - textWidth) / 2.0); + // The graphic is below the text + textY = contentY; + graphicY = textY + textHeight + gap; + } else if (contentDisplay == ContentDisplay.LEFT) { + // The graphic is to the left of the text, so the graphicX is + // simply the contentX and the textX is to the right of it. + graphicX = contentX; + textX = graphicX + graphicWidth + gap; + graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); + textY = contentY + ((contentHeight - textHeight) / 2.0); + } else if (contentDisplay == ContentDisplay.CENTER) { + graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); + textX = contentX + ((contentWidth - textWidth) / 2.0); + graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); + textY = contentY + ((contentHeight - textHeight) / 2.0); + } + text.relocate(snapPosition(textX), snapPosition(textY)); + if (containsMnemonic) { + mnemonic_underscore.setEndX(mnemonicWidth); + mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); + mnemonic_underscore.relocate(snapPosition(textX+preMnemonicWidth), snapPosition(textY+mnemonicHeight-1)); + } + graphic.relocate(snapPosition(graphicX), snapPosition(graphicY)); + } + + /** + * check if the label text overflows it's bounds. + * If there's an overflow, and no text clip then + * we'll clip it. + * If there is no overflow, and the label text has a + * clip, then remove it. + */ + if ((text != null) && + ((text.getLayoutBounds().getHeight() > wrapHeight) || + (text.getLayoutBounds().getWidth() > wrapWidth))) { + + if (textClip == null) { + textClip = new Rectangle(); + } + + if (labeled.getEffectiveNodeOrientation() == NodeOrientation.LEFT_TO_RIGHT) { + textClip.setX(text.getLayoutBounds().getMinX()); + } else { + textClip.setX(text.getLayoutBounds().getMaxX() - wrapWidth); + } + textClip.setY(text.getLayoutBounds().getMinY()); + textClip.setWidth(wrapWidth); + textClip.setHeight(wrapHeight); + if (text.getClip() == null) { + text.setClip(textClip); + } + } + else { + /** + * content fits inside bounds, no need + * for a clip + */ + if (text.getClip() != null) { + text.setClip(null); + } + } + } + + /** {@inheritDoc} */ + @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { + switch (attribute) { + case TEXT: { + Labeled labeled = getSkinnable(); + String accText = labeled.getAccessibleText(); + if (accText != null && !accText.isEmpty()) return accText; + + /* Use the text in the binding if available to handle mnemonics */ + if (bindings != null) { + String text = bindings.getText(); + if (text != null && !text.isEmpty()) return text; + } + /* Avoid the content in text.getText() as it can contain ellipses + * for clipping + */ + if (labeled != null) { + String text = labeled.getText(); + if (text != null && !text.isEmpty()) return text; + } + /* Use the graphic as last resource. Note that this implementation + * does not attempt to combine the label and graphics if both + * are being displayed + */ + if (graphic != null) { + Object result = graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT); + if (result != null) return result; + } + return null; + } + case MNEMONIC: { + if (bindings != null) { + return bindings.getMnemonic(); + } + return null; + } + default: return super.queryAccessibleAttribute(attribute, parameters); + } + } + + + + /*************************************************************************** + * * + * Private implementation * + * * + **************************************************************************/ + + private double computeMinLabeledPartWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + // First compute the minTextWidth by checking the width of the string + // made by the ellipsis "...", and then by checking the width of the + // string made up by labeled.text. We want the smaller of the two. + final Labeled labeled = getSkinnable(); + final ContentDisplay contentDisplay = labeled.getContentDisplay(); + final double gap = labeled.getGraphicTextGap(); + double minTextWidth = 0; + + final Font font = text.getFont(); + OverrunStyle truncationStyle = labeled.getTextOverrun(); + String ellipsisString = labeled.getEllipsisString(); + final String string = labeled.getText(); + final boolean emptyText = string == null || string.isEmpty(); + + if (!emptyText) { + // We only want to recompute the full text width if the font or text changed + if (truncationStyle == CLIP) { + if (textWidth == Double.NEGATIVE_INFINITY) { + // Show at minimum the first character + textWidth = Utils.computeTextWidth(font, string.substring(0, 1), 0); + } + minTextWidth = textWidth; + } else { + if (textWidth == Double.NEGATIVE_INFINITY) { + textWidth = Utils.computeTextWidth(font, string, 0); + } + // We only want to recompute the ellipsis width if the font has changed + if (ellipsisWidth == Double.NEGATIVE_INFINITY) { + ellipsisWidth = Utils.computeTextWidth(font, ellipsisString, 0); + } + minTextWidth = Math.min(textWidth, ellipsisWidth); + } + } + + // Now inspect the graphic and the hpos to determine the the minWidth + final Node graphic = labeled.getGraphic(); + double width; + if (isIgnoreGraphic()) { + width = minTextWidth; + } else if (isIgnoreText()) { + width = graphic.minWidth(-1); + } else if (contentDisplay == LEFT || contentDisplay == RIGHT){ + width = (minTextWidth + graphic.minWidth(-1) + gap); + } else { + width = Math.max(minTextWidth, graphic.minWidth(-1)); + } + + return width + leftInset + leftLabelPadding() + + rightInset + rightLabelPadding(); + } + + private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + final Labeled labeled = getSkinnable(); + final Font font = text.getFont(); + + String str = labeled.getText(); + if (str != null && str.length() > 0) { + int newlineIndex = str.indexOf('\n'); + if (newlineIndex >= 0) { + str = str.substring(0, newlineIndex); + } + } + + // TODO figure out how to cache this effectively. + // Base minimum height on one line (ignoring wrapping here). + double s = labeled.getLineSpacing(); + final double textHeight = Utils.computeTextHeight(font, str, 0, s, text.getBoundsType()); + + double h = textHeight; + + // Now we want to add on the graphic if necessary! + if (!isIgnoreGraphic()) { + final Node graphic = labeled.getGraphic(); + if (labeled.getContentDisplay() == ContentDisplay.TOP + || labeled.getContentDisplay() == ContentDisplay.BOTTOM) { + h = graphic.minHeight(width) + labeled.getGraphicTextGap() + textHeight; + } else { + h = Math.max(textHeight, graphic.minHeight(width)); + } + } + + return topInset + h + bottomInset + topLabelPadding() - bottomLabelPadding(); + } + + double topLabelPadding() { + return snapSize(getSkinnable().getLabelPadding().getTop()); + } + + double bottomLabelPadding() { + return snapSize(getSkinnable().getLabelPadding().getBottom()); + } + + double leftLabelPadding() { + return snapSize(getSkinnable().getLabelPadding().getLeft()); + } + + double rightLabelPadding() { + return snapSize(getSkinnable().getLabelPadding().getRight()); + } + + + /** + * Called whenever some state has changed that affects the text metrics. + * Changes here will involve invalidating the display text so the next + * call to updateDisplayedText computes a new value, and call requestLayout. + */ + private void textMetricsChanged() { + invalidText = true; + getSkinnable().requestLayout(); + } + + /* + ** The Label is a mnemonic, and it's target node + ** has changed, but it's label hasn't so just + ** swap them over, and tidy up. + */ + void mnemonicTargetChanged() { + if (containsMnemonic == true) { + /* + ** was there previously a labelFor + */ + removeMnemonic(); + + /* + ** is there a new labelFor + */ + Control control = getSkinnable(); + if (control instanceof Label) { + labeledNode = ((Label)control).getLabelFor(); + addMnemonic(); + } + else { + labeledNode = null; + } + } + } + + private void sceneChanged() { + final Labeled labeled = getSkinnable(); + Scene scene = labeled.getScene(); + + if (scene != null && containsMnemonic) { + addMnemonic(); + } + + } + + /** + * Marks minWidth as being invalid and in need of recomputation. + */ + private void invalidateWidths() { + textWidth = Double.NEGATIVE_INFINITY; + } + + /** + * Updates the content of the underlying Text node. This method should + * only be called when necessary. If the invalidText flag is not set, then + * the method is a no-op. This care is taken because recomputing the + * text to display is an expensive operation. Package private ONLY FOR THE + * SAKE OF TESTING. + */ + void updateDisplayedText() { + updateDisplayedText(-1, -1); + } + + private void updateDisplayedText(double w, double h) { + if (invalidText) { + final Labeled labeled = getSkinnable(); + String s = labeled.getText(); + + int mnemonicIndex = -1; + + /* + ** if there's a valid string then parse it + */ + if (s != null && s.length() > 0) { + bindings = new TextBinding(s); + + if (!com.sun.javafx.PlatformUtil.isMac() && getSkinnable().isMnemonicParsing() == true) { + /* + ** the Labeled has a MnemonicParsing property, + ** if set true, then auto-parsing will check for + ** a mnemonic + */ + if (labeled instanceof Label) { + // buttons etc + labeledNode = ((Label)labeled).getLabelFor(); + } else { + labeledNode = labeled; + } + + if (labeledNode == null) { + labeledNode = labeled; + } + mnemonicIndex = bindings.getMnemonicIndex() ; + } + } + + /* + ** we were previously a mnemonic + */ + if (containsMnemonic) { + /* + ** are we no longer a mnemonic, or have we changed code? + */ + if (mnemonicScene != null) { + if (mnemonicIndex == -1 || + (bindings != null && !bindings.getMnemonicKeyCombination().equals(mnemonicCode))) { + removeMnemonic(); + } + containsMnemonic = false; + } + } + else { + /* + ** this can happen if mnemonic parsing is + ** disabled on a previously valid mnemonic + */ + removeMnemonic(); + } + + /* + ** check we have a labeled + */ + if (s != null && s.length() > 0) { + if (mnemonicIndex >= 0 && containsMnemonic == false) { + containsMnemonic = true; + mnemonicCode = bindings.getMnemonicKeyCombination(); + addMnemonic(); + } + } + + if (containsMnemonic == true) { + s = bindings.getText(); + if (mnemonic_underscore == null) { + mnemonic_underscore = new Line(); + mnemonic_underscore.setStartX(0.0f); + mnemonic_underscore.setStartY(0.0f); + mnemonic_underscore.setEndY(0.0f); + mnemonic_underscore.getStyleClass().clear(); + mnemonic_underscore.getStyleClass().setAll("mnemonic-underline"); + } + if (!getChildren().contains(mnemonic_underscore)) { + getChildren().add(mnemonic_underscore); + } + } else { + /* + ** we don't need a mnemonic.... + */ + if (getSkinnable().isMnemonicParsing() == true && com.sun.javafx.PlatformUtil.isMac() && bindings != null) { + s = bindings.getText(); + } + else { + s = labeled.getText(); + } + if (mnemonic_underscore != null) { + if (getChildren().contains(mnemonic_underscore)) { + Platform.runLater(() -> { + getChildren().remove(mnemonic_underscore); + mnemonic_underscore = null; + }); + } + } + } + + int len = s != null ? s.length() : 0; + boolean multiline = false; + + if (s != null && len > 0) { + int i = s.indexOf('\n'); + if (i > -1 && i < len - 1) { + // Multiline text with embedded newlines - not + // taking into account a potential trailing newline. + multiline = true; + } + } + + String result; + boolean horizontalPosition = + (labeled.getContentDisplay() == ContentDisplay.LEFT || + labeled.getContentDisplay() == ContentDisplay.RIGHT); + + double availableWidth = labeled.getWidth() - snappedLeftInset() - leftLabelPadding() - + snappedRightInset() - rightLabelPadding(); + availableWidth = Math.max(availableWidth, 0); + + if (w == -1) { + w = availableWidth; + } + double minW = Math.min(computeMinLabeledPartWidth(-1, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableWidth); + if (horizontalPosition && !isIgnoreGraphic()) { + double graphicW = (labeled.getGraphic().getLayoutBounds().getWidth() + labeled.getGraphicTextGap()); + w -= graphicW; + minW -= graphicW; + } + wrapWidth = Math.max(minW, w); + + boolean verticalPosition = + (labeled.getContentDisplay() == ContentDisplay.TOP || + labeled.getContentDisplay() == ContentDisplay.BOTTOM); + + double availableHeight = labeled.getHeight() - snappedTopInset() - topLabelPadding() - + snappedBottomInset() - bottomLabelPadding(); + availableHeight = Math.max(availableHeight, 0); + + if (h == -1) { + h = availableHeight; + } + double minH = Math.min(computeMinLabeledPartHeight(wrapWidth, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableHeight); + if (verticalPosition && labeled.getGraphic() != null) { + double graphicH = labeled.getGraphic().getLayoutBounds().getHeight() + labeled.getGraphicTextGap(); + h -= graphicH; + minH -= graphicH; + } + wrapHeight = Math.max(minH, h); + + updateWrappingWidth(); + + Font font = text.getFont(); + OverrunStyle truncationStyle = labeled.getTextOverrun(); + String ellipsisString = labeled.getEllipsisString(); + + if (labeled.isWrapText()) { + result = Utils.computeClippedWrappedText(font, s, wrapWidth, wrapHeight, truncationStyle, ellipsisString, text.getBoundsType()); + } else if (multiline) { + StringBuilder sb = new StringBuilder(); + + String[] splits = s.split("\n"); + for (int i = 0; i < splits.length; i++) { + sb.append(Utils.computeClippedText(font, splits[i], wrapWidth, truncationStyle, ellipsisString)); + if (i < splits.length - 1) { + sb.append('\n'); + } + } + + // TODO: Consider what to do in the case where vertical space is + // limited and the last visible line isn't already truncated + // with a trailing ellipsis. What if the style calls for leading + // or center ellipses? We could possibly add an additional + // trailing ellipsis to the last visible line, like this: + + // +--------------------------------+ + // | This is some long text with multiple lines\n + // | where more than one exceed the|width\n + // | and wrapText is false, and all|lines\n + // +--don't fit.--------------------+ + // + // +--------------------------------+ + // | This is some...multiple lines | + // | where more t...ceed the width | + // | and wrapText...d all lines... | + // +--------------------------------+ + + result = sb.toString(); + } else { + result = Utils.computeClippedText(font, s, wrapWidth, truncationStyle, ellipsisString); + } + + if (result != null && result.endsWith("\n")) { + // Strip ending newline so we don't display another row. + result = result.substring(0, result.length() - 1); + } + + text.setText(result); + updateWrappingWidth(); + invalidText = false; + } + } + + private void addMnemonic() { + if (labeledNode != null) { + mnemonicScene = labeledNode.getScene(); + if (mnemonicScene != null) { + mnemonicScene.addMnemonic(new Mnemonic(labeledNode, mnemonicCode)); + } + } + } + + + private void removeMnemonic() { + if (mnemonicScene != null && labeledNode != null) { + mnemonicScene.removeMnemonic(new Mnemonic(labeledNode, mnemonicCode)); + mnemonicScene = null; + } + } + + /** + * Updates the wrapping width of the text node. Although changing the font + * does affect the metrics used for text layout, this method does not + * call requestLayout or invalidate the text, since it may be called + * from the constructor and such work would be duplicative and wasted. + */ + private void updateWrappingWidth() { + final Labeled labeled = getSkinnable(); + text.setWrappingWidth(0); + if (labeled.isWrapText()) { + // Note that the wrapping width needs to be set to zero before + // getting the text's real preferred width. + double w = Math.min(text.prefWidth(-1), wrapWidth); + text.setWrappingWidth(w); + } + } + + /** + * Gets whether for various computations we can ignore the presence of the graphic + * (or lack thereof). + * @return + */ + boolean isIgnoreGraphic() { + return (graphic == null || + !graphic.isManaged() || + getSkinnable().getContentDisplay() == ContentDisplay.TEXT_ONLY); + } + + /** + * Gets whether for various computations we can ignore the presence of the text. + * @return + */ + boolean isIgnoreText() { + final Labeled labeled = getSkinnable(); + final String txt = labeled.getText(); + return (txt == null || + txt.equals("") || + labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY); + } +}