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