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