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                     }
1278                 }
1279             });
1280 
1281             updateGraphicRotation();
1282 
1283             final Region focusIndicator = new Region();
1284             focusIndicator.setMouseTransparent(true);
1285             focusIndicator.getStyleClass().add("focus-indicator");
1286 
1287             inner = new StackPane() {
1288                 @Override protected void layoutChildren() {
1289                     final TabPane skinnable = getSkinnable();
1290 
1291                     final double paddingTop = snappedTopInset();
1292                     final double paddingRight = snappedRightInset();
1293                     final double paddingBottom = snappedBottomInset();
1294                     final double paddingLeft = snappedLeftInset();
1295                     final double w = getWidth() - (paddingLeft + paddingRight);
1296                     final double h = getHeight() - (paddingTop + paddingBottom);
1297 
1298                     final double prefLabelWidth = snapSize(label.prefWidth(-1));
1299                     final double prefLabelHeight = snapSize(label.prefHeight(-1));
1300 
1301                     final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0;
1302                     final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0;
1303                     final double minWidth = snapSize(skinnable.getTabMinWidth());
1304                     final double maxWidth = snapSize(skinnable.getTabMaxWidth());
1305                     final double maxHeight = snapSize(skinnable.getTabMaxHeight());
1306 
1307                     double labelAreaWidth = prefLabelWidth;
1308                     double labelWidth = prefLabelWidth;
1309                     double labelHeight = prefLabelHeight;
1310 
1311                     final double childrenWidth = labelAreaWidth + closeBtnWidth;
1312                     final double childrenHeight = Math.max(labelHeight, closeBtnHeight);
1313 
1314                     if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) {
1315                         labelAreaWidth = maxWidth - closeBtnWidth;
1316                         labelWidth = maxWidth - closeBtnWidth;
1317                     } else if (childrenWidth < minWidth) {
1318                         labelAreaWidth = minWidth - closeBtnWidth;
1319                     }
1320 
1321                     if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) {
1322                         labelHeight = maxHeight;
1323                     }
1324 
1325                     if (animationState != TabAnimationState.NONE) {
1326 //                        if (prefWidth.getValue() < labelAreaWidth) {
1327 //                            labelAreaWidth = prefWidth.getValue();
1328 //                        }
1329                         labelAreaWidth *= animationTransition.get();
1330                         closeBtn.setVisible(false);
1331                     } else {
1332                         closeBtn.setVisible(showCloseButton());
1333                     }
1334 
1335 
1336                     label.resize(labelWidth, labelHeight);
1337 
1338 
1339                     double labelStartX = paddingLeft;
1340 
1341                     // If maxWidth is less than Double.MAX_VALUE, the user has
1342                     // clamped the max width, but we should
1343                     // position the close button at the end of the tab,
1344                     // which may not necessarily be the entire width of the
1345                     // provided max width.
1346                     double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth;
1347 
1348                     positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h,
1349                             /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1350 
1351                     if (closeBtn.isVisible()) {
1352                         closeBtn.resize(closeBtnWidth, closeBtnHeight);
1353                         positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h,
1354                                 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1355                     }
1356 
1357                     // Magic numbers regretfully introduced for RT-28944 (so that
1358                     // the focus rect appears as expected on Windows and Mac).
1359                     // In short we use the vPadding to shift the focus rect down
1360                     // into the content area (whereas previously it was being clipped
1361                     // on Windows, whilst it still looked fine on Mac). In the
1362                     // future we may want to improve this code to remove the
1363                     // magic number. Similarly, the hPadding differs on Mac.
1364                     final int vPadding = Utils.isMac() ? 2 : 3;
1365                     final int hPadding = Utils.isMac() ? 2 : 1;
1366                     focusIndicator.resizeRelocate(
1367                             paddingLeft - hPadding,
1368                             paddingTop + vPadding,
1369                             w + 2 * hPadding,
1370                             h - 2 * vPadding);
1371                 }
1372             };
1373             inner.getStyleClass().add("tab-container");
1374             inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
1375             inner.getChildren().addAll(label, closeBtn, focusIndicator);
1376 
1377             getChildren().addAll(inner);
1378 
1379             tooltip = tab.getTooltip();
1380             if (tooltip != null) {
1381                 Tooltip.install(this, tooltip);
1382                 oldTooltip = tooltip;
1383             }
1384 
1385             listener.registerChangeListener(tab.closableProperty(), e -> {
1386                 inner.requestLayout();
1387                 requestLayout();
1388             });
1389             listener.registerChangeListener(tab.selectedProperty(), e -> {
1390                 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1391                 // Need to request a layout pass for inner because if the width
1392                 // and height didn't not change the label or close button may have
1393                 // changed.
1394                 inner.requestLayout();
1395                 requestLayout();
1396             });
1397             listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText()));
1398             listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic()));
1399             listener.registerChangeListener(tab.tooltipProperty(), e -> {
1400                 // uninstall the old tooltip
1401                 if (oldTooltip != null) {
1402                     Tooltip.uninstall(this, oldTooltip);
1403                 }
1404                 tooltip = tab.getTooltip();
1405                 if (tooltip != null) {
1406                     // install new tooltip and save as old tooltip.
1407                     Tooltip.install(this, tooltip);
1408                     oldTooltip = tooltip;
1409                 }
1410             });
1411             listener.registerChangeListener(tab.disabledProperty(), e -> {
1412                 updateTabDisabledState();
1413             });
1414             listener.registerChangeListener(tab.getTabPane().disabledProperty(), e -> {
1415                 updateTabDisabledState();
1416             });
1417             listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle()));
1418 
1419             tab.getStyleClass().addListener(weakStyleClassListener);
1420 
1421             listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> {
1422                 inner.requestLayout();
1423                 requestLayout();
1424             });
1425             listener.registerChangeListener(getSkinnable().sideProperty(),e -> {
1426                 final Side side = getSkinnable().getSide();
1427                 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1428                 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1429                 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1430                 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1431                 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
1432                 if (getSkinnable().isRotateGraphic()) {
1433                     updateGraphicRotation();
1434                 }
1435             });
1436             listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation());
1437             listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> {
1438                 requestLayout();
1439                 getSkinnable().requestLayout();
1440             });
1441             listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> {
1442                 requestLayout();
1443                 getSkinnable().requestLayout();
1444             });
1445             listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> {
1446                 requestLayout();
1447                 getSkinnable().requestLayout();
1448             });
1449             listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> {
1450                 requestLayout();
1451                 getSkinnable().requestLayout();
1452             });
1453 
1454             getProperties().put(Tab.class, tab);
1455             getProperties().put(ContextMenu.class, tab.getContextMenu());
1456 
1457             setOnContextMenuRequested((ContextMenuEvent me) -> {
1458                if (getTab().getContextMenu() != null) {
1459                     getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
1460                     me.consume();
1461                 }
1462             });
1463             setOnMousePressed(new EventHandler<MouseEvent>() {
1464                 @Override public void handle(MouseEvent me) {
1465                     if (getTab().isDisable()) {
1466                         return;
1467                     }
1468                     if (me.getButton().equals(MouseButton.MIDDLE)) {
1469                         if (showCloseButton()) {
1470                             Tab tab = getTab();
1471                             if (behavior.canCloseTab(tab)) {
1472                                 removeListeners(tab);
1473                                 behavior.closeTab(tab);
1474                             }
1475                         }
1476                     } else if (me.getButton().equals(MouseButton.PRIMARY)) {
1477                         behavior.selectTab(getTab());
1478                     }
1479                 }
1480             });
1481 
1482             // initialize pseudo-class state
1483             pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1484             pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled());
1485             final Side side = getSkinnable().getSide();
1486             pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1487             pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1488             pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1489             pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1490         }
1491 
1492         private void updateTabDisabledState() {
1493             pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled());
1494             inner.requestLayout();
1495             requestLayout();
1496         }
1497 
1498         private void updateGraphicRotation() {
1499             if (label.getGraphic() != null) {
1500                 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F :
1501                     (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F :
1502                         (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F)));
1503             }
1504         }
1505 
1506         private boolean showCloseButton() {
1507             return tab.isClosable() &&
1508                     (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) ||
1509                     getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected());
1510         }
1511 
1512         private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) {
1513             @Override protected void invalidated() {
1514                 requestLayout();
1515             }
1516         };
1517 
1518         private void removeListeners(Tab tab) {
1519             listener.dispose();
1520             inner.getChildren().clear();
1521             getChildren().clear();
1522         }
1523 
1524         private TabAnimationState animationState = TabAnimationState.NONE;
1525         private Timeline currentAnimation;
1526 
1527         @Override protected double computePrefWidth(double height) {
1528 //            if (animating) {
1529 //                return prefWidth.getValue();
1530 //            }
1531             double minWidth = snapSize(getSkinnable().getTabMinWidth());
1532             double maxWidth = snapSize(getSkinnable().getTabMaxWidth());
1533             double paddingRight = snappedRightInset();
1534             double paddingLeft = snappedLeftInset();
1535             double tmpPrefWidth = snapSize(label.prefWidth(-1));
1536 
1537             // only include the close button width if it is relevant
1538             if (showCloseButton()) {
1539                 tmpPrefWidth += snapSize(closeBtn.prefWidth(-1));
1540             }
1541 
1542             if (tmpPrefWidth > maxWidth) {
1543                 tmpPrefWidth = maxWidth;
1544             } else if (tmpPrefWidth < minWidth) {
1545                 tmpPrefWidth = minWidth;
1546             }
1547             tmpPrefWidth += paddingRight + paddingLeft;
1548 //            prefWidth.setValue(tmpPrefWidth);
1549             return tmpPrefWidth;
1550         }
1551 
1552         @Override protected double computePrefHeight(double width) {
1553             double minHeight = snapSize(getSkinnable().getTabMinHeight());
1554             double maxHeight = snapSize(getSkinnable().getTabMaxHeight());
1555             double paddingTop = snappedTopInset();
1556             double paddingBottom = snappedBottomInset();
1557             double tmpPrefHeight = snapSize(label.prefHeight(width));
1558 
1559             if (tmpPrefHeight > maxHeight) {
1560                 tmpPrefHeight = maxHeight;
1561             } else if (tmpPrefHeight < minHeight) {
1562                 tmpPrefHeight = minHeight;
1563             }
1564             tmpPrefHeight += paddingTop + paddingBottom;
1565             return tmpPrefHeight;
1566         }
1567 
1568         @Override protected void layoutChildren() {
1569             double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue();
1570             inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset());
1571             inner.relocate(snappedLeftInset(), snappedTopInset());
1572         }
1573 
1574         @Override protected void setWidth(double value) {
1575             super.setWidth(value);
1576             clip.setWidth(value);
1577         }
1578 
1579         @Override protected void setHeight(double value) {
1580             super.setHeight(value);
1581             clip.setHeight(value);
1582         }
1583 
1584         /** {@inheritDoc} */
1585         @Override
1586         public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1587             switch (attribute) {
1588                 case TEXT: return getTab().getText();
1589                 case SELECTED: return selectedTab == getTab();
1590                 default: return super.queryAccessibleAttribute(attribute, parameters);
1591             }
1592         }
1593 
1594         /** {@inheritDoc} */
1595         @Override
1596         public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
1597             switch (action) {
1598                 case REQUEST_FOCUS:
1599                     getSkinnable().getSelectionModel().select(getTab());
1600                     break;
1601                 default: super.executeAccessibleAction(action, parameters);
1602             }
1603         }
1604 
1605     } /* End TabHeaderSkin */
1606 
1607     private static final PseudoClass SELECTED_PSEUDOCLASS_STATE =
1608             PseudoClass.getPseudoClass("selected");
1609     private static final PseudoClass TOP_PSEUDOCLASS_STATE =
1610             PseudoClass.getPseudoClass("top");
1611     private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
1612             PseudoClass.getPseudoClass("bottom");
1613     private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
1614             PseudoClass.getPseudoClass("left");
1615     private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
1616             PseudoClass.getPseudoClass("right");
1617     private static final PseudoClass DISABLED_PSEUDOCLASS_STATE =
1618             PseudoClass.getPseudoClass("disabled");
1619 
1620 
1621     /**************************************************************************
1622      *
1623      * TabContentRegion: each tab has one to contain the tab's content node
1624      *
1625      **************************************************************************/
1626     static class TabContentRegion extends StackPane {
1627 
1628         private Tab tab;
1629 
1630         private InvalidationListener tabContentListener = valueModel -> {
1631             updateContent();
1632         };
1633         private InvalidationListener tabSelectedListener = new InvalidationListener() {
1634             @Override public void invalidated(Observable valueModel) {
1635                 setVisible(tab.isSelected());
1636             }
1637         };
1638 
1639         private WeakInvalidationListener weakTabContentListener =
1640                 new WeakInvalidationListener(tabContentListener);
1641         private WeakInvalidationListener weakTabSelectedListener =
1642                 new WeakInvalidationListener(tabSelectedListener);
1643 
1644         public Tab getTab() {
1645             return tab;
1646         }
1647 
1648         public TabContentRegion(Tab tab) {
1649             getStyleClass().setAll("tab-content-area");
1650             setManaged(false);
1651             this.tab = tab;
1652             updateContent();
1653             setVisible(tab.isSelected());
1654 
1655             tab.selectedProperty().addListener(weakTabSelectedListener);
1656             tab.contentProperty().addListener(weakTabContentListener);
1657         }
1658 
1659         private void updateContent() {
1660             Node newContent = getTab().getContent();
1661             if (newContent == null) {
1662                 getChildren().clear();
1663             } else {
1664                 getChildren().setAll(newContent);
1665             }
1666         }
1667 
1668         private void removeListeners(Tab tab) {
1669             tab.selectedProperty().removeListener(weakTabSelectedListener);
1670             tab.contentProperty().removeListener(weakTabContentListener);
1671         }
1672 
1673     } /* End TabContentRegion */
1674 
1675     /**************************************************************************
1676      *
1677      * TabControlButtons: controls to manipulate tab interaction
1678      *
1679      **************************************************************************/
1680     class TabControlButtons extends StackPane {
1681         private StackPane inner;
1682         private StackPane downArrow;
1683         private Pane downArrowBtn;
1684         private boolean showControlButtons;
1685         private ContextMenu popup;
1686 
1687         public TabControlButtons() {
1688             getStyleClass().setAll("control-buttons-tab");
1689 
1690             TabPane tabPane = getSkinnable();
1691 
1692             downArrowBtn = new Pane();
1693             downArrowBtn.getStyleClass().setAll("tab-down-button");
1694             downArrowBtn.setVisible(isShowTabsMenu());
1695             downArrow = new StackPane();
1696             downArrow.setManaged(false);
1697             downArrow.getStyleClass().setAll("arrow");
1698             downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
1699             downArrowBtn.getChildren().add(downArrow);
1700             downArrowBtn.setOnMouseClicked(me -> {
1701                 showPopupMenu();
1702             });
1703 
1704             setupPopupMenu();
1705 
1706             inner = new StackPane() {
1707                 @Override protected double computePrefWidth(double height) {
1708                     double pw;
1709                     double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight()));
1710                     pw = 0.0F;
1711                     if (isShowTabsMenu()) {
1712                         pw += maxArrowWidth;
1713                     }
1714                     if (pw > 0) {
1715                         pw += snappedLeftInset() + snappedRightInset();
1716                     }
1717                     return pw;
1718                 }
1719 
1720                 @Override protected double computePrefHeight(double width) {
1721                     double height = 0.0F;
1722                     if (isShowTabsMenu()) {
1723                         height = Math.max(height, snapSize(downArrowBtn.prefHeight(width)));
1724                     }
1725                     if (height > 0) {
1726                         height += snappedTopInset() + snappedBottomInset();
1727                     }
1728                     return height;
1729                 }
1730 
1731                 @Override protected void layoutChildren() {
1732                     if (isShowTabsMenu()) {
1733                         double x = 0;
1734                         double y = snappedTopInset();
1735                         double w = snapSize(getWidth()) - x + snappedLeftInset();
1736                         double h = snapSize(getHeight()) - y + snappedBottomInset();
1737                         positionArrow(downArrowBtn, downArrow, x, y, w, h);
1738                     }
1739                 }
1740 
1741                 private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) {
1742                     btn.resize(width, height);
1743                     positionInArea(btn, x, y, width, height, /*baseline ignored*/0,
1744                             HPos.CENTER, VPos.CENTER);
1745                     // center arrow region within arrow button
1746                     double arrowWidth = snapSize(arrow.prefWidth(-1));
1747                     double arrowHeight = snapSize(arrow.prefHeight(-1));
1748                     arrow.resize(arrowWidth, arrowHeight);
1749                     positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(),
1750                             width - btn.snappedLeftInset() - btn.snappedRightInset(),
1751                             height - btn.snappedTopInset() - btn.snappedBottomInset(),
1752                             /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1753                 }
1754             };
1755             inner.getStyleClass().add("container");
1756             inner.getChildren().add(downArrowBtn);
1757 
1758             getChildren().add(inner);
1759 
1760             tabPane.sideProperty().addListener(valueModel -> {
1761                 Side tabPosition = getSkinnable().getSide();
1762                 downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F);
1763             });
1764             tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu());
1765             showControlButtons = false;
1766             if (isShowTabsMenu()) {
1767                 showControlButtons = true;
1768                 requestLayout();
1769             }
1770             getProperties().put(ContextMenu.class, popup);
1771         }
1772 
1773         private boolean showTabsMenu = false;
1774 
1775         private void showTabsMenu(boolean value) {
1776             final boolean wasTabsMenuShowing = isShowTabsMenu();
1777             this.showTabsMenu = value;
1778 
1779             if (showTabsMenu && !wasTabsMenuShowing) {
1780                 downArrowBtn.setVisible(true);
1781                 showControlButtons = true;
1782                 inner.requestLayout();
1783                 tabHeaderArea.requestLayout();
1784             } else if (!showTabsMenu && wasTabsMenuShowing) {
1785                 hideControlButtons();
1786             }
1787         }
1788 
1789         private boolean isShowTabsMenu() {
1790             return showTabsMenu;
1791         }
1792 
1793         @Override protected double computePrefWidth(double height) {
1794             double pw = snapSize(inner.prefWidth(height));
1795             if (pw > 0) {
1796                 pw += snappedLeftInset() + snappedRightInset();
1797             }
1798             return pw;
1799         }
1800 
1801         @Override protected double computePrefHeight(double width) {
1802             return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) +
1803                     snappedTopInset() + snappedBottomInset();
1804         }
1805 
1806         @Override protected void layoutChildren() {
1807             double x = snappedLeftInset();
1808             double y = snappedTopInset();
1809             double w = snapSize(getWidth()) - x + snappedRightInset();
1810             double h = snapSize(getHeight()) - y + snappedBottomInset();
1811 
1812             if (showControlButtons) {
1813                 showControlButtons();
1814                 showControlButtons = false;
1815             }
1816 
1817             inner.resize(w, h);
1818             positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM);
1819         }
1820 
1821         private void showControlButtons() {
1822             setVisible(true);
1823             if (popup == null) {
1824                 setupPopupMenu();
1825             }
1826         }
1827 
1828         private void hideControlButtons() {
1829             // If the scroll arrows or tab menu is still visible we don't want
1830             // to hide it animate it back it.
1831             if (isShowTabsMenu()) {
1832                 showControlButtons = true;
1833             } else {
1834                 setVisible(false);
1835                 popup.getItems().clear();
1836                 popup = null;
1837             }
1838 
1839             // This needs to be called when we are in the left tabPosition
1840             // to allow for the clip offset to move properly (otherwise
1841             // it jumps too early - before the animation is done).
1842             requestLayout();
1843         }
1844 
1845         private void setupPopupMenu() {
1846             if (popup == null) {
1847                 popup = new ContextMenu();
1848             }
1849             popup.getItems().clear();
1850             ToggleGroup group = new ToggleGroup();
1851             ObservableList<RadioMenuItem> menuitems = FXCollections.<RadioMenuItem>observableArrayList();
1852             for (final Tab tab : getSkinnable().getTabs()) {
1853                 TabMenuItem item = new TabMenuItem(tab);
1854                 item.setToggleGroup(group);
1855                 item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab));
1856                 menuitems.add(item);
1857             }
1858             popup.getItems().addAll(menuitems);
1859         }
1860 
1861         private void showPopupMenu() {
1862             for (MenuItem mi: popup.getItems()) {
1863                 TabMenuItem tmi = (TabMenuItem)mi;
1864                 if (selectedTab.equals(tmi.getTab())) {
1865                     tmi.setSelected(true);
1866                     break;
1867                 }
1868             }
1869             popup.show(downArrowBtn, Side.BOTTOM, 0, 0);
1870         }
1871     } /* End TabControlButtons*/
1872 
1873     static class TabMenuItem extends RadioMenuItem {
1874         Tab tab;
1875 
1876         private InvalidationListener disableListener = new InvalidationListener() {
1877             @Override public void invalidated(Observable o) {
1878                 setDisable(tab.isDisable());
1879             }
1880         };
1881 
1882         private WeakInvalidationListener weakDisableListener =
1883                 new WeakInvalidationListener(disableListener);
1884 
1885         public TabMenuItem(final Tab tab) {
1886             super(tab.getText(), TabPaneSkin.clone(tab.getGraphic()));
1887             this.tab = tab;
1888             setDisable(tab.isDisable());
1889             tab.disableProperty().addListener(weakDisableListener);
1890             textProperty().bind(tab.textProperty());
1891         }
1892 
1893         public Tab getTab() {
1894             return tab;
1895         }
1896 
1897         public void dispose() {
1898             tab.disableProperty().removeListener(weakDisableListener);
1899         }
1900     }
1901 
1902     @Override
1903     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1904         switch (attribute) {
1905             case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab);
1906             case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size();
1907             case ITEM_AT_INDEX: {
1908                 Integer index = (Integer)parameters[0];
1909                 if (index == null) return null;
1910                 return tabHeaderArea.headersRegion.getChildren().get(index);
1911             }
1912             default: return super.queryAccessibleAttribute(attribute, parameters);
1913         }
1914     }
1915 }