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