1 /* 2 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 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 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 // RT-37420 282 graphic.impl_processCSS(false); 283 284 } 285 286 } 287 288 /** 289 * Compute and return the minimum width of this Labeled. The minimum width is 290 * the smaller of the width of "..." and the width with the actual text. 291 * In this way, if the text width itself is smaller than the ellipsis then 292 * we should use that as the min width, otherwise the ellipsis needs to be the 293 * min width. 294 * <p> 295 * We use the same calculation here regardless of whether we are talking 296 * about a single or multiline labeled. So a multiline labeled may find that 297 * the width of the "..." is as small as it will ever get. 298 */ 299 @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 300 return computeMinLabeledPartWidth(height, topInset, rightInset, bottomInset, leftInset); 301 } 302 303 /** {@inheritDoc} */ 304 @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 305 return computeMinLabeledPartHeight(width, topInset, rightInset, bottomInset, leftInset); 306 } 307 308 /** {@inheritDoc} */ 309 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 310 // Get the preferred width of the text 311 final Labeled labeled = getSkinnable(); 312 final Font font = text.getFont(); 313 final String string = labeled.getText(); 314 boolean emptyText = string == null || string.isEmpty(); 315 double widthPadding = leftInset + leftLabelPadding() + 316 rightInset + rightLabelPadding(); 317 318 double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0); 319 320 // Fix for RT-39889 321 double graphicWidth = graphic == null ? 0.0 : 322 Utils.boundedSize(graphic.prefWidth(-1), graphic.minWidth(-1), graphic.maxWidth(-1)); 323 324 // Now add on the graphic, gap, and padding as appropriate 325 final Node graphic = labeled.getGraphic(); 326 if (isIgnoreGraphic()) { 327 return textWidth + widthPadding; 328 } else if (isIgnoreText()) { 329 return graphicWidth + widthPadding; 330 } else if (labeled.getContentDisplay() == ContentDisplay.LEFT 331 || labeled.getContentDisplay() == ContentDisplay.RIGHT) { 332 return textWidth + labeled.getGraphicTextGap() + graphicWidth + widthPadding; 333 } else { 334 return Math.max(textWidth, graphicWidth) + widthPadding; 335 } 336 } 337 338 /** {@inheritDoc} */ 339 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 340 final Labeled labeled = getSkinnable(); 341 final Font font = text.getFont(); 342 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 343 final double gap = labeled.getGraphicTextGap(); 344 width -= leftInset + leftLabelPadding() + 345 rightInset + rightLabelPadding(); 346 347 String str = labeled.getText(); 348 if (str != null && str.endsWith("\n")) { 349 // Strip ending newline so we don't count another row. 350 str = str.substring(0, str.length() - 1); 351 } 352 353 double textWidth = width; 354 if (!isIgnoreGraphic() && 355 (contentDisplay == LEFT || contentDisplay == RIGHT)) { 356 textWidth -= (graphic.prefWidth(-1) + gap); 357 } 358 359 // TODO figure out how to cache this effectively. 360 final double textHeight = Utils.computeTextHeight(font, str, 361 labeled.isWrapText() ? textWidth : 0, 362 labeled.getLineSpacing(), text.getBoundsType()); 363 364 // Now we want to add on the graphic if necessary! 365 double h = textHeight; 366 if (!isIgnoreGraphic()) { 367 final Node graphic = labeled.getGraphic(); 368 if (contentDisplay == TOP || contentDisplay == BOTTOM) { 369 h = graphic.prefHeight(width) + gap + textHeight; 370 } else { 371 h = Math.max(textHeight, graphic.prefHeight(width)); 372 } 373 } 374 375 return topInset + h + bottomInset + topLabelPadding() + bottomLabelPadding(); 376 } 377 378 /** {@inheritDoc} */ 379 @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 380 return getSkinnable().prefWidth(height); 381 } 382 383 /** {@inheritDoc} */ 384 @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 385 return getSkinnable().prefHeight(width); 386 } 387 388 /** {@inheritDoc} */ 389 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 390 double textBaselineOffset = text.getBaselineOffset(); 391 double h = textBaselineOffset; 392 final Labeled labeled = getSkinnable(); 393 final Node g = labeled.getGraphic(); 394 if (!isIgnoreGraphic()) { 395 ContentDisplay contentDisplay = labeled.getContentDisplay(); 396 if (contentDisplay == ContentDisplay.TOP) { 397 h = g.prefHeight(-1) + labeled.getGraphicTextGap() + textBaselineOffset; 398 } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == RIGHT) { 399 h = textBaselineOffset + (g.prefHeight(-1) - text.prefHeight(-1)) / 2; 400 } 401 } 402 403 return topInset + topLabelPadding() + h; 404 } 405 406 /** 407 * The Layout algorithm works like this: 408 * 409 * - Get the labeled w/h, graphic w/h, text w/h 410 * - Compute content w/h based on graphicVPos, graphicHPos, 411 * graphicTextGap, and graphic w/h and text w/h 412 * - (Note that the text content has been pre-truncated where 413 * necessary) 414 * - compute content x/y based on content w/h and labeled w/h 415 * and the labeled's hpos and vpos 416 * - position the graphic and text 417 */ 418 @Override protected void layoutChildren(final double x, final double y, 419 final double w, final double h) { 420 layoutLabelInArea(x, y, w, h); 421 } 422 423 /** 424 * Performs the actual layout of the label content within the area given. 425 * This method is called by subclasses that override layoutChildren(). 426 * 427 * @param x The x position of the label part of the control, inside padding 428 * 429 * @param y The y position of the label part of the control, inside padding 430 * 431 * @param w The width of the label part of the control, not including padding 432 * 433 * @param h The height of the label part of the control, not including padding 434 */ 435 protected void layoutLabelInArea(double x, double y, double w, double h) { 436 layoutLabelInArea(x, y, w, h, null); 437 } 438 439 /** 440 * Performs the actual layout of the label content within the area given. 441 * This method is called by subclasses that override layoutChildren(). 442 * 443 * @param x The x position of the label part of the control, inside padding 444 * 445 * @param y The y position of the label part of the control, inside padding 446 * 447 * @param w The width of the label part of the control, not including padding 448 * 449 * @param h The height of the label part of the control, not including padding 450 * 451 * @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. 452 */ 453 protected void layoutLabelInArea(double x, double y, double w, double h, Pos alignment) { 454 // References to essential labeled state 455 final Labeled labeled = getSkinnable(); 456 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 457 458 if (alignment == null) { 459 alignment = labeled.getAlignment(); 460 } 461 462 final HPos hpos = alignment == null ? HPos.LEFT : alignment.getHpos(); 463 final VPos vpos = alignment == null ? VPos.CENTER : alignment.getVpos(); 464 465 // Figure out whether we should ignore the Graphic, and/or 466 // ignore the Text 467 final boolean ignoreGraphic = isIgnoreGraphic(); 468 final boolean ignoreText = isIgnoreText(); 469 470 x += leftLabelPadding(); 471 y += topLabelPadding(); 472 w -= leftLabelPadding() + rightLabelPadding(); 473 h -= topLabelPadding() + bottomLabelPadding(); 474 475 // Compute some standard useful numbers for the graphic, text, and gap 476 double graphicWidth; 477 double graphicHeight; 478 double textWidth; 479 double textHeight; 480 481 if (ignoreGraphic) { 482 graphicWidth = graphicHeight = 0; 483 } else if (ignoreText) { 484 if (graphic.isResizable()) { 485 Orientation contentBias = graphic.getContentBias(); 486 if (contentBias == Orientation.HORIZONTAL) { 487 graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); 488 graphicHeight = Utils.boundedSize(h, graphic.minHeight(graphicWidth), graphic.maxHeight(graphicWidth)); 489 } else if (contentBias == Orientation.VERTICAL) { 490 graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); 491 graphicWidth = Utils.boundedSize(w, graphic.minWidth(graphicHeight), graphic.maxWidth(graphicHeight)); 492 } else { 493 graphicWidth = Utils.boundedSize(w, graphic.minWidth(-1), graphic.maxWidth(-1)); 494 graphicHeight = Utils.boundedSize(h, graphic.minHeight(-1), graphic.maxHeight(-1)); 495 } 496 graphic.resize(graphicWidth, graphicHeight); 497 } else { 498 graphicWidth = graphic.getLayoutBounds().getWidth(); 499 graphicHeight = graphic.getLayoutBounds().getHeight(); 500 } 501 } else { 502 graphic.autosize(); // We have to do this before getting metrics 503 graphicWidth = graphic.getLayoutBounds().getWidth(); 504 graphicHeight = graphic.getLayoutBounds().getHeight(); 505 } 506 507 if (ignoreText) { 508 textWidth = textHeight = 0; 509 text.setText(""); 510 } else { 511 updateDisplayedText(w, h); // Have to do this just in case it needs to be recomputed 512 textWidth = snapSize(Math.min(text.getLayoutBounds().getWidth(), wrapWidth)); 513 textHeight = snapSize(Math.min(text.getLayoutBounds().getHeight(), wrapHeight)); 514 } 515 516 final double gap = (ignoreText || ignoreGraphic) ? 0 : labeled.getGraphicTextGap(); 517 518 // Figure out the contentWidth and contentHeight. This is the width 519 // and height of the Labeled and Graphic together, not the available 520 // content area (which would be a different calculation). 521 double contentWidth = Math.max(graphicWidth, textWidth); 522 double contentHeight = Math.max(graphicHeight, textHeight); 523 if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) { 524 contentHeight = graphicHeight + gap + textHeight; 525 } else if (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT) { 526 contentWidth = graphicWidth + gap + textWidth; 527 } 528 529 // Now we want to compute the x/y location to place the content at. 530 531 // Compute the contentX position based on hpos and the space available 532 double contentX; 533 if (hpos == HPos.LEFT) { 534 contentX = x; 535 } else if (hpos == HPos.RIGHT) { 536 contentX = x + (w - contentWidth); 537 } else { 538 // TODO Baseline may not be handled correctly 539 // may have been CENTER or null, treat as center 540 contentX = (x + ((w - contentWidth) / 2.0)); 541 } 542 543 // Compute the contentY position based on vpos and the space available 544 double contentY; 545 if (vpos == VPos.TOP) { 546 contentY = y; 547 } else if (vpos == VPos.BOTTOM) { 548 contentY = (y + (h - contentHeight)); 549 } else { 550 // TODO Baseline may not be handled correctly 551 // may have been CENTER, BASELINE, or null, treat as center 552 contentY = (y + ((h - contentHeight) / 2.0)); 553 } 554 555 double preMnemonicWidth = 0.0; 556 double mnemonicWidth = 0.0; 557 double mnemonicHeight = 0.0; 558 if (containsMnemonic) { 559 final Font font = text.getFont(); 560 String preSt = bindings.getText(); 561 preMnemonicWidth = Utils.computeTextWidth(font, preSt.substring(0, bindings.getMnemonicIndex()), 0); 562 mnemonicWidth = Utils.computeTextWidth(font, preSt.substring(bindings.getMnemonicIndex(), bindings.getMnemonicIndex() + 1), 0); 563 mnemonicHeight = Utils.computeTextHeight(font, "_", 0, text.getBoundsType()); 564 } 565 566 567 // Now to position the graphic and text. At this point I know the 568 // contentX and contentY locations (including the padding and whatnot 569 // that was defined on the Labeled). I also know the content width and 570 // height. So now I just need to lay out the graphic and text within 571 // that content x/y/w/h area. 572 if ((!ignoreGraphic || !ignoreText) && !text.isManaged()) { 573 text.setManaged(true); 574 } 575 576 if (ignoreGraphic && ignoreText) { 577 // There might be a text node as a child, or a graphic node as 578 // a child. However we don't have to do anything for the graphic 579 // node because the only way it can be a child and still have 580 // ignoreGraphic true is if it is unmanaged. Text however might 581 // be a child but still not matter, in which case we will just 582 // stop managing it (although really I wish it just wasn't here 583 // all all in that case) 584 if (text.isManaged()) { 585 text.setManaged(false); 586 } 587 text.relocate(snapPosition(contentX), snapPosition(contentY)); 588 } else if (ignoreGraphic) { 589 // Since I only have to position the text, it goes at the 590 // contentX/contentY location. Note that positionNode will 591 // adjust the text based on the text's minX/minY so no need to 592 // worry about that here 593 text.relocate(snapPosition(contentX), snapPosition(contentY)); 594 if (containsMnemonic) { 595 mnemonic_underscore.setEndX(mnemonicWidth-2.0); 596 mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1); 597 } 598 599 } else if (ignoreText) { 600 // there isn't text to display, so we need to position it 601 // such that it doesn't affect the content area (although when 602 // there is a graphic, the text isn't even in the scene) 603 text.relocate(snapPosition(contentX), snapPosition(contentY)); 604 graphic.relocate(snapPosition(contentX), snapPosition(contentY)); 605 if (containsMnemonic) { 606 mnemonic_underscore.setEndX(mnemonicWidth); 607 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); 608 mnemonic_underscore.relocate(contentX+preMnemonicWidth, contentY+mnemonicHeight-1); 609 610 } 611 } else { 612 // There is both text and a graphic, so I need to position them 613 // relative to each other 614 double graphicX = 0; 615 double graphicY = 0; 616 double textX = 0; 617 double textY = 0; 618 619 if (contentDisplay == ContentDisplay.TOP) { 620 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); 621 textX = contentX + ((contentWidth - textWidth) / 2.0); 622 // The graphic is above the text, so it is positioned at 623 // graphicY and the text below it. 624 graphicY = contentY; 625 textY = graphicY + graphicHeight + gap; 626 } else if (contentDisplay == ContentDisplay.RIGHT) { 627 // The graphic is to the right of the text 628 textX = contentX; 629 graphicX = textX + textWidth + gap; 630 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); 631 textY = contentY + ((contentHeight - textHeight) / 2.0); 632 } else if (contentDisplay == ContentDisplay.BOTTOM) { 633 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); 634 textX = contentX + ((contentWidth - textWidth) / 2.0); 635 // The graphic is below the text 636 textY = contentY; 637 graphicY = textY + textHeight + gap; 638 } else if (contentDisplay == ContentDisplay.LEFT) { 639 // The graphic is to the left of the text, so the graphicX is 640 // simply the contentX and the textX is to the right of it. 641 graphicX = contentX; 642 textX = graphicX + graphicWidth + gap; 643 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); 644 textY = contentY + ((contentHeight - textHeight) / 2.0); 645 } else if (contentDisplay == ContentDisplay.CENTER) { 646 graphicX = contentX + ((contentWidth - graphicWidth) / 2.0); 647 textX = contentX + ((contentWidth - textWidth) / 2.0); 648 graphicY = contentY + ((contentHeight - graphicHeight) / 2.0); 649 textY = contentY + ((contentHeight - textHeight) / 2.0); 650 } 651 text.relocate(snapPosition(textX), snapPosition(textY)); 652 if (containsMnemonic) { 653 mnemonic_underscore.setEndX(mnemonicWidth); 654 mnemonic_underscore.setStrokeWidth(mnemonicHeight/10.0); 655 mnemonic_underscore.relocate(snapPosition(textX+preMnemonicWidth), snapPosition(textY+mnemonicHeight-1)); 656 } 657 graphic.relocate(snapPosition(graphicX), snapPosition(graphicY)); 658 } 659 660 /** 661 * check if the label text overflows it's bounds. 662 * If there's an overflow, and no text clip then 663 * we'll clip it. 664 * If there is no overflow, and the label text has a 665 * clip, then remove it. 666 */ 667 if ((text != null) && 668 ((text.getLayoutBounds().getHeight() > wrapHeight) || 669 (text.getLayoutBounds().getWidth() > wrapWidth))) { 670 671 if (textClip == null) { 672 textClip = new Rectangle(); 673 } 674 675 if (labeled.getEffectiveNodeOrientation() == NodeOrientation.LEFT_TO_RIGHT) { 676 textClip.setX(text.getLayoutBounds().getMinX()); 677 } else { 678 textClip.setX(text.getLayoutBounds().getMaxX() - wrapWidth); 679 } 680 textClip.setY(text.getLayoutBounds().getMinY()); 681 textClip.setWidth(wrapWidth); 682 textClip.setHeight(wrapHeight); 683 if (text.getClip() == null) { 684 text.setClip(textClip); 685 } 686 } 687 else { 688 /** 689 * content fits inside bounds, no need 690 * for a clip 691 */ 692 if (text.getClip() != null) { 693 text.setClip(null); 694 } 695 } 696 } 697 698 /** {@inheritDoc} */ 699 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 700 switch (attribute) { 701 case TEXT: { 702 Labeled labeled = getSkinnable(); 703 String accText = labeled.getAccessibleText(); 704 if (accText != null && !accText.isEmpty()) return accText; 705 706 /* Use the text in the binding if available to handle mnemonics */ 707 if (bindings != null) { 708 String text = bindings.getText(); 709 if (text != null && !text.isEmpty()) return text; 710 } 711 /* Avoid the content in text.getText() as it can contain ellipses 712 * for clipping 713 */ 714 if (labeled != null) { 715 String text = labeled.getText(); 716 if (text != null && !text.isEmpty()) return text; 717 } 718 /* Use the graphic as last resource. Note that this implementation 719 * does not attempt to combine the label and graphics if both 720 * are being displayed 721 */ 722 if (graphic != null) { 723 Object result = graphic.queryAccessibleAttribute(AccessibleAttribute.TEXT); 724 if (result != null) return result; 725 } 726 return null; 727 } 728 case MNEMONIC: { 729 if (bindings != null) { 730 return bindings.getMnemonic(); 731 } 732 return null; 733 } 734 default: return super.queryAccessibleAttribute(attribute, parameters); 735 } 736 } 737 738 739 740 /*************************************************************************** 741 * * 742 * Private implementation * 743 * * 744 **************************************************************************/ 745 746 private double computeMinLabeledPartWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 747 // First compute the minTextWidth by checking the width of the string 748 // made by the ellipsis "...", and then by checking the width of the 749 // string made up by labeled.text. We want the smaller of the two. 750 final Labeled labeled = getSkinnable(); 751 final ContentDisplay contentDisplay = labeled.getContentDisplay(); 752 final double gap = labeled.getGraphicTextGap(); 753 double minTextWidth = 0; 754 755 final Font font = text.getFont(); 756 OverrunStyle truncationStyle = labeled.getTextOverrun(); 757 String ellipsisString = labeled.getEllipsisString(); 758 final String string = labeled.getText(); 759 final boolean emptyText = string == null || string.isEmpty(); 760 761 if (!emptyText) { 762 // We only want to recompute the full text width if the font or text changed 763 if (truncationStyle == CLIP) { 764 if (textWidth == Double.NEGATIVE_INFINITY) { 765 // Show at minimum the first character 766 textWidth = Utils.computeTextWidth(font, string.substring(0, 1), 0); 767 } 768 minTextWidth = textWidth; 769 } else { 770 if (textWidth == Double.NEGATIVE_INFINITY) { 771 textWidth = Utils.computeTextWidth(font, string, 0); 772 } 773 // We only want to recompute the ellipsis width if the font has changed 774 if (ellipsisWidth == Double.NEGATIVE_INFINITY) { 775 ellipsisWidth = Utils.computeTextWidth(font, ellipsisString, 0); 776 } 777 minTextWidth = Math.min(textWidth, ellipsisWidth); 778 } 779 } 780 781 // Now inspect the graphic and the hpos to determine the the minWidth 782 final Node graphic = labeled.getGraphic(); 783 double width; 784 if (isIgnoreGraphic()) { 785 width = minTextWidth; 786 } else if (isIgnoreText()) { 787 width = graphic.minWidth(-1); 788 } else if (contentDisplay == LEFT || contentDisplay == RIGHT){ 789 width = (minTextWidth + graphic.minWidth(-1) + gap); 790 } else { 791 width = Math.max(minTextWidth, graphic.minWidth(-1)); 792 } 793 794 return width + leftInset + leftLabelPadding() + 795 rightInset + rightLabelPadding(); 796 } 797 798 private double computeMinLabeledPartHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 799 final Labeled labeled = getSkinnable(); 800 final Font font = text.getFont(); 801 802 String str = labeled.getText(); 803 if (str != null && str.length() > 0) { 804 int newlineIndex = str.indexOf('\n'); 805 if (newlineIndex >= 0) { 806 str = str.substring(0, newlineIndex); 807 } 808 } 809 810 // TODO figure out how to cache this effectively. 811 // Base minimum height on one line (ignoring wrapping here). 812 double s = labeled.getLineSpacing(); 813 final double textHeight = Utils.computeTextHeight(font, str, 0, s, text.getBoundsType()); 814 815 double h = textHeight; 816 817 // Now we want to add on the graphic if necessary! 818 if (!isIgnoreGraphic()) { 819 final Node graphic = labeled.getGraphic(); 820 if (labeled.getContentDisplay() == ContentDisplay.TOP 821 || labeled.getContentDisplay() == ContentDisplay.BOTTOM) { 822 h = graphic.minHeight(width) + labeled.getGraphicTextGap() + textHeight; 823 } else { 824 h = Math.max(textHeight, graphic.minHeight(width)); 825 } 826 } 827 828 return topInset + h + bottomInset + topLabelPadding() - bottomLabelPadding(); 829 } 830 831 double topLabelPadding() { 832 return snapSize(getSkinnable().getLabelPadding().getTop()); 833 } 834 835 double bottomLabelPadding() { 836 return snapSize(getSkinnable().getLabelPadding().getBottom()); 837 } 838 839 double leftLabelPadding() { 840 return snapSize(getSkinnable().getLabelPadding().getLeft()); 841 } 842 843 double rightLabelPadding() { 844 return snapSize(getSkinnable().getLabelPadding().getRight()); 845 } 846 847 848 /** 849 * Called whenever some state has changed that affects the text metrics. 850 * Changes here will involve invalidating the display text so the next 851 * call to updateDisplayedText computes a new value, and call requestLayout. 852 */ 853 private void textMetricsChanged() { 854 invalidText = true; 855 getSkinnable().requestLayout(); 856 } 857 858 /* 859 ** The Label is a mnemonic, and it's target node 860 ** has changed, but it's label hasn't so just 861 ** swap them over, and tidy up. 862 */ 863 void mnemonicTargetChanged() { 864 if (containsMnemonic == true) { 865 /* 866 ** was there previously a labelFor 867 */ 868 removeMnemonic(); 869 870 /* 871 ** is there a new labelFor 872 */ 873 Control control = getSkinnable(); 874 if (control instanceof Label) { 875 labeledNode = ((Label)control).getLabelFor(); 876 addMnemonic(); 877 } 878 else { 879 labeledNode = null; 880 } 881 } 882 } 883 884 private void sceneChanged() { 885 final Labeled labeled = getSkinnable(); 886 Scene scene = labeled.getScene(); 887 888 if (scene != null && containsMnemonic) { 889 addMnemonic(); 890 } 891 892 } 893 894 /** 895 * Marks minWidth as being invalid and in need of recomputation. 896 */ 897 private void invalidateWidths() { 898 textWidth = Double.NEGATIVE_INFINITY; 899 } 900 901 /** 902 * Updates the content of the underlying Text node. This method should 903 * only be called when necessary. If the invalidText flag is not set, then 904 * the method is a no-op. This care is taken because recomputing the 905 * text to display is an expensive operation. Package private ONLY FOR THE 906 * SAKE OF TESTING. 907 */ 908 void updateDisplayedText() { 909 updateDisplayedText(-1, -1); 910 } 911 912 private void updateDisplayedText(double w, double h) { 913 if (invalidText) { 914 final Labeled labeled = getSkinnable(); 915 String s = labeled.getText(); 916 917 int mnemonicIndex = -1; 918 919 /* 920 ** if there's a valid string then parse it 921 */ 922 if (s != null && s.length() > 0) { 923 bindings = new TextBinding(s); 924 925 if (!com.sun.javafx.PlatformUtil.isMac() && getSkinnable().isMnemonicParsing() == true) { 926 /* 927 ** the Labeled has a MnemonicParsing property, 928 ** if set true, then auto-parsing will check for 929 ** a mnemonic 930 */ 931 if (labeled instanceof Label) { 932 // buttons etc 933 labeledNode = ((Label)labeled).getLabelFor(); 934 } else { 935 labeledNode = labeled; 936 } 937 938 if (labeledNode == null) { 939 labeledNode = labeled; 940 } 941 mnemonicIndex = bindings.getMnemonicIndex() ; 942 } 943 } 944 945 /* 946 ** we were previously a mnemonic 947 */ 948 if (containsMnemonic) { 949 /* 950 ** are we no longer a mnemonic, or have we changed code? 951 */ 952 if (mnemonicScene != null) { 953 if (mnemonicIndex == -1 || 954 (bindings != null && !bindings.getMnemonicKeyCombination().equals(mnemonicCode))) { 955 removeMnemonic(); 956 } 957 containsMnemonic = false; 958 } 959 } 960 else { 961 /* 962 ** this can happen if mnemonic parsing is 963 ** disabled on a previously valid mnemonic 964 */ 965 removeMnemonic(); 966 } 967 968 /* 969 ** check we have a labeled 970 */ 971 if (s != null && s.length() > 0) { 972 if (mnemonicIndex >= 0 && containsMnemonic == false) { 973 containsMnemonic = true; 974 mnemonicCode = bindings.getMnemonicKeyCombination(); 975 addMnemonic(); 976 } 977 } 978 979 if (containsMnemonic == true) { 980 s = bindings.getText(); 981 if (mnemonic_underscore == null) { 982 mnemonic_underscore = new Line(); 983 mnemonic_underscore.setStartX(0.0f); 984 mnemonic_underscore.setStartY(0.0f); 985 mnemonic_underscore.setEndY(0.0f); 986 mnemonic_underscore.getStyleClass().clear(); 987 mnemonic_underscore.getStyleClass().setAll("mnemonic-underline"); 988 } 989 if (!getChildren().contains(mnemonic_underscore)) { 990 getChildren().add(mnemonic_underscore); 991 } 992 } else { 993 /* 994 ** we don't need a mnemonic.... 995 */ 996 if (getSkinnable().isMnemonicParsing() == true && com.sun.javafx.PlatformUtil.isMac() && bindings != null) { 997 s = bindings.getText(); 998 } 999 else { 1000 s = labeled.getText(); 1001 } 1002 if (mnemonic_underscore != null) { 1003 if (getChildren().contains(mnemonic_underscore)) { 1004 Platform.runLater(() -> { 1005 getChildren().remove(mnemonic_underscore); 1006 mnemonic_underscore = null; 1007 }); 1008 } 1009 } 1010 } 1011 1012 int len = s != null ? s.length() : 0; 1013 boolean multiline = false; 1014 1015 if (s != null && len > 0) { 1016 int i = s.indexOf('\n'); 1017 if (i > -1 && i < len - 1) { 1018 // Multiline text with embedded newlines - not 1019 // taking into account a potential trailing newline. 1020 multiline = true; 1021 } 1022 } 1023 1024 String result; 1025 boolean horizontalPosition = 1026 (labeled.getContentDisplay() == ContentDisplay.LEFT || 1027 labeled.getContentDisplay() == ContentDisplay.RIGHT); 1028 1029 double availableWidth = labeled.getWidth() - snappedLeftInset() - leftLabelPadding() - 1030 snappedRightInset() - rightLabelPadding(); 1031 availableWidth = Math.max(availableWidth, 0); 1032 1033 if (w == -1) { 1034 w = availableWidth; 1035 } 1036 double minW = Math.min(computeMinLabeledPartWidth(-1, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableWidth); 1037 if (horizontalPosition && !isIgnoreGraphic()) { 1038 double graphicW = (labeled.getGraphic().getLayoutBounds().getWidth() + labeled.getGraphicTextGap()); 1039 w -= graphicW; 1040 minW -= graphicW; 1041 } 1042 wrapWidth = Math.max(minW, w); 1043 1044 boolean verticalPosition = 1045 (labeled.getContentDisplay() == ContentDisplay.TOP || 1046 labeled.getContentDisplay() == ContentDisplay.BOTTOM); 1047 1048 double availableHeight = labeled.getHeight() - snappedTopInset() - topLabelPadding() - 1049 snappedBottomInset() - bottomLabelPadding(); 1050 availableHeight = Math.max(availableHeight, 0); 1051 1052 if (h == -1) { 1053 h = availableHeight; 1054 } 1055 double minH = Math.min(computeMinLabeledPartHeight(wrapWidth, snappedTopInset() , snappedRightInset(), snappedBottomInset(), snappedLeftInset()), availableHeight); 1056 if (verticalPosition && labeled.getGraphic() != null) { 1057 double graphicH = labeled.getGraphic().getLayoutBounds().getHeight() + labeled.getGraphicTextGap(); 1058 h -= graphicH; 1059 minH -= graphicH; 1060 } 1061 wrapHeight = Math.max(minH, h); 1062 1063 updateWrappingWidth(); 1064 1065 Font font = text.getFont(); 1066 OverrunStyle truncationStyle = labeled.getTextOverrun(); 1067 String ellipsisString = labeled.getEllipsisString(); 1068 1069 if (labeled.isWrapText()) { 1070 result = Utils.computeClippedWrappedText(font, s, wrapWidth, wrapHeight, truncationStyle, ellipsisString, text.getBoundsType()); 1071 } else if (multiline) { 1072 StringBuilder sb = new StringBuilder(); 1073 1074 String[] splits = s.split("\n"); 1075 for (int i = 0; i < splits.length; i++) { 1076 sb.append(Utils.computeClippedText(font, splits[i], wrapWidth, truncationStyle, ellipsisString)); 1077 if (i < splits.length - 1) { 1078 sb.append('\n'); 1079 } 1080 } 1081 1082 // TODO: Consider what to do in the case where vertical space is 1083 // limited and the last visible line isn't already truncated 1084 // with a trailing ellipsis. What if the style calls for leading 1085 // or center ellipses? We could possibly add an additional 1086 // trailing ellipsis to the last visible line, like this: 1087 1088 // +--------------------------------+ 1089 // | This is some long text with multiple lines\n 1090 // | where more than one exceed the|width\n 1091 // | and wrapText is false, and all|lines\n 1092 // +--don't fit.--------------------+ 1093 // 1094 // +--------------------------------+ 1095 // | This is some...multiple lines | 1096 // | where more t...ceed the width | 1097 // | and wrapText...d all lines... | 1098 // +--------------------------------+ 1099 1100 result = sb.toString(); 1101 } else { 1102 result = Utils.computeClippedText(font, s, wrapWidth, truncationStyle, ellipsisString); 1103 } 1104 1105 if (result != null && result.endsWith("\n")) { 1106 // Strip ending newline so we don't display another row. 1107 result = result.substring(0, result.length() - 1); 1108 } 1109 1110 text.setText(result); 1111 updateWrappingWidth(); 1112 invalidText = false; 1113 } 1114 } 1115 1116 private void addMnemonic() { 1117 if (labeledNode != null) { 1118 mnemonicScene = labeledNode.getScene(); 1119 if (mnemonicScene != null) { 1120 mnemonicScene.addMnemonic(new Mnemonic(labeledNode, mnemonicCode)); 1121 } 1122 } 1123 } 1124 1125 1126 private void removeMnemonic() { 1127 if (mnemonicScene != null && labeledNode != null) { 1128 mnemonicScene.removeMnemonic(new Mnemonic(labeledNode, mnemonicCode)); 1129 mnemonicScene = null; 1130 } 1131 } 1132 1133 /** 1134 * Updates the wrapping width of the text node. Although changing the font 1135 * does affect the metrics used for text layout, this method does not 1136 * call requestLayout or invalidate the text, since it may be called 1137 * from the constructor and such work would be duplicative and wasted. 1138 */ 1139 private void updateWrappingWidth() { 1140 final Labeled labeled = getSkinnable(); 1141 text.setWrappingWidth(0); 1142 if (labeled.isWrapText()) { 1143 // Note that the wrapping width needs to be set to zero before 1144 // getting the text's real preferred width. 1145 double w = Math.min(text.prefWidth(-1), wrapWidth); 1146 text.setWrappingWidth(w); 1147 } 1148 } 1149 1150 /** 1151 * Gets whether for various computations we can ignore the presence of the graphic 1152 * (or lack thereof). 1153 * @return 1154 */ 1155 boolean isIgnoreGraphic() { 1156 return (graphic == null || 1157 !graphic.isManaged() || 1158 getSkinnable().getContentDisplay() == ContentDisplay.TEXT_ONLY); 1159 } 1160 1161 /** 1162 * Gets whether for various computations we can ignore the presence of the text. 1163 * @return 1164 */ 1165 boolean isIgnoreText() { 1166 final Labeled labeled = getSkinnable(); 1167 final String txt = labeled.getText(); 1168 return (txt == null || 1169 txt.equals("") || 1170 labeled.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY); 1171 } 1172 }