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