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