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