1 /* 2 * Copyright (c) 2011, 2017, 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 * @return the CssMetaData associated with this class, which may include the 756 * CssMetaData of its superclasses 757 */ 758 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 759 return StyleableProperties.STYLEABLES; 760 } 761 762 /** 763 * {@inheritDoc} 764 */ 765 @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() { 766 return getClassCssMetaData(); 767 } 768 769 770 771 /*************************************************************************** 772 * * 773 * Support classes * 774 * * 775 **************************************************************************/ 776 777 /************************************************************************** 778 * 779 * TabHeaderArea: Area responsible for painting all tabs 780 * 781 **************************************************************************/ 782 class TabHeaderArea extends StackPane { 783 private Rectangle headerClip; 784 private StackPane headersRegion; 785 private StackPane headerBackground; 786 private TabControlButtons controlButtons; 787 788 private boolean measureClosingTabs = false; 789 790 private double scrollOffset; 791 792 public TabHeaderArea() { 793 getStyleClass().setAll("tab-header-area"); 794 setManaged(false); 795 final TabPane tabPane = getSkinnable(); 796 797 headerClip = new Rectangle(); 798 799 headersRegion = new StackPane() { 800 @Override protected double computePrefWidth(double height) { 801 double width = 0.0F; 802 for (Node child : getChildren()) { 803 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 804 if (tabHeaderSkin.isVisible() && (measureClosingTabs || ! tabHeaderSkin.isClosing)) { 805 width += tabHeaderSkin.prefWidth(height); 806 } 807 } 808 return snapSize(width) + snappedLeftInset() + snappedRightInset(); 809 } 810 811 @Override protected double computePrefHeight(double width) { 812 double height = 0.0F; 813 for (Node child : getChildren()) { 814 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 815 height = Math.max(height, tabHeaderSkin.prefHeight(width)); 816 } 817 return snapSize(height) + snappedTopInset() + snappedBottomInset(); 818 } 819 820 @Override protected void layoutChildren() { 821 if (tabsFit()) { 822 setScrollOffset(0.0); 823 } else { 824 if (!removeTab.isEmpty()) { 825 double offset = 0; 826 double w = tabHeaderArea.getWidth() - snapSize(controlButtons.prefWidth(-1)) - firstTabIndent() - SPACER; 827 Iterator<Node> i = getChildren().iterator(); 828 while (i.hasNext()) { 829 TabHeaderSkin tabHeader = (TabHeaderSkin)i.next(); 830 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 831 if (removeTab.contains(tabHeader)) { 832 if (offset < w) { 833 isSelectingTab = true; 834 } 835 i.remove(); 836 removeTab.remove(tabHeader); 837 if (removeTab.isEmpty()) { 838 break; 839 } 840 } 841 offset += tabHeaderPrefWidth; 842 } 843 // } else { 844 // isSelectingTab = true; 845 } 846 } 847 848 if (isSelectingTab) { 849 ensureSelectedTabIsVisible(); 850 isSelectingTab = false; 851 } else { 852 validateScrollOffset(); 853 } 854 855 Side tabPosition = getSkinnable().getSide(); 856 double tabBackgroundHeight = snapSize(prefHeight(-1)); 857 double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ? 858 snapSize(getWidth()) - getScrollOffset() : getScrollOffset(); 859 860 updateHeaderClip(); 861 for (Node node : getChildren()) { 862 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 863 864 // size and position the header relative to the other headers 865 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get()); 866 double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1)); 867 tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight); 868 869 // This ensures that the tabs are located in the correct position 870 // when there are tabs of differing heights. 871 double startY = tabPosition.equals(Side.BOTTOM) ? 872 0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset(); 873 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 874 // build from the right 875 tabX -= tabHeaderPrefWidth; 876 tabHeader.relocate(tabX, startY); 877 } else { 878 // build from the left 879 tabHeader.relocate(tabX, startY); 880 tabX += tabHeaderPrefWidth; 881 } 882 } 883 } 884 885 }; 886 headersRegion.getStyleClass().setAll("headers-region"); 887 headersRegion.setClip(headerClip); 888 889 headerBackground = new StackPane(); 890 headerBackground.getStyleClass().setAll("tab-header-background"); 891 892 int i = 0; 893 for (Tab tab: tabPane.getTabs()) { 894 addTab(tab, i++); 895 } 896 897 controlButtons = new TabControlButtons(); 898 controlButtons.setVisible(false); 899 if (controlButtons.isVisible()) { 900 controlButtons.setVisible(true); 901 } 902 getChildren().addAll(headerBackground, headersRegion, controlButtons); 903 904 // support for mouse scroll of header area (for when the tabs exceed 905 // the available space). 906 // Scrolling the mouse wheel downwards results in the tabs scrolling left (i.e. exposing the right-most tabs) 907 // Scrolling the mouse wheel upwards results in the tabs scrolling right (i.e. exposing th left-most tabs) 908 addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> { 909 Side side = getSkinnable().getSide(); 910 side = side == null ? Side.TOP : side; 911 switch (side) { 912 default: 913 case TOP: 914 case BOTTOM: 915 setScrollOffset(scrollOffset + e.getDeltaY()); 916 break; 917 case LEFT: 918 case RIGHT: 919 setScrollOffset(scrollOffset - e.getDeltaY()); 920 break; 921 } 922 923 }); 924 } 925 926 private void updateHeaderClip() { 927 Side tabPosition = getSkinnable().getSide(); 928 929 double x = 0; 930 double y = 0; 931 double clipWidth = 0; 932 double clipHeight = 0; 933 double maxWidth = 0; 934 double shadowRadius = 0; 935 double clipOffset = firstTabIndent(); 936 double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1)); 937 938 measureClosingTabs = true; 939 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 940 measureClosingTabs = false; 941 942 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 943 944 // Add the spacer if isShowTabsMenu is true. 945 if (controlButtonPrefWidth > 0) { 946 controlButtonPrefWidth = controlButtonPrefWidth + SPACER; 947 } 948 949 if (headersRegion.getEffect() instanceof DropShadow) { 950 DropShadow shadow = (DropShadow)headersRegion.getEffect(); 951 shadowRadius = shadow.getRadius(); 952 } 953 954 maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset; 955 if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) { 956 if (headersPrefWidth < maxWidth) { 957 clipWidth = headersPrefWidth + shadowRadius; 958 } else { 959 x = headersPrefWidth - maxWidth; 960 clipWidth = maxWidth + shadowRadius; 961 } 962 clipHeight = headersPrefHeight; 963 } else { 964 // If x = 0 the header region's drop shadow is clipped. 965 x = -shadowRadius; 966 clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius; 967 clipHeight = headersPrefHeight; 968 } 969 970 headerClip.setX(x); 971 headerClip.setY(y); 972 headerClip.setWidth(clipWidth); 973 headerClip.setHeight(clipHeight); 974 } 975 976 private void addTab(Tab tab, int addToIndex) { 977 TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab); 978 headersRegion.getChildren().add(addToIndex, tabHeaderSkin); 979 } 980 981 private List<TabHeaderSkin> removeTab = new ArrayList<>(); 982 private void removeTab(Tab tab) { 983 TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab); 984 if (tabHeaderSkin != null) { 985 if (tabsFit()) { 986 headersRegion.getChildren().remove(tabHeaderSkin); 987 } else { 988 // The tab will be removed during layout because 989 // we need its width to compute the scroll offset. 990 removeTab.add(tabHeaderSkin); 991 tabHeaderSkin.removeListeners(tab); 992 } 993 } 994 } 995 996 private TabHeaderSkin getTabHeaderSkin(Tab tab) { 997 for (Node child: headersRegion.getChildren()) { 998 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child; 999 if (tabHeaderSkin.getTab().equals(tab)) { 1000 return tabHeaderSkin; 1001 } 1002 } 1003 return null; 1004 } 1005 1006 private boolean tabsFit() { 1007 double headerPrefWidth = snapSize(headersRegion.prefWidth(-1)); 1008 double controlTabWidth = snapSize(controlButtons.prefWidth(-1)); 1009 double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER; 1010 return visibleWidth < getWidth(); 1011 } 1012 1013 private void ensureSelectedTabIsVisible() { 1014 // work out the visible width of the tab header 1015 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1016 double controlTabWidth = snapSize(controlButtons.getWidth()); 1017 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1018 1019 // and get where the selected tab is in the header area 1020 double offset = 0.0; 1021 double selectedTabOffset = 0.0; 1022 double selectedTabWidth = 0.0; 1023 for (Node node : headersRegion.getChildren()) { 1024 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1025 1026 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1027 1028 if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) { 1029 selectedTabOffset = offset; 1030 selectedTabWidth = tabHeaderPrefWidth; 1031 } 1032 offset += tabHeaderPrefWidth; 1033 } 1034 1035 final double scrollOffset = getScrollOffset(); 1036 final double selectedTabStartX = selectedTabOffset; 1037 final double selectedTabEndX = selectedTabOffset + selectedTabWidth; 1038 1039 final double visibleAreaEndX = visibleWidth; 1040 1041 if (selectedTabStartX < -scrollOffset) { 1042 setScrollOffset(-selectedTabStartX); 1043 } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) { 1044 setScrollOffset(visibleAreaEndX - selectedTabEndX); 1045 } 1046 } 1047 1048 public double getScrollOffset() { 1049 return scrollOffset; 1050 } 1051 1052 private void validateScrollOffset() { 1053 setScrollOffset(getScrollOffset()); 1054 } 1055 1056 private void setScrollOffset(double newScrollOffset) { 1057 // work out the visible width of the tab header 1058 double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight()); 1059 double controlTabWidth = snapSize(controlButtons.getWidth()); 1060 double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER; 1061 1062 // measure the width of all tabs 1063 double offset = 0.0; 1064 for (Node node : headersRegion.getChildren()) { 1065 TabHeaderSkin tabHeader = (TabHeaderSkin)node; 1066 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1)); 1067 offset += tabHeaderPrefWidth; 1068 } 1069 1070 double actualNewScrollOffset; 1071 1072 if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) { 1073 // need to make sure the right-most tab is attached to the 1074 // right-hand side of the tab header (e.g. if the tab header area width 1075 // is expanded), and if it isn't modify the scroll offset to bring 1076 // it into line. See RT-35194 for a test case. 1077 actualNewScrollOffset = visibleWidth - offset; 1078 } else if (newScrollOffset > 0) { 1079 // need to prevent the left-most tab from becoming detached 1080 // from the left-hand side of the tab header. 1081 actualNewScrollOffset = 0; 1082 } else { 1083 actualNewScrollOffset = newScrollOffset; 1084 } 1085 1086 if (Math.abs(actualNewScrollOffset - scrollOffset) > 0.001) { 1087 scrollOffset = actualNewScrollOffset; 1088 headersRegion.requestLayout(); 1089 } 1090 } 1091 1092 private double firstTabIndent() { 1093 switch (getSkinnable().getSide()) { 1094 case TOP: 1095 case BOTTOM: 1096 return snappedLeftInset(); 1097 case RIGHT: 1098 case LEFT: 1099 return snappedTopInset(); 1100 default: 1101 return 0; 1102 } 1103 } 1104 1105 @Override protected double computePrefWidth(double height) { 1106 double padding = isHorizontal() ? 1107 snappedLeftInset() + snappedRightInset() : 1108 snappedTopInset() + snappedBottomInset(); 1109 return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) + 1110 firstTabIndent() + SPACER + padding; 1111 } 1112 1113 @Override protected double computePrefHeight(double width) { 1114 double padding = isHorizontal() ? 1115 snappedTopInset() + snappedBottomInset() : 1116 snappedLeftInset() + snappedRightInset(); 1117 return snapSize(headersRegion.prefHeight(-1)) + padding; 1118 } 1119 1120 @Override public double getBaselineOffset() { 1121 if (getSkinnable().getSide() == Side.TOP) { 1122 return headersRegion.getBaselineOffset() + snappedTopInset(); 1123 } 1124 return 0; 1125 } 1126 1127 @Override protected void layoutChildren() { 1128 final double leftInset = snappedLeftInset(); 1129 final double rightInset = snappedRightInset(); 1130 final double topInset = snappedTopInset(); 1131 final double bottomInset = snappedBottomInset(); 1132 double w = snapSize(getWidth()) - (isHorizontal() ? 1133 leftInset + rightInset : topInset + bottomInset); 1134 double h = snapSize(getHeight()) - (isHorizontal() ? 1135 topInset + bottomInset : leftInset + rightInset); 1136 double tabBackgroundHeight = snapSize(prefHeight(-1)); 1137 double headersPrefWidth = snapSize(headersRegion.prefWidth(-1)); 1138 double headersPrefHeight = snapSize(headersRegion.prefHeight(-1)); 1139 1140 controlButtons.showTabsMenu(! tabsFit()); 1141 1142 updateHeaderClip(); 1143 headersRegion.requestLayout(); 1144 1145 // RESIZE CONTROL BUTTONS 1146 double btnWidth = snapSize(controlButtons.prefWidth(-1)); 1147 final double btnHeight = controlButtons.prefHeight(btnWidth); 1148 controlButtons.resize(btnWidth, btnHeight); 1149 1150 // POSITION TABS 1151 headersRegion.resize(headersPrefWidth, headersPrefHeight); 1152 1153 if (isFloatingStyleClass()) { 1154 headerBackground.setVisible(false); 1155 } else { 1156 headerBackground.resize(snapSize(getWidth()), snapSize(getHeight())); 1157 headerBackground.setVisible(true); 1158 } 1159 1160 double startX = 0; 1161 double startY = 0; 1162 double controlStartX = 0; 1163 double controlStartY = 0; 1164 Side tabPosition = getSkinnable().getSide(); 1165 1166 if (tabPosition.equals(Side.TOP)) { 1167 startX = leftInset; 1168 startY = tabBackgroundHeight - headersPrefHeight - bottomInset; 1169 controlStartX = w - btnWidth + leftInset; 1170 controlStartY = snapSize(getHeight()) - btnHeight - bottomInset; 1171 } else if (tabPosition.equals(Side.RIGHT)) { 1172 startX = topInset; 1173 startY = tabBackgroundHeight - headersPrefHeight - leftInset; 1174 controlStartX = w - btnWidth + topInset; 1175 controlStartY = snapSize(getHeight()) - btnHeight - leftInset; 1176 } else if (tabPosition.equals(Side.BOTTOM)) { 1177 startX = snapSize(getWidth()) - headersPrefWidth - leftInset; 1178 startY = tabBackgroundHeight - headersPrefHeight - topInset; 1179 controlStartX = rightInset; 1180 controlStartY = snapSize(getHeight()) - btnHeight - topInset; 1181 } else if (tabPosition.equals(Side.LEFT)) { 1182 startX = snapSize(getWidth()) - headersPrefWidth - topInset; 1183 startY = tabBackgroundHeight - headersPrefHeight - rightInset; 1184 controlStartX = leftInset; 1185 controlStartY = snapSize(getHeight()) - btnHeight - rightInset; 1186 } 1187 if (headerBackground.isVisible()) { 1188 positionInArea(headerBackground, 0, 0, 1189 snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1190 } 1191 positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER); 1192 positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight, 1193 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1194 } 1195 } /* End TabHeaderArea */ 1196 1197 1198 1199 1200 /************************************************************************** 1201 * 1202 * TabHeaderSkin: skin for each tab 1203 * 1204 **************************************************************************/ 1205 1206 class TabHeaderSkin extends StackPane { 1207 private final Tab tab; 1208 public Tab getTab() { 1209 return tab; 1210 } 1211 private Label label; 1212 private StackPane closeBtn; 1213 private StackPane inner; 1214 private Tooltip oldTooltip; 1215 private Tooltip tooltip; 1216 private Rectangle clip; 1217 1218 private boolean isClosing = false; 1219 1220 private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler(); 1221 1222 private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() { 1223 @Override 1224 public void onChanged(Change<? extends String> c) { 1225 getStyleClass().setAll(tab.getStyleClass()); 1226 } 1227 }; 1228 1229 private final WeakListChangeListener<String> weakStyleClassListener = 1230 new WeakListChangeListener<>(styleClassListener); 1231 1232 public TabHeaderSkin(final Tab tab) { 1233 getStyleClass().setAll(tab.getStyleClass()); 1234 setId(tab.getId()); 1235 setStyle(tab.getStyle()); 1236 setAccessibleRole(AccessibleRole.TAB_ITEM); 1237 1238 this.tab = tab; 1239 clip = new Rectangle(); 1240 setClip(clip); 1241 1242 label = new Label(tab.getText(), tab.getGraphic()); 1243 label.getStyleClass().setAll("tab-label"); 1244 1245 closeBtn = new StackPane() { 1246 @Override protected double computePrefWidth(double h) { 1247 return CLOSE_BTN_SIZE; 1248 } 1249 @Override protected double computePrefHeight(double w) { 1250 return CLOSE_BTN_SIZE; 1251 } 1252 @Override 1253 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1254 switch (action) { 1255 case FIRE: { 1256 Tab tab = getTab(); 1257 if (behavior.canCloseTab(tab)) { 1258 behavior.closeTab(tab); 1259 setOnMousePressed(null); 1260 } 1261 break; 1262 } 1263 default: super.executeAccessibleAction(action, parameters); 1264 } 1265 } 1266 }; 1267 closeBtn.setAccessibleRole(AccessibleRole.BUTTON); 1268 closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton")); 1269 closeBtn.getStyleClass().setAll("tab-close-button"); 1270 closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() { 1271 @Override 1272 public void handle(MouseEvent me) { 1273 Tab tab = getTab(); 1274 if (behavior.canCloseTab(tab)) { 1275 behavior.closeTab(tab); 1276 setOnMousePressed(null); 1277 me.consume(); 1278 } 1279 } 1280 }); 1281 1282 updateGraphicRotation(); 1283 1284 final Region focusIndicator = new Region(); 1285 focusIndicator.setMouseTransparent(true); 1286 focusIndicator.getStyleClass().add("focus-indicator"); 1287 1288 inner = new StackPane() { 1289 @Override protected void layoutChildren() { 1290 final TabPane skinnable = getSkinnable(); 1291 1292 final double paddingTop = snappedTopInset(); 1293 final double paddingRight = snappedRightInset(); 1294 final double paddingBottom = snappedBottomInset(); 1295 final double paddingLeft = snappedLeftInset(); 1296 final double w = getWidth() - (paddingLeft + paddingRight); 1297 final double h = getHeight() - (paddingTop + paddingBottom); 1298 1299 final double prefLabelWidth = snapSize(label.prefWidth(-1)); 1300 final double prefLabelHeight = snapSize(label.prefHeight(-1)); 1301 1302 final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0; 1303 final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0; 1304 final double minWidth = snapSize(skinnable.getTabMinWidth()); 1305 final double maxWidth = snapSize(skinnable.getTabMaxWidth()); 1306 final double maxHeight = snapSize(skinnable.getTabMaxHeight()); 1307 1308 double labelAreaWidth = prefLabelWidth; 1309 double labelWidth = prefLabelWidth; 1310 double labelHeight = prefLabelHeight; 1311 1312 final double childrenWidth = labelAreaWidth + closeBtnWidth; 1313 final double childrenHeight = Math.max(labelHeight, closeBtnHeight); 1314 1315 if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) { 1316 labelAreaWidth = maxWidth - closeBtnWidth; 1317 labelWidth = maxWidth - closeBtnWidth; 1318 } else if (childrenWidth < minWidth) { 1319 labelAreaWidth = minWidth - closeBtnWidth; 1320 } 1321 1322 if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) { 1323 labelHeight = maxHeight; 1324 } 1325 1326 if (animationState != TabAnimationState.NONE) { 1327 // if (prefWidth.getValue() < labelAreaWidth) { 1328 // labelAreaWidth = prefWidth.getValue(); 1329 // } 1330 labelAreaWidth *= animationTransition.get(); 1331 closeBtn.setVisible(false); 1332 } else { 1333 closeBtn.setVisible(showCloseButton()); 1334 } 1335 1336 1337 label.resize(labelWidth, labelHeight); 1338 1339 1340 double labelStartX = paddingLeft; 1341 1342 // If maxWidth is less than Double.MAX_VALUE, the user has 1343 // clamped the max width, but we should 1344 // position the close button at the end of the tab, 1345 // which may not necessarily be the entire width of the 1346 // provided max width. 1347 double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth; 1348 1349 positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h, 1350 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1351 1352 if (closeBtn.isVisible()) { 1353 closeBtn.resize(closeBtnWidth, closeBtnHeight); 1354 positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h, 1355 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1356 } 1357 1358 // Magic numbers regretfully introduced for RT-28944 (so that 1359 // the focus rect appears as expected on Windows and Mac). 1360 // In short we use the vPadding to shift the focus rect down 1361 // into the content area (whereas previously it was being clipped 1362 // on Windows, whilst it still looked fine on Mac). In the 1363 // future we may want to improve this code to remove the 1364 // magic number. Similarly, the hPadding differs on Mac. 1365 final int vPadding = Utils.isMac() ? 2 : 3; 1366 final int hPadding = Utils.isMac() ? 2 : 1; 1367 focusIndicator.resizeRelocate( 1368 paddingLeft - hPadding, 1369 paddingTop + vPadding, 1370 w + 2 * hPadding, 1371 h - 2 * vPadding); 1372 } 1373 }; 1374 inner.getStyleClass().add("tab-container"); 1375 inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1376 inner.getChildren().addAll(label, closeBtn, focusIndicator); 1377 1378 getChildren().addAll(inner); 1379 1380 tooltip = tab.getTooltip(); 1381 if (tooltip != null) { 1382 Tooltip.install(this, tooltip); 1383 oldTooltip = tooltip; 1384 } 1385 1386 listener.registerChangeListener(tab.closableProperty(), e -> { 1387 inner.requestLayout(); 1388 requestLayout(); 1389 }); 1390 listener.registerChangeListener(tab.selectedProperty(), e -> { 1391 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1392 // Need to request a layout pass for inner because if the width 1393 // and height didn't not change the label or close button may have 1394 // changed. 1395 inner.requestLayout(); 1396 requestLayout(); 1397 }); 1398 listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText())); 1399 listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic())); 1400 listener.registerChangeListener(tab.tooltipProperty(), e -> { 1401 // uninstall the old tooltip 1402 if (oldTooltip != null) { 1403 Tooltip.uninstall(this, oldTooltip); 1404 } 1405 tooltip = tab.getTooltip(); 1406 if (tooltip != null) { 1407 // install new tooltip and save as old tooltip. 1408 Tooltip.install(this, tooltip); 1409 oldTooltip = tooltip; 1410 } 1411 }); 1412 listener.registerChangeListener(tab.disabledProperty(), e -> { 1413 updateTabDisabledState(); 1414 }); 1415 listener.registerChangeListener(tab.getTabPane().disabledProperty(), e -> { 1416 updateTabDisabledState(); 1417 }); 1418 listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle())); 1419 1420 tab.getStyleClass().addListener(weakStyleClassListener); 1421 1422 listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> { 1423 inner.requestLayout(); 1424 requestLayout(); 1425 }); 1426 listener.registerChangeListener(getSkinnable().sideProperty(),e -> { 1427 final Side side = getSkinnable().getSide(); 1428 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1429 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1430 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1431 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1432 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F); 1433 if (getSkinnable().isRotateGraphic()) { 1434 updateGraphicRotation(); 1435 } 1436 }); 1437 listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation()); 1438 listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> { 1439 requestLayout(); 1440 getSkinnable().requestLayout(); 1441 }); 1442 listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> { 1443 requestLayout(); 1444 getSkinnable().requestLayout(); 1445 }); 1446 listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> { 1447 requestLayout(); 1448 getSkinnable().requestLayout(); 1449 }); 1450 listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> { 1451 requestLayout(); 1452 getSkinnable().requestLayout(); 1453 }); 1454 1455 getProperties().put(Tab.class, tab); 1456 getProperties().put(ContextMenu.class, tab.getContextMenu()); 1457 1458 setOnContextMenuRequested((ContextMenuEvent me) -> { 1459 if (getTab().getContextMenu() != null) { 1460 getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY()); 1461 me.consume(); 1462 } 1463 }); 1464 setOnMousePressed(new EventHandler<MouseEvent>() { 1465 @Override public void handle(MouseEvent me) { 1466 if (getTab().isDisable()) { 1467 return; 1468 } 1469 if (me.getButton().equals(MouseButton.MIDDLE)) { 1470 if (showCloseButton()) { 1471 Tab tab = getTab(); 1472 if (behavior.canCloseTab(tab)) { 1473 removeListeners(tab); 1474 behavior.closeTab(tab); 1475 } 1476 } 1477 } else if (me.getButton().equals(MouseButton.PRIMARY)) { 1478 behavior.selectTab(getTab()); 1479 } 1480 } 1481 }); 1482 1483 // initialize pseudo-class state 1484 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected()); 1485 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled()); 1486 final Side side = getSkinnable().getSide(); 1487 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP)); 1488 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT)); 1489 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM)); 1490 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT)); 1491 } 1492 1493 private void updateTabDisabledState() { 1494 pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled()); 1495 inner.requestLayout(); 1496 requestLayout(); 1497 } 1498 1499 private void updateGraphicRotation() { 1500 if (label.getGraphic() != null) { 1501 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F : 1502 (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F : 1503 (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F))); 1504 } 1505 } 1506 1507 private boolean showCloseButton() { 1508 return tab.isClosable() && 1509 (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) || 1510 getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected()); 1511 } 1512 1513 private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) { 1514 @Override protected void invalidated() { 1515 requestLayout(); 1516 } 1517 }; 1518 1519 private void removeListeners(Tab tab) { 1520 listener.dispose(); 1521 inner.getChildren().clear(); 1522 getChildren().clear(); 1523 setOnContextMenuRequested(null); 1524 setOnMousePressed(null); 1525 } 1526 1527 private TabAnimationState animationState = TabAnimationState.NONE; 1528 private Timeline currentAnimation; 1529 1530 @Override protected double computePrefWidth(double height) { 1531 // if (animating) { 1532 // return prefWidth.getValue(); 1533 // } 1534 double minWidth = snapSize(getSkinnable().getTabMinWidth()); 1535 double maxWidth = snapSize(getSkinnable().getTabMaxWidth()); 1536 double paddingRight = snappedRightInset(); 1537 double paddingLeft = snappedLeftInset(); 1538 double tmpPrefWidth = snapSize(label.prefWidth(-1)); 1539 1540 // only include the close button width if it is relevant 1541 if (showCloseButton()) { 1542 tmpPrefWidth += snapSize(closeBtn.prefWidth(-1)); 1543 } 1544 1545 if (tmpPrefWidth > maxWidth) { 1546 tmpPrefWidth = maxWidth; 1547 } else if (tmpPrefWidth < minWidth) { 1548 tmpPrefWidth = minWidth; 1549 } 1550 tmpPrefWidth += paddingRight + paddingLeft; 1551 // prefWidth.setValue(tmpPrefWidth); 1552 return tmpPrefWidth; 1553 } 1554 1555 @Override protected double computePrefHeight(double width) { 1556 double minHeight = snapSize(getSkinnable().getTabMinHeight()); 1557 double maxHeight = snapSize(getSkinnable().getTabMaxHeight()); 1558 double paddingTop = snappedTopInset(); 1559 double paddingBottom = snappedBottomInset(); 1560 double tmpPrefHeight = snapSize(label.prefHeight(width)); 1561 1562 if (tmpPrefHeight > maxHeight) { 1563 tmpPrefHeight = maxHeight; 1564 } else if (tmpPrefHeight < minHeight) { 1565 tmpPrefHeight = minHeight; 1566 } 1567 tmpPrefHeight += paddingTop + paddingBottom; 1568 return tmpPrefHeight; 1569 } 1570 1571 @Override protected void layoutChildren() { 1572 double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue(); 1573 inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset()); 1574 inner.relocate(snappedLeftInset(), snappedTopInset()); 1575 } 1576 1577 @Override protected void setWidth(double value) { 1578 super.setWidth(value); 1579 clip.setWidth(value); 1580 } 1581 1582 @Override protected void setHeight(double value) { 1583 super.setHeight(value); 1584 clip.setHeight(value); 1585 } 1586 1587 /** {@inheritDoc} */ 1588 @Override 1589 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1590 switch (attribute) { 1591 case TEXT: return getTab().getText(); 1592 case SELECTED: return selectedTab == getTab(); 1593 default: return super.queryAccessibleAttribute(attribute, parameters); 1594 } 1595 } 1596 1597 /** {@inheritDoc} */ 1598 @Override 1599 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 1600 switch (action) { 1601 case REQUEST_FOCUS: 1602 getSkinnable().getSelectionModel().select(getTab()); 1603 break; 1604 default: super.executeAccessibleAction(action, parameters); 1605 } 1606 } 1607 1608 } /* End TabHeaderSkin */ 1609 1610 private static final PseudoClass SELECTED_PSEUDOCLASS_STATE = 1611 PseudoClass.getPseudoClass("selected"); 1612 private static final PseudoClass TOP_PSEUDOCLASS_STATE = 1613 PseudoClass.getPseudoClass("top"); 1614 private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE = 1615 PseudoClass.getPseudoClass("bottom"); 1616 private static final PseudoClass LEFT_PSEUDOCLASS_STATE = 1617 PseudoClass.getPseudoClass("left"); 1618 private static final PseudoClass RIGHT_PSEUDOCLASS_STATE = 1619 PseudoClass.getPseudoClass("right"); 1620 private static final PseudoClass DISABLED_PSEUDOCLASS_STATE = 1621 PseudoClass.getPseudoClass("disabled"); 1622 1623 1624 /************************************************************************** 1625 * 1626 * TabContentRegion: each tab has one to contain the tab's content node 1627 * 1628 **************************************************************************/ 1629 static class TabContentRegion extends StackPane { 1630 1631 private Tab tab; 1632 1633 private InvalidationListener tabContentListener = valueModel -> { 1634 updateContent(); 1635 }; 1636 private InvalidationListener tabSelectedListener = new InvalidationListener() { 1637 @Override public void invalidated(Observable valueModel) { 1638 setVisible(tab.isSelected()); 1639 } 1640 }; 1641 1642 private WeakInvalidationListener weakTabContentListener = 1643 new WeakInvalidationListener(tabContentListener); 1644 private WeakInvalidationListener weakTabSelectedListener = 1645 new WeakInvalidationListener(tabSelectedListener); 1646 1647 public Tab getTab() { 1648 return tab; 1649 } 1650 1651 public TabContentRegion(Tab tab) { 1652 getStyleClass().setAll("tab-content-area"); 1653 setManaged(false); 1654 this.tab = tab; 1655 updateContent(); 1656 setVisible(tab.isSelected()); 1657 1658 tab.selectedProperty().addListener(weakTabSelectedListener); 1659 tab.contentProperty().addListener(weakTabContentListener); 1660 } 1661 1662 private void updateContent() { 1663 Node newContent = getTab().getContent(); 1664 if (newContent == null) { 1665 getChildren().clear(); 1666 } else { 1667 getChildren().setAll(newContent); 1668 } 1669 } 1670 1671 private void removeListeners(Tab tab) { 1672 tab.selectedProperty().removeListener(weakTabSelectedListener); 1673 tab.contentProperty().removeListener(weakTabContentListener); 1674 } 1675 1676 } /* End TabContentRegion */ 1677 1678 /************************************************************************** 1679 * 1680 * TabControlButtons: controls to manipulate tab interaction 1681 * 1682 **************************************************************************/ 1683 class TabControlButtons extends StackPane { 1684 private StackPane inner; 1685 private StackPane downArrow; 1686 private Pane downArrowBtn; 1687 private boolean showControlButtons; 1688 private ContextMenu popup; 1689 1690 public TabControlButtons() { 1691 getStyleClass().setAll("control-buttons-tab"); 1692 1693 TabPane tabPane = getSkinnable(); 1694 1695 downArrowBtn = new Pane(); 1696 downArrowBtn.getStyleClass().setAll("tab-down-button"); 1697 downArrowBtn.setVisible(isShowTabsMenu()); 1698 downArrow = new StackPane(); 1699 downArrow.setManaged(false); 1700 downArrow.getStyleClass().setAll("arrow"); 1701 downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F); 1702 downArrowBtn.getChildren().add(downArrow); 1703 downArrowBtn.setOnMouseClicked(me -> { 1704 showPopupMenu(); 1705 }); 1706 1707 setupPopupMenu(); 1708 1709 inner = new StackPane() { 1710 @Override protected double computePrefWidth(double height) { 1711 double pw; 1712 double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight())); 1713 pw = 0.0F; 1714 if (isShowTabsMenu()) { 1715 pw += maxArrowWidth; 1716 } 1717 if (pw > 0) { 1718 pw += snappedLeftInset() + snappedRightInset(); 1719 } 1720 return pw; 1721 } 1722 1723 @Override protected double computePrefHeight(double width) { 1724 double height = 0.0F; 1725 if (isShowTabsMenu()) { 1726 height = Math.max(height, snapSize(downArrowBtn.prefHeight(width))); 1727 } 1728 if (height > 0) { 1729 height += snappedTopInset() + snappedBottomInset(); 1730 } 1731 return height; 1732 } 1733 1734 @Override protected void layoutChildren() { 1735 if (isShowTabsMenu()) { 1736 double x = 0; 1737 double y = snappedTopInset(); 1738 double w = snapSize(getWidth()) - x + snappedLeftInset(); 1739 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1740 positionArrow(downArrowBtn, downArrow, x, y, w, h); 1741 } 1742 } 1743 1744 private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) { 1745 btn.resize(width, height); 1746 positionInArea(btn, x, y, width, height, /*baseline ignored*/0, 1747 HPos.CENTER, VPos.CENTER); 1748 // center arrow region within arrow button 1749 double arrowWidth = snapSize(arrow.prefWidth(-1)); 1750 double arrowHeight = snapSize(arrow.prefHeight(-1)); 1751 arrow.resize(arrowWidth, arrowHeight); 1752 positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(), 1753 width - btn.snappedLeftInset() - btn.snappedRightInset(), 1754 height - btn.snappedTopInset() - btn.snappedBottomInset(), 1755 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER); 1756 } 1757 }; 1758 inner.getStyleClass().add("container"); 1759 inner.getChildren().add(downArrowBtn); 1760 1761 getChildren().add(inner); 1762 1763 tabPane.sideProperty().addListener(valueModel -> { 1764 Side tabPosition = getSkinnable().getSide(); 1765 downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F); 1766 }); 1767 tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu()); 1768 showControlButtons = false; 1769 if (isShowTabsMenu()) { 1770 showControlButtons = true; 1771 requestLayout(); 1772 } 1773 getProperties().put(ContextMenu.class, popup); 1774 } 1775 1776 private boolean showTabsMenu = false; 1777 1778 private void showTabsMenu(boolean value) { 1779 final boolean wasTabsMenuShowing = isShowTabsMenu(); 1780 this.showTabsMenu = value; 1781 1782 if (showTabsMenu && !wasTabsMenuShowing) { 1783 downArrowBtn.setVisible(true); 1784 showControlButtons = true; 1785 inner.requestLayout(); 1786 tabHeaderArea.requestLayout(); 1787 } else if (!showTabsMenu && wasTabsMenuShowing) { 1788 hideControlButtons(); 1789 } 1790 } 1791 1792 private boolean isShowTabsMenu() { 1793 return showTabsMenu; 1794 } 1795 1796 @Override protected double computePrefWidth(double height) { 1797 double pw = snapSize(inner.prefWidth(height)); 1798 if (pw > 0) { 1799 pw += snappedLeftInset() + snappedRightInset(); 1800 } 1801 return pw; 1802 } 1803 1804 @Override protected double computePrefHeight(double width) { 1805 return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) + 1806 snappedTopInset() + snappedBottomInset(); 1807 } 1808 1809 @Override protected void layoutChildren() { 1810 double x = snappedLeftInset(); 1811 double y = snappedTopInset(); 1812 double w = snapSize(getWidth()) - x + snappedRightInset(); 1813 double h = snapSize(getHeight()) - y + snappedBottomInset(); 1814 1815 if (showControlButtons) { 1816 showControlButtons(); 1817 showControlButtons = false; 1818 } 1819 1820 inner.resize(w, h); 1821 positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM); 1822 } 1823 1824 private void showControlButtons() { 1825 setVisible(true); 1826 if (popup == null) { 1827 setupPopupMenu(); 1828 } 1829 } 1830 1831 private void hideControlButtons() { 1832 // If the scroll arrows or tab menu is still visible we don't want 1833 // to hide it animate it back it. 1834 if (isShowTabsMenu()) { 1835 showControlButtons = true; 1836 } else { 1837 setVisible(false); 1838 popup.getItems().clear(); 1839 popup = null; 1840 } 1841 1842 // This needs to be called when we are in the left tabPosition 1843 // to allow for the clip offset to move properly (otherwise 1844 // it jumps too early - before the animation is done). 1845 requestLayout(); 1846 } 1847 1848 private void setupPopupMenu() { 1849 if (popup == null) { 1850 popup = new ContextMenu(); 1851 } 1852 popup.getItems().clear(); 1853 ToggleGroup group = new ToggleGroup(); 1854 ObservableList<RadioMenuItem> menuitems = FXCollections.<RadioMenuItem>observableArrayList(); 1855 for (final Tab tab : getSkinnable().getTabs()) { 1856 TabMenuItem item = new TabMenuItem(tab); 1857 item.setToggleGroup(group); 1858 item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab)); 1859 menuitems.add(item); 1860 } 1861 popup.getItems().addAll(menuitems); 1862 } 1863 1864 private void showPopupMenu() { 1865 for (MenuItem mi: popup.getItems()) { 1866 TabMenuItem tmi = (TabMenuItem)mi; 1867 if (selectedTab.equals(tmi.getTab())) { 1868 tmi.setSelected(true); 1869 break; 1870 } 1871 } 1872 popup.show(downArrowBtn, Side.BOTTOM, 0, 0); 1873 } 1874 } /* End TabControlButtons*/ 1875 1876 static class TabMenuItem extends RadioMenuItem { 1877 Tab tab; 1878 1879 private InvalidationListener disableListener = new InvalidationListener() { 1880 @Override public void invalidated(Observable o) { 1881 setDisable(tab.isDisable()); 1882 } 1883 }; 1884 1885 private WeakInvalidationListener weakDisableListener = 1886 new WeakInvalidationListener(disableListener); 1887 1888 public TabMenuItem(final Tab tab) { 1889 super(tab.getText(), TabPaneSkin.clone(tab.getGraphic())); 1890 this.tab = tab; 1891 setDisable(tab.isDisable()); 1892 tab.disableProperty().addListener(weakDisableListener); 1893 textProperty().bind(tab.textProperty()); 1894 } 1895 1896 public Tab getTab() { 1897 return tab; 1898 } 1899 1900 public void dispose() { 1901 tab.disableProperty().removeListener(weakDisableListener); 1902 } 1903 } 1904 1905 @Override 1906 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1907 switch (attribute) { 1908 case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab); 1909 case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size(); 1910 case ITEM_AT_INDEX: { 1911 Integer index = (Integer)parameters[0]; 1912 if (index == null) return null; 1913 return tabHeaderArea.headersRegion.getChildren().get(index); 1914 } 1915 default: return super.queryAccessibleAttribute(attribute, parameters); 1916 } 1917 } 1918 }