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