javafx.scene.control.skin.LabeledSkinBase Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2024, 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 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;
import java.util.concurrent.atomic.AtomicBoolean;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.geometry.HPos;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
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.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 com.sun.javafx.scene.control.LabeledHelper;
import com.sun.javafx.scene.control.LabeledText;
import com.sun.javafx.scene.control.behavior.MnemonicInfo;
import com.sun.javafx.scene.control.skin.Utils;
/**
* Default skin implementation for controls extends {@link Labeled}.
*
* @param the type of the labeled control
* @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 MnemonicInfo mnemonicInfo;
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;
private final AtomicBoolean textTruncated = new AtomicBoolean();
/* *************************************************************************
* *
* 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 *
* *
**************************************************************************/
@Override
public void dispose() {
if (graphic != null) {
graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener);
graphic = null;
}
super.dispose();
}
/**
* 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);
}
}
}
/**
* 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();
boolean isIgnoreText = isIgnoreText();
double widthPadding = leftInset + rightInset;
double txWidth;
if (isIgnoreText) {
txWidth = 0.0;
} else {
widthPadding += (leftLabelPadding() + rightLabelPadding());
String cleanText = getCleanText();
boolean emptyText = cleanText == null || cleanText.isEmpty();
txWidth = emptyText ? 0.0 : Utils.computeTextWidth(text.getFont(), cleanText, 0);
}
double width;
if (isIgnoreGraphic()) {
width = txWidth;
} else {
// Fix for RT-39889
double graphicWidth = graphic == null ? 0.0 :
Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1));
if (isIgnoreText) {
width = graphicWidth;
} else {
ContentDisplay contentDisplay = labeled.getContentDisplay();
if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) {
width = txWidth + labeled.getGraphicTextGap() + graphicWidth;
} else {
width = Math.max(txWidth, graphicWidth);
}
}
}
return width + 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();
boolean isIgnoreText = isIgnoreText();
boolean isIgnoreGraphic = isIgnoreGraphic();
width -= leftInset + rightInset;
double padding = topInset + bottomInset;
if (!isIgnoreText) {
width -= leftLabelPadding() + rightLabelPadding();
padding += topLabelPadding() + bottomLabelPadding();
}
String cleanText = getCleanText();
if (cleanText != null && cleanText.endsWith("\n")) {
// Strip ending newline so we don't count another row.
cleanText = cleanText.substring(0, cleanText.length() - 1);
}
double txWidth = width;
if (!isIgnoreGraphic &&
(contentDisplay == LEFT || contentDisplay == RIGHT)) {
txWidth -= (graphic.prefWidth(-1) + gap);
}
// TODO figure out how to cache this effectively.
final double textHeight = Utils.computeTextHeight(font, cleanText,
labeled.isWrapText() ? txWidth : 0,
labeled.getLineSpacing(), text.getBoundsType());
double height;
if (isIgnoreGraphic) {
height = textHeight;
} else {
// Calculate the graphic height and use based on contentDisplay value
double graphicHeight = graphic == null ? 0.0 :
Utils.boundedSize(graphic.prefHeight(width), graphic.minHeight(width), graphic.maxHeight(width));
// Add the graphic, gap, and padding as appropriate
if (isIgnoreText) {
height = graphicHeight;
} else if (contentDisplay == TOP || contentDisplay == BOTTOM) {
height = graphicHeight + gap + textHeight;
} else {
height = Math.max(textHeight, graphicHeight);
}
}
return height + padding;
}
/** {@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()) {
double graphicHeight = g.prefHeight(-1);
double textHeight = text.prefHeight(-1);
ContentDisplay contentDisplay = labeled.getContentDisplay();
if (contentDisplay == ContentDisplay.TOP) {
h = graphicHeight + labeled.getGraphicTextGap() + textBaselineOffset;
} else if ((contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT)
&& (graphicHeight > textHeight)) {
h = textBaselineOffset + (graphicHeight - textHeight) / 2;
}
}
double offset = topInset + h;
if (!isIgnoreText()) {
offset += topLabelPadding();
}
return offset;
}
/**
* 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();
if (!ignoreText) {
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 = snapSizeX(Math.min(text.getLayoutBounds().getWidth(), wrapWidth));
textHeight = snapSizeY(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));
}
Point2D mnemonicPos = null;
double mnemonicWidth = 0.0;
double mnemonicHeight = 0.0;
if (containsMnemonic) {
final Font font = text.getFont();
String preSt = mnemonicInfo.getText();
boolean isRTL = (labeledNode.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT);
mnemonicPos = Utils.computeMnemonicPosition(font, preSt, mnemonicInfo.getMnemonicIndex(), this.wrapWidth, labeled.getLineSpacing(), isRTL);
mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(mnemonicInfo.getMnemonicIndex(), mnemonicInfo.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(snapPositionX(contentX), snapPositionY(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(snapPositionX(contentX), snapPositionY(contentY));
if (containsMnemonic && (mnemonicPos != null)) {
mnemonic_underscore.setEndX(mnemonicWidth-2.0);
mnemonic_underscore.relocate(snapPositionX(contentX + mnemonicPos.getX()),
snapPositionY(contentY + mnemonicPos.getY()));
}
} 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(snapPositionX(contentX), snapPositionY(contentY));
graphic.relocate(snapPositionX(contentX), snapPositionY(contentY));
if (containsMnemonic && (mnemonicPos != null)) {
mnemonic_underscore.setEndX(mnemonicWidth);
mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0);
mnemonic_underscore.relocate(snapPositionX(contentX + mnemonicPos.getX()),
snapPositionY(contentY + mnemonicPos.getY()));
}
} 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(snapPositionX(textX), snapPositionY(textY));
if (containsMnemonic && (mnemonicPos != null)) {
mnemonic_underscore.setEndX(mnemonicWidth);
mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0);
mnemonic_underscore.relocate(snapPositionX(textX + mnemonicPos.getX()),
snapPositionY(textY + mnemonicPos.getY()));
}
graphic.relocate(snapPositionX(graphicX), snapPositionY(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 clean text, in which mnemonic symbols have been removed. */
String cleanText = getCleanText();
if (cleanText != null && !cleanText.isEmpty()) {
return cleanText;
}
/* 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 (mnemonicInfo != null) {
return mnemonicInfo.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 cleanText = getCleanText();
final boolean emptyText = cleanText == null || cleanText.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, cleanText.substring(0, 1), 0);
}
minTextWidth = textWidth;
} else {
if (textWidth == Double.NEGATIVE_INFINITY) {
textWidth = Utils.computeTextWidth(font, cleanText, 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));
}
double padding = leftInset + rightInset;
if (!isIgnoreText()) {
padding += leftLabelPadding() + rightLabelPadding();
}
return width + padding;
}
private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
final Labeled labeled = getSkinnable();
final Font font = text.getFont();
String cleanText = getCleanText();
if (cleanText != null && cleanText.length() > 0) {
int newlineIndex = cleanText.indexOf('\n');
if (newlineIndex >= 0) {
cleanText = cleanText.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, cleanText, 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));
}
}
double padding = topInset + bottomInset;
if (!isIgnoreText()) {
padding += topLabelPadding() - bottomLabelPadding();
}
return h + padding;
}
double topLabelPadding() {
return snapSizeY(getSkinnable().getLabelPadding().getTop());
}
double bottomLabelPadding() {
return snapSizeY(getSkinnable().getLabelPadding().getBottom());
}
double leftLabelPadding() {
return snapSizeX(getSkinnable().getLabelPadding().getLeft());
}
double rightLabelPadding() {
return snapSizeX(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) {
/*
** 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) {
textTruncated.set(false);
if (invalidText) {
final Labeled labeled = getSkinnable();
String cleanText = getCleanText();
int mnemonicIndex = -1;
if (cleanText != null && cleanText.length() > 0
&& mnemonicInfo != null
&& !com.sun.javafx.PlatformUtil.isMac()
&& getSkinnable().isMnemonicParsing()) {
/*
** 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 = mnemonicInfo.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 ||
(mnemonicInfo != null && !mnemonicInfo.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 (cleanText != null && cleanText.length() > 0
&& mnemonicIndex >= 0 && !containsMnemonic) {
containsMnemonic = true;
mnemonicCode = mnemonicInfo.getMnemonicKeyCombination();
addMnemonic();
}
if (containsMnemonic) {
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 if (mnemonic_underscore != null && getChildren().contains(mnemonic_underscore)) {
Platform.runLater(() -> {
getChildren().remove(mnemonic_underscore);
mnemonic_underscore = null;
});
}
int len = cleanText != null ? cleanText.length() : 0;
boolean multiline = false;
if (cleanText != null && len > 0) {
int i = cleanText.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() - snappedRightInset();
if (!isIgnoreText()) {
availableWidth -= leftLabelPadding() + 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() - snappedBottomInset();
if (!isIgnoreText()) {
availableHeight -= topLabelPadding() + 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,
cleanText,
wrapWidth,
wrapHeight,
labeled.getLineSpacing(),
truncationStyle,
ellipsisString,
textTruncated,
text.getBoundsType()
);
} else if (multiline) {
StringBuilder sb = new StringBuilder();
String[] splits = cleanText.split("\n");
for (int i = 0; i < splits.length; i++) {
sb.append(Utils.computeClippedText(
font,
splits[i],
wrapWidth,
truncationStyle,
ellipsisString,
textTruncated
));
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, cleanText, wrapWidth, truncationStyle, ellipsisString, textTruncated);
}
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();
LabeledHelper.setTextTruncated(getSkinnable(), textTruncated.get());
invalidText = false;
}
}
/**
* Gets the clean text, which is the source text after mnemonic symbols have been removed.
*/
private String getCleanText() {
Labeled labeled = getSkinnable();
String sourceText = labeled.getText();
if (sourceText != null && labeled.isMnemonicParsing()) {
if (mnemonicInfo == null) {
mnemonicInfo = new MnemonicInfo(sourceText);
} else {
mnemonicInfo.update(sourceText);
}
return mnemonicInfo.getText();
}
return sourceText;
}
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 = getCleanText();
return (txt == null ||
txt.equals("") ||
labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY);
}
}