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