1 /* 2 * Copyright (c) 2010, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control.skin; 27 28 import javafx.geometry.Point2D; 29 import com.sun.javafx.scene.control.LabeledText; 30 import com.sun.javafx.scene.control.behavior.TextBinding; 31 import com.sun.javafx.scene.control.skin.Utils; 32 import javafx.application.Platform; 33 import javafx.beans.InvalidationListener; 34 import javafx.geometry.HPos; 35 import javafx.geometry.NodeOrientation; 36 import javafx.geometry.Orientation; 37 import javafx.geometry.Pos; 38 import javafx.geometry.VPos; 39 import javafx.scene.AccessibleAttribute; 40 import javafx.scene.Node; 41 import javafx.scene.Scene; 42 import javafx.scene.control.Accordion; 43 import javafx.scene.control.ContentDisplay; 44 import javafx.scene.control.Control; 45 import javafx.scene.control.Label; 46 import javafx.scene.control.Labeled; 47 import javafx.scene.control.OverrunStyle; 48 import javafx.scene.control.SkinBase; 49 import javafx.scene.image.ImageView; 50 import javafx.scene.input.KeyCombination; 51 import javafx.scene.input.Mnemonic; 52 import javafx.scene.shape.Line; 53 import javafx.scene.shape.Rectangle; 54 import javafx.scene.text.Font; 55 56 import static javafx.scene.control.ContentDisplay.BOTTOM; 57 import static javafx.scene.control.ContentDisplay.LEFT; 58 import static javafx.scene.control.ContentDisplay.RIGHT; 59 import static javafx.scene.control.ContentDisplay.TOP; 60 import static javafx.scene.control.OverrunStyle.CLIP; 61 62 /** 63 * Default skin implementation for controls extends {@link Labeled}. 64 * 65 * @see Labeled 66 * @since 9 67 */ 68 public abstract class LabeledSkinBase<C extends Labeled> extends SkinBase<C> { 69 70 /*************************************************************************** 71 * * 72 * Private fields * 73 * * 74 **************************************************************************/ 75 76 /** 77 * The Text node used to display the text. This is package only 78 * for the sake of testing! 79 */ 80 LabeledText text; 81 82 /** 83 * Indicates that the text content is invalid and needs to be updated. 84 * This is package private only for the sake of testing. 85 */ 86 boolean invalidText = true; 87 88 /** 89 * A reference to the last-known graphic on the Labeled. This reference 90 * is kept so that we can remove listeners from the old graphic later 91 */ 92 Node graphic; 93 94 /** 95 * The cached full width of the non-truncated text. We only want to 96 * recompute this if the text has itself changed, or if the font has changed. 97 * This is package private ONLY FOR THE SAKE OF TESTING 98 */ 99 double textWidth = Double.NEGATIVE_INFINITY; 100 101 /** 102 * The cached width of the ellipsis string. This will be recomputed 103 * if the font or the ellipsisString property have changed. 104 * This is package private ONLY FOR THE SAKE OF TESTING 105 */ 106 double ellipsisWidth = Double.NEGATIVE_INFINITY; 107 108 /** 109 * A listener which is applied to the graphic whenever the graphic is set 110 * and is visible within the labeled. For example, if there is a graphic 111 * defined on the Labeled but the ContentDisplay is set to TEXT_ONLY, then 112 * we will not bother installing this listener on the graphic. In all 113 * other cases, if the graphic is defined, it will have this listener 114 * added to it, which ensures that if the graphic's layout bounds change, 115 * we end up performing a layout and potentially update the visible text. 116 * 117 * This is package private ONLY FOR THE SAKE OF TESTING 118 */ 119 final InvalidationListener graphicPropertyChangedListener = valueModel -> { 120 invalidText = true; 121 if (getSkinnable() != null) getSkinnable().requestLayout(); 122 }; 123 124 private Rectangle textClip; 125 private double wrapWidth; 126 private double wrapHeight; 127 128 private TextBinding bindings; 129 private Line mnemonic_underscore; 130 131 private boolean containsMnemonic = false; 132 private Scene mnemonicScene = null; 133 private KeyCombination mnemonicCode; 134 // needs to be an object, as MenuItem isn't a node 135 private Node labeledNode = null; 136 137 138 139 /*************************************************************************** 140 * * 141 * Constructors * 142 * * 143 **************************************************************************/ 144 145 /** 146 * Constructor for LabeledSkinBase. The Labeled must be specified, and cannot be null. 147 * At the conclusion of the constructor call, the skin will be marked as 148 * needsLayout, and will be fully configured based on the current state of 149 * the labeled. Any subsequent changes to the Labeled will be handled via 150 * listeners and applied appropriately. 151 * 152 * @param labeled The labeled that this skin should be installed onto. 153 */ 154 public LabeledSkinBase(final C labeled) { 155 super(labeled); 156 157 // Configure the Text node with all of the attributes from the 158 // Labeled which apply to it. 159 text = new LabeledText(labeled); 160 161 updateChildren(); 162 163 // Labels do not block the mouse by default, unlike most other UI Controls. 164 //consumeMouseEvents(false); 165 166 // Register listeners 167 /* 168 * There are basically 2 things to worry about in each of these handlers 169 * 1) Update the Text node 170 * 2) Have the text metrics changed? 171 * 172 * If the metrics have changed, we need to request a layout and invalidate 173 * the text so that we recompute the display text on next read. 174 */ 175 registerChangeListener(labeled.ellipsisStringProperty(), o -> { 176 textMetricsChanged(); 177 invalidateWidths(); 178 ellipsisWidth = Double.NEGATIVE_INFINITY; 179 }); 180 registerChangeListener(labeled.widthProperty(), o -> { 181 updateWrappingWidth(); 182 invalidText = true; 183 // No requestLayout() because Control will force a layout 184 }); 185 registerChangeListener(labeled.heightProperty(), o -> { 186 invalidText = true; 187 // No requestLayout() because Control will force a layout 188 }); 189 registerChangeListener(labeled.fontProperty(), o -> { 190 textMetricsChanged(); 191 invalidateWidths(); 192 ellipsisWidth = Double.NEGATIVE_INFINITY; 193 }); 194 registerChangeListener(labeled.graphicProperty(), o -> { 195 updateChildren(); 196 textMetricsChanged(); 197 }); 198 registerChangeListener(labeled.contentDisplayProperty(), o -> { 199 updateChildren(); 200 textMetricsChanged(); 201 }); 202 registerChangeListener(labeled.labelPaddingProperty(), o -> textMetricsChanged()); 203 registerChangeListener(labeled.graphicTextGapProperty(), o -> textMetricsChanged()); 204 registerChangeListener(labeled.alignmentProperty(), o -> { 205 // Doesn't involve text metrics because if the text is too long, then 206 // it will already have fit all available width and a change to hpos 207 // has no effect. Or it is too short (i.e. it all fits) and we don't 208 // have to worry about truncation. So just call request layout. 209 // Doesn't involve text metrics because if the text is too long, then 210 // it will already have fit all available height and a change to vpos 211 // has no effect. Or it is too short (i.e. it all fits) and we don't 212 // have to worry about truncation. So just call request layout. 213 getSkinnable().requestLayout(); 214 }); 215 registerChangeListener(labeled.mnemonicParsingProperty(), o -> { 216 containsMnemonic = false; 217 textMetricsChanged(); 218 }); 219 registerChangeListener(labeled.textProperty(), o -> { 220 updateChildren(); 221 textMetricsChanged(); 222 invalidateWidths(); 223 }); 224 registerChangeListener(labeled.textAlignmentProperty(), o -> { /* NO-OP */ }); 225 registerChangeListener(labeled.textOverrunProperty(), o -> textMetricsChanged()); 226 registerChangeListener(labeled.wrapTextProperty(), o -> { 227 updateWrappingWidth(); 228 textMetricsChanged(); 229 }); 230 registerChangeListener(labeled.underlineProperty(), o -> textMetricsChanged()); 231 registerChangeListener(labeled.lineSpacingProperty(), o -> textMetricsChanged()); 232 registerChangeListener(labeled.sceneProperty(), o -> sceneChanged()); 233 } 234 235 236 237 /*************************************************************************** 238 * * 239 * Public API * 240 * * 241 **************************************************************************/ 242 243 /** 244 * Updates the children managed by LabeledSkinBase, which can be the Labeled 245 * graphic and/or a Text node. Only those nodes which actually must 246 * be used are used. For example, with a ContentDisplay of 247 * GRAPHIC_ONLY the text node is not added, and with a ContentDisplay 248 * of TEXT_ONLY, the graphic is not added. 249 */ 250 protected void updateChildren() { 251 final Labeled labeled = getSkinnable(); 252 // Only in some situations do we want to have the graphicPropertyChangedListener 253 // installed. Since updateChildren() is not called much, we'll just remove it always 254 // and reinstall it later if it is necessary to do so. 255 if (graphic != null) { 256 graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener); 257 } 258 // Now update the graphic (since it may have changed) 259 graphic = labeled.getGraphic(); 260 261 // RT-19851 Only setMouseTransparent(true) for an ImageView. This allows the button 262 // to be picked regardless of the changing images on top of it. 263 if (graphic instanceof ImageView) { 264 graphic.setMouseTransparent(true); 265 } 266 267 // Now update the children (and add the graphicPropertyChangedListener as necessary) 268 if (isIgnoreGraphic()) { 269 if (labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) { 270 getChildren().clear(); 271 } else { 272 getChildren().setAll(text); 273 } 274 } else { 275 graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener); 276 if (isIgnoreText()) { 277 getChildren().setAll(graphic); 278 } else { 279 getChildren().setAll(graphic, text); 280 } 281 } 282 } 283 284 /** 285 * Compute and return the minimum width of this Labeled. The minimum width is 286 * the smaller of the width of "..." and the width with the actual text. 287 * In this way, if the text width itself is smaller than the ellipsis then 288 * we should use that as the min width, otherwise the ellipsis needs to be the 289 * min width. 290 * <p> 291 * We use the same calculation here regardless of whether we are talking 292 * about a single or multiline labeled. So a multiline labeled may find that 293 * the width of the "..." is as small as it will ever get. 294 */ 295 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 296 return computeMinLabeledPartWidth(height, topInset, rightInset, bottomInset, leftInset); 297 } 298 299 /** {@inheritDoc} */ 300 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 301 return computeMinLabeledPartHeight(width, topInset, rightInset, bottomInset, leftInset); 302 } 303 304 /** {@inheritDoc} */ 305 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 306 // Get the preferred width of the text 307 final Labeled labeled = getSkinnable(); 308 final Font font = text.getFont(); 309 String string = labeled.getText(); 310 boolean emptyText = string == null || string.isEmpty(); 311 double widthPadding = leftInset + rightInset; 312 313 if (!isIgnoreText()) { 314 widthPadding += leftLabelPadding() + rightLabelPadding(); 315 } 316 317 double textWidth = 0.0; 318 if (!emptyText) { 319 if (labeled.isMnemonicParsing()) { 320 if (string.contains("_") && (string.indexOf("_") != string.length()-1)) { 321 string = string.replaceFirst("_", ""); 322 } 323 } 324 textWidth = Utils.computeTextWidth(font, string, 0); 325 } 326 327 // Fix for RT-39889 328 double graphicWidth = graphic == null ? 0.0 : 329 Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1)); 330 331 // Now add on the graphic, gap, and padding as appropriate 332 if (isIgnoreGraphic()) { 333 return textWidth + widthPadding; 334 } else if (isIgnoreText()) { 335 return graphicWidth + widthPadding; 336 } else if (labeled.getContentDisplay() == ContentDisplay.LEFT 337 || labeled.getContentDisplay() == ContentDisplay.RIGHT) { 338 return textWidth + labeled.getGraphicTextGap() + graphicWidth + widthPadding; 339 } else { 340 return Math.max(textWidth, graphicWidth) + widthPadding; 341 } 342 } 343 344 /** {@inheritDoc} */ 345 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 346 final Labeled labeled = getSkinnable(); 347 final Font font = text.getFont(); 348 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 349 final double gap = labeled.getGraphicTextGap(); 350 351 width -= leftInset + rightInset; 352 353 if (!isIgnoreText()) { 354 width -= leftLabelPadding() + rightLabelPadding(); 355 } 356 357 String str = labeled.getText(); 358 if (str != null && str.endsWith("\n")) { 359 // Strip ending newline so we don't count another row. 360 str = str.substring(0, str.length() - 1); 361 } 362 363 double textWidth = width; 364 if (!isIgnoreGraphic() && 365 (contentDisplay == LEFT || contentDisplay == RIGHT)) { 366 textWidth -= (graphic.prefWidth(-1) + gap); 367 } 368 369 // TODO figure out how to cache this effectively. 370 final double textHeight = Utils.computeTextHeight(font, str, 371 labeled.isWrapText() ? textWidth : 0, 372 labeled.getLineSpacing(), text.getBoundsType()); 373 374 // Now we want to add on the graphic if necessary! 375 double h = textHeight; 376 if (!isIgnoreGraphic()) { 377 final Node graphic = labeled.getGraphic(); 378 if (contentDisplay == TOP || contentDisplay == BOTTOM) { 379 h = graphic.prefHeight(width) + gap + textHeight; 380 } else { 381 h = Math.max(textHeight, graphic.prefHeight(width)); 382 } 383 } 384 385 double padding = topInset + bottomInset; 386 387 if (!isIgnoreText()) { 388 padding += topLabelPadding() + bottomLabelPadding(); 389 } 390 391 return h + padding; 392 } 393 394 /** {@inheritDoc} */ 395 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 396 return getSkinnable().prefWidth(height); 397 } 398 399 /** {@inheritDoc} */ 400 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 401 return getSkinnable().prefHeight(width); 402 } 403 404 /** {@inheritDoc} */ 405 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 406 double textBaselineOffset = text.getBaselineOffset(); 407 double h = textBaselineOffset; 408 final Labeled labeled = getSkinnable(); 409 final Node g = labeled.getGraphic(); 410 if (!isIgnoreGraphic()) { 411 ContentDisplay contentDisplay = labeled.getContentDisplay(); 412 if (contentDisplay == ContentDisplay.TOP) { 413 h = g.prefHeight(-1) + labeled.getGraphicTextGap() + textBaselineOffset; 414 } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT) { 415 h = textBaselineOffset + (g.prefHeight(-1) - text.prefHeight(-1)) / 2; 416 } 417 } 418 419 double offset = topInset + h; 420 if (!isIgnoreText()) { 421 offset += topLabelPadding(); 422 } 423 return offset; 424 } 425 426 /** 427 * The Layout algorithm works like this: 428 * 429 * - Get the labeled w/h, graphic w/h, text w/h 430 * - Compute content w/h based on graphicVPos, graphicHPos, 431 * graphicTextGap, and graphic w/h and text w/h 432 * - (Note that the text content has been pre-truncated where 433 * necessary) 434 * - compute content x/y based on content w/h and labeled w/h 435 * and the labeled's hpos and vpos 436 * - position the graphic and text 437 */ 438 @Override protected void layoutChildren(final double x, final double y, 439 final double w, final double h) { 440 layoutLabelInArea(x, y, w, h); 441 } 442 443 /** 444 * Performs the actual layout of the label content within the area given. 445 * This method is called by subclasses that override layoutChildren(). 446 * 447 * @param x The x position of the label part of the control, inside padding 448 * 449 * @param y The y position of the label part of the control, inside padding 450 * 451 * @param w The width of the label part of the control, not including padding 452 * 453 * @param h The height of the label part of the control, not including padding 454 */ 455 protected void layoutLabelInArea(double x, double y, double w, double h) { 456 layoutLabelInArea(x, y, w, h, null); 457 } 458 459 /** 460 * Performs the actual layout of the label content within the area given. 461 * This method is called by subclasses that override layoutChildren(). 462 * 463 * @param x The x position of the label part of the control, inside padding 464 * 465 * @param y The y position of the label part of the control, inside padding 466 * 467 * @param w The width of the label part of the control, not including padding 468 * 469 * @param h The height of the label part of the control, not including padding 470 * 471 * @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. 472 */ 473 protected void layoutLabelInArea(double x, double y, double w, double h, Pos alignment) { 474 // References to essential labeled state 475 final Labeled labeled = getSkinnable(); 476 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 477 478 if (alignment == null) { 479 alignment = labeled.getAlignment(); 480 } 481 482 final HPos hpos = alignment == null ? HPos.LEFT : alignment.getHpos(); 483 final VPos vpos = alignment == null ? VPos.CENTER : alignment.getVpos(); 484 485 // Figure out whether we should ignore the Graphic, and/or 486 // ignore the Text 487 final boolean ignoreGraphic = isIgnoreGraphic(); 488 final boolean ignoreText = isIgnoreText(); 489 490 if (!ignoreText) { 491 x += leftLabelPadding(); 492 y += topLabelPadding(); 493 w -= leftLabelPadding() + rightLabelPadding(); 494 h -= topLabelPadding() + bottomLabelPadding(); 495 } 496 497 // Compute some standard useful numbers for the graphic, text, and gap 498 double graphicWidth; 499 double graphicHeight; 500 double textWidth; 501 double textHeight; 502 503 if (ignoreGraphic) { 504 graphicWidth = graphicHeight = 0; 505 } else if (ignoreText) { 506 if (graphic.isResizable()) { 507 Orientation contentBias = graphic.getContentBias(); 508 if (contentBias == Orientation.HORIZONTAL) { 509 graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); 510 graphicHeight = Utils.boundedSize(h, graphic.minHeight(graphicWidth), graphic.maxHeight(graphicWidth)); 511 } else if (contentBias == Orientation.VERTICAL) { 512 graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); 513 graphicWidth = Utils.boundedSize(w, graphic.minWidth(graphicHeight), graphic.maxWidth(graphicHeight)); 514 } else { 515 graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); 516 graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); 517 } 518 graphic.resize(graphicWidth, graphicHeight); 519 } else { 520 graphicWidth = graphic.getLayoutBounds().getWidth(); 521 graphicHeight = graphic.getLayoutBounds().getHeight(); 522 } 523 } else { 524 graphic.autosize(); // We have to do this before getting metrics 525 graphicWidth = graphic.getLayoutBounds().getWidth(); 526 graphicHeight = graphic.getLayoutBounds().getHeight(); 527 } 528 529 if (ignoreText) { 530 textWidth = textHeight = 0; 531 text.setText(""); 532 } else { 533 updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed 534 textWidth = snapSizeX(Math.min(text.getLayoutBounds().getWidth(), wrapWidth)); 535 textHeight = snapSizeY(Math.min(text.getLayoutBounds().getHeight(), wrapHeight)); 536 } 537 538 final double gap = (ignoreText || ignoreGraphic) ? 0 : labeled.getGraphicTextGap(); 539 540 // Figure out the contentWidth and contentHeight. This is the width 541 // and height of the Labeled and Graphic together, not the available 542 // content area (which would be a different calculation). 543 double contentWidth = Math.max(graphicWidth, textWidth); 544 double contentHeight = Math.max(graphicHeight, textHeight); 545 if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) { 546 contentHeight = graphicHeight + gap + textHeight; 547 } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) { 548 contentWidth = graphicWidth + gap + textWidth; 549 } 550 551 // Now we want to compute the x/y location to place the content at. 552 553 // Compute the contentX position based on hpos and the space available 554 double contentX; 555 if (hpos == HPos.LEFT) { 556 contentX = x; 557 } else if (hpos == HPos.RIGHT) { 558 contentX = x + (w - contentWidth); 559 } else { 560 // TODO Baseline may not be handled correctly 561 // may have been CENTER or null, treat as center 562 contentX = (x + ((w - contentWidth) / 2.0)); 563 } 564 565 // Compute the contentY position based on vpos and the space available 566 double contentY; 567 if (vpos == VPos.TOP) { 568 contentY = y; 569 } else if (vpos == VPos.BOTTOM) { 570 contentY = (y + (h - contentHeight)); 571 } else { 572 // TODO Baseline may not be handled correctly 573 // may have been CENTER, BASELINE, or null, treat as center 574 contentY = (y + ((h - contentHeight) / 2.0)); 575 } 576 577 Point2D mnemonicPos = null; 578 double mnemonicWidth = 0.0; 579 double mnemonicHeight = 0.0; 580 if (containsMnemonic) { 581 final Font font = text.getFont(); 582 String preSt = bindings.getText(); 583 mnemonicPos = Utils.computeMnemonicPosition(font, preSt, bindings.getMnemonicIndex(), this.wrapWidth, labeled.getLineSpacing()); 584 mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(bindings.getMnemonicIndex(), bindings.getMnemonicIndex() + 1), 0); 585 mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType()); 586 } 587 588 589 // Now to position the graphic and text. At this point I know the 590 // contentX and contentY locations (including the padding and whatnot 591 // that was defined on the Labeled). I also know the content width and 592 // height. So now I just need to lay out the graphic and text within 593 // that content x/y/w/h area. 594 if ((!ignoreGraphic || !ignoreText) && !text.isManaged()) { 595 text.setManaged(true); 596 } 597 598 if (ignoreGraphic && ignoreText) { 599 // There might be a text node as a child, or a graphic node as 600 // a child. However we don't have to do anything for the graphic 601 // node because the only way it can be a child and still have 602 // ignoreGraphic true is if it is unmanaged. Text however might 603 // be a child but still not matter, in which case we will just 604 // stop managing it (although really I wish it just wasn't here 605 // all all in that case) 606 if (text.isManaged()) { 607 text.setManaged(false); 608 } 609 text.relocate(snapPositionX(contentX), snapPositionY(contentY)); 610 } else if (ignoreGraphic) { 611 // Since I only have to position the text, it goes at the 612 // contentX/contentY location. Note that positionNode will 613 // adjust the text based on the text's minX/minY so no need to 614 // worry about that here 615 text.relocate(snapPositionX(contentX), snapPositionY(contentY)); 616 if (containsMnemonic && (mnemonicPos != null)) { 617 mnemonic_underscore.setEndX(mnemonicWidth-2.0); 618 mnemonic_underscore.relocate(contentX + mnemonicPos.getX(), contentY + mnemonicPos.getY()); 619 } 620 621 } else if (ignoreText) { 622 // there isn't text to display, so we need to position it 623 // such that it doesn't affect the content area (although when 624 // there is a graphic, the text isn't even in the scene) 625 text.relocate(snapPositionX(contentX), snapPositionY(contentY)); 626 graphic.relocate(snapPositionX(contentX), snapPositionY(contentY)); 627 if (containsMnemonic && (mnemonicPos != null)) { 628 mnemonic_underscore.setEndX(mnemonicWidth); 629 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); 630 mnemonic_underscore.relocate(contentX + mnemonicPos.getX(), contentY + mnemonicPos.getY()); 631 } 632 } else { 633 // There is both text and a graphic, so I need to position them 634 // relative to each other 635 double graphicX = 0; 636 double graphicY = 0; 637 double textX = 0; 638 double textY = 0; 639 640 if (contentDisplay == ContentDisplay.TOP) { 641 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); 642 textX = contentX + ((contentWidth - textWidth) / 2.0); 643 // The graphic is above the text, so it is positioned at 644 // graphicY and the text below it. 645 graphicY = contentY; 646 textY = graphicY + graphicHeight + gap; 647 } else if (contentDisplay == ContentDisplay.RIGHT) { 648 // The graphic is to the right of the text 649 textX = contentX; 650 graphicX = textX + textWidth + gap; 651 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); 652 textY = contentY + ((contentHeight - textHeight) / 2.0); 653 } else if (contentDisplay == ContentDisplay.BOTTOM) { 654 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); 655 textX = contentX + ((contentWidth - textWidth) / 2.0); 656 // The graphic is below the text 657 textY = contentY; 658 graphicY = textY + textHeight + gap; 659 } else if (contentDisplay == ContentDisplay.LEFT) { 660 // The graphic is to the left of the text, so the graphicX is 661 // simply the contentX and the textX is to the right of it. 662 graphicX = contentX; 663 textX = graphicX + graphicWidth + gap; 664 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); 665 textY = contentY + ((contentHeight - textHeight) / 2.0); 666 } else if (contentDisplay == ContentDisplay.CENTER) { 667 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); 668 textX = contentX + ((contentWidth - textWidth) / 2.0); 669 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); 670 textY = contentY + ((contentHeight - textHeight) / 2.0); 671 } 672 text.relocate(snapPositionX(textX), snapPositionY(textY)); 673 if (containsMnemonic && (mnemonicPos != null)) { 674 mnemonic_underscore.setEndX(mnemonicWidth); 675 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); 676 mnemonic_underscore.relocate(textX + mnemonicPos.getX(), textY + mnemonicPos.getY()); 677 } 678 graphic.relocate(snapPositionX(graphicX), snapPositionY(graphicY)); 679 } 680 681 /** 682 * check if the label text overflows it's bounds. 683 * If there's an overflow, and no text clip then 684 * we'll clip it. 685 * If there is no overflow, and the label text has a 686 * clip, then remove it. 687 */ 688 if ((text != null) && 689 ((text.getLayoutBounds().getHeight() > wrapHeight) || 690 (text.getLayoutBounds().getWidth() > wrapWidth))) { 691 692 if (textClip == null) { 693 textClip = new Rectangle(); 694 } 695 696 if (labeled.getEffectiveNodeOrientation() == NodeOrientation.LEFT_TO_RIGHT) { 697 textClip.setX(text.getLayoutBounds().getMinX()); 698 } else { 699 textClip.setX(text.getLayoutBounds().getMaxX() - wrapWidth); 700 } 701 textClip.setY(text.getLayoutBounds().getMinY()); 702 textClip.setWidth(wrapWidth); 703 textClip.setHeight(wrapHeight); 704 if (text.getClip() == null) { 705 text.setClip(textClip); 706 } 707 } 708 else { 709 /** 710 * content fits inside bounds, no need 711 * for a clip 712 */ 713 if (text.getClip() != null) { 714 text.setClip(null); 715 } 716 } 717 } 718 719 /** {@inheritDoc} */ 720 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 721 switch (attribute) { 722 case TEXT: { 723 Labeled labeled = getSkinnable(); 724 String accText = labeled.getAccessibleText(); 725 if (accText != null && !accText.isEmpty()) return accText; 726 727 /* Use the text in the binding if available to handle mnemonics */ 728 if (bindings != null) { 729 String text = bindings.getText(); 730 if (text != null && !text.isEmpty()) return text; 731 } 732 /* Avoid the content in text.getText() as it can contain ellipses 733 * for clipping 734 */ 735 String text = labeled.getText(); 736 if (text != null && !text.isEmpty()) return text; 737 738 /* Use the graphic as last resource. Note that this implementation 739 * does not attempt to combine the label and graphics if both 740 * are being displayed 741 */ 742 if (graphic != null) { 743 Object result = graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT); 744 if (result != null) return result; 745 } 746 return null; 747 } 748 case MNEMONIC: { 749 if (bindings != null) { 750 return bindings.getMnemonic(); 751 } 752 return null; 753 } 754 default: return super.queryAccessibleAttribute(attribute, parameters); 755 } 756 } 757 758 759 760 /*************************************************************************** 761 * * 762 * Private implementation * 763 * * 764 **************************************************************************/ 765 766 private double computeMinLabeledPartWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 767 // First compute the minTextWidth by checking the width of the string 768 // made by the ellipsis "...", and then by checking the width of the 769 // string made up by labeled.text. We want the smaller of the two. 770 final Labeled labeled = getSkinnable(); 771 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 772 final double gap = labeled.getGraphicTextGap(); 773 double minTextWidth = 0; 774 775 final Font font = text.getFont(); 776 OverrunStyle truncationStyle = labeled.getTextOverrun(); 777 String ellipsisString = labeled.getEllipsisString(); 778 final String string = labeled.getText(); 779 final boolean emptyText = string == null || string.isEmpty(); 780 781 if (!emptyText) { 782 // We only want to recompute the full text width if the font or text changed 783 if (truncationStyle == CLIP) { 784 if (textWidth == Double.NEGATIVE_INFINITY) { 785 // Show at minimum the first character 786 textWidth = Utils.computeTextWidth(font, string.substring(0, 1), 0); 787 } 788 minTextWidth = textWidth; 789 } else { 790 if (textWidth == Double.NEGATIVE_INFINITY) { 791 textWidth = Utils.computeTextWidth(font, string, 0); 792 } 793 // We only want to recompute the ellipsis width if the font has changed 794 if (ellipsisWidth == Double.NEGATIVE_INFINITY) { 795 ellipsisWidth = Utils.computeTextWidth(font, ellipsisString, 0); 796 } 797 minTextWidth = Math.min(textWidth, ellipsisWidth); 798 } 799 } 800 801 // Now inspect the graphic and the hpos to determine the the minWidth 802 final Node graphic = labeled.getGraphic(); 803 double width; 804 if (isIgnoreGraphic()) { 805 width = minTextWidth; 806 } else if (isIgnoreText()) { 807 width = graphic.minWidth(-1); 808 } else if (contentDisplay == LEFT || contentDisplay == RIGHT){ 809 width = (minTextWidth + graphic.minWidth(-1) + gap); 810 } else { 811 width = Math.max(minTextWidth, graphic.minWidth(-1)); 812 } 813 814 double padding = leftInset + rightInset; 815 if (!isIgnoreText()) { 816 padding += leftLabelPadding() + rightLabelPadding(); 817 } 818 819 return width + padding; 820 } 821 822 private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 823 final Labeled labeled = getSkinnable(); 824 final Font font = text.getFont(); 825 826 String str = labeled.getText(); 827 if (str != null && str.length() > 0) { 828 int newlineIndex = str.indexOf('\n'); 829 if (newlineIndex >= 0) { 830 str = str.substring(0, newlineIndex); 831 } 832 } 833 834 // TODO figure out how to cache this effectively. 835 // Base minimum height on one line (ignoring wrapping here). 836 double s = labeled.getLineSpacing(); 837 final double textHeight = Utils.computeTextHeight(font, str, 0, s, text.getBoundsType()); 838 839 double h = textHeight; 840 841 // Now we want to add on the graphic if necessary! 842 if (!isIgnoreGraphic()) { 843 final Node graphic = labeled.getGraphic(); 844 if (labeled.getContentDisplay() == ContentDisplay.TOP 845 || labeled.getContentDisplay() == ContentDisplay.BOTTOM) { 846 h = graphic.minHeight(width) + labeled.getGraphicTextGap() + textHeight; 847 } else { 848 h = Math.max(textHeight, graphic.minHeight(width)); 849 } 850 } 851 852 double padding = topInset + bottomInset; 853 if (!isIgnoreText()) { 854 padding += topLabelPadding() - bottomLabelPadding(); 855 } 856 return h + padding; 857 } 858 859 double topLabelPadding() { 860 return snapSizeY(getSkinnable().getLabelPadding().getTop()); 861 } 862 863 double bottomLabelPadding() { 864 return snapSizeY(getSkinnable().getLabelPadding().getBottom()); 865 } 866 867 double leftLabelPadding() { 868 return snapSizeX(getSkinnable().getLabelPadding().getLeft()); 869 } 870 871 double rightLabelPadding() { 872 return snapSizeX(getSkinnable().getLabelPadding().getRight()); 873 } 874 875 876 /** 877 * Called whenever some state has changed that affects the text metrics. 878 * Changes here will involve invalidating the display text so the next 879 * call to updateDisplayedText computes a new value, and call requestLayout. 880 */ 881 private void textMetricsChanged() { 882 invalidText = true; 883 getSkinnable().requestLayout(); 884 } 885 886 /* 887 ** The Label is a mnemonic, and it's target node 888 ** has changed, but it's label hasn't so just 889 ** swap them over, and tidy up. 890 */ 891 void mnemonicTargetChanged() { 892 if (containsMnemonic == true) { 893 /* 894 ** was there previously a labelFor 895 */ 896 removeMnemonic(); 897 898 /* 899 ** is there a new labelFor 900 */ 901 Control control = getSkinnable(); 902 if (control instanceof Label) { 903 labeledNode = ((Label)control).getLabelFor(); 904 addMnemonic(); 905 } 906 else { 907 labeledNode = null; 908 } 909 } 910 } 911 912 private void sceneChanged() { 913 final Labeled labeled = getSkinnable(); 914 Scene scene = labeled.getScene(); 915 916 if (scene != null && containsMnemonic) { 917 addMnemonic(); 918 } 919 920 } 921 922 /** 923 * Marks minWidth as being invalid and in need of recomputation. 924 */ 925 private void invalidateWidths() { 926 textWidth = Double.NEGATIVE_INFINITY; 927 } 928 929 /** 930 * Updates the content of the underlying Text node. This method should 931 * only be called when necessary. If the invalidText flag is not set, then 932 * the method is a no-op. This care is taken because recomputing the 933 * text to display is an expensive operation. Package private ONLY FOR THE 934 * SAKE OF TESTING. 935 */ 936 void updateDisplayedText() { 937 updateDisplayedText(-1, -1); 938 } 939 940 private void updateDisplayedText(double w, double h) { 941 if (invalidText) { 942 final Labeled labeled = getSkinnable(); 943 String s = labeled.getText(); 944 945 int mnemonicIndex = -1; 946 947 /* 948 ** if there's a valid string then parse it 949 */ 950 if (s != null && s.length() > 0) { 951 bindings = new TextBinding(s); 952 953 if (!com.sun.javafx.PlatformUtil.isMac() && getSkinnable().isMnemonicParsing() == true) { 954 /* 955 ** the Labeled has a MnemonicParsing property, 956 ** if set true, then auto-parsing will check for 957 ** a mnemonic 958 */ 959 if (labeled instanceof Label) { 960 // buttons etc 961 labeledNode = ((Label)labeled).getLabelFor(); 962 } else { 963 labeledNode = labeled; 964 } 965 966 if (labeledNode == null) { 967 labeledNode = labeled; 968 } 969 mnemonicIndex = bindings.getMnemonicIndex() ; 970 } 971 } 972 973 /* 974 ** we were previously a mnemonic 975 */ 976 if (containsMnemonic) { 977 /* 978 ** are we no longer a mnemonic, or have we changed code? 979 */ 980 if (mnemonicScene != null) { 981 if (mnemonicIndex == -1 || 982 (bindings != null && !bindings.getMnemonicKeyCombination().equals(mnemonicCode))) { 983 removeMnemonic(); 984 containsMnemonic = false; 985 } 986 } 987 } 988 else { 989 /* 990 ** this can happen if mnemonic parsing is 991 ** disabled on a previously valid mnemonic 992 */ 993 removeMnemonic(); 994 } 995 996 /* 997 ** check we have a labeled 998 */ 999 if (s != null && s.length() > 0) { 1000 if (mnemonicIndex >= 0 && containsMnemonic == false) { 1001 containsMnemonic = true; 1002 mnemonicCode = bindings.getMnemonicKeyCombination(); 1003 addMnemonic(); 1004 } 1005 } 1006 1007 if (containsMnemonic == true) { 1008 s = bindings.getText(); 1009 if (mnemonic_underscore == null) { 1010 mnemonic_underscore = new Line(); 1011 mnemonic_underscore.setStartX(0.0f); 1012 mnemonic_underscore.setStartY(0.0f); 1013 mnemonic_underscore.setEndY(0.0f); 1014 mnemonic_underscore.getStyleClass().clear(); 1015 mnemonic_underscore.getStyleClass().setAll("mnemonic-underline"); 1016 } 1017 if (!getChildren().contains(mnemonic_underscore)) { 1018 getChildren().add(mnemonic_underscore); 1019 } 1020 } else { 1021 /* 1022 ** we don't need a mnemonic.... 1023 */ 1024 if (getSkinnable().isMnemonicParsing() == true && com.sun.javafx.PlatformUtil.isMac() && bindings != null) { 1025 s = bindings.getText(); 1026 } 1027 else { 1028 s = labeled.getText(); 1029 } 1030 if (mnemonic_underscore != null) { 1031 if (getChildren().contains(mnemonic_underscore)) { 1032 Platform.runLater(() -> { 1033 getChildren().remove(mnemonic_underscore); 1034 mnemonic_underscore = null; 1035 }); 1036 } 1037 } 1038 } 1039 1040 int len = s != null ? s.length() : 0; 1041 boolean multiline = false; 1042 1043 if (s != null && len > 0) { 1044 int i = s.indexOf('\n'); 1045 if (i > -1 && i < len - 1) { 1046 // Multiline text with embedded newlines - not 1047 // taking into account a potential trailing newline. 1048 multiline = true; 1049 } 1050 } 1051 1052 String result; 1053 boolean horizontalPosition = 1054 (labeled.getContentDisplay() == ContentDisplay.LEFT || 1055 labeled.getContentDisplay() == ContentDisplay.RIGHT); 1056 1057 double availableWidth = labeled.getWidth() - 1058 snappedLeftInset() - snappedRightInset(); 1059 1060 if (!isIgnoreText()) { 1061 availableWidth -= leftLabelPadding() + rightLabelPadding(); 1062 } 1063 availableWidth = Math.max(availableWidth, 0); 1064 1065 if (w == -1) { 1066 w = availableWidth; 1067 } 1068 double minW = Math.min(computeMinLabeledPartWidth(-1, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableWidth); 1069 if (horizontalPosition && !isIgnoreGraphic()) { 1070 double graphicW = (labeled.getGraphic().getLayoutBounds().getWidth() + labeled.getGraphicTextGap()); 1071 w -= graphicW; 1072 minW -= graphicW; 1073 } 1074 wrapWidth = Math.max(minW, w); 1075 1076 boolean verticalPosition = 1077 (labeled.getContentDisplay() == ContentDisplay.TOP || 1078 labeled.getContentDisplay() == ContentDisplay.BOTTOM); 1079 1080 double availableHeight = labeled.getHeight() - 1081 snappedTopInset() - snappedBottomInset(); 1082 1083 if (!isIgnoreText()) { 1084 availableHeight -= topLabelPadding() + bottomLabelPadding(); 1085 } 1086 availableHeight = Math.max(availableHeight, 0); 1087 1088 if (h == -1) { 1089 h = availableHeight; 1090 } 1091 double minH = Math.min(computeMinLabeledPartHeight(wrapWidth, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableHeight); 1092 if (verticalPosition && labeled.getGraphic() != null) { 1093 double graphicH = labeled.getGraphic().getLayoutBounds().getHeight() + labeled.getGraphicTextGap(); 1094 h -= graphicH; 1095 minH -= graphicH; 1096 } 1097 wrapHeight = Math.max(minH, h); 1098 1099 updateWrappingWidth(); 1100 1101 Font font = text.getFont(); 1102 OverrunStyle truncationStyle = labeled.getTextOverrun(); 1103 String ellipsisString = labeled.getEllipsisString(); 1104 1105 if (labeled.isWrapText()) { 1106 result = Utils.computeClippedWrappedText(font, s, wrapWidth, wrapHeight, truncationStyle, ellipsisString, text.getBoundsType()); 1107 } else if (multiline) { 1108 StringBuilder sb = new StringBuilder(); 1109 1110 String[] splits = s.split("\n"); 1111 for (int i = 0; i < splits.length; i++) { 1112 sb.append(Utils.computeClippedText(font, splits[i], wrapWidth, truncationStyle, ellipsisString)); 1113 if (i < splits.length - 1) { 1114 sb.append('\n'); 1115 } 1116 } 1117 1118 // TODO: Consider what to do in the case where vertical space is 1119 // limited and the last visible line isn't already truncated 1120 // with a trailing ellipsis. What if the style calls for leading 1121 // or center ellipses? We could possibly add an additional 1122 // trailing ellipsis to the last visible line, like this: 1123 1124 // +--------------------------------+ 1125 // | This is some long text with multiple lines\n 1126 // | where more than one exceed the|width\n 1127 // | and wrapText is false, and all|lines\n 1128 // +--don't fit.--------------------+ 1129 // 1130 // +--------------------------------+ 1131 // | This is some...multiple lines | 1132 // | where more t...ceed the width | 1133 // | and wrapText...d all lines... | 1134 // +--------------------------------+ 1135 1136 result = sb.toString(); 1137 } else { 1138 result = Utils.computeClippedText(font, s, wrapWidth, truncationStyle, ellipsisString); 1139 } 1140 1141 if (result != null && result.endsWith("\n")) { 1142 // Strip ending newline so we don't display another row. 1143 result = result.substring(0, result.length() - 1); 1144 } 1145 1146 text.setText(result); 1147 updateWrappingWidth(); 1148 invalidText = false; 1149 } 1150 } 1151 1152 private void addMnemonic() { 1153 if (labeledNode != null) { 1154 mnemonicScene = labeledNode.getScene(); 1155 if (mnemonicScene != null) { 1156 mnemonicScene.addMnemonic(new Mnemonic(labeledNode, mnemonicCode)); 1157 } 1158 } 1159 } 1160 1161 1162 private void removeMnemonic() { 1163 if (mnemonicScene != null && labeledNode != null) { 1164 mnemonicScene.removeMnemonic(new Mnemonic(labeledNode, mnemonicCode)); 1165 mnemonicScene = null; 1166 } 1167 } 1168 1169 /** 1170 * Updates the wrapping width of the text node. Although changing the font 1171 * does affect the metrics used for text layout, this method does not 1172 * call requestLayout or invalidate the text, since it may be called 1173 * from the constructor and such work would be duplicative and wasted. 1174 */ 1175 private void updateWrappingWidth() { 1176 final Labeled labeled = getSkinnable(); 1177 text.setWrappingWidth(0); 1178 if (labeled.isWrapText()) { 1179 // Note that the wrapping width needs to be set to zero before 1180 // getting the text's real preferred width. 1181 double w = Math.min(text.prefWidth(-1), wrapWidth); 1182 text.setWrappingWidth(w); 1183 } 1184 } 1185 1186 /** 1187 * Gets whether for various computations we can ignore the presence of the graphic 1188 * (or lack thereof). 1189 * @return 1190 */ 1191 boolean isIgnoreGraphic() { 1192 return (graphic == null || 1193 !graphic.isManaged() || 1194 getSkinnable().getContentDisplay() == ContentDisplay.TEXT_ONLY); 1195 } 1196 1197 /** 1198 * Gets whether for various computations we can ignore the presence of the text. 1199 * @return 1200 */ 1201 boolean isIgnoreText() { 1202 final Labeled labeled = getSkinnable(); 1203 final String txt = labeled.getText(); 1204 return (txt == null || 1205 txt.equals("") || 1206 labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY); 1207 } 1208 }