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