1 /* 2 * Copyright (c) 2011, 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.LambdaMultiplePropertyChangeListenerHandler; 29 import com.sun.javafx.scene.control.Properties; 30 import com.sun.javafx.util.Utils; 31 import javafx.animation.Animation; 32 import javafx.animation.Interpolator; 33 import javafx.animation.KeyFrame; 34 import javafx.animation.KeyValue; 35 import javafx.animation.Timeline; 36 import javafx.beans.InvalidationListener; 37 import javafx.beans.Observable; 38 import javafx.beans.WeakInvalidationListener; 39 import javafx.beans.property.DoubleProperty; 40 import javafx.beans.property.ObjectProperty; 41 import javafx.beans.property.SimpleDoubleProperty; 42 import javafx.beans.value.WritableValue; 43 import javafx.collections.FXCollections; 44 import javafx.collections.ListChangeListener; 45 import javafx.collections.ObservableList; 46 import javafx.collections.WeakListChangeListener; 47 import javafx.css.CssMetaData; 48 import javafx.css.PseudoClass; 49 import javafx.css.Styleable; 50 import javafx.css.StyleableObjectProperty; 51 import javafx.css.StyleableProperty; 52 import javafx.event.ActionEvent; 53 import javafx.event.EventHandler; 54 import javafx.geometry.HPos; 55 import javafx.geometry.Pos; 56 import javafx.geometry.Side; 57 import javafx.geometry.VPos; 58 import javafx.scene.AccessibleAction; 59 import javafx.scene.AccessibleAttribute; 60 import javafx.scene.AccessibleRole; 61 import javafx.scene.Node; 62 import javafx.scene.control.ContextMenu; 63 import javafx.scene.control.Control; 64 import javafx.scene.control.Label; 65 import javafx.scene.control.MenuItem; 66 import javafx.scene.control.RadioMenuItem; 67 import javafx.scene.control.SkinBase; 68 import javafx.scene.control.Tab; 69 import javafx.scene.control.TabPane; 70 import javafx.scene.control.TabPane.TabClosingPolicy; 71 import javafx.scene.control.ToggleGroup; 72 import javafx.scene.control.Tooltip; 73 import javafx.scene.effect.DropShadow; 74 import javafx.scene.image.ImageView; 75 import javafx.scene.input.ContextMenuEvent; 76 import javafx.scene.input.MouseButton; 77 import javafx.scene.input.MouseEvent; 78 import javafx.scene.input.ScrollEvent; 79 import javafx.scene.input.SwipeEvent; 80 import javafx.scene.layout.Pane; 81 import javafx.scene.layout.Region; 82 import javafx.scene.layout.StackPane; 83 import javafx.scene.shape.Rectangle; 84 import javafx.scene.transform.Rotate; 85 import javafx.util.Duration; 86 87 import java.util.ArrayList; 88 import java.util.Collections; 89 import java.util.Iterator; 90 import java.util.List; 91 92 import javafx.css.converter.EnumConverter; 93 import com.sun.javafx.scene.control.behavior.TabPaneBehavior; 94 import com.sun.javafx.scene.traversal.Direction; 95 import com.sun.javafx.scene.traversal.TraversalEngine; 96 97 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString; 98 99 /** 100 * Default skin implementation for the {@link TabPane} control. 101 * 102 * @see TabPane 103 * @since 9 104 */ 105 public class TabPaneSkin extends SkinBase<TabPane> { 106 107 /*************************************************************************** 108 * * 109 * Enums * 110 * * 111 **************************************************************************/ 112 113 private enum TabAnimation { 114 NONE, 115 GROW 116 // In future we could add FADE, ... 117 } 118 119 private enum TabAnimationState { 120 SHOWING, HIDING, NONE; 121 } 122 123 124 125 /*************************************************************************** 126 * * 127 * Static fields * 128 * * 129 **************************************************************************/ 130 131 static int CLOSE_BTN_SIZE = 16; 132 133 134 135 /*************************************************************************** 136 * * 137 * Private fields * 138 * * 139 **************************************************************************/ 140 141 private static final double ANIMATION_SPEED = 150; 142 private static final int SPACER = 10; 143 144 private TabHeaderArea tabHeaderArea; 145 private ObservableList<TabContentRegion> tabContentRegions; 146 private Rectangle clipRect; 147 private Rectangle tabHeaderAreaClipRect; 148 private Tab selectedTab; 149 private boolean isSelectingTab; 150 private double maxw = 0.0d; 151 private double maxh = 0.0d; 152 153 private final TabPaneBehavior behavior; 154 155 156 157 /*************************************************************************** 158 * * 159 * Constructors * 160 * * 161 **************************************************************************/ 162 163 /** 164 * Creates a new TabPaneSkin instance, installing the necessary child 165 * nodes into the Control {@link Control#getChildren() children} list, as 166 * well as the necessary input mappings for handling key, mouse, etc events. 167 * 168 * @param control The control that this skin should be installed onto. 169 */ 170 public TabPaneSkin(TabPane control) { 171 super(control); 172 173 // install default input map for the TabPane control 174 this.behavior = new TabPaneBehavior(control); 175 // control.setInputMap(behavior.getInputMap()); 176 177 clipRect = new Rectangle(control.getWidth(), control.getHeight()); 178 getSkinnable().setClip(clipRect); 179 180 tabContentRegions = FXCollections.<TabContentRegion>observableArrayList(); 181 182 for (Tab tab : getSkinnable().getTabs()) { 183 addTabContent(tab); 184 } 185 186 tabHeaderAreaClipRect = new Rectangle(); 187 tabHeaderArea = new TabHeaderArea(); 188 tabHeaderArea.setClip(tabHeaderAreaClipRect); 189 getChildren().add(tabHeaderArea); 190 if (getSkinnable().getTabs().size() == 0) { 191 tabHeaderArea.setVisible(false); 192 } 193 194 initializeTabListener(); 195 196 registerChangeListener(control.getSelectionModel().selectedItemProperty(), e -> { 197 isSelectingTab = true; 198 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 199 getSkinnable().requestLayout(); 200 }); 201 registerChangeListener(control.sideProperty(), e -> updateTabPosition()); 202 registerChangeListener(control.widthProperty(), e -> clipRect.setWidth(getSkinnable().getWidth())); 203 registerChangeListener(control.heightProperty(), e -> clipRect.setHeight(getSkinnable().getHeight())); 204 205 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 206 // Could not find the selected tab try and get the selected tab using the selected index 207 if (selectedTab == null && getSkinnable().getSelectionModel().getSelectedIndex() != -1) { 208 getSkinnable().getSelectionModel().select(getSkinnable().getSelectionModel().getSelectedIndex()); 209 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 210 } 211 if (selectedTab == null) { 212 // getSelectedItem and getSelectedIndex failed select the first. 213 getSkinnable().getSelectionModel().selectFirst(); 214 } 215 selectedTab = getSkinnable().getSelectionModel().getSelectedItem(); 216 isSelectingTab = false; 217 218 initializeSwipeHandlers(); 219 } 220 221 222 223 /*************************************************************************** 224 * * 225 * Properties * 226 * * 227 **************************************************************************/ 228 229 private ObjectProperty<TabAnimation> openTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) { 230 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() { 231 return StyleableProperties.OPEN_TAB_ANIMATION; 232 } 233 234 @Override public Object getBean() { 235 return TabPaneSkin.this; 236 } 237 238 @Override public String getName() { 239 return "openTabAnimation"; 240 } 241 }; 242 243 private ObjectProperty<TabAnimation> closeTabAnimation = new StyleableObjectProperty<TabAnimation>(TabAnimation.GROW) { 244 @Override public CssMetaData<TabPane,TabAnimation> getCssMetaData() { 245 return StyleableProperties.CLOSE_TAB_ANIMATION; 246 } 247 248 @Override public Object getBean() { 249 return TabPaneSkin.this; 250 } 251 252 @Override public String getName() { 253 return "closeTabAnimation"; 254 } 255 }; 256 257 258 259 /*************************************************************************** 260 * * 261 * Public API * 262 * * 263 **************************************************************************/ 264 265 /** {@inheritDoc} */ 266 @Override public void dispose() { 267 super.dispose(); 268 269 if (behavior != null) { 270 behavior.dispose(); 271 } 272 } 273 274 /** {@inheritDoc} */ 275 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 276 // The TabPane can only be as wide as it widest content width. 277 for (TabContentRegion contentRegion: tabContentRegions) { 278 maxw = Math.max(maxw, snapSize(contentRegion.prefWidth(-1))); 279 } 280 281 final boolean isHorizontal = isHorizontal(); 282 final double tabHeaderAreaSize = snapSize(isHorizontal ? 283 tabHeaderArea.prefWidth(-1) : tabHeaderArea.prefHeight(-1)); 284 285 double prefWidth = isHorizontal ? 286 Math.max(maxw, tabHeaderAreaSize) : maxw + tabHeaderAreaSize; 287 return snapSize(prefWidth) + rightInset + leftInset; 288 } 289 290 /** {@inheritDoc} */ 291 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 292 // The TabPane can only be as high as it highest content height. 293 for (TabContentRegion contentRegion: tabContentRegions) { 294 maxh = Math.max(maxh, snapSize(contentRegion.prefHeight(-1))); 295 } 296 297 final boolean isHorizontal = isHorizontal(); 298 final double tabHeaderAreaSize = snapSize(isHorizontal ? 299 tabHeaderArea.prefHeight(-1) : tabHeaderArea.prefWidth(-1)); 300 301 double prefHeight = isHorizontal ? 302 maxh + snapSize(tabHeaderAreaSize) : Math.max(maxh, tabHeaderAreaSize); 303 return snapSize(prefHeight) + topInset + bottomInset; 304 } 305 306 /** {@inheritDoc} */ 307 @Override public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { 308 Side tabPosition = getSkinnable().getSide(); 309 if (tabPosition == Side.TOP) { 310 return tabHeaderArea.getBaselineOffset() + topInset; 311 } 312 return 0; 313 } 314 315 /** {@inheritDoc} */ 316 @Override protected void layoutChildren(final double x, final double y, 317 final double w, final double h) { 318 TabPane tabPane = getSkinnable(); 319 Side tabPosition = tabPane.getSide(); 320 321 double headerHeight = snapSize(tabHeaderArea.prefHeight(-1)); 322 double tabsStartX = tabPosition.equals(Side.RIGHT)? x + w - headerHeight : x; 323 double tabsStartY = tabPosition.equals(Side.BOTTOM)? y + h - headerHeight : y; 324 325 if (tabPosition == Side.TOP) { 326 tabHeaderArea.resize(w, headerHeight); 327 tabHeaderArea.relocate(tabsStartX, tabsStartY); 328 tabHeaderArea.getTransforms().clear(); 329 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.TOP))); 330 } else if (tabPosition == Side.BOTTOM) { 331 tabHeaderArea.resize(w, headerHeight); 332 tabHeaderArea.relocate(w, tabsStartY - headerHeight); 333 tabHeaderArea.getTransforms().clear(); 334 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.BOTTOM), 0, headerHeight)); 335 } else if (tabPosition == Side.LEFT) { 336 tabHeaderArea.resize(h, headerHeight); 337 tabHeaderArea.relocate(tabsStartX + headerHeight, h - headerHeight); 338 tabHeaderArea.getTransforms().clear(); 339 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.LEFT), 0, headerHeight)); 340 } else if (tabPosition == Side.RIGHT) { 341 tabHeaderArea.resize(h, headerHeight); 342 tabHeaderArea.relocate(tabsStartX, y - headerHeight); 343 tabHeaderArea.getTransforms().clear(); 344 tabHeaderArea.getTransforms().add(new Rotate(getRotation(Side.RIGHT), 0, headerHeight)); 345 } 346 347 tabHeaderAreaClipRect.setX(0); 348 tabHeaderAreaClipRect.setY(0); 349 if (isHorizontal()) { 350 tabHeaderAreaClipRect.setWidth(w); 351 } else { 352 tabHeaderAreaClipRect.setWidth(h); 353 } 354 tabHeaderAreaClipRect.setHeight(headerHeight); 355 356 // ================================== 357 // position the tab content for the selected tab only 358 // ================================== 359 // if the tabs are on the left, the content needs to be indented 360 double contentStartX = 0; 361 double contentStartY = 0; 362 363 if (tabPosition == Side.TOP) { 364 contentStartX = x; 365 contentStartY = y + headerHeight; 366 if (isFloatingStyleClass()) { 367 // This is to hide the top border content 368 contentStartY -= 1; 369 } 370 } else if (tabPosition == Side.BOTTOM) { 371 contentStartX = x; 372 contentStartY = y; 373 if (isFloatingStyleClass()) { 374 // This is to hide the bottom border content 375 contentStartY = 1; 376 } 377 } else if (tabPosition == Side.LEFT) { 378 contentStartX = x + headerHeight; 379 contentStartY = y; 380 if (isFloatingStyleClass()) { 381 // This is to hide the left border content 382 contentStartX -= 1; 383 } 384 } else if (tabPosition == Side.RIGHT) { 385 contentStartX = x; 386 contentStartY = y; 387 if (isFloatingStyleClass()) { 388 // This is to hide the right border content 389 contentStartX = 1; 390 } 391 } 392 393 double contentWidth = w - (isHorizontal() ? 0 : headerHeight); 394 double contentHeight = h - (isHorizontal() ? headerHeight: 0); 395 396 for (int i = 0, max = tabContentRegions.size(); i < max; i++) { 397 TabContentRegion tabContent = tabContentRegions.get(i); 398 399 tabContent.setAlignment(Pos.TOP_LEFT); 400 if (tabContent.getClip() != null) { 401 ((Rectangle)tabContent.getClip()).setWidth(contentWidth); 402 ((Rectangle)tabContent.getClip()).setHeight(contentHeight); 403 } 404 405 // we need to size all tabs, even if they aren't visible. For example, 406 // see RT-29167 407 tabContent.resize(contentWidth, contentHeight); 408 tabContent.relocate(contentStartX, contentStartY); 409 } 410 } 411 412 413 414 /*************************************************************************** 415 * * 416 * Private implementation * 417 * * 418 **************************************************************************/ 419 420 private static int getRotation(Side pos) { 421 switch (pos) { 422 case TOP: 423 return 0; 424 case BOTTOM: 425 return 180; 426 case LEFT: 427 return -90; 428 case RIGHT: 429 return 90; 430 default: 431 return 0; 432 } 433 } 434 435 /** 436 * VERY HACKY - this lets us 'duplicate' Label and ImageView nodes to be used in a 437 * Tab and the tabs menu at the same time. 438 */ 439 private static Node clone(Node n) { 440 if (n == null) { 441 return null; 442 } 443 if (n instanceof ImageView) { 444 ImageView iv = (ImageView) n; 445 ImageView imageview = new ImageView(); 446 imageview.setImage(iv.getImage()); 447 return imageview; 448 } 449 if (n instanceof Label) { 450 Label l = (Label)n; 451 Label label = new Label(l.getText(), l.getGraphic()); 452 return label; 453 } 454 return null; 455 } 456 457 private void removeTabs(List<? extends Tab> removedList) { 458 for (final Tab tab : removedList) { 459 stopCurrentAnimation(tab); 460 // Animate the tab removal 461 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab); 462 if (tabRegion != null) { 463 tabRegion.isClosing = true; 464 465 tabRegion.removeListeners(tab); 466 removeTabContent(tab); 467 468 // remove the menu item from the popup menu 469 ContextMenu popupMenu = tabHeaderArea.controlButtons.popup; 470 TabMenuItem tabItem = null; 471 if (popupMenu != null) { 472 for (MenuItem item : popupMenu.getItems()) { 473 tabItem = (TabMenuItem) item; 474 if (tab == tabItem.getTab()) { 475 break; 476 } 477 tabItem = null; 478 } 479 } 480 if (tabItem != null) { 481 tabItem.dispose(); 482 popupMenu.getItems().remove(tabItem); 483 } 484 // end of removing menu item 485 486 EventHandler<ActionEvent> cleanup = ae -> { 487 tabRegion.animationState = TabAnimationState.NONE; 488 489 tabHeaderArea.removeTab(tab); 490 tabHeaderArea.requestLayout(); 491 if (getSkinnable().getTabs().isEmpty()) { 492 tabHeaderArea.setVisible(false); 493 } 494 }; 495 496 if (closeTabAnimation.get() == TabAnimation.GROW) { 497 tabRegion.animationState = TabAnimationState.HIDING; 498 Timeline closedTabTimeline = tabRegion.currentAnimation = 499 createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 0.0F, cleanup); 500 closedTabTimeline.play(); 501 } else { 502 cleanup.handle(null); 503 } 504 } 505 } 506 } 507 508 private void stopCurrentAnimation(Tab tab) { 509 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab); 510 if (tabRegion != null) { 511 // Execute the code immediately, don't wait for the animation to finish. 512 Timeline timeline = tabRegion.currentAnimation; 513 if (timeline != null && timeline.getStatus() == Animation.Status.RUNNING) { 514 timeline.getOnFinished().handle(null); 515 timeline.stop(); 516 tabRegion.currentAnimation = null; 517 } 518 } 519 } 520 521 private void addTabs(List<? extends Tab> addedList, int from) { 522 int i = 0; 523 524 // RT-39984: check if any other tabs are animating - they must be completed first. 525 List<Node> headers = new ArrayList<>(tabHeaderArea.headersRegion.getChildren()); 526 for (Node n : headers) { 527 TabHeaderSkin header = (TabHeaderSkin) n; 528 if (header.animationState == TabAnimationState.HIDING) { 529 stopCurrentAnimation(header.tab); 530 } 531 } 532 // end of fix for RT-39984 533 534 for (final Tab tab : addedList) { 535 stopCurrentAnimation(tab); // Note that this must happen before addTab() call below 536 // A new tab was added - animate it out 537 if (!tabHeaderArea.isVisible()) { 538 tabHeaderArea.setVisible(true); 539 } 540 int index = from + i++; 541 tabHeaderArea.addTab(tab, index); 542 addTabContent(tab); 543 final TabHeaderSkin tabRegion = tabHeaderArea.getTabHeaderSkin(tab); 544 if (tabRegion != null) { 545 if (openTabAnimation.get() == TabAnimation.GROW) { 546 tabRegion.animationState = TabAnimationState.SHOWING; 547 tabRegion.animationTransition.setValue(0.0); 548 tabRegion.setVisible(true); 549 tabRegion.currentAnimation = createTimeline(tabRegion, Duration.millis(ANIMATION_SPEED), 1.0, event -> { 550 tabRegion.animationState = TabAnimationState.NONE; 551 tabRegion.setVisible(true); 552 tabRegion.inner.requestLayout(); 553 }); 554 tabRegion.currentAnimation.play(); 555 } else { 556 tabRegion.setVisible(true); 557 tabRegion.inner.requestLayout(); 558 } 559 } 560 } 561 } 562 563 private void initializeTabListener() { 564 getSkinnable().getTabs().addListener((ListChangeListener<Tab>) c -> { 565 List<Tab> tabsToRemove = new ArrayList<>(); 566 List<Tab> tabsToAdd = new ArrayList<>(); 567 int insertPos = -1; 568 569 while (c.next()) { 570 if (c.wasPermutated()) { 571 TabPane tabPane = getSkinnable(); 572 List<Tab> tabs = tabPane.getTabs(); 573 574 // tabs sorted : create list of permutated tabs. 575 // clear selection, set tab animation to NONE 576 // remove permutated tabs, add them back in correct order. 577 // restore old selection, and old tab animation states. 578 int size = c.getTo() - c.getFrom(); 579 Tab selTab = tabPane.getSelectionModel().getSelectedItem(); 580 List<Tab> permutatedTabs = new ArrayList<Tab>(size); 581 getSkinnable().getSelectionModel().clearSelection(); 582 583 // save and set tab animation to none - as it is not a good idea 584 // to animate on the same data for open and close. 585 TabAnimation prevOpenAnimation = openTabAnimation.get(); 586 TabAnimation prevCloseAnimation = closeTabAnimation.get(); 587 openTabAnimation.set(TabAnimation.NONE); 588 closeTabAnimation.set(TabAnimation.NONE); 589 for (int i = c.getFrom(); i < c.getTo(); i++) { 590 permutatedTabs.add(tabs.get(i)); 591 } 592 593 removeTabs(permutatedTabs); 594 addTabs(permutatedTabs, c.getFrom()); 595 openTabAnimation.set(prevOpenAnimation); 596 closeTabAnimation.set(prevCloseAnimation); 597 getSkinnable().getSelectionModel().select(selTab); 598 } 599 600 if (c.wasRemoved()) { 601 tabsToRemove.addAll(c.getRemoved()); 602 } 603 604 if (c.wasAdded()) { 605 tabsToAdd.addAll(c.getAddedSubList()); 606 insertPos = c.getFrom(); 607 } 608 } 609 610 // now only remove the tabs that are not in the tabsToAdd list 611 tabsToRemove.removeAll(tabsToAdd); 612 removeTabs(tabsToRemove); 613 614 // and add in any new tabs (that we don't already have showing) 615 if (!tabsToAdd.isEmpty()) { 616 for (TabContentRegion tabContentRegion : tabContentRegions) { 617 Tab tab = tabContentRegion.getTab(); 618 TabHeaderSkin tabHeader = tabHeaderArea.getTabHeaderSkin(tab); 619 if (!tabHeader.isClosing && tabsToAdd.contains(tabContentRegion.getTab())) { 620 tabsToAdd.remove(tabContentRegion.getTab()); 621 } 622 } 623 624 addTabs(tabsToAdd, insertPos == -1 ? tabContentRegions.size() : insertPos); 625 } 626 627 // Fix for RT-34692 628 getSkinnable().requestLayout(); 629 }); 630 } 631 632 private void addTabContent(Tab tab) { 633 TabContentRegion tabContentRegion = new TabContentRegion(tab); 634 tabContentRegion.setClip(new Rectangle()); 635 tabContentRegions.add(tabContentRegion); 636 // We want the tab content to always sit below the tab headers 637 getChildren().add(0, tabContentRegion); 638 } 639 640 private void removeTabContent(Tab tab) { 641 for (TabContentRegion contentRegion : tabContentRegions) { 642 if (contentRegion.getTab().equals(tab)) { 643 contentRegion.removeListeners(tab); 644 getChildren().remove(contentRegion); 645 tabContentRegions.remove(contentRegion); 646 break; 647 } 648 } 649 } 650 651 private void updateTabPosition() { 652 tabHeaderArea.setScrollOffset(0.0F); 653 getSkinnable().applyCss(); 654 getSkinnable().requestLayout(); 655 } 656 657 private Timeline createTimeline(final TabHeaderSkin tabRegion, final Duration duration, final double endValue, final EventHandler<ActionEvent> func) { 658 Timeline timeline = new Timeline(); 659 timeline.setCycleCount(1); 660 661 KeyValue keyValue = new KeyValue(tabRegion.animationTransition, endValue, Interpolator.LINEAR); 662 timeline.getKeyFrames().clear(); 663 timeline.getKeyFrames().add(new KeyFrame(duration, keyValue)); 664 665 timeline.setOnFinished(func); 666 return timeline; 667 } 668 669 private boolean isHorizontal() { 670 Side tabPosition = getSkinnable().getSide(); 671 return Side.TOP.equals(tabPosition) || Side.BOTTOM.equals(tabPosition); 672 } 673 674 private void initializeSwipeHandlers() { 675 if (Properties.IS_TOUCH_SUPPORTED) { 676 getSkinnable().addEventHandler(SwipeEvent.SWIPE_LEFT, t -> { 677 behavior.selectNextTab(); 678 }); 679 680 getSkinnable().addEventHandler(SwipeEvent.SWIPE_RIGHT, t -> { 681 behavior.selectPreviousTab(); 682 }); 683 } 684 } 685 686 //TODO need to cache this. 687 private boolean isFloatingStyleClass() { 688 return getSkinnable().getStyleClass().contains(TabPane.STYLE_CLASS_FLOATING); 689 } 690 691 692 693 /*************************************************************************** 694 * * 695 * CSS * 696 * * 697 **************************************************************************/ 698 699 /** 700 * Super-lazy instantiation pattern from Bill Pugh. 701 * @treatAsPrivate implementation detail 702 */ 703 private static class StyleableProperties { 704 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 705 706 private final static CssMetaData<TabPane,TabAnimation> OPEN_TAB_ANIMATION = 707 new CssMetaData<TabPane, TabPaneSkin.TabAnimation>("-fx-open-tab-animation", 708 new EnumConverter<TabAnimation>(TabAnimation.class), TabAnimation.GROW) { 709 710 @Override public boolean isSettable(TabPane node) { 711 return true; 712 } 713 714 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) { 715 TabPaneSkin skin = (TabPaneSkin) node.getSkin(); 716 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.openTabAnimation; 717 } 718 }; 719 720 private final static CssMetaData<TabPane,TabAnimation> CLOSE_TAB_ANIMATION = 721 new CssMetaData<TabPane, TabPaneSkin.TabAnimation>("-fx-close-tab-animation", 722 new EnumConverter<TabAnimation>(TabAnimation.class), TabAnimation.GROW) { 723 724 @Override public boolean isSettable(TabPane node) { 725 return true; 726 } 727 728 @Override public StyleableProperty<TabAnimation> getStyleableProperty(TabPane node) { 729 TabPaneSkin skin = (TabPaneSkin) node.getSkin(); 730 return (StyleableProperty<TabAnimation>)(WritableValue<TabAnimation>)skin.closeTabAnimation; 731 } 732 }; 733 734 static { 735 736 final List<CssMetaData<? extends Styleable, ?>> styleables = 737 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData()); 738 styleables.add(OPEN_TAB_ANIMATION); 739 styleables.add(CLOSE_TAB_ANIMATION); 740 STYLEABLES = Collections.unmodifiableList(styleables); 741 742 } 743 } 744 745 /** 746 * Returns the CssMetaData associated with this class, which may include the 747 * CssMetaData of its super classes. 748 */ 749 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 750 return StyleableProperties.STYLEABLES; 751 } 752 753 /** 754 * {@inheritDoc} 755 */ 756 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 757 return getClassCssMetaData(); 758 } 759 760 761 762 /*************************************************************************** 763 * * 764 * Support classes * 765 * * 766 **************************************************************************/ 767 768 /************************************************************************** 769 * 770 * TabHeaderArea: Area responsible for painting all tabs 771 * 772 **************************************************************************/ 773 class TabHeaderArea extends StackPane { 774 private Rectangle headerClip; 775 private StackPane headersRegion; 776 private StackPane headerBackground; 777 private TabControlButtons controlButtons; 778 779 private boolean measureClosingTabs = false; 780 781 private double scrollOffset; 782 783 public TabHeaderArea() { 784 getStyleClass().setAll("tab-header-area"); 785 setManaged(false); 786 final TabPane tabPane = getSkinnable(); 787 788 headerClip = new Rectangle(); 789 790 headersRegion = new StackPane() { 791 @Override protected double computePrefWidth(double height) { 792 double width = 0.0F; 793 for (Node child : getChildren()) { 794 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 795 if (tabHeaderSkin.isVisible() && (measureClosingTabs || ! tabHeaderSkin.isClosing)) { 796 width += tabHeaderSkin.prefWidth(height); 797 } 798 } 799 return snapSize(width) + snappedLeftInset() + snappedRightInset(); 800 } 801 802 @Override protected double computePrefHeight(double width) { 803 double height = 0.0F; 804 for (Node child : getChildren()) { 805 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 806 height = Math.max(height, tabHeaderSkin.prefHeight(width)); 807 } 808 return snapSize(height) + snappedTopInset() + snappedBottomInset(); 809 } 810 811 @Override protected void layoutChildren() { 812 if (tabsFit()) { 813 setScrollOffset(0.0); 814 } else { 815 if (!removeTab.isEmpty()) { 816 double offset = 0; 817 double w = tabHeaderArea.getWidth() - snapSize(controlButtons.prefWidth(-1)) - firstTabIndent() - SPACER; 818 Iterator<Node> i = getChildren().iterator(); 819 while (i.hasNext()) { 820 TabHeaderSkin tabHeader = (TabHeaderSkin)i.next(); 821 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 822 if (removeTab.contains(tabHeader)) { 823 if (offset < w) { 824 isSelectingTab = true; 825 } 826 i.remove(); 827 removeTab.remove(tabHeader); 828 if (removeTab.isEmpty()) { 829 break; 830 } 831 } 832 offset += tabHeaderPrefWidth; 833 } 834 // } else { 835 // isSelectingTab = true; 836 } 837 } 838 839 if (isSelectingTab) { 840 ensureSelectedTabIsVisible(); 841 isSelectingTab = false; 842 } else { 843 validateScrollOffset(); 844 } 845 846 Side tabPosition = getSkinnable().getSide(); 847 double tabBackgroundHeight = snapSize(prefHeight(-1)); 848 double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ? 849 snapSize(getWidth()) - getScrollOffset() : getScrollOffset(); 850 851 updateHeaderClip(); 852 for (Node node : getChildren()) { 853 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 854 855 // size and position the header relative to the other headers 856 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get()); 857 double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1)); 858 tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight); 859 860 // This ensures that the tabs are located in the correct position 861 // when there are tabs of differing heights. 862 double startY = tabPosition.equals(Side.BOTTOM) ? 863 0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset(); 864 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 865 // build from the right 866 tabX -= tabHeaderPrefWidth; 867 tabHeader.relocate(tabX, startY); 868 } else { 869 // build from the left 870 tabHeader.relocate(tabX, startY); 871 tabX += tabHeaderPrefWidth; 872 } 873 } 874 } 875 876 }; 877 headersRegion.getStyleClass().setAll("headers-region"); 878 headersRegion.setClip(headerClip); 879 880 headerBackground = new StackPane(); 881 headerBackground.getStyleClass().setAll("tab-header-background"); 882 883 int i = 0; 884 for (Tab tab: tabPane.getTabs()) { 885 addTab(tab, i++); 886 } 887 888 controlButtons = new TabControlButtons(); 889 controlButtons.setVisible(false); 890 if (controlButtons.isVisible()) { 891 controlButtons.setVisible(true); 892 } 893 getChildren().addAll(headerBackground, headersRegion, controlButtons); 894 895 // support for mouse scroll of header area (for when the tabs exceed 896 // the available space) 897 addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> { 898 Side side = getSkinnable().getSide(); 899 side = side == null ? Side.TOP : side; 900 switch (side) { 901 default: 902 case TOP: 903 case BOTTOM: 904 setScrollOffset(scrollOffset - e.getDeltaY()); 905 break; 906 case LEFT: 907 case RIGHT: 908 setScrollOffset(scrollOffset + e.getDeltaY()); 909 break; 910 } 911 912 }); 913 } 914 915 private void updateHeaderClip() { 916 Side tabPosition = getSkinnable().getSide(); 917 918 double x = 0; 919 double y = 0; 920 double clipWidth = 0; 921 double clipHeight = 0; 922 double maxWidth = 0; 923 double shadowRadius = 0; 924 double clipOffset = firstTabIndent(); 925 double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1)); 926 927 measureClosingTabs = true; 928 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 929 measureClosingTabs = false; 930 931 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 932 933 // Add the spacer if isShowTabsMenu is true. 934 if (controlButtonPrefWidth > 0) { 935 controlButtonPrefWidth = controlButtonPrefWidth + SPACER; 936 } 937 938 if (headersRegion.getEffect() instanceof DropShadow) { 939 DropShadow shadow = (DropShadow)headersRegion.getEffect(); 940 shadowRadius = shadow.getRadius(); 941 } 942 943 maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset; 944 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 945 if (headersPrefWidth < maxWidth) { 946 clipWidth = headersPrefWidth + shadowRadius; 947 } else { 948 x = headersPrefWidth - maxWidth; 949 clipWidth = maxWidth + shadowRadius; 950 } 951 clipHeight = headersPrefHeight; 952 } else { 953 // If x = 0 the header region's drop shadow is clipped. 954 x = -shadowRadius; 955 clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius; 956 clipHeight = headersPrefHeight; 957 } 958 959 headerClip.setX(x); 960 headerClip.setY(y); 961 headerClip.setWidth(clipWidth); 962 headerClip.setHeight(clipHeight); 963 } 964 965 private void addTab(Tab tab, int addToIndex) { 966 TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab); 967 headersRegion.getChildren().add(addToIndex, tabHeaderSkin); 968 } 969 970 private List<TabHeaderSkin> removeTab = new ArrayList<>(); 971 private void removeTab(Tab tab) { 972 TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab); 973 if (tabHeaderSkin != null) { 974 if (tabsFit()) { 975 headersRegion.getChildren().remove(tabHeaderSkin); 976 } else { 977 // The tab will be removed during layout because 978 // we need its width to compute the scroll offset. 979 removeTab.add(tabHeaderSkin); 980 tabHeaderSkin.removeListeners(tab); 981 } 982 } 983 } 984 985 private TabHeaderSkin getTabHeaderSkin(Tab tab) { 986 for (Node child: headersRegion.getChildren()) { 987 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 988 if (tabHeaderSkin.getTab().equals(tab)) { 989 return tabHeaderSkin; 990 } 991 } 992 return null; 993 } 994 995 private boolean tabsFit() { 996 double headerPrefWidth = snapSize(headersRegion.prefWidth(-1)); 997 double controlTabWidth = snapSize(controlButtons.prefWidth(-1)); 998 double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER; 999 return visibleWidth < getWidth(); 1000 } 1001 1002 private void ensureSelectedTabIsVisible() { 1003 // work out the visible width of the tab header 1004 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1005 double controlTabWidth = snapSize(controlButtons.getWidth()); 1006 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1007 1008 // and get where the selected tab is in the header area 1009 double offset = 0.0; 1010 double selectedTabOffset = 0.0; 1011 double selectedTabWidth = 0.0; 1012 for (Node node : headersRegion.getChildren()) { 1013 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1014 1015 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1016 1017 if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) { 1018 selectedTabOffset = offset; 1019 selectedTabWidth = tabHeaderPrefWidth; 1020 } 1021 offset += tabHeaderPrefWidth; 1022 } 1023 1024 final double scrollOffset = getScrollOffset(); 1025 final double selectedTabStartX = selectedTabOffset; 1026 final double selectedTabEndX = selectedTabOffset + selectedTabWidth; 1027 1028 final double visibleAreaEndX = visibleWidth; 1029 1030 if (selectedTabStartX < -scrollOffset) { 1031 setScrollOffset(-selectedTabStartX); 1032 } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) { 1033 setScrollOffset(visibleAreaEndX - selectedTabEndX); 1034 } 1035 } 1036 1037 public double getScrollOffset() { 1038 return scrollOffset; 1039 } 1040 1041 private void validateScrollOffset() { 1042 setScrollOffset(getScrollOffset()); 1043 } 1044 1045 private void setScrollOffset(double newScrollOffset) { 1046 // work out the visible width of the tab header 1047 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1048 double controlTabWidth = snapSize(controlButtons.getWidth()); 1049 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1050 1051 // measure the width of all tabs 1052 double offset = 0.0; 1053 for (Node node : headersRegion.getChildren()) { 1054 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1055 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1056 offset += tabHeaderPrefWidth; 1057 } 1058 1059 double actualNewScrollOffset; 1060 1061 if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) { 1062 // need to make sure the right-most tab is attached to the 1063 // right-hand side of the tab header (e.g. if the tab header area width 1064 // is expanded), and if it isn't modify the scroll offset to bring 1065 // it into line. See RT-35194 for a test case. 1066 actualNewScrollOffset = visibleWidth - offset; 1067 } else if (newScrollOffset > 0) { 1068 // need to prevent the left-most tab from becoming detached 1069 // from the left-hand side of the tab header. 1070 actualNewScrollOffset = 0; 1071 } else { 1072 actualNewScrollOffset = newScrollOffset; 1073 } 1074 1075 if (actualNewScrollOffset != scrollOffset) { 1076 scrollOffset = actualNewScrollOffset; 1077 headersRegion.requestLayout(); 1078 } 1079 } 1080 1081 private double firstTabIndent() { 1082 switch (getSkinnable().getSide()) { 1083 case TOP: 1084 case BOTTOM: 1085 return snappedLeftInset(); 1086 case RIGHT: 1087 case LEFT: 1088 return snappedTopInset(); 1089 default: 1090 return 0; 1091 } 1092 } 1093 1094 @Override protected double computePrefWidth(double height) { 1095 double padding = isHorizontal() ? 1096 snappedLeftInset() + snappedRightInset() : 1097 snappedTopInset() + snappedBottomInset(); 1098 return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) + 1099 firstTabIndent() + SPACER + padding; 1100 } 1101 1102 @Override protected double computePrefHeight(double width) { 1103 double padding = isHorizontal() ? 1104 snappedTopInset() + snappedBottomInset() : 1105 snappedLeftInset() + snappedRightInset(); 1106 return snapSize(headersRegion.prefHeight(-1)) + padding; 1107 } 1108 1109 @Override public double getBaselineOffset() { 1110 if (getSkinnable().getSide() == Side.TOP) { 1111 return headersRegion.getBaselineOffset() + snappedTopInset(); 1112 } 1113 return 0; 1114 } 1115 1116 @Override protected void layoutChildren() { 1117 final double leftInset = snappedLeftInset(); 1118 final double rightInset = snappedRightInset(); 1119 final double topInset = snappedTopInset(); 1120 final double bottomInset = snappedBottomInset(); 1121 double w = snapSize(getWidth()) - (isHorizontal() ? 1122 leftInset + rightInset : topInset + bottomInset); 1123 double h = snapSize(getHeight()) - (isHorizontal() ? 1124 topInset + bottomInset : leftInset + rightInset); 1125 double tabBackgroundHeight = snapSize(prefHeight(-1)); 1126 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 1127 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 1128 1129 controlButtons.showTabsMenu(! tabsFit()); 1130 1131 updateHeaderClip(); 1132 headersRegion.requestLayout(); 1133 1134 // RESIZE CONTROL BUTTONS 1135 double btnWidth = snapSize(controlButtons.prefWidth(-1)); 1136 final double btnHeight = controlButtons.prefHeight(btnWidth); 1137 controlButtons.resize(btnWidth, btnHeight); 1138 1139 // POSITION TABS 1140 headersRegion.resize(headersPrefWidth, headersPrefHeight); 1141 1142 if (isFloatingStyleClass()) { 1143 headerBackground.setVisible(false); 1144 } else { 1145 headerBackground.resize(snapSize(getWidth()), snapSize(getHeight())); 1146 headerBackground.setVisible(true); 1147 } 1148 1149 double startX = 0; 1150 double startY = 0; 1151 double controlStartX = 0; 1152 double controlStartY = 0; 1153 Side tabPosition = getSkinnable().getSide(); 1154 1155 if (tabPosition.equals(Side.TOP)) { 1156 startX = leftInset; 1157 startY = tabBackgroundHeight - headersPrefHeight - bottomInset; 1158 controlStartX = w - btnWidth + leftInset; 1159 controlStartY = snapSize(getHeight()) - btnHeight - bottomInset; 1160 } else if (tabPosition.equals(Side.RIGHT)) { 1161 startX = topInset; 1162 startY = tabBackgroundHeight - headersPrefHeight - leftInset; 1163 controlStartX = w - btnWidth + topInset; 1164 controlStartY = snapSize(getHeight()) - btnHeight - leftInset; 1165 } else if (tabPosition.equals(Side.BOTTOM)) { 1166 startX = snapSize(getWidth()) - headersPrefWidth - leftInset; 1167 startY = tabBackgroundHeight - headersPrefHeight - topInset; 1168 controlStartX = rightInset; 1169 controlStartY = snapSize(getHeight()) - btnHeight - topInset; 1170 } else if (tabPosition.equals(Side.LEFT)) { 1171 startX = snapSize(getWidth()) - headersPrefWidth - topInset; 1172 startY = tabBackgroundHeight - headersPrefHeight - rightInset; 1173 controlStartX = leftInset; 1174 controlStartY = snapSize(getHeight()) - btnHeight - rightInset; 1175 } 1176 if (headerBackground.isVisible()) { 1177 positionInArea(headerBackground, 0, 0, 1178 snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1179 } 1180 positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER); 1181 positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight, 1182 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1183 } 1184 } /* End TabHeaderArea */ 1185 1186 1187 1188 1189 /************************************************************************** 1190 * 1191 * TabHeaderSkin: skin for each tab 1192 * 1193 **************************************************************************/ 1194 1195 class TabHeaderSkin extends StackPane { 1196 private final Tab tab; 1197 public Tab getTab() { 1198 return tab; 1199 } 1200 private Label label; 1201 private StackPane closeBtn; 1202 private StackPane inner; 1203 private Tooltip oldTooltip; 1204 private Tooltip tooltip; 1205 private Rectangle clip; 1206 1207 private boolean isClosing = false; 1208 1209 private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler(); 1210 1211 private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() { 1212 @Override 1213 public void onChanged(Change<? extends String> c) { 1214 getStyleClass().setAll(tab.getStyleClass()); 1215 } 1216 }; 1217 1218 private final WeakListChangeListener<String> weakStyleClassListener = 1219 new WeakListChangeListener<>(styleClassListener); 1220 1221 public TabHeaderSkin(final Tab tab) { 1222 getStyleClass().setAll(tab.getStyleClass()); 1223 setId(tab.getId()); 1224 setStyle(tab.getStyle()); 1225 setAccessibleRole(AccessibleRole.TAB_ITEM); 1226 1227 this.tab = tab; 1228 clip = new Rectangle(); 1229 setClip(clip); 1230 1231 label = new Label(tab.getText(), tab.getGraphic()); 1232 label.getStyleClass().setAll("tab-label"); 1233 1234 closeBtn = new StackPane() { 1235 @Override protected double computePrefWidth(double h) { 1236 return CLOSE_BTN_SIZE; 1237 } 1238 @Override protected double computePrefHeight(double w) { 1239 return CLOSE_BTN_SIZE; 1240 } 1241 @Override 1242 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1243 switch (action) { 1244 case FIRE: { 1245 Tab tab = getTab(); 1246 if (behavior.canCloseTab(tab)) { 1247 behavior.closeTab(tab); 1248 setOnMousePressed(null); 1249 } 1250 } 1251 default: super.executeAccessibleAction(action, parameters); 1252 } 1253 } 1254 }; 1255 closeBtn.setAccessibleRole(AccessibleRole.BUTTON); 1256 closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton")); 1257 closeBtn.getStyleClass().setAll("tab-close-button"); 1258 closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() { 1259 @Override 1260 public void handle(MouseEvent me) { 1261 Tab tab = getTab(); 1262 if (behavior.canCloseTab(tab)) { 1263 behavior.closeTab(tab); 1264 setOnMousePressed(null); 1265 } 1266 } 1267 }); 1268 1269 updateGraphicRotation(); 1270 1271 final Region focusIndicator = new Region(); 1272 focusIndicator.setMouseTransparent(true); 1273 focusIndicator.getStyleClass().add("focus-indicator"); 1274 1275 inner = new StackPane() { 1276 @Override protected void layoutChildren() { 1277 final TabPane skinnable = getSkinnable(); 1278 1279 final double paddingTop = snappedTopInset(); 1280 final double paddingRight = snappedRightInset(); 1281 final double paddingBottom = snappedBottomInset(); 1282 final double paddingLeft = snappedLeftInset(); 1283 final double w = getWidth() - (paddingLeft + paddingRight); 1284 final double h = getHeight() - (paddingTop + paddingBottom); 1285 1286 final double prefLabelWidth = snapSize(label.prefWidth(-1)); 1287 final double prefLabelHeight = snapSize(label.prefHeight(-1)); 1288 1289 final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0; 1290 final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0; 1291 final double minWidth = snapSize(skinnable.getTabMinWidth()); 1292 final double maxWidth = snapSize(skinnable.getTabMaxWidth()); 1293 final double maxHeight = snapSize(skinnable.getTabMaxHeight()); 1294 1295 double labelAreaWidth = prefLabelWidth; 1296 double labelWidth = prefLabelWidth; 1297 double labelHeight = prefLabelHeight; 1298 1299 final double childrenWidth = labelAreaWidth + closeBtnWidth; 1300 final double childrenHeight = Math.max(labelHeight, closeBtnHeight); 1301 1302 if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) { 1303 labelAreaWidth = maxWidth - closeBtnWidth; 1304 labelWidth = maxWidth - closeBtnWidth; 1305 } else if (childrenWidth < minWidth) { 1306 labelAreaWidth = minWidth - closeBtnWidth; 1307 } 1308 1309 if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) { 1310 labelHeight = maxHeight; 1311 } 1312 1313 if (animationState != TabAnimationState.NONE) { 1314 // if (prefWidth.getValue() < labelAreaWidth) { 1315 // labelAreaWidth = prefWidth.getValue(); 1316 // } 1317 labelAreaWidth *= animationTransition.get(); 1318 closeBtn.setVisible(false); 1319 } else { 1320 closeBtn.setVisible(showCloseButton()); 1321 } 1322 1323 1324 label.resize(labelWidth, labelHeight); 1325 1326 1327 double labelStartX = paddingLeft; 1328 1329 // If maxWidth is less than Double.MAX_VALUE, the user has 1330 // clamped the max width, but we should 1331 // position the close button at the end of the tab, 1332 // which may not necessarily be the entire width of the 1333 // provided max width. 1334 double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth; 1335 1336 positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h, 1337 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1338 1339 if (closeBtn.isVisible()) { 1340 closeBtn.resize(closeBtnWidth, closeBtnHeight); 1341 positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h, 1342 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1343 } 1344 1345 // Magic numbers regretfully introduced for RT-28944 (so that 1346 // the focus rect appears as expected on Windows and Mac). 1347 // In short we use the vPadding to shift the focus rect down 1348 // into the content area (whereas previously it was being clipped 1349 // on Windows, whilst it still looked fine on Mac). In the 1350 // future we may want to improve this code to remove the 1351 // magic number. Similarly, the hPadding differs on Mac. 1352 final int vPadding = Utils.isMac() ? 2 : 3; 1353 final int hPadding = Utils.isMac() ? 2 : 1; 1354 focusIndicator.resizeRelocate( 1355 paddingLeft - hPadding, 1356 paddingTop + vPadding, 1357 w + 2 * hPadding, 1358 h - 2 * vPadding); 1359 } 1360 }; 1361 inner.getStyleClass().add("tab-container"); 1362 inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1363 inner.getChildren().addAll(label, closeBtn, focusIndicator); 1364 1365 getChildren().addAll(inner); 1366 1367 tooltip = tab.getTooltip(); 1368 if (tooltip != null) { 1369 Tooltip.install(this, tooltip); 1370 oldTooltip = tooltip; 1371 } 1372 1373 listener.registerChangeListener(tab.closableProperty(), e -> { 1374 inner.requestLayout(); 1375 requestLayout(); 1376 }); 1377 listener.registerChangeListener(tab.selectedProperty(), e -> { 1378 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1379 // Need to request a layout pass for inner because if the width 1380 // and height didn't not change the label or close button may have 1381 // changed. 1382 inner.requestLayout(); 1383 requestLayout(); 1384 }); 1385 listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText())); 1386 listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic())); 1387 listener.registerChangeListener(tab.tooltipProperty(), e -> { 1388 // uninstall the old tooltip 1389 if (oldTooltip != null) { 1390 Tooltip.uninstall(this, oldTooltip); 1391 } 1392 tooltip = tab.getTooltip(); 1393 if (tooltip != null) { 1394 // install new tooltip and save as old tooltip. 1395 Tooltip.install(this, tooltip); 1396 oldTooltip = tooltip; 1397 } 1398 }); 1399 listener.registerChangeListener(tab.disableProperty(), e -> { 1400 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable()); 1401 inner.requestLayout(); 1402 requestLayout(); 1403 }); 1404 listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle())); 1405 1406 tab.getStyleClass().addListener(weakStyleClassListener); 1407 1408 listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> { 1409 inner.requestLayout(); 1410 requestLayout(); 1411 }); 1412 listener.registerChangeListener(getSkinnable().sideProperty(),e -> { 1413 final Side side = getSkinnable().getSide(); 1414 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1415 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1416 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1417 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1418 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F); 1419 if (getSkinnable().isRotateGraphic()) { 1420 updateGraphicRotation(); 1421 } 1422 }); 1423 listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation()); 1424 listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> { 1425 requestLayout(); 1426 getSkinnable().requestLayout(); 1427 }); 1428 listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> { 1429 requestLayout(); 1430 getSkinnable().requestLayout(); 1431 }); 1432 listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> { 1433 requestLayout(); 1434 getSkinnable().requestLayout(); 1435 }); 1436 listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> { 1437 requestLayout(); 1438 getSkinnable().requestLayout(); 1439 }); 1440 1441 getProperties().put(Tab.class, tab); 1442 getProperties().put(ContextMenu.class, tab.getContextMenu()); 1443 1444 setOnContextMenuRequested((ContextMenuEvent me) -> { 1445 if (getTab().getContextMenu() != null) { 1446 getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY()); 1447 me.consume(); 1448 } 1449 }); 1450 setOnMousePressed(new EventHandler<MouseEvent>() { 1451 @Override public void handle(MouseEvent me) { 1452 if (getTab().isDisable()) { 1453 return; 1454 } 1455 if (me.getButton().equals(MouseButton.MIDDLE)) { 1456 if (showCloseButton()) { 1457 Tab tab = getTab(); 1458 if (behavior.canCloseTab(tab)) { 1459 removeListeners(tab); 1460 behavior.closeTab(tab); 1461 } 1462 } 1463 } else if (me.getButton().equals(MouseButton.PRIMARY)) { 1464 behavior.selectTab(getTab()); 1465 } 1466 } 1467 }); 1468 1469 // initialize pseudo-class state 1470 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1471 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisable()); 1472 final Side side = getSkinnable().getSide(); 1473 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1474 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1475 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1476 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1477 } 1478 1479 private void updateGraphicRotation() { 1480 if (label.getGraphic() != null) { 1481 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F : 1482 (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F : 1483 (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F))); 1484 } 1485 } 1486 1487 private boolean showCloseButton() { 1488 return tab.isClosable() && 1489 (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) || 1490 getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected()); 1491 } 1492 1493 private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) { 1494 @Override protected void invalidated() { 1495 requestLayout(); 1496 } 1497 }; 1498 1499 private void removeListeners(Tab tab) { 1500 listener.dispose(); 1501 inner.getChildren().clear(); 1502 getChildren().clear(); 1503 } 1504 1505 private TabAnimationState animationState = TabAnimationState.NONE; 1506 private Timeline currentAnimation; 1507 1508 @Override protected double computePrefWidth(double height) { 1509 // if (animating) { 1510 // return prefWidth.getValue(); 1511 // } 1512 double minWidth = snapSize(getSkinnable().getTabMinWidth()); 1513 double maxWidth = snapSize(getSkinnable().getTabMaxWidth()); 1514 double paddingRight = snappedRightInset(); 1515 double paddingLeft = snappedLeftInset(); 1516 double tmpPrefWidth = snapSize(label.prefWidth(-1)); 1517 1518 // only include the close button width if it is relevant 1519 if (showCloseButton()) { 1520 tmpPrefWidth += snapSize(closeBtn.prefWidth(-1)); 1521 } 1522 1523 if (tmpPrefWidth > maxWidth) { 1524 tmpPrefWidth = maxWidth; 1525 } else if (tmpPrefWidth < minWidth) { 1526 tmpPrefWidth = minWidth; 1527 } 1528 tmpPrefWidth += paddingRight + paddingLeft; 1529 // prefWidth.setValue(tmpPrefWidth); 1530 return tmpPrefWidth; 1531 } 1532 1533 @Override protected double computePrefHeight(double width) { 1534 double minHeight = snapSize(getSkinnable().getTabMinHeight()); 1535 double maxHeight = snapSize(getSkinnable().getTabMaxHeight()); 1536 double paddingTop = snappedTopInset(); 1537 double paddingBottom = snappedBottomInset(); 1538 double tmpPrefHeight = snapSize(label.prefHeight(width)); 1539 1540 if (tmpPrefHeight > maxHeight) { 1541 tmpPrefHeight = maxHeight; 1542 } else if (tmpPrefHeight < minHeight) { 1543 tmpPrefHeight = minHeight; 1544 } 1545 tmpPrefHeight += paddingTop + paddingBottom; 1546 return tmpPrefHeight; 1547 } 1548 1549 @Override protected void layoutChildren() { 1550 double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue(); 1551 inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset()); 1552 inner.relocate(snappedLeftInset(), snappedTopInset()); 1553 } 1554 1555 @Override protected void setWidth(double value) { 1556 super.setWidth(value); 1557 clip.setWidth(value); 1558 } 1559 1560 @Override protected void setHeight(double value) { 1561 super.setHeight(value); 1562 clip.setHeight(value); 1563 } 1564 1565 @Override 1566 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1567 switch (attribute) { 1568 case TEXT: return getTab().getText(); 1569 case SELECTED: return selectedTab == getTab(); 1570 default: return super.queryAccessibleAttribute(attribute, parameters); 1571 } 1572 } 1573 1574 @Override 1575 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1576 switch (action) { 1577 case REQUEST_FOCUS: 1578 getSkinnable().getSelectionModel().select(getTab()); 1579 break; 1580 default: super.executeAccessibleAction(action, parameters); 1581 } 1582 } 1583 1584 } /* End TabHeaderSkin */ 1585 1586 private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = 1587 PseudoClass.getPseudoClass("selected"); 1588 private static final PseudoClass TOP_PSEUDOCLASS_STATE = 1589 PseudoClass.getPseudoClass("top"); 1590 private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = 1591 PseudoClass.getPseudoClass("bottom"); 1592 private static final PseudoClass LEFT_PSEUDOCLASS_STATE = 1593 PseudoClass.getPseudoClass("left"); 1594 private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = 1595 PseudoClass.getPseudoClass("right"); 1596 private static final PseudoClass DISABLED_PSEUDOCLASS_STATE = 1597 PseudoClass.getPseudoClass("disabled"); 1598 1599 1600 /************************************************************************** 1601 * 1602 * TabContentRegion: each tab has one to contain the tab's content node 1603 * 1604 **************************************************************************/ 1605 class TabContentRegion extends StackPane { 1606 1607 private TraversalEngine engine; 1608 private Direction direction = Direction.NEXT; 1609 private Tab tab; 1610 1611 private InvalidationListener tabContentListener = valueModel -> { 1612 updateContent(); 1613 }; 1614 private InvalidationListener tabSelectedListener = new InvalidationListener() { 1615 @Override public void invalidated(Observable valueModel) { 1616 setVisible(tab.isSelected()); 1617 } 1618 }; 1619 1620 private WeakInvalidationListener weakTabContentListener = 1621 new WeakInvalidationListener(tabContentListener); 1622 private WeakInvalidationListener weakTabSelectedListener = 1623 new WeakInvalidationListener(tabSelectedListener); 1624 1625 public Tab getTab() { 1626 return tab; 1627 } 1628 1629 public TabContentRegion(Tab tab) { 1630 getStyleClass().setAll("tab-content-area"); 1631 setManaged(false); 1632 this.tab = tab; 1633 updateContent(); 1634 setVisible(tab.isSelected()); 1635 1636 tab.selectedProperty().addListener(weakTabSelectedListener); 1637 tab.contentProperty().addListener(weakTabContentListener); 1638 } 1639 1640 private void updateContent() { 1641 Node newContent = getTab().getContent(); 1642 if (newContent == null) { 1643 getChildren().clear(); 1644 } else { 1645 getChildren().setAll(newContent); 1646 } 1647 } 1648 1649 private void removeListeners(Tab tab) { 1650 tab.selectedProperty().removeListener(weakTabSelectedListener); 1651 tab.contentProperty().removeListener(weakTabContentListener); 1652 } 1653 1654 } /* End TabContentRegion */ 1655 1656 /************************************************************************** 1657 * 1658 * TabControlButtons: controls to manipulate tab interaction 1659 * 1660 **************************************************************************/ 1661 class TabControlButtons extends StackPane { 1662 private StackPane inner; 1663 private StackPane downArrow; 1664 private Pane downArrowBtn; 1665 private boolean showControlButtons; 1666 private ContextMenu popup; 1667 1668 public TabControlButtons() { 1669 getStyleClass().setAll("control-buttons-tab"); 1670 1671 TabPane tabPane = getSkinnable(); 1672 1673 downArrowBtn = new Pane(); 1674 downArrowBtn.getStyleClass().setAll("tab-down-button"); 1675 downArrowBtn.setVisible(isShowTabsMenu()); 1676 downArrow = new StackPane(); 1677 downArrow.setManaged(false); 1678 downArrow.getStyleClass().setAll("arrow"); 1679 downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1680 downArrowBtn.getChildren().add(downArrow); 1681 downArrowBtn.setOnMouseClicked(me -> { 1682 showPopupMenu(); 1683 }); 1684 1685 setupPopupMenu(); 1686 1687 inner = new StackPane() { 1688 @Override protected double computePrefWidth(double height) { 1689 double pw; 1690 double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight())); 1691 pw = 0.0F; 1692 if (isShowTabsMenu()) { 1693 pw += maxArrowWidth; 1694 } 1695 if (pw > 0) { 1696 pw += snappedLeftInset() + snappedRightInset(); 1697 } 1698 return pw; 1699 } 1700 1701 @Override protected double computePrefHeight(double width) { 1702 double height = 0.0F; 1703 if (isShowTabsMenu()) { 1704 height = Math.max(height, snapSize(downArrowBtn.prefHeight(width))); 1705 } 1706 if (height > 0) { 1707 height += snappedTopInset() + snappedBottomInset(); 1708 } 1709 return height; 1710 } 1711 1712 @Override protected void layoutChildren() { 1713 if (isShowTabsMenu()) { 1714 double x = 0; 1715 double y = snappedTopInset(); 1716 double w = snapSize(getWidth()) - x + snappedLeftInset(); 1717 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1718 positionArrow(downArrowBtn, downArrow, x, y, w, h); 1719 } 1720 } 1721 1722 private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) { 1723 btn.resize(width, height); 1724 positionInArea(btn, x, y, width, height, /*baseline ignored*/0, 1725 HPos.CENTER, VPos.CENTER); 1726 // center arrow region within arrow button 1727 double arrowWidth = snapSize(arrow.prefWidth(-1)); 1728 double arrowHeight = snapSize(arrow.prefHeight(-1)); 1729 arrow.resize(arrowWidth, arrowHeight); 1730 positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(), 1731 width - btn.snappedLeftInset() - btn.snappedRightInset(), 1732 height - btn.snappedTopInset() - btn.snappedBottomInset(), 1733 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1734 } 1735 }; 1736 inner.getStyleClass().add("container"); 1737 inner.getChildren().add(downArrowBtn); 1738 1739 getChildren().add(inner); 1740 1741 tabPane.sideProperty().addListener(valueModel -> { 1742 Side tabPosition = getSkinnable().getSide(); 1743 downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F); 1744 }); 1745 tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu()); 1746 showControlButtons = false; 1747 if (isShowTabsMenu()) { 1748 showControlButtons = true; 1749 requestLayout(); 1750 } 1751 getProperties().put(ContextMenu.class, popup); 1752 } 1753 1754 private boolean showTabsMenu = false; 1755 1756 private void showTabsMenu(boolean value) { 1757 final boolean wasTabsMenuShowing = isShowTabsMenu(); 1758 this.showTabsMenu = value; 1759 1760 if (showTabsMenu && !wasTabsMenuShowing) { 1761 downArrowBtn.setVisible(true); 1762 showControlButtons = true; 1763 inner.requestLayout(); 1764 tabHeaderArea.requestLayout(); 1765 } else if (!showTabsMenu && wasTabsMenuShowing) { 1766 hideControlButtons(); 1767 } 1768 } 1769 1770 private boolean isShowTabsMenu() { 1771 return showTabsMenu; 1772 } 1773 1774 @Override protected double computePrefWidth(double height) { 1775 double pw = snapSize(inner.prefWidth(height)); 1776 if (pw > 0) { 1777 pw += snappedLeftInset() + snappedRightInset(); 1778 } 1779 return pw; 1780 } 1781 1782 @Override protected double computePrefHeight(double width) { 1783 return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) + 1784 snappedTopInset() + snappedBottomInset(); 1785 } 1786 1787 @Override protected void layoutChildren() { 1788 double x = snappedLeftInset(); 1789 double y = snappedTopInset(); 1790 double w = snapSize(getWidth()) - x + snappedRightInset(); 1791 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1792 1793 if (showControlButtons) { 1794 showControlButtons(); 1795 showControlButtons = false; 1796 } 1797 1798 inner.resize(w, h); 1799 positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM); 1800 } 1801 1802 private void showControlButtons() { 1803 setVisible(true); 1804 if (popup == null) { 1805 setupPopupMenu(); 1806 } 1807 } 1808 1809 private void hideControlButtons() { 1810 // If the scroll arrows or tab menu is still visible we don't want 1811 // to hide it animate it back it. 1812 if (isShowTabsMenu()) { 1813 showControlButtons = true; 1814 } else { 1815 setVisible(false); 1816 popup.getItems().clear(); 1817 popup = null; 1818 } 1819 1820 // This needs to be called when we are in the left tabPosition 1821 // to allow for the clip offset to move properly (otherwise 1822 // it jumps too early - before the animation is done). 1823 requestLayout(); 1824 } 1825 1826 private void setupPopupMenu() { 1827 if (popup == null) { 1828 popup = new ContextMenu(); 1829 } 1830 popup.getItems().clear(); 1831 ToggleGroup group = new ToggleGroup(); 1832 ObservableList<RadioMenuItem> menuitems = FXCollections.<RadioMenuItem>observableArrayList(); 1833 for (final Tab tab : getSkinnable().getTabs()) { 1834 TabMenuItem item = new TabMenuItem(tab); 1835 item.setToggleGroup(group); 1836 item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab)); 1837 menuitems.add(item); 1838 } 1839 popup.getItems().addAll(menuitems); 1840 } 1841 1842 private void showPopupMenu() { 1843 for (MenuItem mi: popup.getItems()) { 1844 TabMenuItem tmi = (TabMenuItem)mi; 1845 if (selectedTab.equals(tmi.getTab())) { 1846 tmi.setSelected(true); 1847 break; 1848 } 1849 } 1850 popup.show(downArrowBtn, Side.BOTTOM, 0, 0); 1851 } 1852 } /* End TabControlButtons*/ 1853 1854 class TabMenuItem extends RadioMenuItem { 1855 Tab tab; 1856 1857 private InvalidationListener disableListener = new InvalidationListener() { 1858 @Override public void invalidated(Observable o) { 1859 setDisable(tab.isDisable()); 1860 } 1861 }; 1862 1863 private WeakInvalidationListener weakDisableListener = 1864 new WeakInvalidationListener(disableListener); 1865 1866 public TabMenuItem(final Tab tab) { 1867 super(tab.getText(), TabPaneSkin.clone(tab.getGraphic())); 1868 this.tab = tab; 1869 setDisable(tab.isDisable()); 1870 tab.disableProperty().addListener(weakDisableListener); 1871 textProperty().bind(tab.textProperty()); 1872 } 1873 1874 public Tab getTab() { 1875 return tab; 1876 } 1877 1878 public void dispose() { 1879 tab.disableProperty().removeListener(weakDisableListener); 1880 } 1881 } 1882 1883 @Override 1884 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1885 switch (attribute) { 1886 case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab); 1887 case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size(); 1888 case ITEM_AT_INDEX: { 1889 Integer index = (Integer)parameters[0]; 1890 if (index == null) return null; 1891 return tabHeaderArea.headersRegion.getChildren().get(index); 1892 } 1893 default: return super.queryAccessibleAttribute(attribute, parameters); 1894 } 1895 } 1896 }