1 /*
   2  * Copyright (c) 2011, 2018, 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 (isSelectingTab) {
 830                             ensureSelectedTabIsVisible();
 831                         } else {
 832                             validateScrollOffset();
 833                         }
 834                     }
 835                     isSelectingTab = false;
 836 
 837                     Side tabPosition = getSkinnable().getSide();
 838                     double tabBackgroundHeight = snapSize(prefHeight(-1));
 839                     double tabX = (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) ?
 840                         snapSize(getWidth()) - getScrollOffset() : getScrollOffset();
 841 
 842                     updateHeaderClip();
 843                     for (Node node : getChildren()) {
 844                         TabHeaderSkin tabHeader = (TabHeaderSkin)node;
 845 
 846                         // size and position the header relative to the other headers
 847                         double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1) * tabHeader.animationTransition.get());
 848                         double tabHeaderPrefHeight = snapSize(tabHeader.prefHeight(-1));
 849                         tabHeader.resize(tabHeaderPrefWidth, tabHeaderPrefHeight);
 850 
 851                         // This ensures that the tabs are located in the correct position
 852                         // when there are tabs of differing heights.
 853                         double startY = tabPosition.equals(Side.BOTTOM) ?
 854                             0 : tabBackgroundHeight - tabHeaderPrefHeight - snappedBottomInset();
 855                         if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) {
 856                             // build from the right
 857                             tabX -= tabHeaderPrefWidth;
 858                             if (dragState != DragState.REORDER ||
 859                                     (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) {
 860                                 tabHeader.relocate(tabX, startY);
 861                             }
 862                         } else {
 863                             // build from the left
 864                             if (dragState != DragState.REORDER ||
 865                                     (tabHeader != dragTabHeader && tabHeader != dropAnimHeader)) {
 866                                 tabHeader.relocate(tabX, startY);
 867                             }
 868                             tabX += tabHeaderPrefWidth;
 869                         }
 870                     }
 871                 }
 872 
 873             };
 874             headersRegion.getStyleClass().setAll("headers-region");
 875             headersRegion.setClip(headerClip);
 876             setupReordering(headersRegion);
 877 
 878             headerBackground = new StackPane();
 879             headerBackground.getStyleClass().setAll("tab-header-background");
 880 
 881             int i = 0;
 882             for (Tab tab: tabPane.getTabs()) {
 883                 addTab(tab, i++);
 884             }
 885 
 886             controlButtons = new TabControlButtons();
 887             controlButtons.setVisible(false);
 888             if (controlButtons.isVisible()) {
 889                 controlButtons.setVisible(true);
 890             }
 891             getChildren().addAll(headerBackground, headersRegion, controlButtons);
 892 
 893             // support for mouse scroll of header area (for when the tabs exceed
 894             // the available space).
 895             // Scrolling the mouse wheel downwards results in the tabs scrolling left (i.e. exposing the right-most tabs)
 896             // Scrolling the mouse wheel upwards results in the tabs scrolling right (i.e. exposing th left-most tabs)
 897             addEventHandler(ScrollEvent.SCROLL, (ScrollEvent e) -> {
 898                 Side side = getSkinnable().getSide();
 899                 side = side == null ? Side.TOP : side;
 900                 switch (side) {
 901                     default:
 902                     case TOP:
 903                     case BOTTOM:
 904                         setScrollOffset(scrollOffset + e.getDeltaY());
 905                         break;
 906                     case LEFT:
 907                     case RIGHT:
 908                         setScrollOffset(scrollOffset - e.getDeltaY());
 909                         break;
 910                 }
 911 
 912             });
 913         }
 914 
 915         private void updateHeaderClip() {
 916             Side tabPosition = getSkinnable().getSide();
 917 
 918             double x = 0;
 919             double y = 0;
 920             double clipWidth = 0;
 921             double clipHeight = 0;
 922             double maxWidth = 0;
 923             double shadowRadius = 0;
 924             double clipOffset = firstTabIndent();
 925             double controlButtonPrefWidth = snapSize(controlButtons.prefWidth(-1));
 926 
 927             measureClosingTabs = true;
 928             double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
 929             measureClosingTabs = false;
 930 
 931             double headersPrefHeight = snapSize(headersRegion.prefHeight(-1));
 932 
 933             // Add the spacer if isShowTabsMenu is true.
 934             if (controlButtonPrefWidth > 0) {
 935                 controlButtonPrefWidth = controlButtonPrefWidth + SPACER;
 936             }
 937 
 938             if (headersRegion.getEffect() instanceof DropShadow) {
 939                 DropShadow shadow = (DropShadow)headersRegion.getEffect();
 940                 shadowRadius = shadow.getRadius();
 941             }
 942 
 943             maxWidth = snapSize(getWidth()) - controlButtonPrefWidth - clipOffset;
 944             if (tabPosition.equals(Side.LEFT) || tabPosition.equals(Side.BOTTOM)) {
 945                 if (headersPrefWidth < maxWidth) {
 946                     clipWidth = headersPrefWidth + shadowRadius;
 947                 } else {
 948                     x = headersPrefWidth - maxWidth;
 949                     clipWidth = maxWidth + shadowRadius;
 950                 }
 951                 clipHeight = headersPrefHeight;
 952             } else {
 953                 // If x = 0 the header region's drop shadow is clipped.
 954                 x = -shadowRadius;
 955                 clipWidth = (headersPrefWidth < maxWidth ? headersPrefWidth : maxWidth) + shadowRadius;
 956                 clipHeight = headersPrefHeight;
 957             }
 958 
 959             headerClip.setX(x);
 960             headerClip.setY(y);
 961             headerClip.setWidth(clipWidth);
 962             headerClip.setHeight(clipHeight);
 963         }
 964 
 965         private void addTab(Tab tab, int addToIndex) {
 966             TabHeaderSkin tabHeaderSkin = new TabHeaderSkin(tab);
 967             headersRegion.getChildren().add(addToIndex, tabHeaderSkin);
 968         }
 969 
 970         private void removeTab(Tab tab) {
 971             TabHeaderSkin tabHeaderSkin = getTabHeaderSkin(tab);
 972             if (tabHeaderSkin != null) {
 973                 headersRegion.getChildren().remove(tabHeaderSkin);
 974             }
 975         }
 976 
 977         private TabHeaderSkin getTabHeaderSkin(Tab tab) {
 978             for (Node child: headersRegion.getChildren()) {
 979                 TabHeaderSkin tabHeaderSkin = (TabHeaderSkin)child;
 980                 if (tabHeaderSkin.getTab().equals(tab)) {
 981                     return tabHeaderSkin;
 982                 }
 983             }
 984             return null;
 985         }
 986 
 987         private boolean tabsFit() {
 988             double headerPrefWidth = snapSize(headersRegion.prefWidth(-1));
 989             double controlTabWidth = snapSize(controlButtons.prefWidth(-1));
 990             double visibleWidth = headerPrefWidth + controlTabWidth + firstTabIndent() + SPACER;
 991             return visibleWidth < getWidth();
 992         }
 993 
 994         private void ensureSelectedTabIsVisible() {
 995             // work out the visible width of the tab header
 996             double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight());
 997             double controlTabWidth = snapSize(controlButtons.getWidth());
 998             double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER;
 999 
1000             // and get where the selected tab is in the header area
1001             double offset = 0.0;
1002             double selectedTabOffset = 0.0;
1003             double selectedTabWidth = 0.0;
1004             for (Node node : headersRegion.getChildren()) {
1005                 TabHeaderSkin tabHeader = (TabHeaderSkin)node;
1006 
1007                 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
1008 
1009                 if (selectedTab != null && selectedTab.equals(tabHeader.getTab())) {
1010                     selectedTabOffset = offset;
1011                     selectedTabWidth = tabHeaderPrefWidth;
1012                 }
1013                 offset += tabHeaderPrefWidth;
1014             }
1015 
1016             final double scrollOffset = getScrollOffset();
1017             final double selectedTabStartX = selectedTabOffset;
1018             final double selectedTabEndX = selectedTabOffset + selectedTabWidth;
1019 
1020             final double visibleAreaEndX = visibleWidth;
1021 
1022             if (selectedTabStartX < -scrollOffset) {
1023                 setScrollOffset(-selectedTabStartX);
1024             } else if (selectedTabEndX > (visibleAreaEndX - scrollOffset)) {
1025                 setScrollOffset(visibleAreaEndX - selectedTabEndX);
1026             }
1027         }
1028 
1029         public double getScrollOffset() {
1030             return scrollOffset;
1031         }
1032 
1033         private void validateScrollOffset() {
1034             setScrollOffset(getScrollOffset());
1035         }
1036 
1037         private void setScrollOffset(double newScrollOffset) {
1038             // work out the visible width of the tab header
1039             double tabPaneWidth = snapSize(isHorizontal() ? getSkinnable().getWidth() : getSkinnable().getHeight());
1040             double controlTabWidth = snapSize(controlButtons.getWidth());
1041             double visibleWidth = tabPaneWidth - controlTabWidth - firstTabIndent() - SPACER;
1042 
1043             // measure the width of all tabs
1044             double offset = 0.0;
1045             for (Node node : headersRegion.getChildren()) {
1046                 TabHeaderSkin tabHeader = (TabHeaderSkin)node;
1047                 double tabHeaderPrefWidth = snapSize(tabHeader.prefWidth(-1));
1048                 offset += tabHeaderPrefWidth;
1049             }
1050 
1051             double actualNewScrollOffset;
1052 
1053             if ((visibleWidth - newScrollOffset) > offset && newScrollOffset < 0) {
1054                 // need to make sure the right-most tab is attached to the
1055                 // right-hand side of the tab header (e.g. if the tab header area width
1056                 // is expanded), and if it isn't modify the scroll offset to bring
1057                 // it into line. See RT-35194 for a test case.
1058                 actualNewScrollOffset = visibleWidth - offset;
1059             } else if (newScrollOffset > 0) {
1060                 // need to prevent the left-most tab from becoming detached
1061                 // from the left-hand side of the tab header.
1062                 actualNewScrollOffset = 0;
1063             } else {
1064                 actualNewScrollOffset = newScrollOffset;
1065             }
1066 
1067             if (Math.abs(actualNewScrollOffset - scrollOffset) > 0.001) {
1068                 scrollOffset = actualNewScrollOffset;
1069                 headersRegion.requestLayout();
1070             }
1071         }
1072 
1073         private double firstTabIndent() {
1074             switch (getSkinnable().getSide()) {
1075                 case TOP:
1076                 case BOTTOM:
1077                     return snappedLeftInset();
1078                 case RIGHT:
1079                 case LEFT:
1080                     return snappedTopInset();
1081                 default:
1082                     return 0;
1083             }
1084         }
1085 
1086         @Override protected double computePrefWidth(double height) {
1087             double padding = isHorizontal() ?
1088                 snappedLeftInset() + snappedRightInset() :
1089                 snappedTopInset() + snappedBottomInset();
1090             return snapSize(headersRegion.prefWidth(height)) + controlButtons.prefWidth(height) +
1091                     firstTabIndent() + SPACER + padding;
1092         }
1093 
1094         @Override protected double computePrefHeight(double width) {
1095             double padding = isHorizontal() ?
1096                 snappedTopInset() + snappedBottomInset() :
1097                 snappedLeftInset() + snappedRightInset();
1098             return snapSize(headersRegion.prefHeight(-1)) + padding;
1099         }
1100 
1101         @Override public double getBaselineOffset() {
1102             if (getSkinnable().getSide() == Side.TOP) {
1103                 return headersRegion.getBaselineOffset() + snappedTopInset();
1104             }
1105             return 0;
1106         }
1107 
1108         @Override protected void layoutChildren() {
1109             final double leftInset = snappedLeftInset();
1110             final double rightInset = snappedRightInset();
1111             final double topInset = snappedTopInset();
1112             final double bottomInset = snappedBottomInset();
1113             double w = snapSize(getWidth()) - (isHorizontal() ?
1114                     leftInset + rightInset : topInset + bottomInset);
1115             double h = snapSize(getHeight()) - (isHorizontal() ?
1116                     topInset + bottomInset : leftInset + rightInset);
1117             double tabBackgroundHeight = snapSize(prefHeight(-1));
1118             double headersPrefWidth = snapSize(headersRegion.prefWidth(-1));
1119             double headersPrefHeight = snapSize(headersRegion.prefHeight(-1));
1120 
1121             controlButtons.showTabsMenu(! tabsFit());
1122 
1123             updateHeaderClip();
1124             headersRegion.requestLayout();
1125 
1126             // RESIZE CONTROL BUTTONS
1127             double btnWidth = snapSize(controlButtons.prefWidth(-1));
1128             final double btnHeight = controlButtons.prefHeight(btnWidth);
1129             controlButtons.resize(btnWidth, btnHeight);
1130 
1131             // POSITION TABS
1132             headersRegion.resize(headersPrefWidth, headersPrefHeight);
1133 
1134             if (isFloatingStyleClass()) {
1135                 headerBackground.setVisible(false);
1136             } else {
1137                 headerBackground.resize(snapSize(getWidth()), snapSize(getHeight()));
1138                 headerBackground.setVisible(true);
1139             }
1140 
1141             double startX = 0;
1142             double startY = 0;
1143             double controlStartX = 0;
1144             double controlStartY = 0;
1145             Side tabPosition = getSkinnable().getSide();
1146 
1147             if (tabPosition.equals(Side.TOP)) {
1148                 startX = leftInset;
1149                 startY = tabBackgroundHeight - headersPrefHeight - bottomInset;
1150                 controlStartX = w - btnWidth + leftInset;
1151                 controlStartY = snapSize(getHeight()) - btnHeight - bottomInset;
1152             } else if (tabPosition.equals(Side.RIGHT)) {
1153                 startX = topInset;
1154                 startY = tabBackgroundHeight - headersPrefHeight - leftInset;
1155                 controlStartX = w - btnWidth + topInset;
1156                 controlStartY = snapSize(getHeight()) - btnHeight - leftInset;
1157             } else if (tabPosition.equals(Side.BOTTOM)) {
1158                 startX = snapSize(getWidth()) - headersPrefWidth - leftInset;
1159                 startY = tabBackgroundHeight - headersPrefHeight - topInset;
1160                 controlStartX = rightInset;
1161                 controlStartY = snapSize(getHeight()) - btnHeight - topInset;
1162             } else if (tabPosition.equals(Side.LEFT)) {
1163                 startX = snapSize(getWidth()) - headersPrefWidth - topInset;
1164                 startY = tabBackgroundHeight - headersPrefHeight - rightInset;
1165                 controlStartX = leftInset;
1166                 controlStartY = snapSize(getHeight()) - btnHeight - rightInset;
1167             }
1168             if (headerBackground.isVisible()) {
1169                 positionInArea(headerBackground, 0, 0,
1170                         snapSize(getWidth()), snapSize(getHeight()), /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1171             }
1172             positionInArea(headersRegion, startX, startY, w, h, /*baseline ignored*/0, HPos.LEFT, VPos.CENTER);
1173             positionInArea(controlButtons, controlStartX, controlStartY, btnWidth, btnHeight,
1174                         /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1175         }
1176     } /* End TabHeaderArea */
1177 
1178 
1179 
1180 
1181     /**************************************************************************
1182      *
1183      * TabHeaderSkin: skin for each tab
1184      *
1185      **************************************************************************/
1186 
1187     class TabHeaderSkin extends StackPane {
1188         private final Tab tab;
1189         public Tab getTab() {
1190             return tab;
1191         }
1192         private Label label;
1193         private StackPane closeBtn;
1194         private StackPane inner;
1195         private Tooltip oldTooltip;
1196         private Tooltip tooltip;
1197         private Rectangle clip;
1198 
1199         private boolean isClosing = false;
1200 
1201         private LambdaMultiplePropertyChangeListenerHandler listener = new LambdaMultiplePropertyChangeListenerHandler();
1202 
1203         private final ListChangeListener<String> styleClassListener = new ListChangeListener<String>() {
1204             @Override
1205             public void onChanged(Change<? extends String> c) {
1206                 getStyleClass().setAll(tab.getStyleClass());
1207             }
1208         };
1209 
1210         private final WeakListChangeListener<String> weakStyleClassListener =
1211                 new WeakListChangeListener<>(styleClassListener);
1212 
1213         public TabHeaderSkin(final Tab tab) {
1214             getStyleClass().setAll(tab.getStyleClass());
1215             setId(tab.getId());
1216             setStyle(tab.getStyle());
1217             setAccessibleRole(AccessibleRole.TAB_ITEM);
1218             setViewOrder(1);
1219 
1220             this.tab = tab;
1221             clip = new Rectangle();
1222             setClip(clip);
1223 
1224             label = new Label(tab.getText(), tab.getGraphic());
1225             label.getStyleClass().setAll("tab-label");
1226 
1227             closeBtn = new StackPane() {
1228                 @Override protected double computePrefWidth(double h) {
1229                     return CLOSE_BTN_SIZE;
1230                 }
1231                 @Override protected double computePrefHeight(double w) {
1232                     return CLOSE_BTN_SIZE;
1233                 }
1234                 @Override
1235                 public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
1236                     switch (action) {
1237                         case FIRE: {
1238                             Tab tab = getTab();
1239                             if (behavior.canCloseTab(tab)) {
1240                                 behavior.closeTab(tab);
1241                                 setOnMousePressed(null);
1242                             }
1243                             break;
1244                         }
1245                         default: super.executeAccessibleAction(action, parameters);
1246                     }
1247                 }
1248             };
1249             closeBtn.setAccessibleRole(AccessibleRole.BUTTON);
1250             closeBtn.setAccessibleText(getString("Accessibility.title.TabPane.CloseButton"));
1251             closeBtn.getStyleClass().setAll("tab-close-button");
1252             closeBtn.setOnMousePressed(new EventHandler<MouseEvent>() {
1253                 @Override
1254                 public void handle(MouseEvent me) {
1255                     Tab tab = getTab();
1256                     if (behavior.canCloseTab(tab)) {
1257                         behavior.closeTab(tab);
1258                         setOnMousePressed(null);
1259                         me.consume();
1260                     }
1261                 }
1262             });
1263 
1264             updateGraphicRotation();
1265 
1266             final Region focusIndicator = new Region();
1267             focusIndicator.setMouseTransparent(true);
1268             focusIndicator.getStyleClass().add("focus-indicator");
1269 
1270             inner = new StackPane() {
1271                 @Override protected void layoutChildren() {
1272                     final TabPane skinnable = getSkinnable();
1273 
1274                     final double paddingTop = snappedTopInset();
1275                     final double paddingRight = snappedRightInset();
1276                     final double paddingBottom = snappedBottomInset();
1277                     final double paddingLeft = snappedLeftInset();
1278                     final double w = getWidth() - (paddingLeft + paddingRight);
1279                     final double h = getHeight() - (paddingTop + paddingBottom);
1280 
1281                     final double prefLabelWidth = snapSize(label.prefWidth(-1));
1282                     final double prefLabelHeight = snapSize(label.prefHeight(-1));
1283 
1284                     final double closeBtnWidth = showCloseButton() ? snapSize(closeBtn.prefWidth(-1)) : 0;
1285                     final double closeBtnHeight = showCloseButton() ? snapSize(closeBtn.prefHeight(-1)) : 0;
1286                     final double minWidth = snapSize(skinnable.getTabMinWidth());
1287                     final double maxWidth = snapSize(skinnable.getTabMaxWidth());
1288                     final double maxHeight = snapSize(skinnable.getTabMaxHeight());
1289 
1290                     double labelAreaWidth = prefLabelWidth;
1291                     double labelWidth = prefLabelWidth;
1292                     double labelHeight = prefLabelHeight;
1293 
1294                     final double childrenWidth = labelAreaWidth + closeBtnWidth;
1295                     final double childrenHeight = Math.max(labelHeight, closeBtnHeight);
1296 
1297                     if (childrenWidth > maxWidth && maxWidth != Double.MAX_VALUE) {
1298                         labelAreaWidth = maxWidth - closeBtnWidth;
1299                         labelWidth = maxWidth - closeBtnWidth;
1300                     } else if (childrenWidth < minWidth) {
1301                         labelAreaWidth = minWidth - closeBtnWidth;
1302                     }
1303 
1304                     if (childrenHeight > maxHeight && maxHeight != Double.MAX_VALUE) {
1305                         labelHeight = maxHeight;
1306                     }
1307 
1308                     if (animationState != TabAnimationState.NONE) {
1309 //                        if (prefWidth.getValue() < labelAreaWidth) {
1310 //                            labelAreaWidth = prefWidth.getValue();
1311 //                        }
1312                         labelAreaWidth *= animationTransition.get();
1313                         closeBtn.setVisible(false);
1314                     } else {
1315                         closeBtn.setVisible(showCloseButton());
1316                     }
1317 
1318 
1319                     label.resize(labelWidth, labelHeight);
1320 
1321 
1322                     double labelStartX = paddingLeft;
1323 
1324                     // If maxWidth is less than Double.MAX_VALUE, the user has
1325                     // clamped the max width, but we should
1326                     // position the close button at the end of the tab,
1327                     // which may not necessarily be the entire width of the
1328                     // provided max width.
1329                     double closeBtnStartX = (maxWidth < Double.MAX_VALUE ? Math.min(w, maxWidth) : w) - paddingRight - closeBtnWidth;
1330 
1331                     positionInArea(label, labelStartX, paddingTop, labelAreaWidth, h,
1332                             /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1333 
1334                     if (closeBtn.isVisible()) {
1335                         closeBtn.resize(closeBtnWidth, closeBtnHeight);
1336                         positionInArea(closeBtn, closeBtnStartX, paddingTop, closeBtnWidth, h,
1337                                 /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1338                     }
1339 
1340                     // Magic numbers regretfully introduced for RT-28944 (so that
1341                     // the focus rect appears as expected on Windows and Mac).
1342                     // In short we use the vPadding to shift the focus rect down
1343                     // into the content area (whereas previously it was being clipped
1344                     // on Windows, whilst it still looked fine on Mac). In the
1345                     // future we may want to improve this code to remove the
1346                     // magic number. Similarly, the hPadding differs on Mac.
1347                     final int vPadding = Utils.isMac() ? 2 : 3;
1348                     final int hPadding = Utils.isMac() ? 2 : 1;
1349                     focusIndicator.resizeRelocate(
1350                             paddingLeft - hPadding,
1351                             paddingTop + vPadding,
1352                             w + 2 * hPadding,
1353                             h - 2 * vPadding);
1354                 }
1355             };
1356             inner.getStyleClass().add("tab-container");
1357             inner.setRotate(getSkinnable().getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
1358             inner.getChildren().addAll(label, closeBtn, focusIndicator);
1359 
1360             getChildren().addAll(inner);
1361 
1362             tooltip = tab.getTooltip();
1363             if (tooltip != null) {
1364                 Tooltip.install(this, tooltip);
1365                 oldTooltip = tooltip;
1366             }
1367 
1368             listener.registerChangeListener(tab.closableProperty(), e -> {
1369                 inner.requestLayout();
1370                 requestLayout();
1371             });
1372             listener.registerChangeListener(tab.selectedProperty(), e -> {
1373                 pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1374                 // Need to request a layout pass for inner because if the width
1375                 // and height didn't not change the label or close button may have
1376                 // changed.
1377                 inner.requestLayout();
1378                 requestLayout();
1379             });
1380             listener.registerChangeListener(tab.textProperty(),e -> label.setText(getTab().getText()));
1381             listener.registerChangeListener(tab.graphicProperty(), e -> label.setGraphic(getTab().getGraphic()));
1382             listener.registerChangeListener(tab.tooltipProperty(), e -> {
1383                 // uninstall the old tooltip
1384                 if (oldTooltip != null) {
1385                     Tooltip.uninstall(this, oldTooltip);
1386                 }
1387                 tooltip = tab.getTooltip();
1388                 if (tooltip != null) {
1389                     // install new tooltip and save as old tooltip.
1390                     Tooltip.install(this, tooltip);
1391                     oldTooltip = tooltip;
1392                 }
1393             });
1394             listener.registerChangeListener(tab.disabledProperty(), e -> {
1395                 updateTabDisabledState();
1396             });
1397             listener.registerChangeListener(tab.getTabPane().disabledProperty(), e -> {
1398                 updateTabDisabledState();
1399             });
1400             listener.registerChangeListener(tab.styleProperty(), e -> setStyle(tab.getStyle()));
1401 
1402             tab.getStyleClass().addListener(weakStyleClassListener);
1403 
1404             listener.registerChangeListener(getSkinnable().tabClosingPolicyProperty(),e -> {
1405                 inner.requestLayout();
1406                 requestLayout();
1407             });
1408             listener.registerChangeListener(getSkinnable().sideProperty(),e -> {
1409                 final Side side = getSkinnable().getSide();
1410                 pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1411                 pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1412                 pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1413                 pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1414                 inner.setRotate(side == Side.BOTTOM ? 180.0F : 0.0F);
1415                 if (getSkinnable().isRotateGraphic()) {
1416                     updateGraphicRotation();
1417                 }
1418             });
1419             listener.registerChangeListener(getSkinnable().rotateGraphicProperty(), e -> updateGraphicRotation());
1420             listener.registerChangeListener(getSkinnable().tabMinWidthProperty(), e -> {
1421                 requestLayout();
1422                 getSkinnable().requestLayout();
1423             });
1424             listener.registerChangeListener(getSkinnable().tabMaxWidthProperty(), e -> {
1425                 requestLayout();
1426                 getSkinnable().requestLayout();
1427             });
1428             listener.registerChangeListener(getSkinnable().tabMinHeightProperty(), e -> {
1429                 requestLayout();
1430                 getSkinnable().requestLayout();
1431             });
1432             listener.registerChangeListener(getSkinnable().tabMaxHeightProperty(), e -> {
1433                 requestLayout();
1434                 getSkinnable().requestLayout();
1435             });
1436 
1437             getProperties().put(Tab.class, tab);
1438             getProperties().put(ContextMenu.class, tab.getContextMenu());
1439 
1440             setOnContextMenuRequested((ContextMenuEvent me) -> {
1441                if (getTab().getContextMenu() != null) {
1442                     getTab().getContextMenu().show(inner, me.getScreenX(), me.getScreenY());
1443                     me.consume();
1444                 }
1445             });
1446             setOnMousePressed(new EventHandler<MouseEvent>() {
1447                 @Override public void handle(MouseEvent me) {
1448                     if (getTab().isDisable()) {
1449                         return;
1450                     }
1451                     if (me.getButton().equals(MouseButton.MIDDLE)) {
1452                         if (showCloseButton()) {
1453                             Tab tab = getTab();
1454                             if (behavior.canCloseTab(tab)) {
1455                                 removeListeners(tab);
1456                                 behavior.closeTab(tab);
1457                             }
1458                         }
1459                     } else if (me.getButton().equals(MouseButton.PRIMARY)) {
1460                         behavior.selectTab(getTab());
1461                     }
1462                 }
1463             });
1464 
1465             // initialize pseudo-class state
1466             pseudoClassStateChanged(SELECTED_PSEUDOCLASS_STATE, tab.isSelected());
1467             pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled());
1468             final Side side = getSkinnable().getSide();
1469             pseudoClassStateChanged(TOP_PSEUDOCLASS_STATE, (side == Side.TOP));
1470             pseudoClassStateChanged(RIGHT_PSEUDOCLASS_STATE, (side == Side.RIGHT));
1471             pseudoClassStateChanged(BOTTOM_PSEUDOCLASS_STATE, (side == Side.BOTTOM));
1472             pseudoClassStateChanged(LEFT_PSEUDOCLASS_STATE, (side == Side.LEFT));
1473         }
1474 
1475         private void updateTabDisabledState() {
1476             pseudoClassStateChanged(DISABLED_PSEUDOCLASS_STATE, tab.isDisabled());
1477             inner.requestLayout();
1478             requestLayout();
1479         }
1480 
1481         private void updateGraphicRotation() {
1482             if (label.getGraphic() != null) {
1483                 label.getGraphic().setRotate(getSkinnable().isRotateGraphic() ? 0.0F :
1484                     (getSkinnable().getSide().equals(Side.RIGHT) ? -90.0F :
1485                         (getSkinnable().getSide().equals(Side.LEFT) ? 90.0F : 0.0F)));
1486             }
1487         }
1488 
1489         private boolean showCloseButton() {
1490             return tab.isClosable() &&
1491                     (getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.ALL_TABS) ||
1492                     getSkinnable().getTabClosingPolicy().equals(TabClosingPolicy.SELECTED_TAB) && tab.isSelected());
1493         }
1494 
1495         private final DoubleProperty animationTransition = new SimpleDoubleProperty(this, "animationTransition", 1.0) {
1496             @Override protected void invalidated() {
1497                 requestLayout();
1498             }
1499         };
1500 
1501         private void removeListeners(Tab tab) {
1502             listener.dispose();
1503             inner.getChildren().clear();
1504             getChildren().clear();
1505             setOnContextMenuRequested(null);
1506             setOnMousePressed(null);
1507         }
1508 
1509         private TabAnimationState animationState = TabAnimationState.NONE;
1510         private Timeline currentAnimation;
1511 
1512         @Override protected double computePrefWidth(double height) {
1513 //            if (animating) {
1514 //                return prefWidth.getValue();
1515 //            }
1516             double minWidth = snapSize(getSkinnable().getTabMinWidth());
1517             double maxWidth = snapSize(getSkinnable().getTabMaxWidth());
1518             double paddingRight = snappedRightInset();
1519             double paddingLeft = snappedLeftInset();
1520             double tmpPrefWidth = snapSize(label.prefWidth(-1));
1521 
1522             // only include the close button width if it is relevant
1523             if (showCloseButton()) {
1524                 tmpPrefWidth += snapSize(closeBtn.prefWidth(-1));
1525             }
1526 
1527             if (tmpPrefWidth > maxWidth) {
1528                 tmpPrefWidth = maxWidth;
1529             } else if (tmpPrefWidth < minWidth) {
1530                 tmpPrefWidth = minWidth;
1531             }
1532             tmpPrefWidth += paddingRight + paddingLeft;
1533 //            prefWidth.setValue(tmpPrefWidth);
1534             return tmpPrefWidth;
1535         }
1536 
1537         @Override protected double computePrefHeight(double width) {
1538             double minHeight = snapSize(getSkinnable().getTabMinHeight());
1539             double maxHeight = snapSize(getSkinnable().getTabMaxHeight());
1540             double paddingTop = snappedTopInset();
1541             double paddingBottom = snappedBottomInset();
1542             double tmpPrefHeight = snapSize(label.prefHeight(width));
1543 
1544             if (tmpPrefHeight > maxHeight) {
1545                 tmpPrefHeight = maxHeight;
1546             } else if (tmpPrefHeight < minHeight) {
1547                 tmpPrefHeight = minHeight;
1548             }
1549             tmpPrefHeight += paddingTop + paddingBottom;
1550             return tmpPrefHeight;
1551         }
1552 
1553         @Override protected void layoutChildren() {
1554             double w = (snapSize(getWidth()) - snappedRightInset() - snappedLeftInset()) * animationTransition.getValue();
1555             inner.resize(w, snapSize(getHeight()) - snappedTopInset() - snappedBottomInset());
1556             inner.relocate(snappedLeftInset(), snappedTopInset());
1557         }
1558 
1559         @Override protected void setWidth(double value) {
1560             super.setWidth(value);
1561             clip.setWidth(value);
1562         }
1563 
1564         @Override protected void setHeight(double value) {
1565             super.setHeight(value);
1566             clip.setHeight(value);
1567         }
1568 
1569         /** {@inheritDoc} */
1570         @Override
1571         public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1572             switch (attribute) {
1573                 case TEXT: return getTab().getText();
1574                 case SELECTED: return selectedTab == getTab();
1575                 default: return super.queryAccessibleAttribute(attribute, parameters);
1576             }
1577         }
1578 
1579         /** {@inheritDoc} */
1580         @Override
1581         public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
1582             switch (action) {
1583                 case REQUEST_FOCUS:
1584                     getSkinnable().getSelectionModel().select(getTab());
1585                     break;
1586                 default: super.executeAccessibleAction(action, parameters);
1587             }
1588         }
1589 
1590     } /* End TabHeaderSkin */
1591 
1592     private static final PseudoClass SELECTED_PSEUDOCLASS_STATE =
1593             PseudoClass.getPseudoClass("selected");
1594     private static final PseudoClass TOP_PSEUDOCLASS_STATE =
1595             PseudoClass.getPseudoClass("top");
1596     private static final PseudoClass BOTTOM_PSEUDOCLASS_STATE =
1597             PseudoClass.getPseudoClass("bottom");
1598     private static final PseudoClass LEFT_PSEUDOCLASS_STATE =
1599             PseudoClass.getPseudoClass("left");
1600     private static final PseudoClass RIGHT_PSEUDOCLASS_STATE =
1601             PseudoClass.getPseudoClass("right");
1602     private static final PseudoClass DISABLED_PSEUDOCLASS_STATE =
1603             PseudoClass.getPseudoClass("disabled");
1604 
1605 
1606     /**************************************************************************
1607      *
1608      * TabContentRegion: each tab has one to contain the tab's content node
1609      *
1610      **************************************************************************/
1611     static class TabContentRegion extends StackPane {
1612 
1613         private Tab tab;
1614 
1615         private InvalidationListener tabContentListener = valueModel -> {
1616             updateContent();
1617         };
1618         private InvalidationListener tabSelectedListener = new InvalidationListener() {
1619             @Override public void invalidated(Observable valueModel) {
1620                 setVisible(tab.isSelected());
1621             }
1622         };
1623 
1624         private WeakInvalidationListener weakTabContentListener =
1625                 new WeakInvalidationListener(tabContentListener);
1626         private WeakInvalidationListener weakTabSelectedListener =
1627                 new WeakInvalidationListener(tabSelectedListener);
1628 
1629         public Tab getTab() {
1630             return tab;
1631         }
1632 
1633         public TabContentRegion(Tab tab) {
1634             getStyleClass().setAll("tab-content-area");
1635             setManaged(false);
1636             this.tab = tab;
1637             updateContent();
1638             setVisible(tab.isSelected());
1639 
1640             tab.selectedProperty().addListener(weakTabSelectedListener);
1641             tab.contentProperty().addListener(weakTabContentListener);
1642         }
1643 
1644         private void updateContent() {
1645             Node newContent = getTab().getContent();
1646             if (newContent == null) {
1647                 getChildren().clear();
1648             } else {
1649                 getChildren().setAll(newContent);
1650             }
1651         }
1652 
1653         private void removeListeners(Tab tab) {
1654             tab.selectedProperty().removeListener(weakTabSelectedListener);
1655             tab.contentProperty().removeListener(weakTabContentListener);
1656         }
1657 
1658     } /* End TabContentRegion */
1659 
1660     /**************************************************************************
1661      *
1662      * TabControlButtons: controls to manipulate tab interaction
1663      *
1664      **************************************************************************/
1665     class TabControlButtons extends StackPane {
1666         private StackPane inner;
1667         private StackPane downArrow;
1668         private Pane downArrowBtn;
1669         private boolean showControlButtons;
1670         private ContextMenu popup;
1671 
1672         public TabControlButtons() {
1673             getStyleClass().setAll("control-buttons-tab");
1674 
1675             TabPane tabPane = getSkinnable();
1676 
1677             downArrowBtn = new Pane();
1678             downArrowBtn.getStyleClass().setAll("tab-down-button");
1679             downArrowBtn.setVisible(isShowTabsMenu());
1680             downArrow = new StackPane();
1681             downArrow.setManaged(false);
1682             downArrow.getStyleClass().setAll("arrow");
1683             downArrow.setRotate(tabPane.getSide().equals(Side.BOTTOM) ? 180.0F : 0.0F);
1684             downArrowBtn.getChildren().add(downArrow);
1685             downArrowBtn.setOnMouseClicked(me -> {
1686                 showPopupMenu();
1687             });
1688 
1689             setupPopupMenu();
1690 
1691             inner = new StackPane() {
1692                 @Override protected double computePrefWidth(double height) {
1693                     double pw;
1694                     double maxArrowWidth = ! isShowTabsMenu() ? 0 : snapSize(downArrow.prefWidth(getHeight())) + snapSize(downArrowBtn.prefWidth(getHeight()));
1695                     pw = 0.0F;
1696                     if (isShowTabsMenu()) {
1697                         pw += maxArrowWidth;
1698                     }
1699                     if (pw > 0) {
1700                         pw += snappedLeftInset() + snappedRightInset();
1701                     }
1702                     return pw;
1703                 }
1704 
1705                 @Override protected double computePrefHeight(double width) {
1706                     double height = 0.0F;
1707                     if (isShowTabsMenu()) {
1708                         height = Math.max(height, snapSize(downArrowBtn.prefHeight(width)));
1709                     }
1710                     if (height > 0) {
1711                         height += snappedTopInset() + snappedBottomInset();
1712                     }
1713                     return height;
1714                 }
1715 
1716                 @Override protected void layoutChildren() {
1717                     if (isShowTabsMenu()) {
1718                         double x = 0;
1719                         double y = snappedTopInset();
1720                         double w = snapSize(getWidth()) - x + snappedLeftInset();
1721                         double h = snapSize(getHeight()) - y + snappedBottomInset();
1722                         positionArrow(downArrowBtn, downArrow, x, y, w, h);
1723                     }
1724                 }
1725 
1726                 private void positionArrow(Pane btn, StackPane arrow, double x, double y, double width, double height) {
1727                     btn.resize(width, height);
1728                     positionInArea(btn, x, y, width, height, /*baseline ignored*/0,
1729                             HPos.CENTER, VPos.CENTER);
1730                     // center arrow region within arrow button
1731                     double arrowWidth = snapSize(arrow.prefWidth(-1));
1732                     double arrowHeight = snapSize(arrow.prefHeight(-1));
1733                     arrow.resize(arrowWidth, arrowHeight);
1734                     positionInArea(arrow, btn.snappedLeftInset(), btn.snappedTopInset(),
1735                             width - btn.snappedLeftInset() - btn.snappedRightInset(),
1736                             height - btn.snappedTopInset() - btn.snappedBottomInset(),
1737                             /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
1738                 }
1739             };
1740             inner.getStyleClass().add("container");
1741             inner.getChildren().add(downArrowBtn);
1742 
1743             getChildren().add(inner);
1744 
1745             tabPane.sideProperty().addListener(valueModel -> {
1746                 Side tabPosition = getSkinnable().getSide();
1747                 downArrow.setRotate(tabPosition.equals(Side.BOTTOM)? 180.0F : 0.0F);
1748             });
1749             tabPane.getTabs().addListener((ListChangeListener<Tab>) c -> setupPopupMenu());
1750             showControlButtons = false;
1751             if (isShowTabsMenu()) {
1752                 showControlButtons = true;
1753                 requestLayout();
1754             }
1755             getProperties().put(ContextMenu.class, popup);
1756         }
1757 
1758         private boolean showTabsMenu = false;
1759 
1760         private void showTabsMenu(boolean value) {
1761             final boolean wasTabsMenuShowing = isShowTabsMenu();
1762             this.showTabsMenu = value;
1763 
1764             if (showTabsMenu && !wasTabsMenuShowing) {
1765                 downArrowBtn.setVisible(true);
1766                 showControlButtons = true;
1767                 inner.requestLayout();
1768                 tabHeaderArea.requestLayout();
1769             } else if (!showTabsMenu && wasTabsMenuShowing) {
1770                 hideControlButtons();
1771             }
1772         }
1773 
1774         private boolean isShowTabsMenu() {
1775             return showTabsMenu;
1776         }
1777 
1778         @Override protected double computePrefWidth(double height) {
1779             double pw = snapSize(inner.prefWidth(height));
1780             if (pw > 0) {
1781                 pw += snappedLeftInset() + snappedRightInset();
1782             }
1783             return pw;
1784         }
1785 
1786         @Override protected double computePrefHeight(double width) {
1787             return Math.max(getSkinnable().getTabMinHeight(), snapSize(inner.prefHeight(width))) +
1788                     snappedTopInset() + snappedBottomInset();
1789         }
1790 
1791         @Override protected void layoutChildren() {
1792             double x = snappedLeftInset();
1793             double y = snappedTopInset();
1794             double w = snapSize(getWidth()) - x + snappedRightInset();
1795             double h = snapSize(getHeight()) - y + snappedBottomInset();
1796 
1797             if (showControlButtons) {
1798                 showControlButtons();
1799                 showControlButtons = false;
1800             }
1801 
1802             inner.resize(w, h);
1803             positionInArea(inner, x, y, w, h, /*baseline ignored*/0, HPos.CENTER, VPos.BOTTOM);
1804         }
1805 
1806         private void showControlButtons() {
1807             setVisible(true);
1808             if (popup == null) {
1809                 setupPopupMenu();
1810             }
1811         }
1812 
1813         private void hideControlButtons() {
1814             // If the scroll arrows or tab menu is still visible we don't want
1815             // to hide it animate it back it.
1816             if (isShowTabsMenu()) {
1817                 showControlButtons = true;
1818             } else {
1819                 setVisible(false);
1820                 popup.getItems().clear();
1821                 popup = null;
1822             }
1823 
1824             // This needs to be called when we are in the left tabPosition
1825             // to allow for the clip offset to move properly (otherwise
1826             // it jumps too early - before the animation is done).
1827             requestLayout();
1828         }
1829 
1830         private void setupPopupMenu() {
1831             if (popup == null) {
1832                 popup = new ContextMenu();
1833             }
1834             popup.getItems().clear();
1835             ToggleGroup group = new ToggleGroup();
1836             ObservableList<RadioMenuItem> menuitems = FXCollections.<RadioMenuItem>observableArrayList();
1837             for (final Tab tab : getSkinnable().getTabs()) {
1838                 TabMenuItem item = new TabMenuItem(tab);
1839                 item.setToggleGroup(group);
1840                 item.setOnAction(t -> getSkinnable().getSelectionModel().select(tab));
1841                 menuitems.add(item);
1842             }
1843             popup.getItems().addAll(menuitems);
1844         }
1845 
1846         private void showPopupMenu() {
1847             for (MenuItem mi: popup.getItems()) {
1848                 TabMenuItem tmi = (TabMenuItem)mi;
1849                 if (selectedTab.equals(tmi.getTab())) {
1850                     tmi.setSelected(true);
1851                     break;
1852                 }
1853             }
1854             popup.show(downArrowBtn, Side.BOTTOM, 0, 0);
1855         }
1856     } /* End TabControlButtons*/
1857 
1858     static class TabMenuItem extends RadioMenuItem {
1859         Tab tab;
1860 
1861         private InvalidationListener disableListener = new InvalidationListener() {
1862             @Override public void invalidated(Observable o) {
1863                 setDisable(tab.isDisable());
1864             }
1865         };
1866 
1867         private WeakInvalidationListener weakDisableListener =
1868                 new WeakInvalidationListener(disableListener);
1869 
1870         public TabMenuItem(final Tab tab) {
1871             super(tab.getText(), TabPaneSkin.clone(tab.getGraphic()));
1872             this.tab = tab;
1873             setDisable(tab.isDisable());
1874             tab.disableProperty().addListener(weakDisableListener);
1875             textProperty().bind(tab.textProperty());
1876         }
1877 
1878         public Tab getTab() {
1879             return tab;
1880         }
1881 
1882         public void dispose() {
1883             tab.disableProperty().removeListener(weakDisableListener);
1884         }
1885     }
1886 
1887     @Override
1888     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1889         switch (attribute) {
1890             case FOCUS_ITEM: return tabHeaderArea.getTabHeaderSkin(selectedTab);
1891             case ITEM_COUNT: return tabHeaderArea.headersRegion.getChildren().size();
1892             case ITEM_AT_INDEX: {
1893                 Integer index = (Integer)parameters[0];
1894                 if (index == null) return null;
1895                 return tabHeaderArea.headersRegion.getChildren().get(index);
1896             }
1897             default: return super.queryAccessibleAttribute(attribute, parameters);
1898         }
1899     }
1900 
1901     // --------------------------
1902     // Tab Reordering
1903     // --------------------------
1904     private enum DragState {
1905         NONE,
1906         START,
1907         REORDER
1908     }
1909     private EventHandler<MouseEvent> headerDraggedHandler = this::handleHeaderDragged;
1910     private EventHandler<MouseEvent> headerMousePressedHandler = this::handleHeaderMousePressed;
1911     private EventHandler<MouseEvent> headerMouseReleasedHandler = this::handleHeaderMouseReleased;
1912 
1913     private int dragTabHeaderIndex;
1914     private TabHeaderSkin dragTabHeader;
1915     private TabHeaderSkin dropTabHeader;
1916     private StackPane headersRegion;
1917     private DragState dragState;
1918     private final int MIN_TO_MAX = 1;
1919     private final int MAX_TO_MIN = -1;
1920     private int xLayoutDirection;
1921     private double dragEventPrevLoc;
1922     private int prevDragDirection = MIN_TO_MAX;
1923     private final double DRAG_DIST_THRESHOLD = 0.75;
1924 
1925     // Reordering Animation
1926     private final double ANIM_DURATION = 120;
1927     private TabHeaderSkin dropAnimHeader;
1928     private Tab swapTab;
1929     private double dropHeaderSourceX;
1930     private double dropHeaderTransitionX;
1931     private final Animation dropHeaderAnim = new Transition() {
1932         {
1933             setInterpolator(Interpolator.EASE_BOTH);
1934             setCycleDuration(Duration.millis(ANIM_DURATION));
1935             setOnFinished(event -> {
1936                 completeHeaderReordering();
1937             });
1938         }
1939         protected void interpolate(double frac) {
1940             dropAnimHeader.setLayoutX(dropHeaderSourceX + dropHeaderTransitionX * frac);
1941         }
1942     };
1943     private double dragHeaderStartX;
1944     private double dragHeaderDestX;
1945     private double dragHeaderSourceX;
1946     private double dragHeaderTransitionX;
1947     private final Animation dragHeaderAnim = new Transition() {
1948         {
1949             setInterpolator(Interpolator.EASE_OUT);
1950             setCycleDuration(Duration.millis(ANIM_DURATION));
1951             setOnFinished(event -> {
1952                 resetDrag();
1953             });
1954         }
1955         protected void interpolate(double frac) {
1956             dragTabHeader.setLayoutX(dragHeaderSourceX + dragHeaderTransitionX * frac);
1957         }
1958     };
1959 
1960     // Helper methods for managing the listeners based on TabDragPolicy.
1961     private void addReorderListeners(Node n) {
1962         n.addEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler);
1963         n.addEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler);
1964         n.addEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler);
1965     }
1966 
1967     private void removeReorderListeners(Node n) {
1968         n.removeEventHandler(MouseEvent.MOUSE_PRESSED, headerMousePressedHandler);
1969         n.removeEventHandler(MouseEvent.MOUSE_RELEASED, headerMouseReleasedHandler);
1970         n.removeEventHandler(MouseEvent.MOUSE_DRAGGED, headerDraggedHandler);
1971     }
1972 
1973     private ListChangeListener childListener = new ListChangeListener<Node>() {
1974         public void onChanged(Change<? extends Node> change) {
1975             while (change.next()) {
1976                 if (change.wasAdded()) {
1977                     for(Node n : change.getAddedSubList()) {
1978                         addReorderListeners(n);
1979                     }
1980                 }
1981                 if (change.wasRemoved()) {
1982                     for(Node n : change.getRemoved()) {
1983                         removeReorderListeners(n);
1984                     }
1985                 }
1986             }
1987         }
1988     };
1989 
1990     private void updateListeners() {
1991         if (getSkinnable().getTabDragPolicy() == TabDragPolicy.FIXED ||
1992                 getSkinnable().getTabDragPolicy() == null) {
1993             for (Node n : headersRegion.getChildren()) {
1994                 removeReorderListeners(n);
1995             }
1996             headersRegion.getChildren().removeListener(childListener);
1997         } else if (getSkinnable().getTabDragPolicy() == TabDragPolicy.REORDER) {
1998             for (Node n : headersRegion.getChildren()) {
1999                 addReorderListeners(n);
2000             }
2001             headersRegion.getChildren().addListener(childListener);
2002         }
2003     }
2004 
2005     private void setupReordering(StackPane headersRegion) {
2006         dragState = DragState.NONE;
2007         this.headersRegion = headersRegion;
2008         updateListeners();
2009         getSkinnable().tabDragPolicyProperty().addListener((observable, oldValue, newValue) -> {
2010             if (oldValue != newValue) {
2011                 updateListeners();
2012             }
2013         });
2014     }
2015 
2016     private void handleHeaderMousePressed(MouseEvent event) {
2017         ((StackPane)event.getSource()).setMouseTransparent(true);
2018         startDrag(event);
2019     }
2020 
2021     private void handleHeaderMouseReleased(MouseEvent event) {
2022         ((StackPane)event.getSource()).setMouseTransparent(false);
2023         stopDrag();
2024         event.consume();
2025     }
2026 
2027     private void handleHeaderDragged(MouseEvent event) {
2028         perfromDrag(event);
2029     }
2030 
2031     private double getDragDelta(double curr, double prev) {
2032         if (getSkinnable().getSide().equals(Side.TOP) ||
2033                 getSkinnable().getSide().equals(Side.RIGHT)) {
2034             return curr - prev;
2035         } else {
2036             return prev - curr;
2037         }
2038     }
2039 
2040     private int deriveTabHeaderLayoutXDirection() {
2041         if (getSkinnable().getSide().equals(Side.TOP) ||
2042                 getSkinnable().getSide().equals(Side.RIGHT)) {
2043             // TabHeaderSkin are laid out in left to right direction inside headersRegion
2044             return MIN_TO_MAX;
2045         }
2046         // TabHeaderSkin are laid out in right to left direction inside headersRegion
2047         return MAX_TO_MIN;
2048     }
2049 
2050     private void perfromDrag(MouseEvent event) {
2051         int dragDirection;
2052         double dragHeaderNewLayoutX;
2053         Bounds dragHeaderBounds;
2054         Bounds dropHeaderBounds;
2055         double draggedDist;
2056         double mouseCurrentLoc = getHeaderRegionLocalX(event);
2057         double dragDelta = getDragDelta(mouseCurrentLoc, dragEventPrevLoc);
2058 
2059         if (dragDelta > 0) {
2060             // Dragging the tab header towards higher indexed tab headers inside headersRegion.
2061             dragDirection = MIN_TO_MAX;
2062         } else {
2063             // Dragging the tab header towards lower indexed tab headers inside headersRegion.
2064             dragDirection = MAX_TO_MIN;
2065         }
2066         // Stop dropHeaderAnim if direction of drag is changed
2067         if (prevDragDirection != dragDirection) {
2068             stopAnim(dropHeaderAnim);
2069             prevDragDirection = dragDirection;
2070         }
2071 
2072         dragHeaderNewLayoutX = dragTabHeader.getLayoutX() + xLayoutDirection * dragDelta;
2073 
2074         if (dragHeaderNewLayoutX >= 0 &&
2075                 dragHeaderNewLayoutX + dragTabHeader.getWidth() <= headersRegion.getWidth()) {
2076 
2077             dragState = DragState.REORDER;
2078             dragTabHeader.setLayoutX(dragHeaderNewLayoutX);
2079             dragHeaderBounds = dragTabHeader.getBoundsInParent();
2080 
2081             if (dragDirection == MIN_TO_MAX) {
2082                 // Dragging the tab header towards higher indexed tab headers
2083                 // Last tab header can not be dragged outside headersRegion.
2084 
2085                 // When the mouse is moved too fast, sufficient number of events
2086                 // are not generated. Hence it is required to check all possible
2087                 // headers to be reordered.
2088                 for (int i = dragTabHeaderIndex + 1; i < headersRegion.getChildren().size(); i++) {
2089                     dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i);
2090 
2091                     // dropTabHeader should not be already reordering.
2092                     if (dropAnimHeader != dropTabHeader) {
2093                         dropHeaderBounds = dropTabHeader.getBoundsInParent();
2094 
2095                         if (xLayoutDirection == MIN_TO_MAX) {
2096                             draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX();
2097                         } else {
2098                             draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX();
2099                         }
2100 
2101                         // A tab header is reordered when dragged tab header crosses DRAG_DIST_THRESHOLD% of next tab header's width.
2102                         if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) {
2103                             stopAnim(dropHeaderAnim);
2104                             // Distance by which tab header should be animated.
2105                             dropHeaderTransitionX = xLayoutDirection * -dragHeaderBounds.getWidth();
2106                             if (xLayoutDirection == MIN_TO_MAX) {
2107                                 dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth();
2108                             } else {
2109                                 dragHeaderDestX = dropHeaderBounds.getMinX();
2110                             }
2111                             startHeaderReorderingAnim();
2112                         } else {
2113                             break;
2114                         }
2115                     }
2116                 }
2117             } else {
2118                 // dragDirection is MAX_TO_MIN
2119                 // Dragging the tab header towards lower indexed tab headers.
2120                 // First tab header can not be dragged outside headersRegion.
2121 
2122                 // When the mouse is moved too fast, sufficient number of events
2123                 // are not generated. Hence it is required to check all possible
2124                 // tab headers to be reordered.
2125                 for (int i = dragTabHeaderIndex - 1; i >= 0; i--) {
2126                     dropTabHeader = (TabHeaderSkin) headersRegion.getChildren().get(i);
2127 
2128                     // dropTabHeader should not be already reordering.
2129                     if (dropAnimHeader != dropTabHeader) {
2130                         dropHeaderBounds = dropTabHeader.getBoundsInParent();
2131 
2132                         if (xLayoutDirection == MIN_TO_MAX) {
2133                             draggedDist = dropHeaderBounds.getMaxX() - dragHeaderBounds.getMinX();
2134                         } else {
2135                             draggedDist = dragHeaderBounds.getMaxX() - dropHeaderBounds.getMinX();
2136                         }
2137 
2138                         // A tab header is reordered when dragged tab crosses DRAG_DIST_THRESHOLD% of next tab header's width.
2139                         if (draggedDist > dropHeaderBounds.getWidth() * DRAG_DIST_THRESHOLD) {
2140                             stopAnim(dropHeaderAnim);
2141                             // Distance by which tab header should be animated.
2142                             dropHeaderTransitionX = xLayoutDirection * dragHeaderBounds.getWidth();
2143                             if (xLayoutDirection == MIN_TO_MAX) {
2144                                 dragHeaderDestX = dropHeaderBounds.getMinX();
2145                             } else {
2146                                 dragHeaderDestX = dropHeaderBounds.getMaxX() - dragHeaderBounds.getWidth();
2147                             }
2148                             startHeaderReorderingAnim();
2149                         } else {
2150                             break;
2151                         }
2152                     }
2153                 }
2154             }
2155         }
2156         dragEventPrevLoc = mouseCurrentLoc;
2157         event.consume();
2158     }
2159 
2160     private void startDrag(MouseEvent event) {
2161         // Stop the animations if any are running from previous reorder.
2162         stopAnim(dropHeaderAnim);
2163         stopAnim(dragHeaderAnim);
2164 
2165         dragTabHeader = (TabHeaderSkin) event.getSource();
2166         if (dragTabHeader != null) {
2167             dragState = DragState.START;
2168             swapTab = null;
2169             xLayoutDirection = deriveTabHeaderLayoutXDirection();
2170             dragEventPrevLoc = getHeaderRegionLocalX(event);
2171             dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader);
2172             dragTabHeader.setViewOrder(0);
2173             dragHeaderStartX = dragHeaderDestX = dragTabHeader.getLayoutX();
2174         }
2175     }
2176 
2177     private double getHeaderRegionLocalX(MouseEvent ev) {
2178         // The event is converted to tab header's parent i.e. headersRegion's local space.
2179         // This will provide a value of X co-ordinate with all transformations of TabPane
2180         // and transformations of all nodes in the TabPane's parent hierarchy.
2181         Point2D sceneToLocalHR = headersRegion.sceneToLocal(ev.getSceneX(), ev.getSceneY());
2182         return sceneToLocalHR.getX();
2183     }
2184 
2185     private void stopDrag() {
2186         if (dragState == DragState.START) {
2187             // No drag action was performed.
2188             resetDrag();
2189             return;
2190         }
2191         // Animate tab header being dragged to its final position.
2192         dragHeaderSourceX = dragTabHeader.getLayoutX();
2193         dragHeaderTransitionX = dragHeaderDestX - dragHeaderSourceX;
2194         dragHeaderAnim.playFromStart();
2195 
2196         // Reorder the tab list.
2197         if (dragHeaderStartX != dragHeaderDestX) {
2198             ((TabObservableList<Tab>) getSkinnable().getTabs()).reorder(dragTabHeader.tab, swapTab);
2199             swapTab = null;
2200         }
2201     }
2202 
2203     private void resetDrag() {
2204         dragState = DragState.NONE;
2205         dragTabHeader.setViewOrder(1);
2206         dragTabHeader = null;
2207         dropTabHeader = null;
2208         headersRegion.requestLayout();
2209     }
2210 
2211     // Animate tab header being dropped-on to its new position.
2212     private void startHeaderReorderingAnim() {
2213         dropAnimHeader = dropTabHeader;
2214         swapTab = dropAnimHeader.tab;
2215         dropHeaderSourceX = dropAnimHeader.getLayoutX();
2216         dropHeaderAnim.playFromStart();
2217     }
2218 
2219     // Remove dropAnimHeader and add at the index position of dragTabHeader.
2220     private void completeHeaderReordering() {
2221         if (dropAnimHeader != null) {
2222             headersRegion.getChildren().remove(dropAnimHeader);
2223             headersRegion.getChildren().add(dragTabHeaderIndex, dropAnimHeader);
2224             dropAnimHeader = null;
2225             headersRegion.requestLayout();
2226             dragTabHeaderIndex = headersRegion.getChildren().indexOf(dragTabHeader);
2227         }
2228     }
2229 
2230     // Helper method to stop an animation.
2231     private void stopAnim(Animation anim) {
2232         if (anim.getStatus() == Animation.Status.RUNNING) {
2233             anim.getOnFinished().handle(null);
2234             anim.stop();
2235         }
2236     }
2237 }