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